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