statsig_rust/evaluation/
evaluator.rs

1use chrono::Utc;
2use lazy_static::lazy_static;
3
4use crate::evaluation::cmab_evaluator::evaluate_cmab;
5use crate::evaluation::comparisons::{
6    compare_arrays, compare_numbers, compare_str_with_regex, compare_strings_in_array,
7    compare_time, compare_versions,
8};
9use crate::evaluation::dynamic_value::DynamicValue;
10use crate::evaluation::evaluation_details::EvaluationDetails;
11use crate::evaluation::evaluation_types::SecondaryExposure;
12use crate::evaluation::evaluator_context::EvaluatorContext;
13use crate::evaluation::get_unit_id::get_unit_id;
14use crate::evaluation::ua_parser::UserAgentParser;
15use crate::spec_types::{Condition, Rule, Spec};
16use crate::{dyn_value, log_e, unwrap_or_return, StatsigErr};
17
18use super::country_lookup::CountryLookup;
19
20const TAG: &str = stringify!(Evaluator);
21
22pub struct Evaluator;
23
24lazy_static! {
25    static ref EMPTY_STR: String = String::new();
26    static ref DEFAULT_RULE: String = "default".to_string();
27    static ref DISABLED_RULE: String = "disabled".to_string();
28    static ref EMPTY_DYNAMIC_VALUE: DynamicValue = DynamicValue::new();
29}
30
31#[derive(Clone)]
32pub enum SpecType {
33    Gate,
34    DynamicConfig,
35    Experiment,
36    Layer,
37}
38
39#[derive(PartialEq, Eq)]
40pub enum Recognition {
41    Unrecognized,
42    Recognized,
43}
44
45impl Evaluator {
46    pub fn evaluate_with_details(
47        ctx: &mut EvaluatorContext,
48        spec_name: &str,
49        spec_type: &SpecType,
50    ) -> Result<EvaluationDetails, StatsigErr> {
51        let recognition = Self::evaluate(ctx, spec_name, spec_type)?;
52
53        if recognition == Recognition::Unrecognized {
54            return Ok(EvaluationDetails::unrecognized(ctx.spec_store_data));
55        }
56
57        if let Some(reason) = ctx.result.override_reason {
58            return Ok(EvaluationDetails::recognized_but_overridden(
59                ctx.spec_store_data,
60                reason,
61            ));
62        }
63
64        Ok(EvaluationDetails::recognized(
65            ctx.spec_store_data,
66            &ctx.result,
67        ))
68    }
69
70    pub fn evaluate(
71        ctx: &mut EvaluatorContext,
72        spec_name: &str,
73        spec_type: &SpecType,
74    ) -> Result<Recognition, StatsigErr> {
75        let opt_spec = match spec_type {
76            SpecType::Gate => ctx.spec_store_data.values.feature_gates.get(spec_name),
77            SpecType::DynamicConfig => ctx.spec_store_data.values.dynamic_configs.get(spec_name),
78            SpecType::Experiment => ctx.spec_store_data.values.dynamic_configs.get(spec_name),
79            SpecType::Layer => ctx.spec_store_data.values.layer_configs.get(spec_name),
80        };
81
82        if try_apply_override(ctx, spec_name, spec_type, opt_spec) {
83            return Ok(Recognition::Recognized);
84        }
85
86        if try_apply_config_mapping(ctx, spec_name, spec_type, opt_spec) {
87            return Ok(Recognition::Recognized);
88        }
89
90        if evaluate_cmab(ctx, spec_name, spec_type) {
91            return Ok(Recognition::Recognized);
92        }
93
94        let spec = unwrap_or_return!(opt_spec, Ok(Recognition::Unrecognized));
95
96        if ctx.result.id_type.is_none() {
97            ctx.result.id_type = Some(&spec.id_type);
98        }
99
100        if ctx.result.version.is_none() {
101            if let Some(version) = spec.version {
102                ctx.result.version = Some(version);
103            }
104        }
105
106        if let Some(is_active) = spec.is_active {
107            ctx.result.is_experiment_active = is_active;
108        }
109
110        if let Some(has_shared_params) = spec.has_shared_params {
111            ctx.result.is_in_layer = has_shared_params;
112        }
113
114        if let Some(explicit_params) = &spec.explicit_parameters {
115            ctx.result.explicit_parameters = Some(explicit_params);
116        }
117
118        for rule in &spec.rules {
119            evaluate_rule(ctx, rule)?;
120
121            if ctx.result.unsupported {
122                return Ok(Recognition::Recognized);
123            }
124
125            if !ctx.result.bool_value {
126                continue;
127            }
128
129            if evaluate_config_delegate(ctx, rule)? {
130                ctx.finalize_evaluation(spec, Some(rule));
131                return Ok(Recognition::Recognized);
132            }
133
134            let did_pass = evaluate_pass_percentage(ctx, rule, &spec.salt);
135
136            if did_pass {
137                ctx.result.bool_value = rule.return_value.string_value != "false";
138                ctx.result.json_value = rule.return_value.json_value.clone();
139            } else {
140                ctx.result.bool_value = spec.default_value.string_value == "true";
141                ctx.result.json_value = spec.default_value.json_value.clone();
142            }
143
144            ctx.result.rule_id = Some(&rule.id);
145            ctx.result.group_name = rule.group_name.as_ref();
146            ctx.result.is_experiment_group = rule.is_experiment_group.unwrap_or(false);
147            ctx.result.is_experiment_active = spec.is_active.unwrap_or(false);
148            ctx.finalize_evaluation(spec, Some(rule));
149            return Ok(Recognition::Recognized);
150        }
151
152        ctx.result.bool_value = spec.default_value.string_value == "true";
153        ctx.result.json_value = spec.default_value.json_value.clone();
154        ctx.result.rule_id = match spec.enabled {
155            true => Some(&DEFAULT_RULE),
156            false => Some(&DISABLED_RULE),
157        };
158        ctx.finalize_evaluation(spec, None);
159
160        Ok(Recognition::Recognized)
161    }
162}
163
164fn try_apply_config_mapping(
165    ctx: &mut EvaluatorContext,
166    spec_name: &str,
167    spec_type: &SpecType,
168    opt_spec: Option<&Spec>,
169) -> bool {
170    let overrides = match &ctx.spec_store_data.values.overrides {
171        Some(overrides) => overrides,
172        None => return false,
173    };
174
175    let override_rules = match &ctx.spec_store_data.values.override_rules {
176        Some(override_rules) => override_rules,
177        None => return false,
178    };
179
180    let mapping_list = match overrides.get(spec_name) {
181        Some(mapping_list) => mapping_list,
182        None => return false,
183    };
184
185    let spec_salt = match opt_spec {
186        Some(spec) => &spec.salt,
187        None => &EMPTY_STR,
188    };
189
190    for mapping in mapping_list {
191        for override_rule in &mapping.rules {
192            let start_time = override_rule.start_time.unwrap_or_default();
193
194            if start_time > Utc::now().timestamp_millis() {
195                continue;
196            }
197
198            let rule = match override_rules.get(&override_rule.rule_name) {
199                Some(rule) => rule,
200                None => continue,
201            };
202            match evaluate_rule(ctx, rule) {
203                Ok(_) => {}
204                Err(_) => {
205                    ctx.reset_result();
206                    continue;
207                }
208            }
209
210            if !ctx.result.bool_value || ctx.result.unsupported {
211                ctx.reset_result();
212                continue;
213            }
214            ctx.reset_result();
215            let pass = evaluate_pass_percentage(ctx, rule, spec_salt);
216            if pass {
217                ctx.result.override_config_name = Some(mapping.new_config_name.clone());
218                match Evaluator::evaluate(ctx, mapping.new_config_name.as_str(), spec_type) {
219                    Ok(Recognition::Recognized) => {
220                        return true;
221                    }
222                    _ => {
223                        ctx.reset_result();
224                        break;
225                    }
226                }
227            }
228        }
229    }
230
231    false
232}
233
234fn try_apply_override(
235    ctx: &mut EvaluatorContext,
236    spec_name: &str,
237    spec_type: &SpecType,
238    opt_spec: Option<&Spec>,
239) -> bool {
240    let adapter = match ctx.override_adapter {
241        Some(adapter) => adapter,
242        None => return false,
243    };
244
245    match spec_type {
246        SpecType::Gate => adapter.get_gate_override(ctx.user.user_data, spec_name, &mut ctx.result),
247
248        SpecType::DynamicConfig => {
249            adapter.get_dynamic_config_override(ctx.user.user_data, spec_name, &mut ctx.result)
250        }
251
252        SpecType::Experiment => adapter.get_experiment_override(
253            ctx.user.user_data,
254            spec_name,
255            &mut ctx.result,
256            opt_spec,
257        ),
258
259        SpecType::Layer => {
260            adapter.get_layer_override(ctx.user.user_data, spec_name, &mut ctx.result)
261        }
262    }
263}
264
265fn evaluate_rule<'a>(ctx: &mut EvaluatorContext<'a>, rule: &'a Rule) -> Result<(), StatsigErr> {
266    let mut all_conditions_pass = true;
267    // println!("--- Eval Rule {} ---", rule.id);
268    for condition_hash in &rule.conditions {
269        // println!("Condition Hash {}", condition_hash);
270        let opt_condition = ctx.spec_store_data.values.condition_map.get(condition_hash);
271        let condition = if let Some(c) = opt_condition {
272            c
273        } else {
274            // todo: log condition not found error
275            ctx.result.unsupported = true;
276            return Ok(());
277        };
278
279        evaluate_condition(ctx, condition)?;
280
281        if !ctx.result.bool_value {
282            all_conditions_pass = false;
283        }
284    }
285
286    ctx.result.bool_value = all_conditions_pass;
287
288    Ok(())
289}
290
291fn evaluate_condition<'a>(
292    ctx: &mut EvaluatorContext<'a>,
293    condition: &'a Condition,
294) -> Result<(), StatsigErr> {
295    let temp_value;
296    let target_value = condition
297        .target_value
298        .as_ref()
299        .unwrap_or(&EMPTY_DYNAMIC_VALUE);
300    let condition_type = &condition.condition_type;
301
302    let value = match condition_type as &str {
303        "public" => {
304            ctx.result.bool_value = true;
305            return Ok(());
306        }
307        "fail_gate" | "pass_gate" => {
308            evaluate_nested_gate(ctx, target_value, condition_type)?;
309            return Ok(());
310        }
311        "ua_based" => match ctx.user.get_user_value(&condition.field) {
312            Some(value) => Some(value),
313            None => {
314                temp_value = UserAgentParser::get_value_from_user_agent(ctx.user, &condition.field);
315                temp_value.as_ref()
316            }
317        },
318        "ip_based" => match ctx.user.get_user_value(&condition.field) {
319            Some(value) => Some(value),
320            None => {
321                temp_value = CountryLookup::get_value_from_ip(ctx.user, &condition.field);
322                temp_value.as_ref()
323            }
324        },
325        "user_field" => ctx.user.get_user_value(&condition.field),
326        "environment_field" => {
327            temp_value = ctx.user.get_value_from_environment(&condition.field);
328            temp_value.as_ref()
329        }
330        "current_time" => {
331            temp_value = Some(DynamicValue::for_timestamp_evaluation(
332                Utc::now().timestamp_millis(),
333            ));
334            temp_value.as_ref()
335        }
336        "user_bucket" => {
337            temp_value = Some(get_hash_for_user_bucket(ctx, condition));
338            temp_value.as_ref()
339        }
340        "target_app" => *ctx.app_id,
341        "unit_id" => ctx.user.get_unit_id(&condition.id_type),
342        _ => {
343            ctx.result.unsupported = true;
344            return Ok(());
345        }
346    }
347    .unwrap_or(&EMPTY_DYNAMIC_VALUE);
348
349    // println!("Eval Condition {}, {:?}", condition_type, value);
350
351    let operator = match &condition.operator {
352        Some(operator) => operator,
353        None => {
354            ctx.result.unsupported = true;
355            return Ok(());
356        }
357    };
358
359    ctx.result.bool_value = match operator as &str {
360        // numerical comparisons
361        "gt" | "gte" | "lt" | "lte" => compare_numbers(value, target_value, operator),
362
363        // version comparisons
364        "version_gt" | "version_gte" | "version_lt" | "version_lte" | "version_eq"
365        | "version_neq" => compare_versions(value, target_value, operator),
366
367        // string/array comparisons
368        "any"
369        | "none"
370        | "str_starts_with_any"
371        | "str_ends_with_any"
372        | "str_contains_any"
373        | "str_contains_none" => compare_strings_in_array(value, target_value, operator, true),
374        "any_case_sensitive" | "none_case_sensitive" => {
375            compare_strings_in_array(value, target_value, operator, false)
376        }
377        "str_matches" => compare_str_with_regex(value, target_value),
378
379        // time comparisons
380        "before" | "after" | "on" => compare_time(value, target_value, operator),
381
382        // strict equals
383        "eq" => value == target_value,
384        "neq" => value != target_value,
385
386        // id_lists
387        "in_segment_list" | "not_in_segment_list" => {
388            evaluate_id_list(ctx, operator, target_value, value)
389        }
390
391        "array_contains_any"
392        | "array_contains_none"
393        | "array_contains_all"
394        | "not_array_contains_all" => compare_arrays(value, target_value, operator),
395
396        _ => {
397            ctx.result.unsupported = true;
398            return Ok(());
399        }
400    };
401
402    Ok(())
403}
404
405fn evaluate_id_list(
406    ctx: &mut EvaluatorContext<'_>,
407    op: &str,
408    target_value: &DynamicValue,
409    value: &DynamicValue,
410) -> bool {
411    let list_name = unwrap_or_return!(&target_value.string_value, false);
412    let id_lists = &ctx.spec_store_data.id_lists;
413
414    let list = unwrap_or_return!(id_lists.get(list_name), false);
415
416    let value = unwrap_or_return!(&value.string_value, false);
417    let hashed = ctx.hashing.sha256(value);
418    let lookup_id: String = hashed.chars().take(8).collect();
419
420    let is_in_list = list.ids.contains(&lookup_id);
421
422    if op == "not_in_segment_list" {
423        return !is_in_list;
424    }
425
426    is_in_list
427}
428
429fn evaluate_nested_gate<'a>(
430    ctx: &mut EvaluatorContext<'a>,
431    target_value: &'a DynamicValue,
432    condition_type: &'a String,
433) -> Result<(), StatsigErr> {
434    let gate_name = if let Some(name) = target_value.string_value.as_ref() {
435        name
436    } else {
437        log_e!(
438            TAG,
439            "Invalid target_value for condition {}, {:?}",
440            condition_type,
441            target_value
442        );
443        ctx.result.unsupported = true;
444        return Ok(());
445    };
446
447    ctx.prep_for_nested_evaluation()?;
448    let _ = Evaluator::evaluate(ctx, gate_name, &SpecType::Gate)?;
449
450    if ctx.result.unsupported {
451        return Ok(());
452    }
453
454    if !&gate_name.starts_with("segment:") {
455        let res = &ctx.result;
456        let expo = SecondaryExposure {
457            gate: gate_name.clone(),
458            gate_value: res.bool_value.to_string(),
459            rule_id: res.rule_id.unwrap_or(&EMPTY_STR).clone(),
460        };
461
462        if res.sampling_rate.is_none() {
463            ctx.result.has_seen_analytical_gates = Option::from(true);
464        }
465
466        ctx.result.secondary_exposures.push(expo);
467    }
468
469    if condition_type == "fail_gate" {
470        ctx.result.bool_value = !ctx.result.bool_value;
471    }
472    Ok(())
473}
474
475fn evaluate_config_delegate<'a>(
476    ctx: &mut EvaluatorContext<'a>,
477    rule: &'a Rule,
478) -> Result<bool, StatsigErr> {
479    let delegate = unwrap_or_return!(&rule.config_delegate, Ok(false));
480    let delegate_spec = unwrap_or_return!(
481        ctx.spec_store_data.values.dynamic_configs.get(delegate),
482        Ok(false)
483    );
484
485    ctx.result.undelegated_secondary_exposures = Some(ctx.result.secondary_exposures.clone());
486
487    ctx.prep_for_nested_evaluation()?;
488    let recognition = Evaluator::evaluate(ctx, delegate, &SpecType::Experiment)?;
489    if recognition == Recognition::Unrecognized {
490        ctx.result.undelegated_secondary_exposures = None;
491        return Ok(false);
492    }
493
494    ctx.result.explicit_parameters = delegate_spec.explicit_parameters.as_ref();
495    ctx.result.config_delegate = rule.config_delegate.as_ref();
496
497    Ok(true)
498}
499
500fn evaluate_pass_percentage(ctx: &mut EvaluatorContext, rule: &Rule, spec_salt: &String) -> bool {
501    if rule.pass_percentage == 100f64 {
502        return true;
503    }
504
505    if rule.pass_percentage == 0f64 {
506        return false;
507    }
508
509    let rule_salt = rule.salt.as_ref().unwrap_or(&rule.id);
510    let unit_id = get_unit_id(ctx, &rule.id_type);
511    let input = format!("{spec_salt}.{rule_salt}.{unit_id}");
512    match ctx.hashing.evaluation_hash(&input) {
513        Some(hash) => ((hash % 10000) as f64) < rule.pass_percentage * 100.0,
514        None => false,
515    }
516}
517
518fn get_hash_for_user_bucket(ctx: &mut EvaluatorContext, condition: &Condition) -> DynamicValue {
519    let unit_id = get_unit_id(ctx, &condition.id_type);
520
521    let mut salt: &String = &EMPTY_STR;
522
523    if let Some(add_values) = &condition.additional_values {
524        if let Some(v) = &add_values["salt"].string_value {
525            salt = v;
526        }
527    }
528
529    let input = format!("{salt}.{unit_id}");
530    let hash = ctx.hashing.evaluation_hash(&input).unwrap_or(1);
531    dyn_value!(hash % 1000)
532}