Skip to main content

statsig_rust/evaluation/
evaluator.rs

1use std::collections::HashMap;
2
3use chrono::Utc;
4use lazy_static::lazy_static;
5use serde_json::Value;
6
7use crate::evaluation::cmab_evaluator::evaluate_cmab;
8use crate::evaluation::comparisons::{
9    compare_arrays, compare_numbers, compare_str_with_regex, compare_strings_in_array,
10    compare_time, compare_versions,
11};
12use crate::evaluation::dynamic_returnable::DynamicReturnable;
13use crate::evaluation::dynamic_string::DynamicString;
14use crate::evaluation::dynamic_value::DynamicValue;
15use crate::evaluation::evaluation_data::{InternedStrRef, RuleRef, SpecAccess, SpecView};
16use crate::evaluation::evaluation_types::SecondaryExposure;
17use crate::evaluation::evaluator_context::{EvaluatorContext, IdListResolution};
18use crate::evaluation::evaluator_value::{EvaluatorValue, EvaluatorValueRef};
19use crate::evaluation::get_unit_id::get_unit_id;
20use crate::evaluation::user_agent_parsing::UserAgentParser;
21use crate::interned_string::InternedString;
22use crate::specs_response::explicit_params::ExplicitParameters;
23use crate::specs_response::spec_types::{Condition, ConditionOperator, ConditionType};
24use crate::user::user_value::UserValueRef;
25use crate::{dyn_value, log_w, unwrap_or_return, ExperimentEvaluationOptions, StatsigErr};
26
27use super::country_lookup::CountryLookup;
28
29const TAG: &str = "Evaluator";
30
31pub struct Evaluator;
32
33lazy_static! {
34    static ref EMPTY_STR: String = String::new();
35    static ref EMPTY_DYNAMIC_VALUE: DynamicValue = DynamicValue::new();
36    static ref DISABLED_RULE: InternedString = InternedString::from_str_ref("disabled");
37    static ref SALT: InternedString = InternedString::from_str_ref("salt");
38}
39
40#[derive(Clone, Debug)]
41pub enum SpecType {
42    Gate,
43    DynamicConfig,
44    Experiment,
45    Layer,
46    ParameterStore,
47}
48
49#[derive(PartialEq, Eq, Debug)]
50pub enum Recognition {
51    Unrecognized,
52    Recognized,
53}
54
55impl Evaluator {
56    pub fn evaluate(
57        ctx: &mut EvaluatorContext,
58        spec_name: &str,
59        spec_type: &SpecType,
60    ) -> Result<Recognition, StatsigErr> {
61        let spec_name_intern = InternedString::from_str_ref(spec_name);
62        Self::evaluate_with_name(ctx, &spec_name_intern, spec_type)
63    }
64
65    pub(crate) fn evaluate_with_name(
66        ctx: &mut EvaluatorContext,
67        spec_name_intern: &InternedString,
68        spec_type: &SpecType,
69    ) -> Result<Recognition, StatsigErr> {
70        let spec_name = spec_name_intern.as_str();
71
72        let opt_spec = match spec_type {
73            SpecType::Gate => ctx.specs_data.feature_gates.get(spec_name_intern),
74            SpecType::DynamicConfig => ctx.specs_data.dynamic_configs.get(spec_name_intern),
75            SpecType::Experiment => ctx.specs_data.dynamic_configs.get(spec_name_intern),
76            SpecType::Layer => ctx.specs_data.layer_configs.get(spec_name_intern),
77            SpecType::ParameterStore => {
78                return evaluate_param_store_reason(ctx, spec_name.to_string());
79            }
80        }
81        .map(|spec| spec.view());
82
83        if try_apply_override(ctx, spec_name, spec_type, opt_spec) {
84            return Ok(Recognition::Recognized);
85        }
86
87        if try_apply_config_mapping(ctx, spec_name, spec_type, opt_spec) {
88            return Ok(Recognition::Recognized);
89        }
90
91        if evaluate_cmab(ctx, spec_name, spec_type) {
92            return Ok(Recognition::Recognized);
93        }
94
95        let spec = unwrap_or_return!(opt_spec, Ok(Recognition::Unrecognized));
96
97        if ctx.result.name.is_none() {
98            ctx.result.name = Some(spec_name_intern.clone());
99        }
100
101        if ctx.result.id_type.is_none() {
102            ctx.result.id_type = Some(spec.id_type().to_interned());
103        }
104
105        if ctx.result.version.is_none() {
106            if let Some(version) = spec.version() {
107                ctx.result.version = Some(version);
108            }
109        }
110
111        if let Some(is_active) = spec.is_active() {
112            ctx.result.is_experiment_active = is_active;
113        }
114
115        if let Some(has_shared_params) = spec.has_shared_params() {
116            ctx.result.is_in_layer = has_shared_params;
117        }
118
119        if let Some(explicit_params) = spec.explicit_parameters() {
120            ctx.result.explicit_parameters = Some(explicit_params);
121        }
122
123        if spec.uses_new_layer_eval() && matches!(spec_type, SpecType::Layer) {
124            return new_layer_eval(ctx, spec);
125        }
126
127        for index in 0..spec.rules_len() {
128            let rule = spec.rule(index);
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_values(rule.sampling_rate(), spec.forward_all_exposures());
141                return Ok(Recognition::Recognized);
142            }
143
144            let did_pass = evaluate_pass_percentage(ctx, rule, spec.salt());
145
146            let return_value = if did_pass {
147                rule.return_value()
148            } else {
149                spec.default_value()
150            };
151            ctx.result.bool_value = if did_pass {
152                return_value.bool_value() != Some(false)
153            } else {
154                return_value.bool_value() == Some(true)
155            };
156            ctx.result.json_value = Some(return_value.to_owned());
157
158            ctx.result.rule_id = Some(rule.id().to_interned());
159            ctx.result.group_name = rule.group_name().map(InternedStrRef::to_interned);
160            ctx.result.is_experiment_group = rule.is_experiment_group();
161            ctx.result.is_experiment_active = spec.is_active().unwrap_or(false);
162            ctx.finalize_evaluation_values(rule.sampling_rate(), spec.forward_all_exposures());
163            return Ok(Recognition::Recognized);
164        }
165
166        let default_value = spec.default_value();
167        ctx.result.bool_value = default_value.bool_value() == Some(true);
168        ctx.result.json_value = Some(default_value.to_owned());
169        ctx.result.rule_id = match spec.enabled() {
170            true => Some(InternedString::default_rule_id()),
171            false => Some(DISABLED_RULE.clone()),
172        };
173        ctx.finalize_evaluation_values(None, spec.forward_all_exposures());
174
175        Ok(Recognition::Recognized)
176    }
177}
178
179fn new_layer_eval<'a>(
180    ctx: &mut EvaluatorContext<'a>,
181    spec: SpecView<'a>,
182) -> Result<Recognition, StatsigErr> {
183    let mut has_passed_rule = false;
184    let mut passed = false;
185    let mut rule_id = Some(InternedString::default_rule_id());
186    let mut delegate_name: Option<InternedString> = None;
187    let mut rule_ids: HashMap<InternedString, InternedString> = HashMap::new();
188    let mut value: HashMap<String, Value> = HashMap::new();
189    let mut group_name: Option<InternedString> = None;
190    let mut is_experiment_group = false;
191    let mut is_experiment_active = false;
192    let mut explicit_parameters: Option<ExplicitParameters> = None;
193    let mut secondary_exposures: Vec<SecondaryExposure> = Vec::new();
194    let mut undelegated_secondary_exposures: Vec<SecondaryExposure> = Vec::new();
195
196    for index in 0..spec.rules_len() {
197        let rule = spec.rule(index);
198        evaluate_rule(ctx, rule)?;
199        let rule_secondary_exposures = std::mem::take(&mut ctx.result.secondary_exposures);
200        secondary_exposures.extend(rule_secondary_exposures.iter().cloned());
201        undelegated_secondary_exposures.extend(rule_secondary_exposures);
202
203        if ctx.result.unsupported {
204            return Ok(Recognition::Recognized);
205        }
206
207        if !ctx.result.bool_value {
208            continue;
209        }
210
211        let did_pass = evaluate_pass_percentage(ctx, rule, spec.salt());
212        if !did_pass {
213            continue;
214        }
215
216        if evaluate_config_delegate(ctx, rule)? {
217            if has_passed_rule {
218                continue;
219            }
220            let delegate_value = match &ctx.result.json_value {
221                Some(val) => val.get_json(),
222                None => continue,
223            };
224            let mut has_reused_parameter = false;
225            if let Some(json_map) = &delegate_value {
226                for k in json_map.keys() {
227                    if value.contains_key(k) {
228                        has_reused_parameter = true;
229                        break;
230                    }
231                }
232            }
233
234            if has_reused_parameter {
235                continue;
236            }
237
238            let delegate_rule_id = ctx
239                .result
240                .rule_id
241                .clone()
242                .unwrap_or_else(InternedString::default_rule_id);
243            update_parameter_values(
244                &mut value,
245                &mut rule_ids,
246                delegate_value,
247                (&delegate_rule_id).into(),
248            );
249
250            secondary_exposures.append(&mut ctx.result.secondary_exposures);
251            ctx.result.secondary_exposures.clear();
252            passed = ctx.result.bool_value;
253            delegate_name = ctx.result.config_delegate.clone();
254            group_name = ctx.result.group_name.clone();
255            rule_id = Some(delegate_rule_id);
256            is_experiment_group = ctx.result.is_experiment_group;
257            is_experiment_active = ctx.result.is_experiment_active;
258            explicit_parameters = ctx.result.explicit_parameters.clone();
259        } else {
260            passed = ctx.result.bool_value;
261            update_parameter_values(
262                &mut value,
263                &mut rule_ids,
264                rule.return_value().json_value(),
265                rule.id(),
266            );
267        }
268        has_passed_rule = true;
269    }
270    update_parameter_values(
271        &mut value,
272        &mut rule_ids,
273        spec.default_value().json_value(),
274        InternedString::default_rule_id_ref().into(),
275    );
276    ctx.result.bool_value = passed;
277    ctx.result.config_delegate = delegate_name;
278    ctx.result.group_name = group_name;
279    ctx.result.rule_id = rule_id;
280    ctx.result.json_value = Some(DynamicReturnable::from_map(value));
281    ctx.result.is_experiment_group = is_experiment_group;
282    ctx.result.is_experiment_active = is_experiment_active;
283    ctx.result.explicit_parameters = explicit_parameters;
284    ctx.result.secondary_exposures = secondary_exposures;
285    ctx.result.undelegated_secondary_exposures = Some(undelegated_secondary_exposures);
286    ctx.result.parameter_rule_ids = Some(rule_ids);
287    ctx.finalize_evaluation_values(None, spec.forward_all_exposures());
288    Ok(Recognition::Recognized)
289}
290
291fn update_parameter_values(
292    value: &mut HashMap<String, Value>,
293    rule_ids: &mut HashMap<InternedString, InternedString>,
294    values_to_apply: Option<HashMap<String, Value>>,
295    rule_id: InternedStrRef<'_>,
296) {
297    let json_map = match values_to_apply {
298        Some(map) => map,
299        None => return,
300    };
301    for (k, v) in json_map {
302        let parameter_name = InternedString::from_string_uninterned(k.clone());
303        if let std::collections::hash_map::Entry::Vacant(e) = value.entry(k) {
304            e.insert(v);
305            rule_ids.insert(parameter_name.clone(), rule_id.to_interned());
306        }
307    }
308}
309
310fn try_apply_config_mapping(
311    ctx: &mut EvaluatorContext,
312    spec_name: &str,
313    spec_type: &SpecType,
314    opt_spec: Option<SpecView<'_>>,
315) -> bool {
316    let overrides = match &ctx.specs_data.overrides {
317        Some(overrides) => overrides,
318        None => return false,
319    };
320
321    let override_rules = match &ctx.specs_data.override_rules {
322        Some(override_rules) => override_rules,
323        None => return false,
324    };
325
326    let mapping_list = match overrides.get(spec_name) {
327        Some(mapping_list) => mapping_list,
328        None => return false,
329    };
330
331    let spec_salt = match opt_spec {
332        Some(spec) => spec.salt(),
333        None => InternedString::empty_ref().into(),
334    };
335
336    for mapping in mapping_list {
337        for override_rule in &mapping.rules {
338            let start_time = override_rule.start_time.unwrap_or_default();
339
340            if start_time > Utc::now().timestamp_millis() {
341                continue;
342            }
343
344            let rule = match override_rules.get(&override_rule.rule_name) {
345                Some(rule) => rule,
346                None => continue,
347            };
348            let rule = RuleRef::from(rule);
349            match evaluate_rule(ctx, rule) {
350                Ok(_) => {}
351                Err(_) => {
352                    ctx.reset_result();
353                    continue;
354                }
355            }
356
357            if !ctx.result.bool_value || ctx.result.unsupported {
358                ctx.reset_result();
359                continue;
360            }
361            ctx.reset_result();
362            let pass = evaluate_pass_percentage(ctx, rule, spec_salt);
363            if pass {
364                ctx.result.override_config_name = Some(mapping.new_config_name.clone());
365                match Evaluator::evaluate_with_name(ctx, &mapping.new_config_name, spec_type) {
366                    Ok(Recognition::Recognized) => {
367                        return true;
368                    }
369                    _ => {
370                        ctx.reset_result();
371                        break;
372                    }
373                }
374            }
375        }
376    }
377
378    false
379}
380
381fn try_apply_override(
382    ctx: &mut EvaluatorContext,
383    spec_name: &str,
384    spec_type: &SpecType,
385    opt_spec: Option<SpecView<'_>>,
386) -> bool {
387    let adapter = match &ctx.override_adapter {
388        Some(adapter) => adapter,
389        None => return false,
390    };
391
392    let has_any_override_for_spec = match spec_type {
393        SpecType::Gate => adapter.has_any_overrides_for_gate(spec_name),
394        SpecType::DynamicConfig => adapter.has_any_overrides_for_dynamic_config(spec_name),
395        SpecType::Experiment => adapter.has_any_overrides_for_experiment(spec_name),
396        SpecType::Layer => adapter.has_any_overrides_for_layer(spec_name),
397        SpecType::ParameterStore => adapter.has_any_overrides_for_parameter_store(spec_name),
398    };
399
400    if !has_any_override_for_spec {
401        return false;
402    }
403
404    ctx.user.with_public_user(|user| match spec_type {
405        SpecType::Gate => adapter.get_gate_override(user, spec_name, &mut ctx.result),
406
407        SpecType::DynamicConfig => {
408            adapter.get_dynamic_config_override(user, spec_name, &mut ctx.result)
409        }
410
411        SpecType::Experiment => {
412            let override_spec = opt_spec.map(SpecView::materialize);
413            adapter.get_experiment_override(
414                user,
415                spec_name,
416                &mut ctx.result,
417                override_spec.as_ref().map(SpecAccess::as_ref),
418            )
419        }
420
421        SpecType::Layer => adapter.get_layer_override(user, spec_name, &mut ctx.result),
422
423        SpecType::ParameterStore => {
424            adapter.get_parameter_store_override(user, spec_name, &mut ctx.result)
425        }
426    })
427}
428
429fn evaluate_rule<'a>(ctx: &mut EvaluatorContext<'a>, rule: RuleRef<'a>) -> Result<(), StatsigErr> {
430    let mut all_conditions_pass = true;
431    // println!("--- Eval Rule {} ---", rule.id);
432    for index in 0..rule.conditions_len() {
433        let condition_hash = rule.condition_id(index);
434        // println!("Condition Hash {}", condition_hash);
435        let opt_condition = condition_hash.get_from(&ctx.specs_data.condition_map);
436        let condition = if let Some(c) = opt_condition {
437            c
438        } else {
439            log_w!(
440                TAG,
441                "Unsupported - Condition not found: {}",
442                condition_hash.as_str()
443            );
444            ctx.result.unsupported = true;
445            return Ok(());
446        };
447
448        evaluate_condition(ctx, condition)?;
449
450        if !ctx.result.bool_value {
451            all_conditions_pass = false;
452        }
453    }
454
455    ctx.result.bool_value = all_conditions_pass;
456
457    Ok(())
458}
459
460fn evaluate_condition<'a>(
461    ctx: &mut EvaluatorContext<'a>,
462    condition: &'a Condition,
463) -> Result<(), StatsigErr> {
464    let temp_value: Option<DynamicValue>;
465    let target_value = condition
466        .target_value
467        .as_ref()
468        .map(EvaluatorValue::as_value_ref)
469        .unwrap_or_else(|| EvaluatorValue::empty().as_value_ref());
470    let condition_type = condition.condition_type.as_str();
471
472    let value: UserValueRef<'_> = match condition.compiled_condition_type {
473        ConditionType::Public => {
474            ctx.result.bool_value = true;
475            return Ok(());
476        }
477        ConditionType::FailGate => {
478            evaluate_nested_gate(ctx, target_value, true)?;
479            return Ok(());
480        }
481        ConditionType::PassGate => {
482            evaluate_nested_gate(ctx, target_value, false)?;
483            return Ok(());
484        }
485        ConditionType::ExperimentGroup => {
486            let group_name = evaluate_experiment_group(ctx, &condition.field);
487            match group_name {
488                Some(name) => {
489                    temp_value = Some(DynamicValue::from(name));
490                    temp_value.as_ref().map(UserValueRef::Dynamic)
491                }
492                None => None,
493            }
494        }
495        ConditionType::UaBased => match ctx.user.get_user_value(&condition.field) {
496            Some(value) => Some(value),
497            None => {
498                temp_value = UserAgentParser::get_value_from_user_agent(
499                    ctx.user,
500                    &condition.field,
501                    &mut ctx.result.override_reason,
502                    ctx.should_user_third_party_parser,
503                );
504                temp_value.as_ref().map(UserValueRef::Dynamic)
505            }
506        },
507        ConditionType::IpBased => match ctx.user.get_user_value(&condition.field) {
508            Some(value) => Some(value),
509            None => {
510                temp_value = CountryLookup::get_value_from_ip(ctx.user, &condition.field, ctx);
511                temp_value.as_ref().map(UserValueRef::Dynamic)
512            }
513        },
514        ConditionType::UserField => ctx.user.get_user_value(&condition.field),
515        ConditionType::EnvironmentField => {
516            temp_value = ctx.user.get_value_from_environment(&condition.field);
517            temp_value.as_ref().map(UserValueRef::Dynamic)
518        }
519        ConditionType::CurrentTime => {
520            temp_value = Some(DynamicValue::for_timestamp_evaluation(
521                Utc::now().timestamp_millis(),
522            ));
523            temp_value.as_ref().map(UserValueRef::Dynamic)
524        }
525        ConditionType::UserBucket => {
526            temp_value = Some(get_hash_for_user_bucket(ctx, condition));
527            temp_value.as_ref().map(UserValueRef::Dynamic)
528        }
529        ConditionType::TargetApp => ctx.app_id.map(UserValueRef::Dynamic),
530        ConditionType::UnitId => ctx.user.get_unit_id(&condition.id_type),
531        ConditionType::Unknown => {
532            log_w!(
533                TAG,
534                "Unsupported - Unknown condition type: {}",
535                condition_type
536            );
537            ctx.result.unsupported = true;
538            return Ok(());
539        }
540    }
541    .unwrap_or(UserValueRef::Dynamic(&EMPTY_DYNAMIC_VALUE));
542
543    // println!("Eval Condition {}, {:?}", condition_type, value);
544
545    ctx.result.bool_value = match condition.compiled_operator {
546        ConditionOperator::Missing => {
547            log_w!(TAG, "Unsupported - Operator is None",);
548            ctx.result.unsupported = true;
549            return Ok(());
550        }
551
552        // numerical comparisons
553        op @ (ConditionOperator::Gt
554        | ConditionOperator::Gte
555        | ConditionOperator::Lt
556        | ConditionOperator::Lte) => compare_numbers(value, target_value, op),
557
558        // version comparisons
559        op @ (ConditionOperator::VersionGt
560        | ConditionOperator::VersionGte
561        | ConditionOperator::VersionLt
562        | ConditionOperator::VersionLte
563        | ConditionOperator::VersionEq
564        | ConditionOperator::VersionNeq) => compare_versions(value, target_value, op),
565
566        // string/array comparisons
567        op @ (ConditionOperator::Any
568        | ConditionOperator::NoneOf
569        | ConditionOperator::StrStartsWithAny
570        | ConditionOperator::StrEndsWithAny
571        | ConditionOperator::StrContainsAny
572        | ConditionOperator::StrContainsNone
573        | ConditionOperator::AnyCaseSensitive
574        | ConditionOperator::NoneCaseSensitive) => {
575            compare_strings_in_array(value, target_value, op)
576        }
577        ConditionOperator::StrMatches => compare_str_with_regex(value, target_value),
578
579        // time comparisons
580        op @ (ConditionOperator::Before | ConditionOperator::After | ConditionOperator::On) => {
581            compare_time(value, target_value, op)
582        }
583
584        // strict equals
585        ConditionOperator::Eq => target_value.is_equal_to_user_value(value),
586        ConditionOperator::Neq => !target_value.is_equal_to_user_value(value),
587
588        // id_lists
589        op @ (ConditionOperator::InSegmentList | ConditionOperator::NotInSegmentList) => {
590            evaluate_id_list(ctx, op, target_value, value)
591        }
592
593        op @ (ConditionOperator::ArrayContainsAny
594        | ConditionOperator::ArrayContainsNone
595        | ConditionOperator::ArrayContainsAll
596        | ConditionOperator::NotArrayContainsAll) => compare_arrays(value, target_value, op),
597
598        ConditionOperator::Unknown => {
599            log_w!(
600                TAG,
601                "Unsupported - Unknown operator: {}",
602                condition.operator.as_deref().unwrap_or_default()
603            );
604            ctx.result.unsupported = true;
605            return Ok(());
606        }
607    };
608
609    Ok(())
610}
611
612fn evaluate_id_list(
613    ctx: &mut EvaluatorContext<'_>,
614    op: ConditionOperator,
615    target_value: EvaluatorValueRef<'_>,
616    value: UserValueRef<'_>,
617) -> bool {
618    let is_in_list = is_in_id_list(ctx, target_value, value);
619
620    if matches!(op, ConditionOperator::NotInSegmentList) {
621        return !is_in_list;
622    }
623
624    is_in_list
625}
626
627fn is_in_id_list(
628    ctx: &mut EvaluatorContext<'_>,
629    target_value: EvaluatorValueRef<'_>,
630    value: UserValueRef<'_>,
631) -> bool {
632    let list_name = unwrap_or_return!(target_value.string_value(), false);
633    let dyn_str = unwrap_or_return!(value.string_value(), false);
634    let hashed = ctx.hashing.sha256(dyn_str);
635    let lookup_id: String = hashed.chars().take(8).collect();
636
637    match ctx.id_list_resolver {
638        IdListResolution::MapLookup(id_lists) => {
639            let list = unwrap_or_return!(id_lists.get(list_name), false);
640
641            list.ids.contains(&lookup_id)
642        }
643        IdListResolution::Callback(callback) => callback(list_name, lookup_id.as_str()),
644    }
645}
646
647fn evaluate_experiment_group<'a>(
648    ctx: &mut EvaluatorContext<'a>,
649    experiment_name: &Option<DynamicString>,
650) -> Option<String> {
651    let exp_name = match experiment_name {
652        Some(name) => &name.value,
653        None => {
654            return None;
655        }
656    };
657    let statsig = match &ctx.statsig {
658        Some(s) => s,
659        None => {
660            ctx.result.unsupported = true;
661            return None;
662        }
663    };
664    let res = ctx.user.with_public_user(|user| {
665        statsig.get_experiment_with_options(
666            user,
667            exp_name.as_str(),
668            ExperimentEvaluationOptions {
669                disable_exposure_logging: ctx.disable_exposure_logging,
670                user_persisted_values: None,
671            },
672        )
673    });
674    res.group_name
675}
676
677fn evaluate_nested_gate<'a>(
678    ctx: &mut EvaluatorContext<'a>,
679    target_value: EvaluatorValueRef<'a>,
680    is_fail_gate: bool,
681) -> Result<(), StatsigErr> {
682    let gate_name = match target_value.interned_string_value() {
683        Some(name) => name,
684        None => InternedString::empty_ref().into(),
685    };
686
687    match gate_name.get_from(&ctx.nested_gate_memo) {
688        Some((previous_bool, previous_rule_id, previous_secondary_exposures)) => {
689            ctx.result.bool_value = *previous_bool;
690            ctx.result.rule_id = previous_rule_id.clone();
691            ctx.result
692                .secondary_exposures
693                .extend_from_slice(previous_secondary_exposures);
694        }
695        None => {
696            let parent_nested_count = ctx.nested_count;
697            if let Err(error) = ctx.prep_for_nested_evaluation() {
698                ctx.nested_count = parent_nested_count;
699                return Err(error);
700            }
701
702            let parent_exposures = std::mem::take(&mut ctx.result.secondary_exposures);
703
704            let gate_name_owned = gate_name.to_interned();
705            let recognition = Evaluator::evaluate_with_name(ctx, &gate_name_owned, &SpecType::Gate);
706            ctx.nested_count = parent_nested_count;
707
708            let recognition = match recognition {
709                Ok(recognition) => recognition,
710                Err(error) => {
711                    ctx.result.secondary_exposures = parent_exposures;
712                    return Err(error);
713                }
714            };
715
716            let mut nested_exposures = std::mem::take(&mut ctx.result.secondary_exposures);
717
718            if recognition == Recognition::Unrecognized {
719                ctx.result.bool_value = false;
720                ctx.result.rule_id = None;
721            }
722
723            if ctx.result.unsupported {
724                ctx.result.secondary_exposures = parent_exposures;
725                return Ok(());
726            }
727
728            if !gate_name.as_str().is_empty() {
729                ctx.nested_gate_memo.insert(
730                    gate_name.to_interned(),
731                    (
732                        ctx.result.bool_value,
733                        ctx.result.rule_id.clone(),
734                        nested_exposures.clone(),
735                    ),
736                );
737            }
738
739            ctx.result.secondary_exposures = parent_exposures;
740            ctx.result.secondary_exposures.append(&mut nested_exposures);
741        }
742    }
743
744    let is_empty_rule_id = match &ctx.result.rule_id {
745        Some(id) => id.as_str().is_empty(),
746        None => true,
747    };
748
749    if !gate_name.as_str().starts_with("segment:") && !is_empty_rule_id {
750        let res = &ctx.result;
751        let expo = SecondaryExposure {
752            gate: gate_name.to_interned(),
753            gate_value: InternedString::from_bool(res.bool_value),
754            rule_id: res.rule_id.clone().unwrap_or_default(),
755        };
756
757        if res.sampling_rate.is_none() {
758            ctx.result.has_seen_analytical_gates = Option::from(true);
759        }
760
761        ctx.result.secondary_exposures.push(expo);
762    }
763
764    if is_fail_gate {
765        ctx.result.bool_value = !ctx.result.bool_value;
766    }
767    Ok(())
768}
769
770fn evaluate_config_delegate<'a>(
771    ctx: &mut EvaluatorContext<'a>,
772    rule: RuleRef<'a>,
773) -> Result<bool, StatsigErr> {
774    let delegate = unwrap_or_return!(rule.config_delegate(), Ok(false));
775    let delegate_spec = unwrap_or_return!(
776        delegate.get_from(&ctx.specs_data.dynamic_configs.0),
777        Ok(false)
778    );
779    let delegate_owned = delegate.to_interned();
780
781    ctx.result.undelegated_secondary_exposures = Some(ctx.result.secondary_exposures.clone());
782
783    ctx.prep_for_nested_evaluation()?;
784    let recognition = Evaluator::evaluate_with_name(ctx, &delegate_owned, &SpecType::Experiment)?;
785    if recognition == Recognition::Unrecognized {
786        ctx.result.undelegated_secondary_exposures = None;
787        return Ok(false);
788    }
789
790    ctx.result.explicit_parameters = delegate_spec.view().explicit_parameters();
791    ctx.result.config_delegate = Some(delegate_owned);
792
793    Ok(true)
794}
795
796fn evaluate_pass_percentage(
797    ctx: &mut EvaluatorContext,
798    rule: RuleRef<'_>,
799    spec_salt: InternedStrRef<'_>,
800) -> bool {
801    let pass_percentage = rule.pass_percentage();
802    if pass_percentage == 100f64 {
803        return true;
804    }
805
806    if pass_percentage == 0f64 {
807        return false;
808    }
809
810    let rule_salt = rule.salt().unwrap_or_else(|| rule.id()).as_str();
811    let unit_id = get_unit_id(ctx, rule.id_type());
812    let input = format!("{}.{rule_salt}.{unit_id}", spec_salt.as_str());
813    match ctx.hashing.evaluation_hash(&input) {
814        Some(hash) => ((hash % 10000) as f64) < pass_percentage * 100.0,
815        None => false,
816    }
817}
818
819fn get_hash_for_user_bucket(ctx: &mut EvaluatorContext, condition: &Condition) -> DynamicValue {
820    let unit_id = get_unit_id(ctx, (&condition.id_type).into());
821
822    let mut salt = InternedString::empty_ref();
823
824    if let Some(add_values) = &condition.additional_values {
825        if let Some(v) = add_values.get(&SALT) {
826            salt = v;
827        }
828    }
829
830    let input = format!("{salt}.{unit_id}");
831    let hash = ctx.hashing.evaluation_hash(&input).unwrap_or(1);
832    dyn_value!(hash % 1000)
833}
834
835fn evaluate_param_store_reason(
836    ctx: &mut EvaluatorContext,
837    spec_name: String,
838) -> Result<Recognition, StatsigErr> {
839    let spec_name_intern = InternedString::from_str_ref(&spec_name);
840    let has_param_store = ctx
841        .specs_data
842        .param_stores
843        .as_ref()
844        .and_then(|stores| stores.get(&spec_name_intern))
845        .is_some();
846    Ok(if has_param_store {
847        Recognition::Recognized
848    } else {
849        Recognition::Unrecognized
850    })
851}