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;
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 mut evaluation =
142 result_to_experiment_eval(&hashed_name, Some(spec), &mut context.result);
143 evaluation.undelegated_secondary_exposures = None;
144 dynamic_configs.insert(hashed_name, AnyConfigEvaluation::Experiment(evaluation));
145 }
146 }
147
148 let mut layer_configs = HashMap::new();
149 for (name, spec) in &data.values.layer_configs {
150 if should_filter_spec_for_app(spec, &app_id, &options.client_sdk_key) {
151 continue;
152 }
153
154 context.reset_result();
155 if let Err(_err) = Evaluator::evaluate(&mut context, name, &SpecType::Layer) {
156 return InitializeResponse::blank(user_internal);
157 }
158
159 let hashed_name = context.hashing.hash(name, hash_used);
160 hash_secondary_exposures(&mut context.result, hashing, hash_used);
161
162 let mut evaluation = result_to_layer_eval(&hashed_name, &mut context.result);
163
164 if let Some(allocated_experiment_name) = evaluation.allocated_experiment_name {
165 evaluation.allocated_experiment_name =
166 Some(context.hashing.hash(&allocated_experiment_name, hash_used));
167 }
168
169 layer_configs.insert(hashed_name, evaluation);
170 }
171
172 let mut param_stores = HashMap::new();
173 let default_store = HashMap::new();
174 let stores = match &data.values.param_stores {
175 Some(stores) => stores,
176 None => &default_store,
177 };
178 for (name, store) in stores {
179 if should_filter_config_for_app(&store.target_app_ids, &app_id, &options.client_sdk_key)
180 {
181 continue;
182 }
183
184 let hashed_name = context.hashing.hash(name, hash_used);
185 let parameters = get_parameters_from_store(store, hash_used, &context);
186 param_stores.insert(hashed_name, parameters);
187 }
188
189 let evaluated_keys = get_evaluated_keys(&user_internal);
190 let metadata = StatsigMetadata::get_metadata();
191 InitializeResponse {
192 feature_gates,
193 dynamic_configs,
194 layer_configs,
195 time: data.values.time,
196 has_updates: true,
197 hash_used: hash_used.to_string(),
198 user: user_internal.to_loggable(),
199 sdk_params: HashMap::new(),
200 evaluated_keys,
201 sdk_info: HashMap::from([
202 ("sdkType".to_string(), metadata.sdk_type),
203 ("sdkVersion".to_string(), metadata.sdk_version),
204 ]),
205 param_stores,
206 }
207 }
208}
209
210fn get_parameters_from_store(
211 store: &ParameterStore,
212 hash_used: &HashAlgorithm,
213 context: &EvaluatorContext,
214) -> HashMap<String, Parameter> {
215 let mut parameters = HashMap::new();
216 for (param_name, param) in &store.parameters {
217 match param {
218 Parameter::StaticValue(_static_value) => {
219 parameters.insert(param_name.clone(), param.clone());
220 }
221 Parameter::Gate(gate) => {
222 let new_param = GateParameter {
223 ref_type: gate.ref_type.clone(),
224 param_type: gate.param_type.clone(),
225 gate_name: context.hashing.hash(&gate.gate_name, hash_used),
226 pass_value: gate.pass_value.clone(),
227 fail_value: gate.fail_value.clone(),
228 };
229 parameters.insert(param_name.clone(), Parameter::Gate(new_param));
230 }
231 Parameter::DynamicConfig(dynamic_config) => {
232 let new_param = DynamicConfigParameter {
233 ref_type: dynamic_config.ref_type.clone(),
234 param_type: dynamic_config.param_type.clone(),
235 config_name: context.hashing.hash(&dynamic_config.config_name, hash_used),
236 param_name: dynamic_config.param_name.clone(),
237 };
238 parameters.insert(param_name.clone(), Parameter::DynamicConfig(new_param));
239 }
240 Parameter::Experiment(experiment) => {
241 let new_param = ExperimentParameter {
242 ref_type: experiment.ref_type.clone(),
243 param_type: experiment.param_type.clone(),
244 experiment_name: context.hashing.hash(&experiment.experiment_name, hash_used),
245 param_name: experiment.param_name.clone(),
246 };
247 parameters.insert(param_name.clone(), Parameter::Experiment(new_param));
248 }
249 Parameter::Layer(layer) => {
250 let new_param = LayerParameter {
251 ref_type: layer.ref_type.clone(),
252 param_type: layer.param_type.clone(),
253 layer_name: context.hashing.hash(&layer.layer_name, hash_used),
254 param_name: layer.param_name.clone(),
255 };
256 parameters.insert(param_name.clone(), Parameter::Layer(new_param));
257 }
258 }
259 }
260 parameters
261}
262
263fn should_filter_spec_for_app(
264 spec: &Spec,
265 app_id: &Option<&DynamicValue>,
266 client_sdk_key: &Option<String>,
267) -> bool {
268 should_filter_config_for_app(&spec.target_app_ids, app_id, client_sdk_key)
269}
270
271fn should_filter_config_for_app(
272 target_app_ids: &Option<Vec<String>>,
273 app_id: &Option<&DynamicValue>,
274 client_sdk_key: &Option<String>,
275) -> bool {
276 let _client_sdk_key = match client_sdk_key {
277 Some(client_sdk_key) => client_sdk_key,
278 None => return false,
279 };
280
281 let app_id = match app_id {
282 Some(app_id) => app_id,
283 None => return false,
284 };
285
286 let string_app_id = match app_id.string_value.as_ref() {
287 Some(string_app_id) => string_app_id,
288 None => return false,
289 };
290
291 let target_app_ids = match target_app_ids {
292 Some(target_app_ids) => target_app_ids,
293 None => return true,
294 };
295
296 if !target_app_ids.contains(string_app_id) {
297 return true;
298 }
299 false
300}
301
302fn get_evaluated_keys(user_internal: &StatsigUserInternal) -> HashMap<String, String> {
303 let mut evaluated_keys = HashMap::new();
304
305 if let Some(user_id) = user_internal.user_data.user_id.as_ref() {
306 evaluated_keys.insert(
307 "userID".to_string(),
308 user_id.string_value.clone().unwrap_or_default(),
309 );
310 }
311
312 if let Some(custom_ids) = user_internal.user_data.custom_ids.as_ref() {
313 for (key, value) in custom_ids {
314 evaluated_keys.insert(key.clone(), value.string_value.clone().unwrap_or_default());
315 }
316 }
317
318 evaluated_keys
319}
320
321fn hash_secondary_exposures(
322 result: &mut EvaluatorResult,
323 hashing: &HashUtil,
324 hash_algorithm: &HashAlgorithm,
325) {
326 fn loop_and_hash(
327 exposures: &[SecondaryExposure],
328 hashing: &HashUtil,
329 hash_algorithm: &HashAlgorithm,
330 ) -> Vec<SecondaryExposure> {
331 exposures
332 .iter()
333 .map(|exposure| {
334 let hashed_gate = hashing.hash(&exposure.gate, hash_algorithm);
335 SecondaryExposure {
336 gate: hashed_gate,
337 ..exposure.clone()
338 }
339 })
340 .collect()
341 }
342
343 if !result.secondary_exposures.is_empty() {
344 result.secondary_exposures =
345 loop_and_hash(&result.secondary_exposures, hashing, hash_algorithm);
346 }
347
348 if let Some(undelegated_secondary_exposures) = result.undelegated_secondary_exposures.as_ref() {
349 result.undelegated_secondary_exposures = Some(loop_and_hash(
350 undelegated_secondary_exposures,
351 hashing,
352 hash_algorithm,
353 ));
354 }
355}