statsig_rust/
client_init_response_formatter.rs

1use serde::Deserialize;
2
3use crate::evaluation::dynamic_value::DynamicValue;
4use crate::evaluation::evaluation_types::{AnyConfigEvaluation, SecondaryExposure};
5use crate::evaluation::evaluator::{Evaluator, SpecType};
6use crate::evaluation::evaluator_context::EvaluatorContext;
7use crate::evaluation::evaluator_result::{
8    result_to_dynamic_config_eval, result_to_experiment_eval, result_to_gate_eval,
9    result_to_layer_eval, EvaluatorResult,
10};
11use crate::hashing::{HashAlgorithm, HashUtil};
12use crate::initialize_response::InitializeResponse;
13use crate::spec_store::SpecStore;
14use crate::spec_types::{
15    DynamicConfigParameter, ExperimentParameter, GateParameter, LayerParameter, Parameter,
16    ParameterStore, Spec,
17};
18use crate::statsig_metadata::StatsigMetadata;
19use crate::user::StatsigUserInternal;
20use crate::{read_lock_or_else, OverrideAdapter};
21use std::collections::{HashMap, HashSet};
22use std::sync::Arc;
23
24#[derive(Default, Deserialize)]
25pub struct ClientInitResponseOptions {
26    pub hash_algorithm: Option<HashAlgorithm>,
27    pub client_sdk_key: Option<String>,
28    pub include_local_overrides: Option<bool>,
29}
30
31pub struct ClientInitResponseFormatter {
32    spec_store: Arc<SpecStore>,
33    default_options: ClientInitResponseOptions,
34    override_adapter: Option<Arc<dyn OverrideAdapter>>,
35}
36
37impl ClientInitResponseFormatter {
38    pub fn new(
39        spec_store: &Arc<SpecStore>,
40        override_adapter: &Option<Arc<dyn OverrideAdapter>>,
41    ) -> Self {
42        Self {
43            spec_store: spec_store.clone(),
44            override_adapter: override_adapter.as_ref().map(Arc::clone),
45            default_options: ClientInitResponseOptions {
46                hash_algorithm: Some(HashAlgorithm::Djb2),
47                client_sdk_key: None,
48                include_local_overrides: Some(false),
49            },
50        }
51    }
52
53    pub fn get_default_options(&self) -> &ClientInitResponseOptions {
54        &self.default_options
55    }
56
57    pub fn get(
58        &self,
59        user_internal: StatsigUserInternal,
60        hashing: &HashUtil,
61        options: &ClientInitResponseOptions,
62    ) -> InitializeResponse {
63        let data = read_lock_or_else!(self.spec_store.data, {
64            return InitializeResponse::blank(user_internal);
65        });
66
67        let mut sec_expo_hash_memo = HashMap::new();
68        let mut app_id = data.values.app_id.as_ref();
69
70        if let Some(client_sdk_key) = &options.client_sdk_key {
71            if let Some(app_id_value) = &data.values.sdk_keys_to_app_ids {
72                app_id = app_id_value.get(client_sdk_key);
73            }
74            if let Some(app_id_value) = &data.values.hashed_sdk_keys_to_app_ids {
75                let hashed_key = &hashing.hash(client_sdk_key, &HashAlgorithm::Djb2);
76                app_id = app_id_value.get(hashed_key);
77            }
78        }
79        let include_local_overrides = options.include_local_overrides.unwrap_or(false);
80        let mut feature_gates = HashMap::new();
81        let mut context = EvaluatorContext::new(
82            &user_internal,
83            &data,
84            hashing,
85            &app_id,
86            if include_local_overrides {
87                &self.override_adapter
88            } else {
89                &None
90            },
91        );
92
93        let hash_used = options
94            .hash_algorithm
95            .as_ref()
96            .unwrap_or(&HashAlgorithm::Djb2);
97
98        for (name, spec) in &data.values.feature_gates {
99            if spec.entity == "segment" || spec.entity == "holdout" {
100                continue;
101            }
102
103            if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
104                continue;
105            }
106
107            context.reset_between_top_level_evaluations();
108            if let Err(_err) = Evaluator::evaluate(&mut context, name, &SpecType::Gate) {
109                return InitializeResponse::blank(user_internal);
110            }
111
112            let hashed_name = context.hashing.hash(name, hash_used);
113            hash_secondary_exposures(
114                &mut context.result,
115                hashing,
116                hash_used,
117                &mut sec_expo_hash_memo,
118            );
119
120            let eval = result_to_gate_eval(&hashed_name, &mut context.result);
121            feature_gates.insert(hashed_name, eval);
122        }
123
124        let mut dynamic_configs = HashMap::new();
125        for (name, spec) in &data.values.dynamic_configs {
126            if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
127                continue;
128            }
129
130            context.reset_between_top_level_evaluations();
131            let spec_type = if spec.entity == "dynamic_config" {
132                &SpecType::DynamicConfig
133            } else {
134                &SpecType::Experiment
135            };
136            if let Err(_err) = Evaluator::evaluate(&mut context, name, spec_type) {
137                return InitializeResponse::blank(user_internal);
138            }
139
140            let hashed_name = context.hashing.hash(name, hash_used);
141            hash_secondary_exposures(
142                &mut context.result,
143                hashing,
144                hash_used,
145                &mut sec_expo_hash_memo,
146            );
147
148            if spec.entity == "dynamic_config" {
149                let evaluation = result_to_dynamic_config_eval(&hashed_name, &mut context.result);
150                dynamic_configs.insert(hashed_name, AnyConfigEvaluation::DynamicConfig(evaluation));
151            } else {
152                let mut evaluation =
153                    result_to_experiment_eval(&hashed_name, Some(spec), &mut context.result);
154                evaluation.undelegated_secondary_exposures = None;
155                dynamic_configs.insert(hashed_name, AnyConfigEvaluation::Experiment(evaluation));
156            }
157        }
158
159        let mut layer_configs = HashMap::new();
160        for (name, spec) in &data.values.layer_configs {
161            if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
162                continue;
163            }
164
165            context.reset_between_top_level_evaluations();
166            if let Err(_err) = Evaluator::evaluate(&mut context, name, &SpecType::Layer) {
167                return InitializeResponse::blank(user_internal);
168            }
169
170            let hashed_name = context.hashing.hash(name, hash_used);
171            hash_secondary_exposures(
172                &mut context.result,
173                hashing,
174                hash_used,
175                &mut sec_expo_hash_memo,
176            );
177
178            let mut evaluation = result_to_layer_eval(&hashed_name, &mut context.result);
179
180            if let Some(allocated_experiment_name) = evaluation.allocated_experiment_name {
181                evaluation.allocated_experiment_name =
182                    Some(context.hashing.hash(&allocated_experiment_name, hash_used));
183            }
184
185            layer_configs.insert(hashed_name, evaluation);
186        }
187
188        let mut param_stores = HashMap::new();
189        let default_store = HashMap::new();
190        let stores = match &data.values.param_stores {
191            Some(stores) => stores,
192            None => &default_store,
193        };
194        for (name, store) in stores {
195            if should_filter_config_for_app(&store.target_app_ids, &app_id, &options.client_sdk_key)
196            {
197                continue;
198            }
199
200            let hashed_name = context.hashing.hash(name, hash_used);
201            let parameters = get_parameters_from_store(store, hash_used, &context);
202            param_stores.insert(hashed_name, parameters);
203        }
204
205        let evaluated_keys = get_evaluated_keys(&user_internal);
206        let metadata = StatsigMetadata::get_metadata();
207        InitializeResponse {
208            feature_gates,
209            dynamic_configs,
210            layer_configs,
211            time: data.values.time,
212            has_updates: true,
213            hash_used: hash_used.to_string(),
214            user: user_internal.to_loggable(),
215            sdk_params: HashMap::new(),
216            evaluated_keys,
217            sdk_info: HashMap::from([
218                ("sdkType".to_string(), metadata.sdk_type),
219                ("sdkVersion".to_string(), metadata.sdk_version),
220            ]),
221            param_stores,
222        }
223    }
224}
225
226fn get_parameters_from_store(
227    store: &ParameterStore,
228    hash_used: &HashAlgorithm,
229    context: &EvaluatorContext,
230) -> HashMap<String, Parameter> {
231    let mut parameters = HashMap::new();
232    for (param_name, param) in &store.parameters {
233        match param {
234            Parameter::StaticValue(_static_value) => {
235                parameters.insert(param_name.clone(), param.clone());
236            }
237            Parameter::Gate(gate) => {
238                let new_param = GateParameter {
239                    ref_type: gate.ref_type.clone(),
240                    param_type: gate.param_type.clone(),
241                    gate_name: context.hashing.hash(&gate.gate_name, hash_used),
242                    pass_value: gate.pass_value.clone(),
243                    fail_value: gate.fail_value.clone(),
244                };
245                parameters.insert(param_name.clone(), Parameter::Gate(new_param));
246            }
247            Parameter::DynamicConfig(dynamic_config) => {
248                let new_param = DynamicConfigParameter {
249                    ref_type: dynamic_config.ref_type.clone(),
250                    param_type: dynamic_config.param_type.clone(),
251                    config_name: context.hashing.hash(&dynamic_config.config_name, hash_used),
252                    param_name: dynamic_config.param_name.clone(),
253                };
254                parameters.insert(param_name.clone(), Parameter::DynamicConfig(new_param));
255            }
256            Parameter::Experiment(experiment) => {
257                let new_param = ExperimentParameter {
258                    ref_type: experiment.ref_type.clone(),
259                    param_type: experiment.param_type.clone(),
260                    experiment_name: context.hashing.hash(&experiment.experiment_name, hash_used),
261                    param_name: experiment.param_name.clone(),
262                };
263                parameters.insert(param_name.clone(), Parameter::Experiment(new_param));
264            }
265            Parameter::Layer(layer) => {
266                let new_param = LayerParameter {
267                    ref_type: layer.ref_type.clone(),
268                    param_type: layer.param_type.clone(),
269                    layer_name: context.hashing.hash(&layer.layer_name, hash_used),
270                    param_name: layer.param_name.clone(),
271                };
272                parameters.insert(param_name.clone(), Parameter::Layer(new_param));
273            }
274        }
275    }
276    parameters
277}
278
279fn should_filter_spec_for_app(
280    spec: &Spec,
281    app_id: &Option<&DynamicValue>,
282    client_sdk_key: &Option<String>,
283) -> bool {
284    should_filter_config_for_app(&spec.target_app_ids, app_id, client_sdk_key)
285}
286
287fn should_filter_config_for_app(
288    target_app_ids: &Option<Vec<String>>,
289    app_id: &Option<&DynamicValue>,
290    client_sdk_key: &Option<String>,
291) -> bool {
292    let _client_sdk_key = match client_sdk_key {
293        Some(client_sdk_key) => client_sdk_key,
294        None => return false,
295    };
296
297    let app_id = match app_id {
298        Some(app_id) => app_id,
299        None => return false,
300    };
301
302    let string_app_id = match app_id.string_value.as_ref() {
303        Some(string_app_id) => string_app_id,
304        None => return false,
305    };
306
307    let target_app_ids = match target_app_ids {
308        Some(target_app_ids) => target_app_ids,
309        None => return true,
310    };
311
312    if !target_app_ids.contains(string_app_id) {
313        return true;
314    }
315    false
316}
317
318fn get_evaluated_keys(user_internal: &StatsigUserInternal) -> HashMap<String, String> {
319    let mut evaluated_keys = HashMap::new();
320
321    if let Some(user_id) = user_internal.user_data.user_id.as_ref() {
322        evaluated_keys.insert(
323            "userID".to_string(),
324            user_id.string_value.clone().unwrap_or_default(),
325        );
326    }
327
328    if let Some(custom_ids) = user_internal.user_data.custom_ids.as_ref() {
329        for (key, value) in custom_ids {
330            evaluated_keys.insert(key.clone(), value.string_value.clone().unwrap_or_default());
331        }
332    }
333
334    evaluated_keys
335}
336
337fn hash_secondary_exposures(
338    result: &mut EvaluatorResult,
339    hashing: &HashUtil,
340    hash_algorithm: &HashAlgorithm,
341    memo: &mut HashMap<String, String>,
342) {
343    fn loop_filter_n_hash(
344        exposures: &mut Vec<SecondaryExposure>,
345        hashing: &HashUtil,
346        hash_algorithm: &HashAlgorithm,
347        memo: &mut HashMap<String, String>,
348    ) {
349        let mut seen = HashSet::<String>::with_capacity(exposures.len());
350        exposures.retain_mut(|expo| {
351            let expo_key = expo.get_dedupe_key();
352            if seen.contains(&expo_key) {
353                return false;
354            }
355            seen.insert(expo_key);
356
357            match memo.get(&expo.gate) {
358                Some(hash) => {
359                    expo.gate = hash.clone();
360                }
361                None => {
362                    let hash = hashing.hash(&expo.gate, hash_algorithm).to_string();
363                    let old = std::mem::replace(&mut expo.gate, hash.clone());
364                    memo.insert(old, hash);
365                }
366            }
367            true
368        });
369    }
370
371    if !result.secondary_exposures.is_empty() {
372        loop_filter_n_hash(
373            &mut result.secondary_exposures,
374            hashing,
375            hash_algorithm,
376            memo,
377        );
378    }
379
380    if let Some(undelegated_secondary_exposures) = result.undelegated_secondary_exposures.as_mut() {
381        loop_filter_n_hash(
382            undelegated_secondary_exposures,
383            hashing,
384            hash_algorithm,
385            memo,
386        );
387    }
388}