statsig_rust/gcir/
gcir_formatter.rs

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