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