statsig_rust/evaluation/
evaluator_result.rs

1use std::collections::HashMap;
2
3use ahash::AHashMap;
4
5use super::dynamic_returnable::DynamicReturnable;
6use super::evaluation_types::ExtraExposureInfo;
7use super::evaluation_types_v2::{
8    BaseEvaluationV2, DynamicConfigEvaluationV2, ExperimentEvaluationV2, GateEvaluationV2,
9    LayerEvaluationV2,
10};
11use crate::evaluation::evaluation_types::{
12    BaseEvaluation, DynamicConfigEvaluation, ExperimentEvaluation, GateEvaluation, LayerEvaluation,
13    SecondaryExposure,
14};
15use crate::evaluation::evaluation_types_initialize_v2::{
16    BaseEvaluationInitV2, DynamicConfigEvaluationInitV2, ExperimentEvaluationInitV2,
17    GateEvaluationInitV2, LayerEvaluationInitV2,
18};
19use crate::evaluation::secondary_exposure_key::SecondaryExposureKey;
20use crate::hashing::{HashAlgorithm, HashUtil};
21use crate::interned_string::InternedString;
22
23#[derive(Default, Debug)]
24pub struct EvaluatorResult<'a> {
25    pub name: Option<&'a InternedString>,
26    pub bool_value: bool,
27    pub unsupported: bool,
28    pub is_experiment_group: bool,
29    pub is_experiment_active: bool,
30    pub is_in_layer: bool,
31    pub is_in_experiment: bool,
32    pub id_type: Option<InternedString>,
33    pub json_value: Option<DynamicReturnable>,
34    pub rule_id: Option<&'a InternedString>,
35    pub rule_id_suffix: Option<&'static str>,
36    pub group_name: Option<InternedString>,
37    pub explicit_parameters: Option<&'a Vec<InternedString>>,
38    pub config_delegate: Option<InternedString>,
39    pub secondary_exposures: Vec<SecondaryExposure>,
40    pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
41    pub override_reason: Option<&'a str>,
42    pub version: Option<u32>,
43    pub sampling_rate: Option<u64>,
44    pub forward_all_exposures: Option<bool>,
45    pub override_config_name: Option<&'a str>,
46    pub has_seen_analytical_gates: Option<bool>,
47}
48
49pub fn result_to_gate_eval(gate_name: &str, result: &mut EvaluatorResult) -> GateEvaluation {
50    GateEvaluation {
51        base: result_to_base_eval(gate_name, result),
52        id_type: result.id_type.take(),
53        value: result.bool_value,
54    }
55}
56
57pub fn result_to_gate_eval_v2(
58    gate_name: &str,
59    result: &mut EvaluatorResult,
60    hashing: &HashUtil,
61) -> GateEvaluationV2 {
62    GateEvaluationV2 {
63        base: result_to_base_eval_v2(gate_name, result, hashing),
64        id_type: result.id_type.take(),
65        value: result.bool_value,
66    }
67}
68
69pub fn result_to_gate_eval_init_v2(
70    result: &mut EvaluatorResult,
71    expo_id_to_exposure_map: &mut HashMap<InternedString, SecondaryExposure>,
72    expo_key_to_expo_id_map: &mut AHashMap<SecondaryExposureKey, InternedString>,
73) -> GateEvaluationInitV2 {
74    let value: Option<bool> = match result.bool_value {
75        true => Some(true),
76        _ => None,
77    };
78    GateEvaluationInitV2 {
79        base: result_to_base_eval_init_v2(result, expo_id_to_exposure_map, expo_key_to_expo_id_map),
80        id_type: result.id_type.take(),
81        value,
82    }
83}
84
85pub fn result_to_experiment_eval(
86    experiment_name: &str,
87    spec_entity: Option<&str>,
88    result: &mut EvaluatorResult,
89) -> ExperimentEvaluation {
90    let (id_type, is_device_based) = get_id_type_info(result.id_type.as_ref());
91
92    let mut is_experiment_active = None;
93    let mut is_user_in_experiment = None;
94
95    if spec_entity.is_none_or(|s| s == "experiment") {
96        is_experiment_active = Some(result.is_experiment_active);
97        is_user_in_experiment = Some(result.is_experiment_group);
98    }
99
100    ExperimentEvaluation {
101        base: result_to_base_eval(experiment_name, result),
102        id_type: Some(id_type),
103        is_device_based,
104        value: get_json_value(result),
105        is_in_layer: result.is_in_layer,
106        group_name: result.group_name.take(),
107        explicit_parameters: result.explicit_parameters.cloned(),
108        is_experiment_active,
109        is_user_in_experiment,
110        undelegated_secondary_exposures: std::mem::take(
111            &mut result.undelegated_secondary_exposures,
112        ),
113    }
114}
115
116pub fn result_to_experiment_eval_v2(
117    experiment_name: &str,
118    spec_entity: Option<&str>,
119    result: &mut EvaluatorResult,
120    hashing: &HashUtil,
121) -> ExperimentEvaluationV2 {
122    let (id_type, is_device_based) = get_id_type_info(result.id_type.as_ref());
123
124    let mut is_experiment_active = None;
125    let mut is_user_in_experiment = None;
126
127    if let Some(spec_entity) = spec_entity {
128        if spec_entity == "experiment" {
129            is_experiment_active = Some(result.is_experiment_active);
130            is_user_in_experiment = Some(result.is_experiment_group);
131        }
132    }
133
134    ExperimentEvaluationV2 {
135        base: result_to_base_eval_v2(experiment_name, result, hashing),
136        id_type: Some(id_type),
137        is_device_based,
138        value: get_json_value(result),
139        is_in_layer: result.is_in_layer,
140        group_name: result.group_name.take(),
141        explicit_parameters: result.explicit_parameters.cloned(),
142        is_experiment_active,
143        is_user_in_experiment,
144        undelegated_secondary_exposures: result.undelegated_secondary_exposures.clone(),
145    }
146}
147
148pub fn result_to_experiment_eval_init_v2(
149    spec_entity: Option<&str>,
150    result: &mut EvaluatorResult,
151    expo_id_to_exposure_map: &mut HashMap<InternedString, SecondaryExposure>,
152    expo_key_to_expo_id_map: &mut AHashMap<SecondaryExposureKey, InternedString>,
153    value_id_to_value_map: &mut HashMap<InternedString, DynamicReturnable>,
154    value_key_to_value_id: &mut AHashMap<u64, InternedString>,
155) -> ExperimentEvaluationInitV2 {
156    let value: Option<DynamicReturnable> = result.json_value.take();
157
158    let id_type = result.id_type.take();
159
160    let mut is_experiment_active = None;
161    let mut is_user_in_experiment = None;
162
163    if let Some(spec_entity) = spec_entity {
164        if spec_entity == "experiment" {
165            is_experiment_active = Some(result.is_experiment_active);
166            is_user_in_experiment = Some(result.is_experiment_group);
167        }
168    }
169
170    let mapped_value = self::get_mapped_value(value, value_id_to_value_map, value_key_to_value_id);
171
172    ExperimentEvaluationInitV2 {
173        base: result_to_base_eval_init_v2(result, expo_id_to_exposure_map, expo_key_to_expo_id_map),
174        id_type,
175        value: mapped_value,
176        group_name: result.group_name.take(),
177        is_experiment_active,
178        is_user_in_experiment,
179    }
180}
181
182pub fn eval_result_to_experiment_eval(
183    experiment_name: &str,
184    result: &mut EvaluatorResult,
185) -> ExperimentEvaluation {
186    let (id_type, is_device_based) = get_id_type_info(result.id_type.as_ref());
187
188    ExperimentEvaluation {
189        base: result_to_base_eval(experiment_name, result),
190        id_type: Some(id_type),
191        is_device_based,
192        value: get_json_value(result),
193        is_in_layer: result.is_in_layer,
194        group_name: result.group_name.take(),
195        explicit_parameters: result.explicit_parameters.cloned(),
196        is_experiment_active: Some(result.is_experiment_active),
197        is_user_in_experiment: Some(result.is_experiment_group),
198        undelegated_secondary_exposures: std::mem::take(
199            &mut result.undelegated_secondary_exposures,
200        ),
201    }
202}
203
204pub fn result_to_layer_eval(layer_name: &str, result: &mut EvaluatorResult) -> LayerEvaluation {
205    let mut allocated_experiment_name = None;
206    let mut is_experiment_active = None;
207    let mut is_user_in_experiment = None;
208
209    if let Some(config_delegate) = result.config_delegate.take() {
210        if !config_delegate.is_empty() {
211            allocated_experiment_name = Some(config_delegate.clone());
212            is_experiment_active = Some(result.is_experiment_active);
213            is_user_in_experiment = Some(result.is_experiment_group);
214        }
215    }
216
217    let (id_type, is_device_based) = get_id_type_info(result.id_type.as_ref());
218    let undelegated_sec_expos = std::mem::take(&mut result.undelegated_secondary_exposures);
219
220    LayerEvaluation {
221        base: result_to_base_eval(layer_name, result),
222        value: get_json_value(result),
223        is_device_based,
224        group_name: result.group_name.take(),
225        is_experiment_active,
226        is_user_in_experiment,
227        allocated_experiment_name,
228        explicit_parameters: result.explicit_parameters.cloned().unwrap_or_default(),
229        undelegated_secondary_exposures: Some(undelegated_sec_expos.unwrap_or_default()),
230        id_type: Some(id_type),
231    }
232}
233
234pub fn result_to_layer_eval_v2(
235    layer_name: &str,
236    result: &mut EvaluatorResult,
237    hashing: &HashUtil,
238) -> LayerEvaluationV2 {
239    let mut undelegated_secondary_exposures = Vec::new();
240
241    if let Some(u) = &result.undelegated_secondary_exposures {
242        for exposure in u {
243            let key = format!(
244                "{}:{}:{}",
245                exposure.gate,
246                exposure.gate_value,
247                exposure.rule_id.as_str()
248            );
249            let hash = hashing.hash(&key, &HashAlgorithm::Djb2);
250            undelegated_secondary_exposures.push(InternedString::from_string(hash));
251        }
252    }
253
254    let mut allocated_experiment_name = None;
255    let mut is_experiment_active = None;
256    let mut is_user_in_experiment = None;
257
258    if let Some(config_delegate) = result.config_delegate.take() {
259        if !config_delegate.is_empty() {
260            allocated_experiment_name = Some(config_delegate.clone());
261            is_experiment_active = Some(result.is_experiment_active);
262            is_user_in_experiment = Some(result.is_experiment_group);
263        }
264    }
265
266    let (id_type, is_device_based) = get_id_type_info(result.id_type.as_ref());
267
268    LayerEvaluationV2 {
269        base: result_to_base_eval_v2(layer_name, result, hashing),
270        value: get_json_value(result),
271        is_device_based,
272        group_name: result.group_name.take(),
273        is_experiment_active,
274        is_user_in_experiment,
275        allocated_experiment_name,
276        explicit_parameters: result.explicit_parameters.cloned().unwrap_or_default(),
277        undelegated_secondary_exposures: Some(undelegated_secondary_exposures),
278        id_type: Some(id_type),
279    }
280}
281
282pub fn result_to_layer_eval_init_v2(
283    result: &mut EvaluatorResult,
284    expo_id_to_exposure_map: &mut HashMap<InternedString, SecondaryExposure>,
285    expo_key_to_expo_id_map: &mut AHashMap<SecondaryExposureKey, InternedString>,
286    value_id_to_value_map: &mut HashMap<InternedString, DynamicReturnable>,
287    value_key_to_value_id: &mut AHashMap<u64, InternedString>,
288) -> LayerEvaluationInitV2 {
289    let value: Option<DynamicReturnable> = result.json_value.take();
290
291    let mut allocated_experiment_name = None;
292    let mut is_experiment_active = None;
293    let mut is_user_in_experiment = None;
294
295    if let Some(config_delegate) = result.config_delegate.take() {
296        if !config_delegate.is_empty() {
297            allocated_experiment_name = Some(config_delegate.clone());
298            is_experiment_active = Some(result.is_experiment_active);
299            is_user_in_experiment = Some(result.is_experiment_group);
300        }
301    }
302
303    let id_type = result.id_type.take();
304
305    let mapped_exposures: Option<Vec<InternedString>> = match result
306        .undelegated_secondary_exposures
307        .as_mut()
308    {
309        Some(undelegated_secondary_exposures) if !&undelegated_secondary_exposures.is_empty() => {
310            Some(self::map_exposures(
311                undelegated_secondary_exposures,
312                expo_id_to_exposure_map,
313                expo_key_to_expo_id_map,
314            ))
315        }
316        _ => None,
317    };
318
319    let mapped_value = self::get_mapped_value(value, value_id_to_value_map, value_key_to_value_id);
320
321    LayerEvaluationInitV2 {
322        base: result_to_base_eval_init_v2(result, expo_id_to_exposure_map, expo_key_to_expo_id_map),
323        id_type,
324        value: mapped_value,
325        group_name: result.group_name.take(),
326        is_experiment_active,
327        is_user_in_experiment,
328        allocated_experiment_name,
329        explicit_parameters: result.explicit_parameters.cloned(),
330        undelegated_secondary_exposures: mapped_exposures,
331    }
332}
333
334pub fn result_to_dynamic_config_eval(
335    dynamic_config_name: &str,
336    result: &mut EvaluatorResult,
337) -> DynamicConfigEvaluation {
338    let (id_type, is_device_based) = get_id_type_info(result.id_type.as_ref());
339
340    DynamicConfigEvaluation {
341        base: result_to_base_eval(dynamic_config_name, result),
342        id_type: Some(id_type),
343        is_device_based,
344        value: get_json_value(result),
345        passed: result.bool_value,
346    }
347}
348
349pub fn result_to_dynamic_config_eval_v2(
350    dynamic_config_name: &str,
351    result: &mut EvaluatorResult,
352    hashing: &HashUtil,
353) -> DynamicConfigEvaluationV2 {
354    let (id_type, is_device_based) = get_id_type_info(result.id_type.as_ref());
355
356    DynamicConfigEvaluationV2 {
357        base: result_to_base_eval_v2(dynamic_config_name, result, hashing),
358        id_type: Some(id_type),
359        is_device_based,
360        value: get_json_value(result),
361        passed: result.bool_value,
362    }
363}
364
365pub fn result_to_dynamic_config_eval_init_v2(
366    result: &mut EvaluatorResult,
367    expo_id_to_exposure_map: &mut HashMap<InternedString, SecondaryExposure>,
368    expo_key_to_expo_id_map: &mut AHashMap<SecondaryExposureKey, InternedString>,
369    value_id_to_value_map: &mut HashMap<InternedString, DynamicReturnable>,
370    value_key_to_value_id: &mut AHashMap<u64, InternedString>,
371) -> DynamicConfigEvaluationInitV2 {
372    let value: Option<DynamicReturnable> = result.json_value.take();
373
374    let id_type = result.id_type.take();
375
376    let mapped_value = self::get_mapped_value(value, value_id_to_value_map, value_key_to_value_id);
377
378    DynamicConfigEvaluationInitV2 {
379        base: result_to_base_eval_init_v2(result, expo_id_to_exposure_map, expo_key_to_expo_id_map),
380        id_type,
381        value: mapped_value,
382        passed: result.bool_value,
383    }
384}
385
386fn get_mapped_value(
387    value: Option<DynamicReturnable>,
388    value_id_to_value_map: &mut HashMap<InternedString, DynamicReturnable>,
389    value_key_to_value_id: &mut AHashMap<u64, InternedString>,
390) -> InternedString {
391    let val = match &value {
392        Some(v) => v,
393        None => &DynamicReturnable::empty(),
394    };
395    let hash = val.get_hash();
396    match value_key_to_value_id.get(&hash) {
397        Some(mapped_key) => mapped_key.clone(),
398        None => {
399            let value_id = InternedString::from_string(value_id_to_value_map.len().to_string());
400            value_id_to_value_map.insert(value_id.clone(), val.clone());
401            value_key_to_value_id.insert(hash, value_id.clone());
402            value_id
403        }
404    }
405}
406
407fn get_id_type_info(id_type: Option<&InternedString>) -> (InternedString, bool) {
408    let id_type = id_type.cloned().unwrap_or_default();
409    let is_device_based = id_type == "stableID" || id_type == "stableid";
410    (id_type, is_device_based)
411}
412
413fn get_json_value(result: &mut EvaluatorResult) -> DynamicReturnable {
414    result
415        .json_value
416        .take()
417        .unwrap_or_else(DynamicReturnable::empty)
418}
419
420// todo: remove when 'QueuedExposure' does not use `BaseEvaluation`
421fn get_exposure_name_if_not_hashed(
422    possibly_hashed_name: &str,
423    exposure_name: Option<&InternedString>,
424) -> InternedString {
425    let exposure_name = exposure_name.unwrap_or(InternedString::empty_ref());
426    if possibly_hashed_name == exposure_name.as_str() {
427        exposure_name.clone()
428    } else {
429        InternedString::from_str_ref(possibly_hashed_name)
430    }
431}
432
433fn result_to_base_eval(spec_name: &str, result: &mut EvaluatorResult) -> BaseEvaluation {
434    let rule_id = create_suffixed_rule_id(result.rule_id, result.rule_id_suffix);
435
436    let exposure_info = ExtraExposureInfo {
437        sampling_rate: result.sampling_rate,
438        forward_all_exposures: result.forward_all_exposures,
439        has_seen_analytical_gates: result.has_seen_analytical_gates,
440        override_config_name: result.override_config_name.map(|s| s.to_string()),
441        version: result.version,
442    };
443
444    let name = get_exposure_name_if_not_hashed(spec_name, result.name);
445
446    BaseEvaluation {
447        name,
448        rule_id,
449        secondary_exposures: std::mem::take(&mut result.secondary_exposures),
450        exposure_info: Some(exposure_info),
451    }
452}
453
454fn result_to_base_eval_v2(
455    spec_name: &str,
456    result: &mut EvaluatorResult,
457    hashing: &HashUtil,
458) -> BaseEvaluationV2 {
459    let mut exposures = Vec::new();
460
461    for exposure in &result.secondary_exposures {
462        let key = format!(
463            "{}:{}:{}",
464            exposure.gate,
465            exposure.gate_value,
466            exposure.rule_id.as_str()
467        );
468        let hash = hashing.hash(&key, &HashAlgorithm::Djb2);
469        exposures.push(hash.clone());
470    }
471
472    let rule_id = create_suffixed_rule_id(result.rule_id, result.rule_id_suffix);
473
474    BaseEvaluationV2 {
475        name: spec_name.to_string(),
476        rule_id,
477        secondary_exposures: exposures,
478    }
479}
480
481fn result_to_base_eval_init_v2(
482    result: &mut EvaluatorResult,
483    expo_id_to_exposure_map: &mut HashMap<InternedString, SecondaryExposure>,
484    expo_key_to_expo_id_map: &mut AHashMap<SecondaryExposureKey, InternedString>,
485) -> BaseEvaluationInitV2 {
486    let rule_id = create_suffixed_rule_id(result.rule_id, result.rule_id_suffix);
487    let opt_rule_id = match rule_id.as_str() {
488        "default" => None,
489        _ => Some(rule_id),
490    };
491
492    if result.secondary_exposures.is_empty() {
493        return BaseEvaluationInitV2 {
494            rule_id: opt_rule_id,
495            secondary_exposures: None,
496        };
497    }
498
499    BaseEvaluationInitV2 {
500        rule_id: opt_rule_id,
501        secondary_exposures: Some(self::map_exposures(
502            &mut result.secondary_exposures,
503            expo_id_to_exposure_map,
504            expo_key_to_expo_id_map,
505        )),
506    }
507}
508
509fn map_exposures(
510    input_exposures: &mut Vec<SecondaryExposure>,
511    expo_id_to_exposure_map: &mut HashMap<InternedString, SecondaryExposure>,
512    expo_key_to_expo_id_map: &mut AHashMap<SecondaryExposureKey, InternedString>,
513) -> Vec<InternedString> {
514    let my_exposures = std::mem::take(input_exposures);
515    my_exposures
516        .into_iter()
517        .map(|exposure| {
518            let expo_key = SecondaryExposureKey::from(&exposure);
519
520            match expo_key_to_expo_id_map.get(&expo_key) {
521                Some(expo_id) => expo_id.clone(),
522                None => {
523                    let expo_id =
524                        InternedString::from_string(expo_id_to_exposure_map.len().to_string());
525
526                    expo_id_to_exposure_map.insert(expo_id.clone(), exposure);
527                    expo_key_to_expo_id_map.insert(expo_key, expo_id.clone());
528
529                    expo_id
530                }
531            }
532        })
533        .collect()
534}
535
536fn create_suffixed_rule_id(
537    rule_id: Option<&InternedString>,
538    suffix: Option<&str>,
539) -> InternedString {
540    let id_arc = match &rule_id {
541        Some(rule_id) => rule_id.as_str(),
542        None => "",
543    };
544
545    match &suffix {
546        Some(suffix) => InternedString::from_str_parts(&[id_arc, ":", suffix]),
547        None => rule_id.cloned().unwrap_or_default(),
548    }
549}