statsig_rust/
client_init_response_formatter.rs

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    // todo: what the heck is this mess?
467    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}