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