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}