statsig_rust/evaluation/
evaluator_result.rs

1use super::dynamic_returnable::DynamicReturnable;
2use super::evaluation_types::ExtraExposureInfo;
3use super::evaluation_types_v2::{
4    BaseEvaluationV2, DynamicConfigEvaluationV2, ExperimentEvaluationV2, GateEvaluationV2,
5    LayerEvaluationV2,
6};
7use crate::evaluation::evaluation_types::{
8    BaseEvaluation, DynamicConfigEvaluation, ExperimentEvaluation, GateEvaluation, LayerEvaluation,
9    SecondaryExposure,
10};
11use crate::event_logging::exposable_string::{self, ExposableString};
12use crate::hashing::{HashAlgorithm, HashUtil};
13use crate::specs_response::spec_types::Spec;
14
15#[derive(Default, Debug)]
16pub struct EvaluatorResult<'a> {
17    pub name: Option<&'a ExposableString>,
18    pub bool_value: bool,
19    pub unsupported: bool,
20    pub is_experiment_group: bool,
21    pub is_experiment_active: bool,
22    pub is_in_layer: bool,
23    pub is_in_experiment: bool,
24    pub id_type: Option<&'a String>,
25    pub json_value: Option<DynamicReturnable>,
26    pub rule_id: Option<&'a ExposableString>,
27    pub rule_id_suffix: Option<&'static str>,
28    pub group_name: Option<&'a String>,
29    pub explicit_parameters: Option<&'a Vec<String>>,
30    pub config_delegate: Option<&'a String>,
31    pub secondary_exposures: Vec<SecondaryExposure>,
32    pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
33    pub override_reason: Option<&'a str>,
34    pub version: Option<u32>,
35    pub sampling_rate: Option<u64>,
36    pub forward_all_exposures: Option<bool>,
37    pub override_config_name: Option<&'a str>,
38    pub has_seen_analytical_gates: Option<bool>,
39}
40
41pub fn result_to_gate_eval(gate_name: &str, result: &mut EvaluatorResult) -> GateEvaluation {
42    GateEvaluation {
43        base: result_to_base_eval(gate_name, result),
44        id_type: result.id_type.cloned().unwrap_or_default(),
45        value: result.bool_value,
46    }
47}
48
49pub fn result_to_gate_eval_v2(
50    gate_name: &str,
51    result: &mut EvaluatorResult,
52    hashing: &HashUtil,
53) -> GateEvaluationV2 {
54    GateEvaluationV2 {
55        base: result_to_base_eval_v2(gate_name, result, hashing),
56        id_type: result.id_type.cloned().unwrap_or_default(),
57        value: result.bool_value,
58    }
59}
60
61pub fn result_to_experiment_eval(
62    experiment_name: &str,
63    spec: Option<&Spec>,
64    result: &mut EvaluatorResult,
65) -> ExperimentEvaluation {
66    let (id_type, is_device_based) = get_id_type_info(result.id_type);
67
68    let mut is_experiment_active = None;
69    let mut is_user_in_experiment = None;
70
71    if spec.as_ref().is_none_or(|s| s.entity == "experiment") {
72        is_experiment_active = Some(result.is_experiment_active);
73        is_user_in_experiment = Some(result.is_experiment_group);
74    }
75
76    ExperimentEvaluation {
77        base: result_to_base_eval(experiment_name, result),
78        id_type,
79        group: result.rule_id.cloned().unwrap_or_default(),
80        is_device_based,
81        value: get_json_value(result),
82        is_in_layer: result.is_in_layer,
83        group_name: result.group_name.cloned(),
84        explicit_parameters: result.explicit_parameters.cloned(),
85        is_experiment_active,
86        is_user_in_experiment,
87        undelegated_secondary_exposures: std::mem::take(
88            &mut result.undelegated_secondary_exposures,
89        ),
90    }
91}
92
93pub fn result_to_experiment_eval_v2(
94    experiment_name: &str,
95    spec: Option<&Spec>,
96    result: &mut EvaluatorResult,
97    hashing: &HashUtil,
98) -> ExperimentEvaluationV2 {
99    let (id_type, is_device_based) = get_id_type_info(result.id_type);
100
101    let mut is_experiment_active = None;
102    let mut is_user_in_experiment = None;
103
104    if let Some(spec) = spec {
105        if spec.entity == "experiment" {
106            is_experiment_active = Some(result.is_experiment_active);
107            is_user_in_experiment = Some(result.is_experiment_group);
108        }
109    }
110
111    ExperimentEvaluationV2 {
112        base: result_to_base_eval_v2(experiment_name, result, hashing),
113        id_type,
114        group: result.rule_id.cloned().unwrap_or_default(),
115        is_device_based,
116        value: get_json_value(result),
117        is_in_layer: result.is_in_layer,
118        group_name: result.group_name.cloned(),
119        explicit_parameters: result.explicit_parameters.cloned(),
120        is_experiment_active,
121        is_user_in_experiment,
122        undelegated_secondary_exposures: result.undelegated_secondary_exposures.clone(),
123    }
124}
125
126pub fn eval_result_to_experiment_eval(
127    experiment_name: &str,
128    result: &mut EvaluatorResult,
129) -> ExperimentEvaluation {
130    let (id_type, is_device_based) = get_id_type_info(result.id_type);
131
132    ExperimentEvaluation {
133        base: result_to_base_eval(experiment_name, result),
134        id_type,
135        group: result.rule_id.cloned().unwrap_or_default(),
136        is_device_based,
137        value: get_json_value(result),
138        is_in_layer: result.is_in_layer,
139        group_name: result.group_name.cloned(),
140        explicit_parameters: result.explicit_parameters.cloned(),
141        is_experiment_active: Some(result.is_experiment_active),
142        is_user_in_experiment: Some(result.is_experiment_group),
143        undelegated_secondary_exposures: std::mem::take(
144            &mut result.undelegated_secondary_exposures,
145        ),
146    }
147}
148
149pub fn result_to_layer_eval(layer_name: &str, result: &mut EvaluatorResult) -> LayerEvaluation {
150    let mut allocated_experiment_name = None;
151    let mut is_experiment_active = None;
152    let mut is_user_in_experiment = None;
153
154    if let Some(config_delegate) = result.config_delegate {
155        if !config_delegate.is_empty() {
156            allocated_experiment_name = Some(config_delegate.clone());
157            is_experiment_active = Some(result.is_experiment_active);
158            is_user_in_experiment = Some(result.is_experiment_group);
159        }
160    }
161
162    let (id_type, is_device_based) = get_id_type_info(result.id_type);
163    let undelegated_sec_expos = std::mem::take(&mut result.undelegated_secondary_exposures);
164
165    LayerEvaluation {
166        base: result_to_base_eval(layer_name, result),
167        group: result
168            .rule_id
169            .map(|r| r.unperformant_to_string())
170            .unwrap_or_default(),
171        value: get_json_value(result),
172        is_device_based,
173        group_name: result.group_name.cloned(),
174        is_experiment_active,
175        is_user_in_experiment,
176        allocated_experiment_name,
177        explicit_parameters: result.explicit_parameters.cloned().unwrap_or_default(),
178        undelegated_secondary_exposures: Some(undelegated_sec_expos.unwrap_or_default()),
179        id_type,
180    }
181}
182
183pub fn result_to_layer_eval_v2(
184    layer_name: &str,
185    result: &mut EvaluatorResult,
186    hashing: &HashUtil,
187) -> LayerEvaluationV2 {
188    let mut undelegated_secondary_exposures = Vec::new();
189
190    if let Some(u) = &result.undelegated_secondary_exposures {
191        for exposure in u {
192            let key = format!(
193                "{}:{}:{}",
194                exposure.gate,
195                exposure.gate_value,
196                exposure.rule_id.as_str()
197            );
198            let hash = hashing.hash(&key, &HashAlgorithm::Djb2);
199            undelegated_secondary_exposures.push(hash.clone());
200        }
201    }
202
203    let mut allocated_experiment_name = None;
204    let mut is_experiment_active = None;
205    let mut is_user_in_experiment = None;
206
207    if let Some(config_delegate) = result.config_delegate {
208        if !config_delegate.is_empty() {
209            allocated_experiment_name = Some(config_delegate.clone());
210            is_experiment_active = Some(result.is_experiment_active);
211            is_user_in_experiment = Some(result.is_experiment_group);
212        }
213    }
214
215    let (id_type, is_device_based) = get_id_type_info(result.id_type);
216
217    LayerEvaluationV2 {
218        base: result_to_base_eval_v2(layer_name, result, hashing),
219        group: result.rule_id.cloned().unwrap_or_default(),
220        value: get_json_value(result),
221        is_device_based,
222        group_name: result.group_name.cloned(),
223        is_experiment_active,
224        is_user_in_experiment,
225        allocated_experiment_name,
226        explicit_parameters: result.explicit_parameters.cloned().unwrap_or_default(),
227        undelegated_secondary_exposures: Some(undelegated_secondary_exposures),
228        id_type,
229    }
230}
231
232pub fn result_to_dynamic_config_eval(
233    dynamic_config_name: &str,
234    result: &mut EvaluatorResult,
235) -> DynamicConfigEvaluation {
236    let (id_type, is_device_based) = get_id_type_info(result.id_type);
237
238    DynamicConfigEvaluation {
239        base: result_to_base_eval(dynamic_config_name, result),
240        id_type,
241        is_device_based,
242        value: get_json_value(result),
243        group: result.rule_id.cloned().unwrap_or_default(),
244        passed: result.bool_value,
245    }
246}
247
248pub fn result_to_dynamic_config_eval_v2(
249    dynamic_config_name: &str,
250    result: &mut EvaluatorResult,
251    hashing: &HashUtil,
252) -> DynamicConfigEvaluationV2 {
253    let (id_type, is_device_based) = get_id_type_info(result.id_type);
254
255    DynamicConfigEvaluationV2 {
256        base: result_to_base_eval_v2(dynamic_config_name, result, hashing),
257        id_type,
258        is_device_based,
259        value: get_json_value(result),
260        group: result.rule_id.cloned().unwrap_or_default(),
261        passed: result.bool_value,
262    }
263}
264
265fn get_id_type_info(id_type: Option<&String>) -> (String, bool) {
266    let id_type = id_type.cloned().unwrap_or_default();
267    let is_device_based = id_type == "stableID" || id_type == "stableid";
268    (id_type, is_device_based)
269}
270
271fn get_json_value(result: &mut EvaluatorResult) -> DynamicReturnable {
272    result
273        .json_value
274        .take()
275        .unwrap_or_else(DynamicReturnable::empty)
276}
277
278// todo: remove when 'QueuedExposure' does not use `BaseEvaluation`
279fn get_exposure_name_if_not_hashed(
280    possibly_hashed_name: &str,
281    exposure_name: Option<&ExposableString>,
282) -> ExposableString {
283    let exposure_name = exposure_name.unwrap_or(&exposable_string::EMPTY_STRING);
284    if possibly_hashed_name == exposure_name.as_str() {
285        exposure_name.clone()
286    } else {
287        ExposableString::from_str_ref(possibly_hashed_name)
288    }
289}
290
291fn result_to_base_eval(spec_name: &str, result: &mut EvaluatorResult) -> BaseEvaluation {
292    let rule_id = create_suffixed_rule_id(result.rule_id, result.rule_id_suffix);
293
294    let exposure_info = ExtraExposureInfo {
295        sampling_rate: result.sampling_rate,
296        forward_all_exposures: result.forward_all_exposures,
297        has_seen_analytical_gates: result.has_seen_analytical_gates,
298        override_config_name: result.override_config_name.map(|s| s.to_string()),
299        version: result.version,
300    };
301
302    let name = get_exposure_name_if_not_hashed(spec_name, result.name);
303
304    BaseEvaluation {
305        name,
306        rule_id,
307        secondary_exposures: std::mem::take(&mut result.secondary_exposures),
308        exposure_info: Some(exposure_info),
309    }
310}
311
312fn result_to_base_eval_v2(
313    spec_name: &str,
314    result: &mut EvaluatorResult,
315    hashing: &HashUtil,
316) -> BaseEvaluationV2 {
317    let mut exposures = Vec::new();
318
319    for exposure in &result.secondary_exposures {
320        let key = format!(
321            "{}:{}:{}",
322            exposure.gate,
323            exposure.gate_value,
324            exposure.rule_id.as_str()
325        );
326        let hash = hashing.hash(&key, &HashAlgorithm::Djb2);
327        exposures.push(hash.clone());
328    }
329
330    let rule_id = create_suffixed_rule_id(result.rule_id, result.rule_id_suffix);
331
332    BaseEvaluationV2 {
333        name: spec_name.to_string(),
334        rule_id,
335        secondary_exposures: exposures,
336    }
337}
338
339fn create_suffixed_rule_id(
340    rule_id: Option<&ExposableString>,
341    suffix: Option<&str>,
342) -> ExposableString {
343    let id_arc = match &rule_id {
344        Some(rule_id) => rule_id.as_str(),
345        None => "",
346    };
347
348    match &suffix {
349        Some(suffix) => ExposableString::from_str_parts(&[id_arc, ":", suffix]),
350        None => rule_id.cloned().unwrap_or_default(),
351    }
352}