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