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_types::SecondaryExposure;
16use crate::evaluation::evaluator_context::{EvaluatorContext, IdListResolution};
17use crate::evaluation::evaluator_value::{EvaluatorValue, MemoizedEvaluatorValue};
18use crate::evaluation::get_unit_id::get_unit_id;
19use crate::evaluation::user_agent_parsing::UserAgentParser;
20use crate::interned_string::InternedString;
21use crate::specs_response::explicit_params::ExplicitParameters;
22use crate::specs_response::spec_types::{Condition, Rule, Spec};
23use crate::{dyn_value, log_w, unwrap_or_return, ExperimentEvaluationOptions, StatsigErr};
24
25use super::country_lookup::CountryLookup;
26
27const TAG: &str = "Evaluator";
28
29pub struct Evaluator;
30
31lazy_static! {
32    static ref EMPTY_STR: String = String::new();
33    static ref EMPTY_DYNAMIC_VALUE: DynamicValue = DynamicValue::new();
34    static ref DISABLED_RULE: InternedString = InternedString::from_str_ref("disabled");
35    static ref SALT: InternedString = InternedString::from_str_ref("salt");
36}
37
38#[derive(Clone, Debug)]
39pub enum SpecType {
40    Gate,
41    DynamicConfig,
42    Experiment,
43    Layer,
44    ParameterStore,
45}
46
47#[derive(PartialEq, Eq, Debug)]
48pub enum Recognition {
49    Unrecognized,
50    Recognized,
51}
52
53impl Evaluator {
54    pub fn evaluate(
55        ctx: &mut EvaluatorContext,
56        spec_name: &str,
57        spec_type: &SpecType,
58    ) -> Result<Recognition, StatsigErr> {
59        let spec_name_intern = InternedString::from_str_ref(spec_name);
60
61        let opt_spec = match spec_type {
62            SpecType::Gate => ctx.specs_data.feature_gates.get(&spec_name_intern),
63            SpecType::DynamicConfig => ctx.specs_data.dynamic_configs.get(&spec_name_intern),
64            SpecType::Experiment => ctx.specs_data.dynamic_configs.get(&spec_name_intern),
65            SpecType::Layer => ctx.specs_data.layer_configs.get(&spec_name_intern),
66            SpecType::ParameterStore => {
67                return evaluate_param_store_reason(ctx, spec_name.to_string())
68            }
69        }
70        .map(|sp| sp.as_spec_ref());
71
72        if try_apply_override(ctx, spec_name, spec_type, opt_spec) {
73            return Ok(Recognition::Recognized);
74        }
75
76        if try_apply_config_mapping(ctx, spec_name, spec_type, opt_spec) {
77            return Ok(Recognition::Recognized);
78        }
79
80        if evaluate_cmab(ctx, spec_name, spec_type) {
81            return Ok(Recognition::Recognized);
82        }
83
84        let spec = unwrap_or_return!(opt_spec, Ok(Recognition::Unrecognized));
85
86        if ctx.result.name.is_none() {
87            ctx.result.name = Some(spec_name_intern);
88        }
89
90        if ctx.result.id_type.is_none() {
91            ctx.result.id_type = Some(InternedString::from_str_ref(&spec.id_type));
92        }
93
94        if ctx.result.version.is_none() {
95            if let Some(version) = spec.version {
96                ctx.result.version = Some(version);
97            }
98        }
99
100        if let Some(is_active) = spec.is_active {
101            ctx.result.is_experiment_active = is_active;
102        }
103
104        if let Some(has_shared_params) = spec.has_shared_params {
105            ctx.result.is_in_layer = has_shared_params;
106        }
107
108        if let Some(explicit_params) = &spec.explicit_parameters {
109            ctx.result.explicit_parameters = Some(explicit_params.clone());
110        }
111
112        if spec.use_new_layer_eval == Some(true) && matches!(spec_type, SpecType::Layer) {
113            return new_layer_eval(ctx, spec);
114        }
115
116        for rule in &spec.rules {
117            evaluate_rule(ctx, rule)?;
118
119            if ctx.result.unsupported {
120                return Ok(Recognition::Recognized);
121            }
122
123            if !ctx.result.bool_value {
124                continue;
125            }
126
127            if evaluate_config_delegate(ctx, rule)? {
128                ctx.finalize_evaluation(spec, Some(rule));
129                return Ok(Recognition::Recognized);
130            }
131
132            let did_pass = evaluate_pass_percentage(ctx, rule, &spec.salt);
133
134            if did_pass {
135                ctx.result.bool_value = rule.return_value.get_bool() != Some(false);
136                ctx.result.json_value = Some(rule.return_value.clone());
137            } else {
138                ctx.result.bool_value = spec.default_value.get_bool() == Some(true);
139                ctx.result.json_value = Some(spec.default_value.clone());
140            }
141
142            ctx.result.rule_id = Some(rule.id.clone());
143            ctx.result.group_name = rule.group_name.clone();
144            ctx.result.is_experiment_group = rule.is_experiment_group.unwrap_or(false);
145            ctx.result.is_experiment_active = spec.is_active.unwrap_or(false);
146            ctx.finalize_evaluation(spec, Some(rule));
147            return Ok(Recognition::Recognized);
148        }
149
150        ctx.result.bool_value = spec.default_value.get_bool() == Some(true);
151        ctx.result.json_value = Some(spec.default_value.clone());
152        ctx.result.rule_id = match spec.enabled {
153            true => Some(InternedString::default_rule_id()),
154            false => Some(DISABLED_RULE.clone()),
155        };
156        ctx.finalize_evaluation(spec, None);
157
158        Ok(Recognition::Recognized)
159    }
160}
161
162fn new_layer_eval<'a>(
163    ctx: &mut EvaluatorContext<'a>,
164    spec: &'a Spec,
165) -> Result<Recognition, StatsigErr> {
166    let mut has_delegate = false;
167    let mut passed = false;
168    let mut rule_id: Option<&'a InternedString> = Some(InternedString::default_rule_id_ref());
169    let mut delegate_name: Option<InternedString> = None;
170    let mut rule_ids: HashMap<InternedString, InternedString> = HashMap::new();
171    let mut value: HashMap<String, Value> = HashMap::new();
172    let mut group_name: Option<InternedString> = None;
173    let mut is_experiment_group = false;
174    let mut explicit_parameters: Option<ExplicitParameters> = None;
175    let mut secondary_exposures: Vec<SecondaryExposure> = Vec::new();
176    let mut undelegated_secondary_exposures: Vec<SecondaryExposure> = Vec::new();
177
178    for rule in &spec.rules {
179        evaluate_rule(ctx, rule)?;
180        secondary_exposures.append(&mut ctx.result.secondary_exposures);
181        undelegated_secondary_exposures.append(&mut ctx.result.secondary_exposures);
182        ctx.result.secondary_exposures.clear();
183
184        if ctx.result.unsupported {
185            return Ok(Recognition::Recognized);
186        }
187
188        if !ctx.result.bool_value {
189            continue;
190        }
191
192        let did_pass = evaluate_pass_percentage(ctx, rule, &spec.salt);
193        if !did_pass {
194            continue;
195        }
196
197        if evaluate_config_delegate(ctx, rule)? {
198            if has_delegate {
199                continue;
200            }
201            let delegate_value = match &ctx.result.json_value {
202                Some(val) => val.get_json(),
203                None => continue,
204            };
205            let mut has_reused_parameter = false;
206            if let Some(json_map) = &delegate_value {
207                for k in json_map.keys() {
208                    if value.contains_key(k) {
209                        has_reused_parameter = true;
210                        break;
211                    }
212                }
213            }
214
215            if has_reused_parameter {
216                continue;
217            }
218
219            update_parameter_values(&mut value, &mut rule_ids, delegate_value, &rule.id);
220
221            secondary_exposures.append(&mut ctx.result.secondary_exposures);
222            ctx.result.secondary_exposures.clear();
223
224            has_delegate = true;
225            passed = ctx.result.bool_value;
226            delegate_name = ctx.result.config_delegate.clone();
227            group_name = ctx.result.group_name.clone();
228            rule_id = Some(&rule.id);
229            is_experiment_group = rule.is_experiment_group.unwrap_or(false);
230            explicit_parameters = ctx.result.explicit_parameters.clone();
231        } else {
232            update_parameter_values(
233                &mut value,
234                &mut rule_ids,
235                rule.return_value.get_json(),
236                &rule.id,
237            );
238        }
239    }
240    update_parameter_values(
241        &mut value,
242        &mut rule_ids,
243        spec.default_value.get_json(),
244        InternedString::default_rule_id_ref(),
245    );
246    ctx.result.bool_value = passed;
247    ctx.result.config_delegate = delegate_name;
248    ctx.result.group_name = group_name;
249    ctx.result.rule_id = rule_id.cloned();
250    ctx.result.json_value = Some(DynamicReturnable::from_map(value));
251    ctx.result.is_experiment_group = is_experiment_group;
252    ctx.result.is_experiment_active = spec.is_active.unwrap_or(false);
253    ctx.result.explicit_parameters = explicit_parameters;
254    ctx.result.secondary_exposures = secondary_exposures;
255    ctx.result.undelegated_secondary_exposures = Some(undelegated_secondary_exposures);
256    ctx.result.parameter_rule_ids = Some(rule_ids);
257    ctx.finalize_evaluation(spec, None);
258    Ok(Recognition::Recognized)
259}
260
261fn update_parameter_values(
262    value: &mut HashMap<String, Value>,
263    rule_ids: &mut HashMap<InternedString, InternedString>,
264    values_to_apply: Option<HashMap<String, Value>>,
265    rule_id: &InternedString,
266) {
267    let json_map = match values_to_apply {
268        Some(map) => map,
269        None => return,
270    };
271    for (k, v) in json_map {
272        let parameter_name = InternedString::from_str_ref(&k);
273        if let std::collections::hash_map::Entry::Vacant(e) = value.entry(k) {
274            e.insert(v);
275            rule_ids.insert(parameter_name.clone(), rule_id.clone());
276        }
277    }
278}
279
280fn try_apply_config_mapping(
281    ctx: &mut EvaluatorContext,
282    spec_name: &str,
283    spec_type: &SpecType,
284    opt_spec: Option<&Spec>,
285) -> bool {
286    let overrides = match &ctx.specs_data.overrides {
287        Some(overrides) => overrides,
288        None => return false,
289    };
290
291    let override_rules = match &ctx.specs_data.override_rules {
292        Some(override_rules) => override_rules,
293        None => return false,
294    };
295
296    let mapping_list = match overrides.get(spec_name) {
297        Some(mapping_list) => mapping_list,
298        None => return false,
299    };
300
301    let spec_salt = match opt_spec {
302        Some(spec) => &spec.salt,
303        None => InternedString::empty_ref(),
304    };
305
306    for mapping in mapping_list {
307        for override_rule in &mapping.rules {
308            let start_time = override_rule.start_time.unwrap_or_default();
309
310            if start_time > Utc::now().timestamp_millis() {
311                continue;
312            }
313
314            let rule = match override_rules.get(&override_rule.rule_name) {
315                Some(rule) => rule,
316                None => continue,
317            };
318            match evaluate_rule(ctx, rule) {
319                Ok(_) => {}
320                Err(_) => {
321                    ctx.reset_result();
322                    continue;
323                }
324            }
325
326            if !ctx.result.bool_value || ctx.result.unsupported {
327                ctx.reset_result();
328                continue;
329            }
330            ctx.reset_result();
331            let pass = evaluate_pass_percentage(ctx, rule, spec_salt);
332            if pass {
333                ctx.result.override_config_name = Some(mapping.new_config_name.clone());
334                match Evaluator::evaluate(ctx, mapping.new_config_name.as_str(), spec_type) {
335                    Ok(Recognition::Recognized) => {
336                        return true;
337                    }
338                    _ => {
339                        ctx.reset_result();
340                        break;
341                    }
342                }
343            }
344        }
345    }
346
347    false
348}
349
350fn try_apply_override(
351    ctx: &mut EvaluatorContext,
352    spec_name: &str,
353    spec_type: &SpecType,
354    opt_spec: Option<&Spec>,
355) -> bool {
356    let adapter = match &ctx.override_adapter {
357        Some(adapter) => adapter,
358        None => return false,
359    };
360
361    match spec_type {
362        SpecType::Gate => adapter.get_gate_override(ctx.user.user_ref, spec_name, &mut ctx.result),
363
364        SpecType::DynamicConfig => {
365            adapter.get_dynamic_config_override(ctx.user.user_ref, spec_name, &mut ctx.result)
366        }
367
368        SpecType::Experiment => {
369            adapter.get_experiment_override(ctx.user.user_ref, spec_name, &mut ctx.result, opt_spec)
370        }
371
372        SpecType::Layer => {
373            adapter.get_layer_override(ctx.user.user_ref, spec_name, &mut ctx.result)
374        }
375
376        SpecType::ParameterStore => {
377            adapter.get_parameter_store_override(ctx.user.user_ref, spec_name, &mut ctx.result)
378        }
379    }
380}
381
382fn evaluate_rule<'a>(ctx: &mut EvaluatorContext<'a>, rule: &'a Rule) -> Result<(), StatsigErr> {
383    let mut all_conditions_pass = true;
384    // println!("--- Eval Rule {} ---", rule.id);
385    for condition_hash in &rule.conditions {
386        // println!("Condition Hash {}", condition_hash);
387        let opt_condition = ctx.specs_data.condition_map.get(condition_hash);
388        let condition = if let Some(c) = opt_condition {
389            c
390        } else {
391            log_w!(TAG, "Unsupported - Condition not found: {}", condition_hash);
392            ctx.result.unsupported = true;
393            return Ok(());
394        };
395
396        evaluate_condition(ctx, condition)?;
397
398        if !ctx.result.bool_value {
399            all_conditions_pass = false;
400        }
401    }
402
403    ctx.result.bool_value = all_conditions_pass;
404
405    Ok(())
406}
407
408fn evaluate_condition<'a>(
409    ctx: &mut EvaluatorContext<'a>,
410    condition: &'a Condition,
411) -> Result<(), StatsigErr> {
412    let temp_value: Option<DynamicValue>;
413    let target_value = condition
414        .target_value
415        .as_ref()
416        .map(|v| v.as_ref())
417        .unwrap_or(EvaluatorValue::empty().as_ref());
418    let condition_type = condition.condition_type.as_str();
419
420    let value: &DynamicValue = match condition_type {
421        "public" => {
422            ctx.result.bool_value = true;
423            return Ok(());
424        }
425        "fail_gate" | "pass_gate" => {
426            evaluate_nested_gate(ctx, target_value, condition_type)?;
427            return Ok(());
428        }
429        "experiment_group" => {
430            let group_name = evaluate_experiment_group(ctx, &condition.field);
431            match group_name {
432                Some(name) => {
433                    temp_value = Some(DynamicValue::from(name));
434                    temp_value.as_ref()
435                }
436                None => None,
437            }
438        }
439        "ua_based" => match ctx.user.get_user_value(&condition.field) {
440            Some(value) => Some(value),
441            None => {
442                temp_value = UserAgentParser::get_value_from_user_agent(
443                    ctx.user,
444                    &condition.field,
445                    &mut ctx.result.override_reason,
446                    ctx.should_user_third_party_parser,
447                );
448                temp_value.as_ref()
449            }
450        },
451        "ip_based" => match ctx.user.get_user_value(&condition.field) {
452            Some(value) => Some(value),
453            None => {
454                temp_value = CountryLookup::get_value_from_ip(ctx.user, &condition.field, ctx);
455                temp_value.as_ref()
456            }
457        },
458        "user_field" => ctx.user.get_user_value(&condition.field),
459        "environment_field" => {
460            temp_value = ctx.user.get_value_from_environment(&condition.field);
461            temp_value.as_ref()
462        }
463        "current_time" => {
464            temp_value = Some(DynamicValue::for_timestamp_evaluation(
465                Utc::now().timestamp_millis(),
466            ));
467            temp_value.as_ref()
468        }
469        "user_bucket" => {
470            temp_value = Some(get_hash_for_user_bucket(ctx, condition));
471            temp_value.as_ref()
472        }
473        "target_app" => ctx.app_id,
474        "unit_id" => ctx.user.get_unit_id(&condition.id_type),
475        _ => {
476            log_w!(
477                TAG,
478                "Unsupported - Unknown condition type: {}",
479                condition_type
480            );
481            ctx.result.unsupported = true;
482            return Ok(());
483        }
484    }
485    .unwrap_or(&EMPTY_DYNAMIC_VALUE);
486
487    // println!("Eval Condition {}, {:?}", condition_type, value);
488
489    let operator = match &condition.operator {
490        Some(operator) => operator.as_str(),
491        None => {
492            log_w!(TAG, "Unsupported - Operator is None",);
493            ctx.result.unsupported = true;
494            return Ok(());
495        }
496    };
497
498    ctx.result.bool_value = match operator {
499        // numerical comparisons
500        "gt" | "gte" | "lt" | "lte" => compare_numbers(value, target_value, operator),
501
502        // version comparisons
503        "version_gt" | "version_gte" | "version_lt" | "version_lte" | "version_eq"
504        | "version_neq" => compare_versions(value, target_value, operator),
505
506        // string/array comparisons
507        "any"
508        | "none"
509        | "str_starts_with_any"
510        | "str_ends_with_any"
511        | "str_contains_any"
512        | "str_contains_none" => compare_strings_in_array(value, target_value, operator, true),
513        "any_case_sensitive" | "none_case_sensitive" => {
514            compare_strings_in_array(value, target_value, operator, false)
515        }
516        "str_matches" => compare_str_with_regex(value, target_value),
517
518        // time comparisons
519        "before" | "after" | "on" => compare_time(value, target_value, operator),
520
521        // strict equals
522        "eq" => target_value.is_equal_to_dynamic_value(value),
523        "neq" => !target_value.is_equal_to_dynamic_value(value),
524
525        // id_lists
526        "in_segment_list" | "not_in_segment_list" => {
527            evaluate_id_list(ctx, operator, target_value, value)
528        }
529
530        "array_contains_any"
531        | "array_contains_none"
532        | "array_contains_all"
533        | "not_array_contains_all" => compare_arrays(value, target_value, operator),
534
535        _ => {
536            log_w!(TAG, "Unsupported - Unknown operator: {}", operator);
537            ctx.result.unsupported = true;
538            return Ok(());
539        }
540    };
541
542    Ok(())
543}
544
545fn evaluate_id_list(
546    ctx: &mut EvaluatorContext<'_>,
547    op: &str,
548    target_value: &MemoizedEvaluatorValue,
549    value: &DynamicValue,
550) -> bool {
551    let is_in_list = is_in_id_list(ctx, target_value, value);
552
553    if op == "not_in_segment_list" {
554        return !is_in_list;
555    }
556
557    is_in_list
558}
559
560fn is_in_id_list(
561    ctx: &mut EvaluatorContext<'_>,
562    target_value: &MemoizedEvaluatorValue,
563    value: &DynamicValue,
564) -> bool {
565    let list_name = unwrap_or_return!(&target_value.string_value, false);
566    let dyn_str = unwrap_or_return!(&value.string_value, false);
567    let hashed = ctx.hashing.sha256(&dyn_str.value);
568    let lookup_id: String = hashed.chars().take(8).collect();
569
570    match ctx.id_list_resolver {
571        IdListResolution::MapLookup(id_lists) => {
572            let list = unwrap_or_return!(id_lists.get(list_name.value.as_str()), false);
573
574            list.ids.contains(&lookup_id)
575        }
576        IdListResolution::Callback(callback) => {
577            callback(list_name.value.as_str(), lookup_id.as_str())
578        }
579    }
580}
581
582fn evaluate_experiment_group<'a>(
583    ctx: &mut EvaluatorContext<'a>,
584    experiment_name: &Option<DynamicString>,
585) -> Option<String> {
586    let exp_name = match experiment_name {
587        Some(name) => &name.value,
588        None => {
589            return None;
590        }
591    };
592    let statsig = match &ctx.statsig {
593        Some(s) => s,
594        None => {
595            ctx.result.unsupported = true;
596            return None;
597        }
598    };
599    let res = statsig.get_experiment_with_options(
600        ctx.user.user_ref,
601        exp_name.as_str(),
602        ExperimentEvaluationOptions {
603            disable_exposure_logging: ctx.disable_exposure_logging,
604            user_persisted_values: None,
605        },
606    );
607    res.group_name
608}
609
610fn evaluate_nested_gate<'a>(
611    ctx: &mut EvaluatorContext<'a>,
612    target_value: &'a MemoizedEvaluatorValue,
613    condition_type: &'a str,
614) -> Result<(), StatsigErr> {
615    let gate_name = target_value
616        .string_value
617        .as_ref()
618        .map(|name| &name.value)
619        .unwrap_or(InternedString::empty_ref());
620
621    match ctx.nested_gate_memo.get(gate_name) {
622        Some((previous_bool, previous_rule_id)) => {
623            ctx.result.bool_value = *previous_bool;
624            ctx.result.rule_id = previous_rule_id.clone();
625        }
626        None => {
627            ctx.prep_for_nested_evaluation()?;
628
629            let _ = Evaluator::evaluate(ctx, gate_name.as_str(), &SpecType::Gate)?;
630
631            if ctx.result.unsupported {
632                return Ok(());
633            }
634
635            if !gate_name.as_str().is_empty() {
636                ctx.nested_gate_memo.insert(
637                    gate_name.clone(),
638                    (ctx.result.bool_value, ctx.result.rule_id.clone()),
639                );
640            }
641        }
642    }
643
644    if !&gate_name.starts_with("segment:") {
645        let res = &ctx.result;
646        let expo = SecondaryExposure {
647            gate: gate_name.clone(),
648            gate_value: InternedString::from_bool(res.bool_value),
649            rule_id: res.rule_id.clone().unwrap_or_default(),
650        };
651
652        if res.sampling_rate.is_none() {
653            ctx.result.has_seen_analytical_gates = Option::from(true);
654        }
655
656        ctx.result.secondary_exposures.push(expo);
657    }
658
659    if condition_type == "fail_gate" {
660        ctx.result.bool_value = !ctx.result.bool_value;
661    }
662    Ok(())
663}
664
665fn evaluate_config_delegate<'a>(
666    ctx: &mut EvaluatorContext<'a>,
667    rule: &'a Rule,
668) -> Result<bool, StatsigErr> {
669    let delegate = unwrap_or_return!(&rule.config_delegate, Ok(false));
670    let delegate_spec = unwrap_or_return!(ctx.specs_data.dynamic_configs.get(delegate), Ok(false));
671
672    ctx.result.undelegated_secondary_exposures = Some(ctx.result.secondary_exposures.clone());
673
674    ctx.prep_for_nested_evaluation()?;
675    let recognition = Evaluator::evaluate(ctx, delegate, &SpecType::Experiment)?;
676    if recognition == Recognition::Unrecognized {
677        ctx.result.undelegated_secondary_exposures = None;
678        return Ok(false);
679    }
680
681    ctx.result.explicit_parameters = delegate_spec.as_spec_ref().explicit_parameters.clone();
682    ctx.result.config_delegate = rule.config_delegate.clone();
683
684    Ok(true)
685}
686
687fn evaluate_pass_percentage(
688    ctx: &mut EvaluatorContext,
689    rule: &Rule,
690    spec_salt: &InternedString,
691) -> bool {
692    if rule.pass_percentage == 100f64 {
693        return true;
694    }
695
696    if rule.pass_percentage == 0f64 {
697        return false;
698    }
699
700    let rule_salt = rule.salt.as_deref().unwrap_or(rule.id.as_str());
701    let unit_id = get_unit_id(ctx, &rule.id_type);
702    let input = format!("{spec_salt}.{rule_salt}.{unit_id}");
703    match ctx.hashing.evaluation_hash(&input) {
704        Some(hash) => ((hash % 10000) as f64) < rule.pass_percentage * 100.0,
705        None => false,
706    }
707}
708
709fn get_hash_for_user_bucket(ctx: &mut EvaluatorContext, condition: &Condition) -> DynamicValue {
710    let unit_id = get_unit_id(ctx, &condition.id_type);
711
712    let mut salt = InternedString::empty_ref();
713
714    if let Some(add_values) = &condition.additional_values {
715        if let Some(v) = add_values.get(&SALT) {
716            salt = v;
717        }
718    }
719
720    let input = format!("{salt}.{unit_id}");
721    let hash = ctx.hashing.evaluation_hash(&input).unwrap_or(1);
722    dyn_value!(hash % 1000)
723}
724
725fn evaluate_param_store_reason(
726    ctx: &mut EvaluatorContext,
727    spec_name: String,
728) -> Result<Recognition, StatsigErr> {
729    let spec_name_intern = InternedString::from_str_ref(&spec_name);
730    let has_param_store = ctx
731        .specs_data
732        .param_stores
733        .as_ref()
734        .and_then(|stores| stores.get(&spec_name_intern))
735        .is_some();
736    Ok(if has_param_store {
737        Recognition::Recognized
738    } else {
739        Recognition::Unrecognized
740    })
741}