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