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