statsig_rust/gcir/
gcir_formatter.rs

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