statsig_rust/gcir/
gcir_formatter.rs

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