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