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