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, InitializeWithSecondaryExposureMapping, }
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}