statsig_rust/gcir/
gcir_formatter.rs

1use crate::gcir::feature_gates_processor::get_gate_evaluations;
2
3use crate::interned_string::InternedString;
4use crate::observability::ops_stats::OpsStatsForInstance;
5use crate::observability::ErrorBoundaryEvent;
6use crate::specs_response::spec_types::SessionReplayTrigger;
7use crate::{
8    evaluation::evaluator::{Evaluator, SpecType},
9    evaluation::evaluator_context::EvaluatorContext,
10    hashing::{HashAlgorithm, HashUtil},
11    initialize_evaluations_response::InitializeEvaluationsResponse,
12    initialize_response::InitializeResponse,
13    read_lock_or_else,
14    spec_store::{SpecStore, SpecStoreData},
15    statsig_metadata::StatsigMetadata,
16    user::StatsigUserInternal,
17    OverrideAdapter, StatsigErr,
18};
19
20use crate::log_error_to_statsig_and_console;
21use rand::Rng;
22use serde::Deserialize;
23use std::collections::HashMap;
24use std::sync::Arc;
25
26use super::dynamic_configs_processor::{
27    get_dynamic_config_evaluations, get_dynamic_config_evaluations_v2,
28};
29use super::feature_gates_processor::get_gate_evaluations_v2;
30use super::gcir_options::ClientInitResponseOptions;
31use super::layer_configs_processor::{get_layer_evaluations, get_layer_evaluations_v2};
32use super::param_stores_processor::get_serializeable_param_stores;
33use super::target_app_id_utils::select_app_id;
34
35pub struct GCIRFormatter {
36    spec_store: Arc<SpecStore>,
37    default_options: ClientInitResponseOptions,
38    override_adapter: Option<Arc<dyn OverrideAdapter>>,
39    ops_stats: Arc<OpsStatsForInstance>,
40    use_third_party_ua_parser: bool,
41}
42
43#[derive(Deserialize)]
44pub enum GCIRResponseFormat {
45    Initialize,                             // v1
46    InitializeWithSecondaryExposureMapping, // v2
47}
48
49impl GCIRResponseFormat {
50    #[must_use]
51    pub fn from_string(input: &str) -> Option<Self> {
52        match input {
53            "v1" => Some(GCIRResponseFormat::Initialize),
54            "v2" => Some(GCIRResponseFormat::InitializeWithSecondaryExposureMapping),
55            _ => None,
56        }
57    }
58}
59
60const TAG: &str = "GCIRFormatter";
61
62impl GCIRFormatter {
63    pub fn new(
64        spec_store: &Arc<SpecStore>,
65        override_adapter: &Option<Arc<dyn OverrideAdapter>>,
66        ops_stats: &Arc<OpsStatsForInstance>,
67        use_third_party_ua_parser: bool,
68    ) -> Self {
69        Self {
70            spec_store: spec_store.clone(),
71            override_adapter: override_adapter.as_ref().map(Arc::clone),
72            ops_stats: ops_stats.clone(),
73            default_options: ClientInitResponseOptions {
74                hash_algorithm: Some(HashAlgorithm::Djb2),
75                client_sdk_key: None,
76                include_local_overrides: Some(false),
77                feature_gate_filter: None,
78                experiment_filter: None,
79                dynamic_config_filter: None,
80                layer_filter: None,
81                param_store_filter: None,
82                response_format: None,
83                remove_id_type: Some(false),
84            },
85            use_third_party_ua_parser,
86        }
87    }
88
89    pub fn get_default_options(&self) -> &ClientInitResponseOptions {
90        &self.default_options
91    }
92
93    pub fn get_as_v1_format(
94        &self,
95        user_internal: StatsigUserInternal,
96        hashing: &HashUtil,
97        options: &ClientInitResponseOptions,
98    ) -> InitializeResponse {
99        self.get_v1_impl(&user_internal, hashing, options)
100            .unwrap_or_else(|e| {
101                log_error_to_statsig_and_console!(
102                    &self.ops_stats,
103                    TAG,
104                    StatsigErr::GCIRError(e.to_string())
105                );
106                InitializeResponse::blank(user_internal)
107            })
108    }
109
110    pub fn get_as_v2_format(
111        &self,
112        user_internal: StatsigUserInternal,
113        hashing: &HashUtil,
114        options: &ClientInitResponseOptions,
115    ) -> InitializeEvaluationsResponse {
116        self.get_v2_impl(&user_internal, hashing, options)
117            .unwrap_or_else(|e| {
118                log_error_to_statsig_and_console!(
119                    &self.ops_stats,
120                    TAG,
121                    StatsigErr::GCIRError(e.to_string())
122                );
123                InitializeEvaluationsResponse::blank(user_internal)
124            })
125    }
126
127    fn get_v2_impl(
128        &self,
129        user_internal: &StatsigUserInternal,
130        hashing: &HashUtil,
131        options: &ClientInitResponseOptions,
132    ) -> Result<InitializeEvaluationsResponse, StatsigErr> {
133        let data = read_lock_or_else!(self.spec_store.data, {
134            return Err(StatsigErr::LockFailure(
135                "Failed to acquire read lock for spec store data".to_string(),
136            ));
137        });
138
139        let mut sec_expo_hash_memo = HashMap::new();
140        let mut context = self.setup_evaluator_context(user_internal, &data, options, hashing);
141        let mut exposures = HashMap::new();
142
143        let param_stores = get_serializeable_param_stores(&mut context, options);
144        let evaluated_keys = get_evaluated_keys(user_internal);
145        let session_replay_info = get_session_replay_info(&mut context, options, hashing);
146
147        Ok(InitializeEvaluationsResponse {
148            feature_gates: get_gate_evaluations_v2(
149                &mut context,
150                options,
151                &mut sec_expo_hash_memo,
152                &mut exposures,
153            )?,
154            dynamic_configs: get_dynamic_config_evaluations_v2(
155                &mut context,
156                options,
157                &mut sec_expo_hash_memo,
158                &mut exposures,
159            )?,
160            layer_configs: get_layer_evaluations_v2(
161                &mut context,
162                options,
163                &mut sec_expo_hash_memo,
164                &mut exposures,
165            )?,
166            time: data.values.time,
167            has_updates: true,
168            hash_used: options.get_hash_algorithm().to_string(),
169            user: user_internal.to_loggable(),
170            pa_hash: user_internal.get_hashed_private_attributes(),
171            sdk_params: HashMap::new(),
172            evaluated_keys,
173            sdk_info: get_sdk_info(),
174            param_stores,
175            exposures,
176            can_record_session: session_replay_info.can_record_session,
177            session_recording_rate: session_replay_info.session_recording_rate,
178            recording_blocked: session_replay_info.recording_blocked,
179            passes_session_recording_targeting: session_replay_info
180                .passes_session_recording_targeting,
181            session_recording_event_triggers: session_replay_info.session_recording_event_triggers,
182            session_recording_exposure_triggers: session_replay_info
183                .session_recording_exposure_triggers,
184        })
185    }
186
187    fn get_v1_impl(
188        &self,
189        user_internal: &StatsigUserInternal,
190        hashing: &HashUtil,
191        options: &ClientInitResponseOptions,
192    ) -> Result<InitializeResponse, StatsigErr> {
193        let data = read_lock_or_else!(self.spec_store.data, {
194            return Err(StatsigErr::LockFailure(
195                "Failed to acquire read lock for spec store data".to_string(),
196            ));
197        });
198
199        let mut sec_expo_hash_memo = HashMap::new();
200        let mut context = self.setup_evaluator_context(user_internal, &data, options, hashing);
201
202        let param_stores = get_serializeable_param_stores(&mut context, options);
203        let evaluated_keys = get_evaluated_keys(user_internal);
204        let session_replay_info = get_session_replay_info(&mut context, options, hashing);
205        let gates = get_gate_evaluations(&mut context, options, &mut sec_expo_hash_memo)?;
206        let configs =
207            get_dynamic_config_evaluations(&mut context, options, &mut sec_expo_hash_memo)?;
208        let layers = get_layer_evaluations(&mut context, options, &mut sec_expo_hash_memo)?;
209
210        Ok(InitializeResponse {
211            feature_gates: gates,
212            dynamic_configs: configs,
213            layer_configs: layers,
214            time: data.values.time,
215            has_updates: true,
216            hash_used: options.get_hash_algorithm().to_string(),
217            user: user_internal.to_loggable(),
218            sdk_params: HashMap::new(),
219            evaluated_keys,
220            sdk_info: get_sdk_info(),
221            param_stores,
222            can_record_session: session_replay_info.can_record_session,
223            session_recording_rate: session_replay_info.session_recording_rate,
224            recording_blocked: session_replay_info.recording_blocked,
225            passes_session_recording_targeting: session_replay_info
226                .passes_session_recording_targeting,
227            session_recording_event_triggers: session_replay_info.session_recording_event_triggers,
228            session_recording_exposure_triggers: session_replay_info
229                .session_recording_exposure_triggers,
230            pa_hash: user_internal.get_hashed_private_attributes(),
231        })
232    }
233
234    fn setup_evaluator_context<'a>(
235        &'a self,
236        user_internal: &'a StatsigUserInternal,
237        data: &'a SpecStoreData,
238        options: &'a ClientInitResponseOptions,
239        hashing: &'a HashUtil,
240    ) -> EvaluatorContext<'a> {
241        let app_id = select_app_id(options, &data.values, hashing);
242
243        let override_adapter = match options.include_local_overrides {
244            Some(true) => self.override_adapter.as_ref(),
245            _ => None,
246        };
247
248        EvaluatorContext::new(
249            user_internal,
250            data,
251            hashing,
252            app_id,
253            override_adapter,
254            self.use_third_party_ua_parser,
255        )
256    }
257}
258
259fn get_evaluated_keys(
260    user_internal: &StatsigUserInternal,
261) -> HashMap<InternedString, InternedString> {
262    let mut evaluated_keys = HashMap::new();
263
264    if let Some(user_id) = user_internal.user_ref.data.user_id.as_ref() {
265        evaluated_keys.insert(
266            InternedString::from_str_ref("userID"),
267            user_id
268                .string_value
269                .as_ref()
270                .map(|s| s.value.clone())
271                .unwrap_or_default(),
272        );
273    }
274
275    if let Some(custom_ids) = user_internal.user_ref.data.custom_ids.as_ref() {
276        for (key, value) in custom_ids {
277            evaluated_keys.insert(
278                InternedString::from_str_ref(key.as_str()),
279                value
280                    .string_value
281                    .as_ref()
282                    .map(|s| s.value.clone())
283                    .unwrap_or_default(),
284            );
285        }
286    }
287
288    evaluated_keys
289}
290
291fn get_sdk_info() -> HashMap<String, String> {
292    let metadata = StatsigMetadata::get_metadata();
293    HashMap::from([
294        ("sdkType".to_string(), metadata.sdk_type),
295        ("sdkVersion".to_string(), metadata.sdk_version),
296        ("sessionId".to_string(), metadata.session_id),
297    ])
298}
299
300pub struct GCIRSessionReplayInfo {
301    pub can_record_session: Option<bool>,
302    pub session_recording_rate: Option<f64>,
303    pub recording_blocked: Option<bool>,
304    pub passes_session_recording_targeting: Option<bool>,
305    pub session_recording_event_triggers: Option<HashMap<String, SessionReplayTrigger>>,
306    pub session_recording_exposure_triggers: Option<HashMap<String, SessionReplayTrigger>>,
307}
308
309fn get_session_replay_info(
310    context: &mut EvaluatorContext,
311    options: &ClientInitResponseOptions,
312    hashing: &HashUtil,
313) -> GCIRSessionReplayInfo {
314    let mut session_replay_info = GCIRSessionReplayInfo {
315        can_record_session: None,
316        session_recording_rate: None,
317        recording_blocked: None,
318        passes_session_recording_targeting: None,
319        session_recording_event_triggers: None,
320        session_recording_exposure_triggers: None,
321    };
322
323    let session_replay_data = match &context.spec_store_data.values.session_replay_info {
324        Some(data) => data,
325        None => return session_replay_info,
326    };
327
328    session_replay_info.can_record_session = Some(true);
329    session_replay_info.recording_blocked = session_replay_data.recording_blocked;
330    if session_replay_data.recording_blocked == Some(true) {
331        session_replay_info.can_record_session = Some(false);
332    }
333
334    let targeting_gate_name = &session_replay_data.targeting_gate;
335
336    if let Some(gate_name) = targeting_gate_name {
337        match Evaluator::evaluate(context, gate_name.clone().as_str(), &SpecType::Gate) {
338            Ok(_result) => {
339                session_replay_info.passes_session_recording_targeting =
340                    Some(context.result.bool_value);
341                if !context.result.bool_value {
342                    session_replay_info.can_record_session = Some(false);
343                }
344            }
345            Err(_e) => {
346                session_replay_info.passes_session_recording_targeting = Some(false);
347                session_replay_info.can_record_session = Some(false);
348            }
349        }
350    }
351
352    let mut rng = rand::thread_rng();
353    let random: f64 = rng.gen::<f64>();
354
355    if let Some(rate) = &session_replay_data.sampling_rate {
356        session_replay_info.session_recording_rate = Some(*rate);
357        if random > *rate {
358            session_replay_info.can_record_session = Some(false);
359        }
360    }
361
362    if let Some(triggers) = &session_replay_data.session_recording_event_triggers {
363        let mut new_event_triggers = HashMap::new();
364        for (key, trigger) in triggers {
365            let mut new_trigger = SessionReplayTrigger {
366                values: trigger.values.clone(),
367                sampling_rate: None,
368                passes_sampling: None,
369            };
370            if let Some(rate) = &trigger.sampling_rate {
371                new_trigger.passes_sampling = Some(random <= *rate);
372            }
373            new_event_triggers.insert(key.clone(), new_trigger);
374        }
375        session_replay_info.session_recording_event_triggers = Some(new_event_triggers);
376    }
377
378    if let Some(triggers) = &session_replay_data.session_recording_exposure_triggers {
379        let mut new_exposure_triggers = HashMap::new();
380        for (key, trigger) in triggers {
381            let mut new_trigger = SessionReplayTrigger {
382                values: trigger.values.clone(),
383                sampling_rate: None,
384                passes_sampling: None,
385            };
386            if let Some(rate) = &trigger.sampling_rate {
387                new_trigger.passes_sampling = Some(random <= *rate);
388            }
389            new_exposure_triggers.insert(
390                hashing.hash(key.as_str(), options.get_hash_algorithm()),
391                new_trigger,
392            );
393        }
394        session_replay_info.session_recording_exposure_triggers = Some(new_exposure_triggers);
395    }
396
397    session_replay_info
398}