1use chrono::Utc;
2use lazy_static::lazy_static;
3
4use crate::evaluation::cmab_evaluator::evaluate_cmab;
5use crate::evaluation::comparisons::{
6 compare_arrays, compare_numbers, compare_str_with_regex, compare_strings_in_array,
7 compare_time, compare_versions,
8};
9use crate::evaluation::dynamic_string::DynamicString;
10use crate::evaluation::dynamic_value::DynamicValue;
11use crate::evaluation::evaluation_types::SecondaryExposure;
12use crate::evaluation::evaluator_context::{EvaluatorContext, IdListResolution};
13use crate::evaluation::evaluator_value::{EvaluatorValue, MemoizedEvaluatorValue};
14use crate::evaluation::get_unit_id::get_unit_id;
15use crate::evaluation::user_agent_parsing::UserAgentParser;
16use crate::interned_string::InternedString;
17use crate::specs_response::spec_types::{Condition, Rule, Spec};
18use crate::{dyn_value, log_w, unwrap_or_return, ExperimentEvaluationOptions, StatsigErr};
19
20use super::country_lookup::CountryLookup;
21
22const TAG: &str = "Evaluator";
23
24pub struct Evaluator;
25
26lazy_static! {
27 static ref EMPTY_STR: String = String::new();
28 static ref EMPTY_DYNAMIC_VALUE: DynamicValue = DynamicValue::new();
29 static ref DEFAULT_RULE: InternedString = InternedString::from_str_ref("default");
30 static ref DISABLED_RULE: InternedString = InternedString::from_str_ref("disabled");
31 static ref SALT: InternedString = InternedString::from_str_ref("salt");
32}
33
34#[derive(Clone)]
35pub enum SpecType {
36 Gate,
37 DynamicConfig,
38 Experiment,
39 Layer,
40}
41
42#[derive(PartialEq, Eq, Debug)]
43pub enum Recognition {
44 Unrecognized,
45 Recognized,
46}
47
48impl Evaluator {
49 pub fn evaluate(
50 ctx: &mut EvaluatorContext,
51 spec_name: &str,
52 spec_type: &SpecType,
53 ) -> Result<Recognition, StatsigErr> {
54 let opt_addressable_spec = match spec_type {
55 SpecType::Gate => ctx.specs_data.feature_gates.get(spec_name),
56 SpecType::DynamicConfig => ctx.specs_data.dynamic_configs.get(spec_name),
57 SpecType::Experiment => ctx.specs_data.dynamic_configs.get(spec_name),
58 SpecType::Layer => ctx.specs_data.layer_configs.get(spec_name),
59 };
60
61 let opt_spec = opt_addressable_spec.map(|a| a.spec.as_ref());
62
63 if try_apply_override(ctx, spec_name, spec_type, opt_spec) {
64 return Ok(Recognition::Recognized);
65 }
66
67 if try_apply_config_mapping(ctx, spec_name, spec_type, opt_spec) {
68 return Ok(Recognition::Recognized);
69 }
70
71 if evaluate_cmab(ctx, spec_name, spec_type) {
72 return Ok(Recognition::Recognized);
73 }
74
75 let addressable_spec =
76 unwrap_or_return!(opt_addressable_spec, Ok(Recognition::Unrecognized));
77 let spec = unwrap_or_return!(opt_spec, Ok(Recognition::Unrecognized));
78
79 if ctx.result.name.is_none() {
80 ctx.result.name = Some(&addressable_spec.name);
81 }
82
83 if ctx.result.id_type.is_none() {
84 ctx.result.id_type = Some(InternedString::from_str_ref(&spec.id_type));
85 }
86
87 if ctx.result.version.is_none() {
88 if let Some(version) = spec.version {
89 ctx.result.version = Some(version);
90 }
91 }
92
93 if let Some(is_active) = spec.is_active {
94 ctx.result.is_experiment_active = is_active;
95 }
96
97 if let Some(has_shared_params) = spec.has_shared_params {
98 ctx.result.is_in_layer = has_shared_params;
99 }
100
101 if let Some(explicit_params) = &spec.explicit_parameters {
102 ctx.result.explicit_parameters = Some(explicit_params);
103 }
104
105 for rule in &spec.rules {
106 evaluate_rule(ctx, rule)?;
107
108 if ctx.result.unsupported {
109 return Ok(Recognition::Recognized);
110 }
111
112 if !ctx.result.bool_value {
113 continue;
114 }
115
116 if evaluate_config_delegate(ctx, rule)? {
117 ctx.finalize_evaluation(spec, Some(rule));
118 return Ok(Recognition::Recognized);
119 }
120
121 let did_pass = evaluate_pass_percentage(ctx, rule, &spec.salt);
122
123 if did_pass {
124 ctx.result.bool_value = rule.return_value.get_bool() != Some(false);
125 ctx.result.json_value = Some(rule.return_value.clone());
126 } else {
127 ctx.result.bool_value = spec.default_value.get_bool() == Some(true);
128 ctx.result.json_value = Some(spec.default_value.clone());
129 }
130
131 ctx.result.rule_id = Some(&rule.id);
132 ctx.result.group_name = rule.group_name.clone();
133 ctx.result.is_experiment_group = rule.is_experiment_group.unwrap_or(false);
134 ctx.result.is_experiment_active = spec.is_active.unwrap_or(false);
135 ctx.finalize_evaluation(spec, Some(rule));
136 return Ok(Recognition::Recognized);
137 }
138
139 ctx.result.bool_value = spec.default_value.get_bool() == Some(true);
140 ctx.result.json_value = Some(spec.default_value.clone());
141 ctx.result.rule_id = match spec.enabled {
142 true => Some(&DEFAULT_RULE),
143 false => Some(&DISABLED_RULE),
144 };
145 ctx.finalize_evaluation(spec, None);
146
147 Ok(Recognition::Recognized)
148 }
149}
150
151fn try_apply_config_mapping(
152 ctx: &mut EvaluatorContext,
153 spec_name: &str,
154 spec_type: &SpecType,
155 opt_spec: Option<&Spec>,
156) -> bool {
157 let overrides = match &ctx.specs_data.overrides {
158 Some(overrides) => overrides,
159 None => return false,
160 };
161
162 let override_rules = match &ctx.specs_data.override_rules {
163 Some(override_rules) => override_rules,
164 None => return false,
165 };
166
167 let mapping_list = match overrides.get(spec_name) {
168 Some(mapping_list) => mapping_list,
169 None => return false,
170 };
171
172 let spec_salt = match opt_spec {
173 Some(spec) => &spec.salt,
174 None => InternedString::empty_ref(),
175 };
176
177 for mapping in mapping_list {
178 for override_rule in &mapping.rules {
179 let start_time = override_rule.start_time.unwrap_or_default();
180
181 if start_time > Utc::now().timestamp_millis() {
182 continue;
183 }
184
185 let rule = match override_rules.get(&override_rule.rule_name) {
186 Some(rule) => rule,
187 None => continue,
188 };
189 match evaluate_rule(ctx, rule) {
190 Ok(_) => {}
191 Err(_) => {
192 ctx.reset_result();
193 continue;
194 }
195 }
196
197 if !ctx.result.bool_value || ctx.result.unsupported {
198 ctx.reset_result();
199 continue;
200 }
201 ctx.reset_result();
202 let pass = evaluate_pass_percentage(ctx, rule, spec_salt);
203 if pass {
204 ctx.result.override_config_name = Some(&mapping.new_config_name);
205 match Evaluator::evaluate(ctx, mapping.new_config_name.as_str(), spec_type) {
206 Ok(Recognition::Recognized) => {
207 return true;
208 }
209 _ => {
210 ctx.reset_result();
211 break;
212 }
213 }
214 }
215 }
216 }
217
218 false
219}
220
221fn try_apply_override(
222 ctx: &mut EvaluatorContext,
223 spec_name: &str,
224 spec_type: &SpecType,
225 opt_spec: Option<&Spec>,
226) -> bool {
227 let adapter = match &ctx.override_adapter {
228 Some(adapter) => adapter,
229 None => return false,
230 };
231
232 match spec_type {
233 SpecType::Gate => adapter.get_gate_override(ctx.user.user_ref, spec_name, &mut ctx.result),
234
235 SpecType::DynamicConfig => {
236 adapter.get_dynamic_config_override(ctx.user.user_ref, spec_name, &mut ctx.result)
237 }
238
239 SpecType::Experiment => {
240 adapter.get_experiment_override(ctx.user.user_ref, spec_name, &mut ctx.result, opt_spec)
241 }
242
243 SpecType::Layer => {
244 adapter.get_layer_override(ctx.user.user_ref, spec_name, &mut ctx.result)
245 }
246 }
247}
248
249fn evaluate_rule<'a>(ctx: &mut EvaluatorContext<'a>, rule: &'a Rule) -> Result<(), StatsigErr> {
250 let mut all_conditions_pass = true;
251 for condition_hash in &rule.conditions {
253 let opt_condition = ctx.specs_data.condition_map.get(condition_hash);
255 let condition = if let Some(c) = opt_condition {
256 c
257 } else {
258 log_w!(TAG, "Unsupported - Condition not found: {}", condition_hash);
259 ctx.result.unsupported = true;
260 return Ok(());
261 };
262
263 evaluate_condition(ctx, condition)?;
264
265 if !ctx.result.bool_value {
266 all_conditions_pass = false;
267 }
268 }
269
270 ctx.result.bool_value = all_conditions_pass;
271
272 Ok(())
273}
274
275fn evaluate_condition<'a>(
276 ctx: &mut EvaluatorContext<'a>,
277 condition: &'a Condition,
278) -> Result<(), StatsigErr> {
279 let temp_value: Option<DynamicValue>;
280 let target_value = condition
281 .target_value
282 .as_ref()
283 .map(|v| v.inner.as_ref())
284 .unwrap_or(EvaluatorValue::empty().inner.as_ref());
285 let condition_type = condition.condition_type.as_str();
286
287 let value: &DynamicValue = match condition_type {
288 "public" => {
289 ctx.result.bool_value = true;
290 return Ok(());
291 }
292 "fail_gate" | "pass_gate" => {
293 evaluate_nested_gate(ctx, target_value, condition_type)?;
294 return Ok(());
295 }
296 "experiment_group" => {
297 let group_name = evaluate_experiment_group(ctx, &condition.field);
298 match group_name {
299 Some(name) => {
300 temp_value = Some(DynamicValue::from(name));
301 temp_value.as_ref()
302 }
303 None => None,
304 }
305 }
306 "ua_based" => match ctx.user.get_user_value(&condition.field) {
307 Some(value) => Some(value),
308 None => {
309 temp_value = UserAgentParser::get_value_from_user_agent(
310 ctx.user,
311 &condition.field,
312 &mut ctx.result.override_reason,
313 ctx.should_user_third_party_parser,
314 );
315 temp_value.as_ref()
316 }
317 },
318 "ip_based" => match ctx.user.get_user_value(&condition.field) {
319 Some(value) => Some(value),
320 None => {
321 temp_value = CountryLookup::get_value_from_ip(ctx.user, &condition.field, ctx);
322 temp_value.as_ref()
323 }
324 },
325 "user_field" => ctx.user.get_user_value(&condition.field),
326 "environment_field" => {
327 temp_value = ctx.user.get_value_from_environment(&condition.field);
328 temp_value.as_ref()
329 }
330 "current_time" => {
331 temp_value = Some(DynamicValue::for_timestamp_evaluation(
332 Utc::now().timestamp_millis(),
333 ));
334 temp_value.as_ref()
335 }
336 "user_bucket" => {
337 temp_value = Some(get_hash_for_user_bucket(ctx, condition));
338 temp_value.as_ref()
339 }
340 "target_app" => ctx.app_id,
341 "unit_id" => ctx.user.get_unit_id(&condition.id_type),
342 _ => {
343 log_w!(
344 TAG,
345 "Unsupported - Unknown condition type: {}",
346 condition_type
347 );
348 ctx.result.unsupported = true;
349 return Ok(());
350 }
351 }
352 .unwrap_or(&EMPTY_DYNAMIC_VALUE);
353
354 let operator = match &condition.operator {
357 Some(operator) => operator.as_str(),
358 None => {
359 log_w!(TAG, "Unsupported - Operator is None",);
360 ctx.result.unsupported = true;
361 return Ok(());
362 }
363 };
364
365 ctx.result.bool_value = match operator {
366 "gt" | "gte" | "lt" | "lte" => compare_numbers(value, target_value, operator),
368
369 "version_gt" | "version_gte" | "version_lt" | "version_lte" | "version_eq"
371 | "version_neq" => compare_versions(value, target_value, operator),
372
373 "any"
375 | "none"
376 | "str_starts_with_any"
377 | "str_ends_with_any"
378 | "str_contains_any"
379 | "str_contains_none" => compare_strings_in_array(value, target_value, operator, true),
380 "any_case_sensitive" | "none_case_sensitive" => {
381 compare_strings_in_array(value, target_value, operator, false)
382 }
383 "str_matches" => compare_str_with_regex(value, target_value),
384
385 "before" | "after" | "on" => compare_time(value, target_value, operator),
387
388 "eq" => target_value.is_equal_to_dynamic_value(value),
390 "neq" => !target_value.is_equal_to_dynamic_value(value),
391
392 "in_segment_list" | "not_in_segment_list" => {
394 evaluate_id_list(ctx, operator, target_value, value)
395 }
396
397 "array_contains_any"
398 | "array_contains_none"
399 | "array_contains_all"
400 | "not_array_contains_all" => compare_arrays(value, target_value, operator),
401
402 _ => {
403 log_w!(TAG, "Unsupported - Unknown operator: {}", operator);
404 ctx.result.unsupported = true;
405 return Ok(());
406 }
407 };
408
409 Ok(())
410}
411
412fn evaluate_id_list(
413 ctx: &mut EvaluatorContext<'_>,
414 op: &str,
415 target_value: &MemoizedEvaluatorValue,
416 value: &DynamicValue,
417) -> bool {
418 let is_in_list = is_in_id_list(ctx, target_value, value);
419
420 if op == "not_in_segment_list" {
421 return !is_in_list;
422 }
423
424 is_in_list
425}
426
427fn is_in_id_list(
428 ctx: &mut EvaluatorContext<'_>,
429 target_value: &MemoizedEvaluatorValue,
430 value: &DynamicValue,
431) -> bool {
432 let list_name = unwrap_or_return!(&target_value.string_value, false);
433 let dyn_str = unwrap_or_return!(&value.string_value, false);
434 let hashed = ctx.hashing.sha256(&dyn_str.value);
435 let lookup_id: String = hashed.chars().take(8).collect();
436
437 match ctx.id_list_resolver {
438 IdListResolution::MapLookup(id_lists) => {
439 let list = unwrap_or_return!(id_lists.get(list_name.value.as_str()), false);
440
441 list.ids.contains(&lookup_id)
442 }
443 IdListResolution::Callback(callback) => {
444 callback(list_name.value.as_str(), lookup_id.as_str())
445 }
446 }
447}
448
449fn evaluate_experiment_group<'a>(
450 ctx: &mut EvaluatorContext<'a>,
451 experiment_name: &Option<DynamicString>,
452) -> Option<String> {
453 let exp_name = match experiment_name {
454 Some(name) => &name.value,
455 None => {
456 return None;
457 }
458 };
459 let statsig = match &ctx.statsig {
460 Some(s) => s,
461 None => {
462 ctx.result.unsupported = true;
463 return None;
464 }
465 };
466 let res = statsig.get_experiment_with_options(
467 ctx.user.user_ref,
468 exp_name.as_str(),
469 ExperimentEvaluationOptions {
470 disable_exposure_logging: ctx.disable_exposure_logging,
471 user_persisted_values: None,
472 },
473 );
474 res.group_name
475}
476
477fn evaluate_nested_gate<'a>(
478 ctx: &mut EvaluatorContext<'a>,
479 target_value: &'a MemoizedEvaluatorValue,
480 condition_type: &'a str,
481) -> Result<(), StatsigErr> {
482 let gate_name = target_value
483 .string_value
484 .as_ref()
485 .map(|name| &name.value)
486 .unwrap_or(InternedString::empty_ref());
487
488 match ctx.nested_gate_memo.get(gate_name.as_str()) {
489 Some((previous_bool, previous_rule_id)) => {
490 ctx.result.bool_value = *previous_bool;
491 ctx.result.rule_id = *previous_rule_id;
492 }
493 None => {
494 ctx.prep_for_nested_evaluation()?;
495
496 let _ = Evaluator::evaluate(ctx, gate_name.as_str(), &SpecType::Gate)?;
497
498 if ctx.result.unsupported {
499 return Ok(());
500 }
501
502 if !gate_name.as_str().is_empty() {
503 ctx.nested_gate_memo.insert(
504 gate_name.as_str(),
505 (ctx.result.bool_value, ctx.result.rule_id),
506 );
507 }
508 }
509 }
510
511 if !&gate_name.starts_with("segment:") {
512 let res = &ctx.result;
513 let expo = SecondaryExposure {
514 gate: gate_name.clone(),
515 gate_value: InternedString::from_bool(res.bool_value),
516 rule_id: res.rule_id.unwrap_or(InternedString::empty_ref()).clone(),
517 };
518
519 if res.sampling_rate.is_none() {
520 ctx.result.has_seen_analytical_gates = Option::from(true);
521 }
522
523 ctx.result.secondary_exposures.push(expo);
524 }
525
526 if condition_type == "fail_gate" {
527 ctx.result.bool_value = !ctx.result.bool_value;
528 }
529 Ok(())
530}
531
532fn evaluate_config_delegate<'a>(
533 ctx: &mut EvaluatorContext<'a>,
534 rule: &'a Rule,
535) -> Result<bool, StatsigErr> {
536 let delegate = unwrap_or_return!(&rule.config_delegate, Ok(false));
537 let delegate_spec = unwrap_or_return!(ctx.specs_data.dynamic_configs.get(delegate), Ok(false));
538
539 ctx.result.undelegated_secondary_exposures = Some(ctx.result.secondary_exposures.clone());
540
541 ctx.prep_for_nested_evaluation()?;
542 let recognition = Evaluator::evaluate(ctx, delegate, &SpecType::Experiment)?;
543 if recognition == Recognition::Unrecognized {
544 ctx.result.undelegated_secondary_exposures = None;
545 return Ok(false);
546 }
547
548 ctx.result.explicit_parameters = delegate_spec.spec.explicit_parameters.as_ref();
549 ctx.result.config_delegate = rule.config_delegate.clone();
550
551 Ok(true)
552}
553
554fn evaluate_pass_percentage(
555 ctx: &mut EvaluatorContext,
556 rule: &Rule,
557 spec_salt: &InternedString,
558) -> bool {
559 if rule.pass_percentage == 100f64 {
560 return true;
561 }
562
563 if rule.pass_percentage == 0f64 {
564 return false;
565 }
566
567 let rule_salt = rule.salt.as_deref().unwrap_or(rule.id.as_str());
568 let unit_id = get_unit_id(ctx, &rule.id_type);
569 let input = format!("{spec_salt}.{rule_salt}.{unit_id}");
570 match ctx.hashing.evaluation_hash(&input) {
571 Some(hash) => ((hash % 10000) as f64) < rule.pass_percentage * 100.0,
572 None => false,
573 }
574}
575
576fn get_hash_for_user_bucket(ctx: &mut EvaluatorContext, condition: &Condition) -> DynamicValue {
577 let unit_id = get_unit_id(ctx, &condition.id_type);
578
579 let mut salt = InternedString::empty_ref();
580
581 if let Some(add_values) = &condition.additional_values {
582 if let Some(v) = add_values.get(&SALT) {
583 salt = v;
584 }
585 }
586
587 let input = format!("{salt}.{unit_id}");
588 let hash = ctx.hashing.evaluation_hash(&input).unwrap_or(1);
589 dyn_value!(hash % 1000)
590}