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::statsig_user_internal::{StatsigUserInternal, StatsigUserLoggable};
20use crate::{read_lock_or_else, OverrideAdapter};
21use std::collections::HashMap;
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 app_id = data.values.app_id.as_ref();
68
69        if let Some(client_sdk_key) = &options.client_sdk_key {
70            if let Some(app_id_value) = &data.values.sdk_keys_to_app_ids {
71                app_id = app_id_value.get(client_sdk_key);
72            }
73            if let Some(app_id_value) = &data.values.hashed_sdk_keys_to_app_ids {
74                let hashed_key = &hashing.hash(client_sdk_key, &HashAlgorithm::Djb2);
75                app_id = app_id_value.get(hashed_key);
76            }
77        }
78        let include_local_overrides = options.include_local_overrides.unwrap_or(false);
79        let mut feature_gates = HashMap::new();
80        let mut context = EvaluatorContext::new(
81            &user_internal,
82            &data,
83            hashing,
84            &app_id,
85            if include_local_overrides {
86                &self.override_adapter
87            } else {
88                &None
89            },
90        );
91
92        let hash_used = options
93            .hash_algorithm
94            .as_ref()
95            .unwrap_or(&HashAlgorithm::Djb2);
96
97        for (name, spec) in &data.values.feature_gates {
98            if spec.entity == "segment" || spec.entity == "holdout" {
99                continue;
100            }
101
102            if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
103                continue;
104            }
105
106            context.reset_result();
107            if let Err(_err) = Evaluator::evaluate(&mut context, name, &SpecType::Gate) {
108                return InitializeResponse::blank(user_internal);
109            }
110
111            let hashed_name = context.hashing.hash(name, hash_used);
112            hash_secondary_exposures(&mut context.result, hashing, hash_used);
113
114            let eval = result_to_gate_eval(&hashed_name, &mut context.result);
115            feature_gates.insert(hashed_name, eval);
116        }
117
118        let mut dynamic_configs = HashMap::new();
119        for (name, spec) in &data.values.dynamic_configs {
120            if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
121                continue;
122            }
123
124            context.reset_result();
125            let spec_type = if spec.entity == "dynamic_config" {
126                &SpecType::DynamicConfig
127            } else {
128                &SpecType::Experiment
129            };
130            if let Err(_err) = Evaluator::evaluate(&mut context, name, spec_type) {
131                return InitializeResponse::blank(user_internal);
132            }
133
134            let hashed_name = context.hashing.hash(name, hash_used);
135            hash_secondary_exposures(&mut context.result, hashing, hash_used);
136
137            if spec.entity == "dynamic_config" {
138                let evaluation = result_to_dynamic_config_eval(&hashed_name, &mut context.result);
139                dynamic_configs.insert(hashed_name, AnyConfigEvaluation::DynamicConfig(evaluation));
140            } else {
141                let evaluation =
142                    result_to_experiment_eval(&hashed_name, Some(spec), &mut context.result);
143                dynamic_configs.insert(hashed_name, AnyConfigEvaluation::Experiment(evaluation));
144            }
145        }
146
147        let mut layer_configs = HashMap::new();
148        for (name, spec) in &data.values.layer_configs {
149            if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
150                continue;
151            }
152
153            context.reset_result();
154            if let Err(_err) = Evaluator::evaluate(&mut context, name, &SpecType::Layer) {
155                return InitializeResponse::blank(user_internal);
156            }
157
158            let hashed_name = context.hashing.hash(name, hash_used);
159            hash_secondary_exposures(&mut context.result, hashing, hash_used);
160
161            let mut evaluation = result_to_layer_eval(&hashed_name, &mut context.result);
162
163            if let Some(allocated_experiment_name) = evaluation.allocated_experiment_name {
164                evaluation.allocated_experiment_name =
165                    Some(context.hashing.hash(&allocated_experiment_name, hash_used));
166            }
167
168            layer_configs.insert(hashed_name, evaluation);
169        }
170
171        let mut param_stores = HashMap::new();
172        let default_store = HashMap::new();
173        let stores = match &data.values.param_stores {
174            Some(stores) => stores,
175            None => &default_store,
176        };
177        for (name, store) in stores {
178            if should_filter_config_for_app(&store.target_app_ids, &app_id, &options.client_sdk_key)
179            {
180                continue;
181            }
182
183            let hashed_name = context.hashing.hash(name, hash_used);
184            let parameters = get_parameters_from_store(store, hash_used, &context);
185            param_stores.insert(hashed_name, parameters);
186        }
187
188        let evaluated_keys = get_evaluated_keys(&user_internal);
189        let metadata = StatsigMetadata::get_metadata();
190        InitializeResponse {
191            feature_gates,
192            dynamic_configs,
193            layer_configs,
194            time: data.values.time,
195            has_updates: true,
196            hash_used: hash_used.to_string(),
197            user: StatsigUserLoggable::new(user_internal),
198            sdk_params: HashMap::new(),
199            evaluated_keys,
200            sdk_info: HashMap::from([
201                ("sdkType".to_string(), metadata.sdk_type),
202                ("sdkVersion".to_string(), metadata.sdk_version),
203            ]),
204            param_stores,
205        }
206    }
207}
208
209fn get_parameters_from_store(
210    store: &ParameterStore,
211    hash_used: &HashAlgorithm,
212    context: &EvaluatorContext,
213) -> HashMap<String, Parameter> {
214    let mut parameters = HashMap::new();
215    for (param_name, param) in &store.parameters {
216        match param {
217            Parameter::StaticValue(_static_value) => {
218                parameters.insert(param_name.clone(), param.clone());
219            }
220            Parameter::Gate(gate) => {
221                let new_param = GateParameter {
222                    ref_type: gate.ref_type.clone(),
223                    param_type: gate.param_type.clone(),
224                    gate_name: context.hashing.hash(&gate.gate_name, hash_used),
225                    pass_value: gate.pass_value.clone(),
226                    fail_value: gate.fail_value.clone(),
227                };
228                parameters.insert(param_name.clone(), Parameter::Gate(new_param));
229            }
230            Parameter::DynamicConfig(dynamic_config) => {
231                let new_param = DynamicConfigParameter {
232                    ref_type: dynamic_config.ref_type.clone(),
233                    param_type: dynamic_config.param_type.clone(),
234                    config_name: context.hashing.hash(&dynamic_config.config_name, hash_used),
235                    param_name: dynamic_config.param_name.clone(),
236                };
237                parameters.insert(param_name.clone(), Parameter::DynamicConfig(new_param));
238            }
239            Parameter::Experiment(experiment) => {
240                let new_param = ExperimentParameter {
241                    ref_type: experiment.ref_type.clone(),
242                    param_type: experiment.param_type.clone(),
243                    experiment_name: context.hashing.hash(&experiment.experiment_name, hash_used),
244                    param_name: experiment.param_name.clone(),
245                };
246                parameters.insert(param_name.clone(), Parameter::Experiment(new_param));
247            }
248            Parameter::Layer(layer) => {
249                let new_param = LayerParameter {
250                    ref_type: layer.ref_type.clone(),
251                    param_type: layer.param_type.clone(),
252                    layer_name: context.hashing.hash(&layer.layer_name, hash_used),
253                    param_name: layer.param_name.clone(),
254                };
255                parameters.insert(param_name.clone(), Parameter::Layer(new_param));
256            }
257        }
258    }
259    parameters
260}
261
262fn should_filter_spec_for_app(
263    spec: &Spec,
264    app_id: &Option<&DynamicValue>,
265    client_sdk_key: &Option<String>,
266) -> bool {
267    should_filter_config_for_app(&spec.target_app_ids, app_id, client_sdk_key)
268}
269
270fn should_filter_config_for_app(
271    target_app_ids: &Option<Vec<String>>,
272    app_id: &Option<&DynamicValue>,
273    client_sdk_key: &Option<String>,
274) -> bool {
275    let _client_sdk_key = match client_sdk_key {
276        Some(client_sdk_key) => client_sdk_key,
277        None => return false,
278    };
279
280    let app_id = match app_id {
281        Some(app_id) => app_id,
282        None => return false,
283    };
284
285    let string_app_id = match app_id.string_value.as_ref() {
286        Some(string_app_id) => string_app_id,
287        None => return false,
288    };
289
290    let target_app_ids = match target_app_ids {
291        Some(target_app_ids) => target_app_ids,
292        None => return true,
293    };
294
295    if !target_app_ids.contains(string_app_id) {
296        return true;
297    }
298    false
299}
300
301fn get_evaluated_keys(user_internal: &StatsigUserInternal) -> HashMap<String, String> {
302    let mut evaluated_keys = HashMap::new();
303
304    if let Some(user_id) = user_internal.user_data.user_id.as_ref() {
305        evaluated_keys.insert(
306            "userID".to_string(),
307            user_id.string_value.clone().unwrap_or_default(),
308        );
309    }
310
311    if let Some(custom_ids) = user_internal.user_data.custom_ids.as_ref() {
312        for (key, value) in custom_ids {
313            evaluated_keys.insert(key.clone(), value.string_value.clone().unwrap_or_default());
314        }
315    }
316
317    evaluated_keys
318}
319
320fn hash_secondary_exposures(
321    result: &mut EvaluatorResult,
322    hashing: &HashUtil,
323    hash_algorithm: &HashAlgorithm,
324) {
325    fn loop_and_hash(
326        exposures: &[SecondaryExposure],
327        hashing: &HashUtil,
328        hash_algorithm: &HashAlgorithm,
329    ) -> Vec<SecondaryExposure> {
330        exposures
331            .iter()
332            .map(|exposure| {
333                let hashed_gate = hashing.hash(&exposure.gate, hash_algorithm);
334                SecondaryExposure {
335                    gate: hashed_gate,
336                    ..exposure.clone()
337                }
338            })
339            .collect()
340    }
341
342    if !result.secondary_exposures.is_empty() {
343        result.secondary_exposures =
344            loop_and_hash(&result.secondary_exposures, hashing, hash_algorithm);
345    }
346
347    if let Some(undelegated_secondary_exposures) = result.undelegated_secondary_exposures.as_ref() {
348        result.undelegated_secondary_exposures = Some(loop_and_hash(
349            undelegated_secondary_exposures,
350            hashing,
351            hash_algorithm,
352        ));
353    }
354}