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