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