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