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