statsig_rust/evaluation/
evaluator_result.rs

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