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