1use crate::initialize_evaluations_response::InitializeEvaluationsResponse;
2use crate::{
3 evaluation::{
4 dynamic_value::DynamicValue,
5 evaluation_types::{AnyConfigEvaluation, SecondaryExposure},
6 evaluation_types_v2::AnyConfigEvaluationV2,
7 evaluator::{Evaluator, SpecType},
8 evaluator_context::EvaluatorContext,
9 evaluator_result::{
10 result_to_dynamic_config_eval, result_to_dynamic_config_eval_v2,
11 result_to_experiment_eval, result_to_experiment_eval_v2, result_to_gate_eval,
12 result_to_gate_eval_v2, result_to_layer_eval, result_to_layer_eval_v2, EvaluatorResult,
13 },
14 },
15 hashing::{HashAlgorithm, HashUtil},
16 initialize_response::InitializeResponse,
17 read_lock_or_else,
18 spec_store::SpecStore,
19 specs_response::param_store_types::{
20 DynamicConfigParameter, ExperimentParameter, GateParameter, LayerParameter, Parameter,
21 ParameterStore,
22 },
23 specs_response::spec_types::Spec,
24 statsig_metadata::StatsigMetadata,
25 user::StatsigUserInternal,
26 OverrideAdapter,
27};
28
29use serde::Deserialize;
30use std::collections::{HashMap, HashSet};
31use std::sync::Arc;
32
33#[derive(Default, Deserialize)]
34pub struct ClientInitResponseOptions {
35 pub hash_algorithm: Option<HashAlgorithm>,
36 pub client_sdk_key: Option<String>,
37 pub include_local_overrides: Option<bool>,
38 pub response_format: Option<GCIRResponseFormat>,
39}
40
41pub struct ClientInitResponseFormatter {
42 spec_store: Arc<SpecStore>,
43 default_options: ClientInitResponseOptions,
44 override_adapter: Option<Arc<dyn OverrideAdapter>>,
45}
46
47#[derive(Deserialize)]
48pub enum GCIRResponseFormat {
49 InitializeV1,
50 Evaluations,
51}
52
53impl ClientInitResponseFormatter {
54 pub fn new(
55 spec_store: &Arc<SpecStore>,
56 override_adapter: &Option<Arc<dyn OverrideAdapter>>,
57 ) -> Self {
58 Self {
59 spec_store: spec_store.clone(),
60 override_adapter: override_adapter.as_ref().map(Arc::clone),
61 default_options: ClientInitResponseOptions {
62 hash_algorithm: Some(HashAlgorithm::Djb2),
63 client_sdk_key: None,
64 include_local_overrides: Some(false),
65 response_format: None,
66 },
67 }
68 }
69
70 pub fn get_default_options(&self) -> &ClientInitResponseOptions {
71 &self.default_options
72 }
73
74 pub fn get(
75 &self,
76 user_internal: StatsigUserInternal,
77 hashing: &HashUtil,
78 options: &ClientInitResponseOptions,
79 ) -> InitializeResponse {
80 let data = read_lock_or_else!(self.spec_store.data, {
81 return InitializeResponse::blank(user_internal);
82 });
83
84 let mut sec_expo_hash_memo = HashMap::new();
85 let mut app_id = data.values.app_id.as_ref();
86
87 if let Some(client_sdk_key) = &options.client_sdk_key {
88 if let Some(app_id_value) = &data.values.sdk_keys_to_app_ids {
89 app_id = app_id_value.get(client_sdk_key);
90 }
91 if let Some(app_id_value) = &data.values.hashed_sdk_keys_to_app_ids {
92 let hashed_key = &hashing.hash(client_sdk_key, &HashAlgorithm::Djb2);
93 app_id = app_id_value.get(hashed_key);
94 }
95 }
96 let include_local_overrides = options.include_local_overrides.unwrap_or(false);
97 let mut feature_gates = HashMap::new();
98 let mut context = EvaluatorContext::new(
99 &user_internal,
100 &data,
101 hashing,
102 &app_id,
103 if include_local_overrides {
104 &self.override_adapter
105 } else {
106 &None
107 },
108 );
109
110 let hash_used = options
111 .hash_algorithm
112 .as_ref()
113 .unwrap_or(&HashAlgorithm::Djb2);
114
115 for (name, spec) in &data.values.feature_gates {
116 if spec.entity == "segment" || spec.entity == "holdout" {
117 continue;
118 }
119
120 if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
121 continue;
122 }
123
124 context.reset_between_top_level_evaluations();
125 if let Err(_err) = Evaluator::evaluate(&mut context, name, &SpecType::Gate) {
126 return InitializeResponse::blank(user_internal);
127 }
128
129 let hashed_name = context.hashing.hash(name, hash_used);
130 hash_secondary_exposures(
131 &mut context.result,
132 hashing,
133 hash_used,
134 &mut sec_expo_hash_memo,
135 );
136
137 let eval = result_to_gate_eval(&hashed_name, &mut context.result);
138 feature_gates.insert(hashed_name, eval);
139 }
140
141 let mut dynamic_configs = HashMap::new();
142 for (name, spec) in &data.values.dynamic_configs {
143 if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
144 continue;
145 }
146
147 context.reset_between_top_level_evaluations();
148 let spec_type = if spec.entity == "dynamic_config" {
149 &SpecType::DynamicConfig
150 } else {
151 &SpecType::Experiment
152 };
153 if let Err(_err) = Evaluator::evaluate(&mut context, name, spec_type) {
154 return InitializeResponse::blank(user_internal);
155 }
156
157 let hashed_name = context.hashing.hash(name, hash_used);
158 hash_secondary_exposures(
159 &mut context.result,
160 hashing,
161 hash_used,
162 &mut sec_expo_hash_memo,
163 );
164
165 if spec.entity == "dynamic_config" {
166 let evaluation = result_to_dynamic_config_eval(&hashed_name, &mut context.result);
167 dynamic_configs.insert(hashed_name, AnyConfigEvaluation::DynamicConfig(evaluation));
168 } else {
169 let mut evaluation =
170 result_to_experiment_eval(&hashed_name, Some(spec), &mut context.result);
171 evaluation.undelegated_secondary_exposures = None;
172 dynamic_configs.insert(hashed_name, AnyConfigEvaluation::Experiment(evaluation));
173 }
174 }
175
176 let mut layer_configs = HashMap::new();
177 for (name, spec) in &data.values.layer_configs {
178 if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
179 continue;
180 }
181
182 context.reset_between_top_level_evaluations();
183 if let Err(_err) = Evaluator::evaluate(&mut context, name, &SpecType::Layer) {
184 return InitializeResponse::blank(user_internal);
185 }
186
187 let hashed_name = context.hashing.hash(name, hash_used);
188 hash_secondary_exposures(
189 &mut context.result,
190 hashing,
191 hash_used,
192 &mut sec_expo_hash_memo,
193 );
194
195 let mut evaluation = result_to_layer_eval(&hashed_name, &mut context.result);
196
197 if let Some(allocated_experiment_name) = evaluation.allocated_experiment_name {
198 evaluation.allocated_experiment_name =
199 Some(context.hashing.hash(&allocated_experiment_name, hash_used));
200 }
201
202 layer_configs.insert(hashed_name, evaluation);
203 }
204
205 let mut param_stores = HashMap::new();
206 let default_store = HashMap::new();
207 let stores = match &data.values.param_stores {
208 Some(stores) => stores,
209 None => &default_store,
210 };
211 for (name, store) in stores {
212 if should_filter_config_for_app(&store.target_app_ids, &app_id, &options.client_sdk_key)
213 {
214 continue;
215 }
216
217 let hashed_name = context.hashing.hash(name, hash_used);
218 let parameters = get_parameters_from_store(store, hash_used, &context);
219 param_stores.insert(hashed_name, parameters);
220 }
221
222 let evaluated_keys = get_evaluated_keys(&user_internal);
223 let metadata = StatsigMetadata::get_metadata();
224 InitializeResponse {
225 feature_gates,
226 dynamic_configs,
227 layer_configs,
228 time: data.values.time,
229 has_updates: true,
230 hash_used: hash_used.to_string(),
231 user: user_internal.to_loggable(),
232 sdk_params: HashMap::new(),
233 evaluated_keys,
234 sdk_info: HashMap::from([
235 ("sdkType".to_string(), metadata.sdk_type),
236 ("sdkVersion".to_string(), metadata.sdk_version),
237 ]),
238 param_stores,
239 }
240 }
241
242 pub fn get_evaluations(
243 &self,
244 user_internal: StatsigUserInternal,
245 hashing: &HashUtil,
246 options: &ClientInitResponseOptions,
247 ) -> InitializeEvaluationsResponse {
248 let data = read_lock_or_else!(self.spec_store.data, {
249 return InitializeEvaluationsResponse::blank(user_internal);
250 });
251
252 let mut sec_expo_hash_memo = HashMap::new();
253 let mut app_id = data.values.app_id.as_ref();
254
255 if let Some(client_sdk_key) = &options.client_sdk_key {
256 if let Some(app_id_value) = &data.values.sdk_keys_to_app_ids {
257 app_id = app_id_value.get(client_sdk_key);
258 }
259 if let Some(app_id_value) = &data.values.hashed_sdk_keys_to_app_ids {
260 let hashed_key = &hashing.hash(client_sdk_key, &HashAlgorithm::Djb2);
261 app_id = app_id_value.get(hashed_key);
262 }
263 }
264 let include_local_overrides = options.include_local_overrides.unwrap_or(false);
265 let mut feature_gates = HashMap::new();
266 let mut context = EvaluatorContext::new(
267 &user_internal,
268 &data,
269 hashing,
270 &app_id,
271 if include_local_overrides {
272 &self.override_adapter
273 } else {
274 &None
275 },
276 );
277 let mut exposures = HashMap::new();
278
279 let hash_used = options
280 .hash_algorithm
281 .as_ref()
282 .unwrap_or(&HashAlgorithm::Djb2);
283
284 for (name, spec) in &data.values.feature_gates {
285 if spec.entity == "segment" || spec.entity == "holdout" {
286 continue;
287 }
288
289 if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
290 continue;
291 }
292
293 context.reset_result();
294 if let Err(_err) = Evaluator::evaluate(&mut context, name, &SpecType::Gate) {
295 return InitializeEvaluationsResponse::blank(user_internal);
296 }
297
298 let hashed_name = context.hashing.hash(name, hash_used);
299 hash_secondary_exposures(
300 &mut context.result,
301 hashing,
302 hash_used,
303 &mut sec_expo_hash_memo,
304 );
305 for exposure in &context.result.secondary_exposures {
306 let key = format!(
307 "{}:{}:{}",
308 exposure.gate, exposure.gate_value, exposure.rule_id
309 );
310 let hash = hashing.hash(&key, &HashAlgorithm::Djb2);
311
312 exposures.insert(hash, exposure.clone());
313 }
314
315 let eval = result_to_gate_eval_v2(&hashed_name, &mut context.result, hashing);
316 feature_gates.insert(hashed_name, eval);
317 }
318
319 let mut dynamic_configs = HashMap::new();
320 for (name, spec) in &data.values.dynamic_configs {
321 if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
322 continue;
323 }
324
325 context.reset_result();
326 let spec_type = if spec.entity == "dynamic_config" {
327 &SpecType::DynamicConfig
328 } else {
329 &SpecType::Experiment
330 };
331 if let Err(_err) = Evaluator::evaluate(&mut context, name, spec_type) {
332 return InitializeEvaluationsResponse::blank(user_internal);
333 }
334
335 let hashed_name = context.hashing.hash(name, hash_used);
336 hash_secondary_exposures(
337 &mut context.result,
338 hashing,
339 hash_used,
340 &mut sec_expo_hash_memo,
341 );
342 for exposure in &context.result.secondary_exposures {
343 let key = format!(
344 "{}:{}:{}",
345 exposure.gate, exposure.gate_value, exposure.rule_id
346 );
347 let hash = hashing.hash(&key, &HashAlgorithm::Djb2);
348
349 exposures.insert(hash, exposure.clone());
350 }
351
352 if spec.entity == "dynamic_config" {
353 let evaluation =
354 result_to_dynamic_config_eval_v2(&hashed_name, &mut context.result, hashing);
355 dynamic_configs.insert(
356 hashed_name,
357 AnyConfigEvaluationV2::DynamicConfig(evaluation),
358 );
359 } else {
360 let mut evaluation = result_to_experiment_eval_v2(
361 &hashed_name,
362 Some(spec),
363 &mut context.result,
364 hashing,
365 );
366 evaluation.undelegated_secondary_exposures = None;
367 dynamic_configs.insert(hashed_name, AnyConfigEvaluationV2::Experiment(evaluation));
368 }
369 }
370
371 let mut layer_configs = HashMap::new();
372 for (name, spec) in &data.values.layer_configs {
373 if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
374 continue;
375 }
376
377 context.reset_result();
378 if let Err(_err) = Evaluator::evaluate(&mut context, name, &SpecType::Layer) {
379 return InitializeEvaluationsResponse::blank(user_internal);
380 }
381
382 let hashed_name = context.hashing.hash(name, hash_used);
383 hash_secondary_exposures(
384 &mut context.result,
385 hashing,
386 hash_used,
387 &mut sec_expo_hash_memo,
388 );
389 for exposure in &context.result.secondary_exposures {
390 let key = format!(
391 "{}:{}:{}",
392 exposure.gate, exposure.gate_value, exposure.rule_id
393 );
394 let hash = hashing.hash(&key, &HashAlgorithm::Djb2);
395
396 exposures.insert(hash, exposure.clone());
397 }
398
399 if let Some(u) = &context.result.undelegated_secondary_exposures {
400 for exposure in u {
401 let key = format!(
402 "{}:{}:{}",
403 exposure.gate, exposure.gate_value, exposure.rule_id
404 );
405 let hash = hashing.hash(&key, &HashAlgorithm::Djb2);
406
407 exposures.insert(hash, exposure.clone());
408 }
409 }
410
411 let mut evaluation =
412 result_to_layer_eval_v2(&hashed_name, &mut context.result, hashing);
413
414 if let Some(allocated_experiment_name) = evaluation.allocated_experiment_name {
415 evaluation.allocated_experiment_name =
416 Some(context.hashing.hash(&allocated_experiment_name, hash_used));
417 }
418
419 layer_configs.insert(hashed_name, evaluation);
420 }
421
422 let mut param_stores = HashMap::new();
423 let default_store = HashMap::new();
424 let stores = match &data.values.param_stores {
425 Some(stores) => stores,
426 None => &default_store,
427 };
428 for (name, store) in stores {
429 if should_filter_config_for_app(&store.target_app_ids, &app_id, &options.client_sdk_key)
430 {
431 continue;
432 }
433
434 let hashed_name = context.hashing.hash(name, hash_used);
435 let parameters = get_parameters_from_store(store, hash_used, &context);
436 param_stores.insert(hashed_name, parameters);
437 }
438
439 let evaluated_keys = get_evaluated_keys(&user_internal);
440 let metadata = StatsigMetadata::get_metadata();
441 InitializeEvaluationsResponse {
442 feature_gates,
443 dynamic_configs,
444 layer_configs,
445 time: data.values.time,
446 has_updates: true,
447 hash_used: hash_used.to_string(),
448 user: user_internal.to_loggable(),
449 sdk_params: HashMap::new(),
450 evaluated_keys,
451 sdk_info: HashMap::from([
452 ("sdkType".to_string(), metadata.sdk_type),
453 ("sdkVersion".to_string(), metadata.sdk_version),
454 ]),
455 param_stores,
456 exposures,
457 }
458 }
459}
460
461fn get_parameters_from_store(
462 store: &ParameterStore,
463 hash_used: &HashAlgorithm,
464 context: &EvaluatorContext,
465) -> HashMap<String, Parameter> {
466 let mut parameters: HashMap<String, Parameter> = HashMap::new();
468 for (param_name, param) in &store.parameters {
469 match param {
470 Parameter::StaticValue(_static_value) => {
471 parameters.insert(param_name.clone(), param.clone());
472 }
473
474 Parameter::Gate(gate) => {
475 let new_param = GateParameter {
476 ref_type: gate.ref_type.clone(),
477 param_type: gate.param_type.clone(),
478 gate_name: context.hashing.hash(&gate.gate_name, hash_used),
479 pass_value: gate.pass_value.clone(),
480 fail_value: gate.fail_value.clone(),
481 };
482 parameters.insert(param_name.clone(), Parameter::Gate(new_param));
483 }
484
485 Parameter::DynamicConfig(dynamic_config) => {
486 let new_param = DynamicConfigParameter {
487 ref_type: dynamic_config.ref_type.clone(),
488 param_type: dynamic_config.param_type.clone(),
489 config_name: context.hashing.hash(&dynamic_config.config_name, hash_used),
490 param_name: dynamic_config.param_name.clone(),
491 };
492 parameters.insert(param_name.clone(), Parameter::DynamicConfig(new_param));
493 }
494
495 Parameter::Experiment(experiment) => {
496 let new_param = ExperimentParameter {
497 ref_type: experiment.ref_type.clone(),
498 param_type: experiment.param_type.clone(),
499 experiment_name: context.hashing.hash(&experiment.experiment_name, hash_used),
500 param_name: experiment.param_name.clone(),
501 };
502 parameters.insert(param_name.clone(), Parameter::Experiment(new_param));
503 }
504
505 Parameter::Layer(layer) => {
506 let new_param = LayerParameter {
507 ref_type: layer.ref_type.clone(),
508 param_type: layer.param_type.clone(),
509 layer_name: context.hashing.hash(&layer.layer_name, hash_used),
510 param_name: layer.param_name.clone(),
511 };
512 parameters.insert(param_name.clone(), Parameter::Layer(new_param));
513 }
514 }
515 }
516 parameters
517}
518
519fn should_filter_spec_for_app(
520 spec: &Spec,
521 app_id: &Option<&DynamicValue>,
522 client_sdk_key: &Option<String>,
523) -> bool {
524 should_filter_config_for_app(&spec.target_app_ids, app_id, client_sdk_key)
525}
526
527fn should_filter_config_for_app(
528 target_app_ids: &Option<Vec<String>>,
529 app_id: &Option<&DynamicValue>,
530 client_sdk_key: &Option<String>,
531) -> bool {
532 let _client_sdk_key = match client_sdk_key {
533 Some(client_sdk_key) => client_sdk_key,
534 None => return false,
535 };
536
537 let app_id = match app_id {
538 Some(app_id) => app_id,
539 None => return false,
540 };
541
542 let string_app_id = match app_id.string_value.as_ref() {
543 Some(string_app_id) => string_app_id,
544 None => return false,
545 };
546
547 let target_app_ids = match target_app_ids {
548 Some(target_app_ids) => target_app_ids,
549 None => return true,
550 };
551
552 if !target_app_ids.contains(&string_app_id.value) {
553 return true;
554 }
555 false
556}
557
558fn get_evaluated_keys(user_internal: &StatsigUserInternal) -> HashMap<String, String> {
559 let mut evaluated_keys = HashMap::new();
560
561 if let Some(user_id) = user_internal.user_data.user_id.as_ref() {
562 evaluated_keys.insert(
563 "userID".to_string(),
564 user_id
565 .string_value
566 .as_ref()
567 .map(|s| s.value.clone())
568 .unwrap_or_default(),
569 );
570 }
571
572 if let Some(custom_ids) = user_internal.user_data.custom_ids.as_ref() {
573 for (key, value) in custom_ids {
574 evaluated_keys.insert(
575 key.clone(),
576 value
577 .string_value
578 .as_ref()
579 .map(|s| s.value.clone())
580 .unwrap_or_default(),
581 );
582 }
583 }
584
585 evaluated_keys
586}
587
588fn hash_secondary_exposures(
589 result: &mut EvaluatorResult,
590 hashing: &HashUtil,
591 hash_algorithm: &HashAlgorithm,
592 memo: &mut HashMap<String, String>,
593) {
594 fn loop_filter_n_hash(
595 exposures: &mut Vec<SecondaryExposure>,
596 hashing: &HashUtil,
597 hash_algorithm: &HashAlgorithm,
598 memo: &mut HashMap<String, String>,
599 ) {
600 let mut seen = HashSet::<String>::with_capacity(exposures.len());
601 exposures.retain_mut(|expo| {
602 let expo_key = expo.get_dedupe_key();
603 if seen.contains(&expo_key) {
604 return false;
605 }
606 seen.insert(expo_key);
607
608 match memo.get(&expo.gate) {
609 Some(hash) => {
610 expo.gate = hash.clone();
611 }
612 None => {
613 let hash = hashing.hash(&expo.gate, hash_algorithm).to_string();
614 let old = std::mem::replace(&mut expo.gate, hash.clone());
615 memo.insert(old, hash);
616 }
617 }
618 true
619 });
620 }
621
622 if !result.secondary_exposures.is_empty() {
623 loop_filter_n_hash(
624 &mut result.secondary_exposures,
625 hashing,
626 hash_algorithm,
627 memo,
628 );
629 }
630
631 if let Some(undelegated_secondary_exposures) = result.undelegated_secondary_exposures.as_mut() {
632 loop_filter_n_hash(
633 undelegated_secondary_exposures,
634 hashing,
635 hash_algorithm,
636 memo,
637 );
638 }
639}