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