1use std::collections::HashMap;
2
3use chrono::Utc;
4use lazy_static::lazy_static;
5use serde_json::Value;
6
7use crate::evaluation::cmab_evaluator::evaluate_cmab;
8use crate::evaluation::comparisons::{
9 compare_arrays, compare_numbers, compare_str_with_regex, compare_strings_in_array,
10 compare_time, compare_versions,
11};
12use crate::evaluation::dynamic_returnable::DynamicReturnable;
13use crate::evaluation::dynamic_string::DynamicString;
14use crate::evaluation::dynamic_value::DynamicValue;
15use crate::evaluation::evaluation_data::{InternedStrRef, RuleRef, SpecAccess, SpecView};
16use crate::evaluation::evaluation_types::SecondaryExposure;
17use crate::evaluation::evaluator_context::{EvaluatorContext, IdListResolution};
18use crate::evaluation::evaluator_value::{EvaluatorValue, EvaluatorValueRef};
19use crate::evaluation::get_unit_id::get_unit_id;
20use crate::evaluation::user_agent_parsing::UserAgentParser;
21use crate::interned_string::InternedString;
22use crate::specs_response::explicit_params::ExplicitParameters;
23use crate::specs_response::spec_types::{Condition, ConditionOperator, ConditionType};
24use crate::user::user_value::UserValueRef;
25use crate::{dyn_value, log_w, unwrap_or_return, ExperimentEvaluationOptions, StatsigErr};
26
27use super::country_lookup::CountryLookup;
28
29const TAG: &str = "Evaluator";
30
31pub struct Evaluator;
32
33lazy_static! {
34 static ref EMPTY_STR: String = String::new();
35 static ref EMPTY_DYNAMIC_VALUE: DynamicValue = DynamicValue::new();
36 static ref DISABLED_RULE: InternedString = InternedString::from_str_ref("disabled");
37 static ref SALT: InternedString = InternedString::from_str_ref("salt");
38}
39
40#[derive(Clone, Debug)]
41pub enum SpecType {
42 Gate,
43 DynamicConfig,
44 Experiment,
45 Layer,
46 ParameterStore,
47}
48
49#[derive(PartialEq, Eq, Debug)]
50pub enum Recognition {
51 Unrecognized,
52 Recognized,
53}
54
55impl Evaluator {
56 pub fn evaluate(
57 ctx: &mut EvaluatorContext,
58 spec_name: &str,
59 spec_type: &SpecType,
60 ) -> Result<Recognition, StatsigErr> {
61 let spec_name_intern = InternedString::from_str_ref(spec_name);
62 Self::evaluate_with_name(ctx, &spec_name_intern, spec_type)
63 }
64
65 pub(crate) fn evaluate_with_name(
66 ctx: &mut EvaluatorContext,
67 spec_name_intern: &InternedString,
68 spec_type: &SpecType,
69 ) -> Result<Recognition, StatsigErr> {
70 let spec_name = spec_name_intern.as_str();
71
72 let opt_spec = match spec_type {
73 SpecType::Gate => ctx.specs_data.feature_gates.get(spec_name_intern),
74 SpecType::DynamicConfig => ctx.specs_data.dynamic_configs.get(spec_name_intern),
75 SpecType::Experiment => ctx.specs_data.dynamic_configs.get(spec_name_intern),
76 SpecType::Layer => ctx.specs_data.layer_configs.get(spec_name_intern),
77 SpecType::ParameterStore => {
78 return evaluate_param_store_reason(ctx, spec_name.to_string());
79 }
80 }
81 .map(|spec| spec.view());
82
83 if try_apply_override(ctx, spec_name, spec_type, opt_spec) {
84 return Ok(Recognition::Recognized);
85 }
86
87 if try_apply_config_mapping(ctx, spec_name, spec_type, opt_spec) {
88 return Ok(Recognition::Recognized);
89 }
90
91 if evaluate_cmab(ctx, spec_name, spec_type) {
92 return Ok(Recognition::Recognized);
93 }
94
95 let spec = unwrap_or_return!(opt_spec, Ok(Recognition::Unrecognized));
96
97 if ctx.result.name.is_none() {
98 ctx.result.name = Some(spec_name_intern.clone());
99 }
100
101 if ctx.result.id_type.is_none() {
102 ctx.result.id_type = Some(spec.id_type().to_interned());
103 }
104
105 if ctx.result.version.is_none() {
106 if let Some(version) = spec.version() {
107 ctx.result.version = Some(version);
108 }
109 }
110
111 if let Some(is_active) = spec.is_active() {
112 ctx.result.is_experiment_active = is_active;
113 }
114
115 if let Some(has_shared_params) = spec.has_shared_params() {
116 ctx.result.is_in_layer = has_shared_params;
117 }
118
119 if let Some(explicit_params) = spec.explicit_parameters() {
120 ctx.result.explicit_parameters = Some(explicit_params);
121 }
122
123 if spec.uses_new_layer_eval() && matches!(spec_type, SpecType::Layer) {
124 return new_layer_eval(ctx, spec);
125 }
126
127 for index in 0..spec.rules_len() {
128 let rule = spec.rule(index);
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_values(rule.sampling_rate(), spec.forward_all_exposures());
141 return Ok(Recognition::Recognized);
142 }
143
144 let did_pass = evaluate_pass_percentage(ctx, rule, spec.salt());
145
146 let return_value = if did_pass {
147 rule.return_value()
148 } else {
149 spec.default_value()
150 };
151 ctx.result.bool_value = if did_pass {
152 return_value.bool_value() != Some(false)
153 } else {
154 return_value.bool_value() == Some(true)
155 };
156 ctx.result.json_value = Some(return_value.to_owned());
157
158 ctx.result.rule_id = Some(rule.id().to_interned());
159 ctx.result.group_name = rule.group_name().map(InternedStrRef::to_interned);
160 ctx.result.is_experiment_group = rule.is_experiment_group();
161 ctx.result.is_experiment_active = spec.is_active().unwrap_or(false);
162 ctx.finalize_evaluation_values(rule.sampling_rate(), spec.forward_all_exposures());
163 return Ok(Recognition::Recognized);
164 }
165
166 let default_value = spec.default_value();
167 ctx.result.bool_value = default_value.bool_value() == Some(true);
168 ctx.result.json_value = Some(default_value.to_owned());
169 ctx.result.rule_id = match spec.enabled() {
170 true => Some(InternedString::default_rule_id()),
171 false => Some(DISABLED_RULE.clone()),
172 };
173 ctx.finalize_evaluation_values(None, spec.forward_all_exposures());
174
175 Ok(Recognition::Recognized)
176 }
177}
178
179fn new_layer_eval<'a>(
180 ctx: &mut EvaluatorContext<'a>,
181 spec: SpecView<'a>,
182) -> Result<Recognition, StatsigErr> {
183 let mut has_passed_rule = false;
184 let mut passed = false;
185 let mut rule_id = Some(InternedString::default_rule_id());
186 let mut delegate_name: Option<InternedString> = None;
187 let mut rule_ids: HashMap<InternedString, InternedString> = HashMap::new();
188 let mut value: HashMap<String, Value> = HashMap::new();
189 let mut group_name: Option<InternedString> = None;
190 let mut is_experiment_group = false;
191 let mut is_experiment_active = false;
192 let mut explicit_parameters: Option<ExplicitParameters> = None;
193 let mut secondary_exposures: Vec<SecondaryExposure> = Vec::new();
194 let mut undelegated_secondary_exposures: Vec<SecondaryExposure> = Vec::new();
195
196 for index in 0..spec.rules_len() {
197 let rule = spec.rule(index);
198 evaluate_rule(ctx, rule)?;
199 let rule_secondary_exposures = std::mem::take(&mut ctx.result.secondary_exposures);
200 secondary_exposures.extend(rule_secondary_exposures.iter().cloned());
201 undelegated_secondary_exposures.extend(rule_secondary_exposures);
202
203 if ctx.result.unsupported {
204 return Ok(Recognition::Recognized);
205 }
206
207 if !ctx.result.bool_value {
208 continue;
209 }
210
211 let did_pass = evaluate_pass_percentage(ctx, rule, spec.salt());
212 if !did_pass {
213 continue;
214 }
215
216 if evaluate_config_delegate(ctx, rule)? {
217 if has_passed_rule {
218 continue;
219 }
220 let delegate_value = match &ctx.result.json_value {
221 Some(val) => val.get_json(),
222 None => continue,
223 };
224 let mut has_reused_parameter = false;
225 if let Some(json_map) = &delegate_value {
226 for k in json_map.keys() {
227 if value.contains_key(k) {
228 has_reused_parameter = true;
229 break;
230 }
231 }
232 }
233
234 if has_reused_parameter {
235 continue;
236 }
237
238 let delegate_rule_id = ctx
239 .result
240 .rule_id
241 .clone()
242 .unwrap_or_else(InternedString::default_rule_id);
243 update_parameter_values(
244 &mut value,
245 &mut rule_ids,
246 delegate_value,
247 (&delegate_rule_id).into(),
248 );
249
250 secondary_exposures.append(&mut ctx.result.secondary_exposures);
251 ctx.result.secondary_exposures.clear();
252 passed = ctx.result.bool_value;
253 delegate_name = ctx.result.config_delegate.clone();
254 group_name = ctx.result.group_name.clone();
255 rule_id = Some(delegate_rule_id);
256 is_experiment_group = ctx.result.is_experiment_group;
257 is_experiment_active = ctx.result.is_experiment_active;
258 explicit_parameters = ctx.result.explicit_parameters.clone();
259 } else {
260 passed = ctx.result.bool_value;
261 update_parameter_values(
262 &mut value,
263 &mut rule_ids,
264 rule.return_value().json_value(),
265 rule.id(),
266 );
267 }
268 has_passed_rule = true;
269 }
270 update_parameter_values(
271 &mut value,
272 &mut rule_ids,
273 spec.default_value().json_value(),
274 InternedString::default_rule_id_ref().into(),
275 );
276 ctx.result.bool_value = passed;
277 ctx.result.config_delegate = delegate_name;
278 ctx.result.group_name = group_name;
279 ctx.result.rule_id = rule_id;
280 ctx.result.json_value = Some(DynamicReturnable::from_map(value));
281 ctx.result.is_experiment_group = is_experiment_group;
282 ctx.result.is_experiment_active = is_experiment_active;
283 ctx.result.explicit_parameters = explicit_parameters;
284 ctx.result.secondary_exposures = secondary_exposures;
285 ctx.result.undelegated_secondary_exposures = Some(undelegated_secondary_exposures);
286 ctx.result.parameter_rule_ids = Some(rule_ids);
287 ctx.finalize_evaluation_values(None, spec.forward_all_exposures());
288 Ok(Recognition::Recognized)
289}
290
291fn update_parameter_values(
292 value: &mut HashMap<String, Value>,
293 rule_ids: &mut HashMap<InternedString, InternedString>,
294 values_to_apply: Option<HashMap<String, Value>>,
295 rule_id: InternedStrRef<'_>,
296) {
297 let json_map = match values_to_apply {
298 Some(map) => map,
299 None => return,
300 };
301 for (k, v) in json_map {
302 let parameter_name = InternedString::from_string_uninterned(k.clone());
303 if let std::collections::hash_map::Entry::Vacant(e) = value.entry(k) {
304 e.insert(v);
305 rule_ids.insert(parameter_name.clone(), rule_id.to_interned());
306 }
307 }
308}
309
310fn try_apply_config_mapping(
311 ctx: &mut EvaluatorContext,
312 spec_name: &str,
313 spec_type: &SpecType,
314 opt_spec: Option<SpecView<'_>>,
315) -> bool {
316 let overrides = match &ctx.specs_data.overrides {
317 Some(overrides) => overrides,
318 None => return false,
319 };
320
321 let override_rules = match &ctx.specs_data.override_rules {
322 Some(override_rules) => override_rules,
323 None => return false,
324 };
325
326 let mapping_list = match overrides.get(spec_name) {
327 Some(mapping_list) => mapping_list,
328 None => return false,
329 };
330
331 let spec_salt = match opt_spec {
332 Some(spec) => spec.salt(),
333 None => InternedString::empty_ref().into(),
334 };
335
336 for mapping in mapping_list {
337 for override_rule in &mapping.rules {
338 let start_time = override_rule.start_time.unwrap_or_default();
339
340 if start_time > Utc::now().timestamp_millis() {
341 continue;
342 }
343
344 let rule = match override_rules.get(&override_rule.rule_name) {
345 Some(rule) => rule,
346 None => continue,
347 };
348 let rule = RuleRef::from(rule);
349 match evaluate_rule(ctx, rule) {
350 Ok(_) => {}
351 Err(_) => {
352 ctx.reset_result();
353 continue;
354 }
355 }
356
357 if !ctx.result.bool_value || ctx.result.unsupported {
358 ctx.reset_result();
359 continue;
360 }
361 ctx.reset_result();
362 let pass = evaluate_pass_percentage(ctx, rule, spec_salt);
363 if pass {
364 ctx.result.override_config_name = Some(mapping.new_config_name.clone());
365 match Evaluator::evaluate_with_name(ctx, &mapping.new_config_name, spec_type) {
366 Ok(Recognition::Recognized) => {
367 return true;
368 }
369 _ => {
370 ctx.reset_result();
371 break;
372 }
373 }
374 }
375 }
376 }
377
378 false
379}
380
381fn try_apply_override(
382 ctx: &mut EvaluatorContext,
383 spec_name: &str,
384 spec_type: &SpecType,
385 opt_spec: Option<SpecView<'_>>,
386) -> bool {
387 let adapter = match &ctx.override_adapter {
388 Some(adapter) => adapter,
389 None => return false,
390 };
391
392 let has_any_override_for_spec = match spec_type {
393 SpecType::Gate => adapter.has_any_overrides_for_gate(spec_name),
394 SpecType::DynamicConfig => adapter.has_any_overrides_for_dynamic_config(spec_name),
395 SpecType::Experiment => adapter.has_any_overrides_for_experiment(spec_name),
396 SpecType::Layer => adapter.has_any_overrides_for_layer(spec_name),
397 SpecType::ParameterStore => adapter.has_any_overrides_for_parameter_store(spec_name),
398 };
399
400 if !has_any_override_for_spec {
401 return false;
402 }
403
404 ctx.user.with_public_user(|user| match spec_type {
405 SpecType::Gate => adapter.get_gate_override(user, spec_name, &mut ctx.result),
406
407 SpecType::DynamicConfig => {
408 adapter.get_dynamic_config_override(user, spec_name, &mut ctx.result)
409 }
410
411 SpecType::Experiment => {
412 let override_spec = opt_spec.map(SpecView::materialize);
413 adapter.get_experiment_override(
414 user,
415 spec_name,
416 &mut ctx.result,
417 override_spec.as_ref().map(SpecAccess::as_ref),
418 )
419 }
420
421 SpecType::Layer => adapter.get_layer_override(user, spec_name, &mut ctx.result),
422
423 SpecType::ParameterStore => {
424 adapter.get_parameter_store_override(user, spec_name, &mut ctx.result)
425 }
426 })
427}
428
429fn evaluate_rule<'a>(ctx: &mut EvaluatorContext<'a>, rule: RuleRef<'a>) -> Result<(), StatsigErr> {
430 let mut all_conditions_pass = true;
431 for index in 0..rule.conditions_len() {
433 let condition_hash = rule.condition_id(index);
434 let opt_condition = condition_hash.get_from(&ctx.specs_data.condition_map);
436 let condition = if let Some(c) = opt_condition {
437 c
438 } else {
439 log_w!(
440 TAG,
441 "Unsupported - Condition not found: {}",
442 condition_hash.as_str()
443 );
444 ctx.result.unsupported = true;
445 return Ok(());
446 };
447
448 evaluate_condition(ctx, condition)?;
449
450 if !ctx.result.bool_value {
451 all_conditions_pass = false;
452 }
453 }
454
455 ctx.result.bool_value = all_conditions_pass;
456
457 Ok(())
458}
459
460fn evaluate_condition<'a>(
461 ctx: &mut EvaluatorContext<'a>,
462 condition: &'a Condition,
463) -> Result<(), StatsigErr> {
464 let temp_value: Option<DynamicValue>;
465 let target_value = condition
466 .target_value
467 .as_ref()
468 .map(EvaluatorValue::as_value_ref)
469 .unwrap_or_else(|| EvaluatorValue::empty().as_value_ref());
470 let condition_type = condition.condition_type.as_str();
471
472 let value: UserValueRef<'_> = match condition.compiled_condition_type {
473 ConditionType::Public => {
474 ctx.result.bool_value = true;
475 return Ok(());
476 }
477 ConditionType::FailGate => {
478 evaluate_nested_gate(ctx, target_value, true)?;
479 return Ok(());
480 }
481 ConditionType::PassGate => {
482 evaluate_nested_gate(ctx, target_value, false)?;
483 return Ok(());
484 }
485 ConditionType::ExperimentGroup => {
486 let group_name = evaluate_experiment_group(ctx, &condition.field);
487 match group_name {
488 Some(name) => {
489 temp_value = Some(DynamicValue::from(name));
490 temp_value.as_ref().map(UserValueRef::Dynamic)
491 }
492 None => None,
493 }
494 }
495 ConditionType::UaBased => match ctx.user.get_user_value(&condition.field) {
496 Some(value) => Some(value),
497 None => {
498 temp_value = UserAgentParser::get_value_from_user_agent(
499 ctx.user,
500 &condition.field,
501 &mut ctx.result.override_reason,
502 ctx.should_user_third_party_parser,
503 );
504 temp_value.as_ref().map(UserValueRef::Dynamic)
505 }
506 },
507 ConditionType::IpBased => match ctx.user.get_user_value(&condition.field) {
508 Some(value) => Some(value),
509 None => {
510 temp_value = CountryLookup::get_value_from_ip(ctx.user, &condition.field, ctx);
511 temp_value.as_ref().map(UserValueRef::Dynamic)
512 }
513 },
514 ConditionType::UserField => ctx.user.get_user_value(&condition.field),
515 ConditionType::EnvironmentField => {
516 temp_value = ctx.user.get_value_from_environment(&condition.field);
517 temp_value.as_ref().map(UserValueRef::Dynamic)
518 }
519 ConditionType::CurrentTime => {
520 temp_value = Some(DynamicValue::for_timestamp_evaluation(
521 Utc::now().timestamp_millis(),
522 ));
523 temp_value.as_ref().map(UserValueRef::Dynamic)
524 }
525 ConditionType::UserBucket => {
526 temp_value = Some(get_hash_for_user_bucket(ctx, condition));
527 temp_value.as_ref().map(UserValueRef::Dynamic)
528 }
529 ConditionType::TargetApp => ctx.app_id.map(UserValueRef::Dynamic),
530 ConditionType::UnitId => ctx.user.get_unit_id(&condition.id_type),
531 ConditionType::Unknown => {
532 log_w!(
533 TAG,
534 "Unsupported - Unknown condition type: {}",
535 condition_type
536 );
537 ctx.result.unsupported = true;
538 return Ok(());
539 }
540 }
541 .unwrap_or(UserValueRef::Dynamic(&EMPTY_DYNAMIC_VALUE));
542
543 ctx.result.bool_value = match condition.compiled_operator {
546 ConditionOperator::Missing => {
547 log_w!(TAG, "Unsupported - Operator is None",);
548 ctx.result.unsupported = true;
549 return Ok(());
550 }
551
552 op @ (ConditionOperator::Gt
554 | ConditionOperator::Gte
555 | ConditionOperator::Lt
556 | ConditionOperator::Lte) => compare_numbers(value, target_value, op),
557
558 op @ (ConditionOperator::VersionGt
560 | ConditionOperator::VersionGte
561 | ConditionOperator::VersionLt
562 | ConditionOperator::VersionLte
563 | ConditionOperator::VersionEq
564 | ConditionOperator::VersionNeq) => compare_versions(value, target_value, op),
565
566 op @ (ConditionOperator::Any
568 | ConditionOperator::NoneOf
569 | ConditionOperator::StrStartsWithAny
570 | ConditionOperator::StrEndsWithAny
571 | ConditionOperator::StrContainsAny
572 | ConditionOperator::StrContainsNone
573 | ConditionOperator::AnyCaseSensitive
574 | ConditionOperator::NoneCaseSensitive) => {
575 compare_strings_in_array(value, target_value, op)
576 }
577 ConditionOperator::StrMatches => compare_str_with_regex(value, target_value),
578
579 op @ (ConditionOperator::Before | ConditionOperator::After | ConditionOperator::On) => {
581 compare_time(value, target_value, op)
582 }
583
584 ConditionOperator::Eq => target_value.is_equal_to_user_value(value),
586 ConditionOperator::Neq => !target_value.is_equal_to_user_value(value),
587
588 op @ (ConditionOperator::InSegmentList | ConditionOperator::NotInSegmentList) => {
590 evaluate_id_list(ctx, op, target_value, value)
591 }
592
593 op @ (ConditionOperator::ArrayContainsAny
594 | ConditionOperator::ArrayContainsNone
595 | ConditionOperator::ArrayContainsAll
596 | ConditionOperator::NotArrayContainsAll) => compare_arrays(value, target_value, op),
597
598 ConditionOperator::Unknown => {
599 log_w!(
600 TAG,
601 "Unsupported - Unknown operator: {}",
602 condition.operator.as_deref().unwrap_or_default()
603 );
604 ctx.result.unsupported = true;
605 return Ok(());
606 }
607 };
608
609 Ok(())
610}
611
612fn evaluate_id_list(
613 ctx: &mut EvaluatorContext<'_>,
614 op: ConditionOperator,
615 target_value: EvaluatorValueRef<'_>,
616 value: UserValueRef<'_>,
617) -> bool {
618 let is_in_list = is_in_id_list(ctx, target_value, value);
619
620 if matches!(op, ConditionOperator::NotInSegmentList) {
621 return !is_in_list;
622 }
623
624 is_in_list
625}
626
627fn is_in_id_list(
628 ctx: &mut EvaluatorContext<'_>,
629 target_value: EvaluatorValueRef<'_>,
630 value: UserValueRef<'_>,
631) -> bool {
632 let list_name = unwrap_or_return!(target_value.string_value(), false);
633 let dyn_str = unwrap_or_return!(value.string_value(), false);
634 let hashed = ctx.hashing.sha256(dyn_str);
635 let lookup_id: String = hashed.chars().take(8).collect();
636
637 match ctx.id_list_resolver {
638 IdListResolution::MapLookup(id_lists) => {
639 let list = unwrap_or_return!(id_lists.get(list_name), false);
640
641 list.ids.contains(&lookup_id)
642 }
643 IdListResolution::Callback(callback) => callback(list_name, lookup_id.as_str()),
644 }
645}
646
647fn evaluate_experiment_group<'a>(
648 ctx: &mut EvaluatorContext<'a>,
649 experiment_name: &Option<DynamicString>,
650) -> Option<String> {
651 let exp_name = match experiment_name {
652 Some(name) => &name.value,
653 None => {
654 return None;
655 }
656 };
657 let statsig = match &ctx.statsig {
658 Some(s) => s,
659 None => {
660 ctx.result.unsupported = true;
661 return None;
662 }
663 };
664 let res = ctx.user.with_public_user(|user| {
665 statsig.get_experiment_with_options(
666 user,
667 exp_name.as_str(),
668 ExperimentEvaluationOptions {
669 disable_exposure_logging: ctx.disable_exposure_logging,
670 user_persisted_values: None,
671 },
672 )
673 });
674 res.group_name
675}
676
677fn evaluate_nested_gate<'a>(
678 ctx: &mut EvaluatorContext<'a>,
679 target_value: EvaluatorValueRef<'a>,
680 is_fail_gate: bool,
681) -> Result<(), StatsigErr> {
682 let gate_name = match target_value.interned_string_value() {
683 Some(name) => name,
684 None => InternedString::empty_ref().into(),
685 };
686
687 match gate_name.get_from(&ctx.nested_gate_memo) {
688 Some((previous_bool, previous_rule_id, previous_secondary_exposures)) => {
689 ctx.result.bool_value = *previous_bool;
690 ctx.result.rule_id = previous_rule_id.clone();
691 ctx.result
692 .secondary_exposures
693 .extend_from_slice(previous_secondary_exposures);
694 }
695 None => {
696 let parent_nested_count = ctx.nested_count;
697 if let Err(error) = ctx.prep_for_nested_evaluation() {
698 ctx.nested_count = parent_nested_count;
699 return Err(error);
700 }
701
702 let parent_exposures = std::mem::take(&mut ctx.result.secondary_exposures);
703
704 let gate_name_owned = gate_name.to_interned();
705 let recognition = Evaluator::evaluate_with_name(ctx, &gate_name_owned, &SpecType::Gate);
706 ctx.nested_count = parent_nested_count;
707
708 let recognition = match recognition {
709 Ok(recognition) => recognition,
710 Err(error) => {
711 ctx.result.secondary_exposures = parent_exposures;
712 return Err(error);
713 }
714 };
715
716 let mut nested_exposures = std::mem::take(&mut ctx.result.secondary_exposures);
717
718 if recognition == Recognition::Unrecognized {
719 ctx.result.bool_value = false;
720 ctx.result.rule_id = None;
721 }
722
723 if ctx.result.unsupported {
724 ctx.result.secondary_exposures = parent_exposures;
725 return Ok(());
726 }
727
728 if !gate_name.as_str().is_empty() {
729 ctx.nested_gate_memo.insert(
730 gate_name.to_interned(),
731 (
732 ctx.result.bool_value,
733 ctx.result.rule_id.clone(),
734 nested_exposures.clone(),
735 ),
736 );
737 }
738
739 ctx.result.secondary_exposures = parent_exposures;
740 ctx.result.secondary_exposures.append(&mut nested_exposures);
741 }
742 }
743
744 let is_empty_rule_id = match &ctx.result.rule_id {
745 Some(id) => id.as_str().is_empty(),
746 None => true,
747 };
748
749 if !gate_name.as_str().starts_with("segment:") && !is_empty_rule_id {
750 let res = &ctx.result;
751 let expo = SecondaryExposure {
752 gate: gate_name.to_interned(),
753 gate_value: InternedString::from_bool(res.bool_value),
754 rule_id: res.rule_id.clone().unwrap_or_default(),
755 };
756
757 if res.sampling_rate.is_none() {
758 ctx.result.has_seen_analytical_gates = Option::from(true);
759 }
760
761 ctx.result.secondary_exposures.push(expo);
762 }
763
764 if is_fail_gate {
765 ctx.result.bool_value = !ctx.result.bool_value;
766 }
767 Ok(())
768}
769
770fn evaluate_config_delegate<'a>(
771 ctx: &mut EvaluatorContext<'a>,
772 rule: RuleRef<'a>,
773) -> Result<bool, StatsigErr> {
774 let delegate = unwrap_or_return!(rule.config_delegate(), Ok(false));
775 let delegate_spec = unwrap_or_return!(
776 delegate.get_from(&ctx.specs_data.dynamic_configs.0),
777 Ok(false)
778 );
779 let delegate_owned = delegate.to_interned();
780
781 ctx.result.undelegated_secondary_exposures = Some(ctx.result.secondary_exposures.clone());
782
783 ctx.prep_for_nested_evaluation()?;
784 let recognition = Evaluator::evaluate_with_name(ctx, &delegate_owned, &SpecType::Experiment)?;
785 if recognition == Recognition::Unrecognized {
786 ctx.result.undelegated_secondary_exposures = None;
787 return Ok(false);
788 }
789
790 ctx.result.explicit_parameters = delegate_spec.view().explicit_parameters();
791 ctx.result.config_delegate = Some(delegate_owned);
792
793 Ok(true)
794}
795
796fn evaluate_pass_percentage(
797 ctx: &mut EvaluatorContext,
798 rule: RuleRef<'_>,
799 spec_salt: InternedStrRef<'_>,
800) -> bool {
801 let pass_percentage = rule.pass_percentage();
802 if pass_percentage == 100f64 {
803 return true;
804 }
805
806 if pass_percentage == 0f64 {
807 return false;
808 }
809
810 let rule_salt = rule.salt().unwrap_or_else(|| rule.id()).as_str();
811 let unit_id = get_unit_id(ctx, rule.id_type());
812 let input = format!("{}.{rule_salt}.{unit_id}", spec_salt.as_str());
813 match ctx.hashing.evaluation_hash(&input) {
814 Some(hash) => ((hash % 10000) as f64) < pass_percentage * 100.0,
815 None => false,
816 }
817}
818
819fn get_hash_for_user_bucket(ctx: &mut EvaluatorContext, condition: &Condition) -> DynamicValue {
820 let unit_id = get_unit_id(ctx, (&condition.id_type).into());
821
822 let mut salt = InternedString::empty_ref();
823
824 if let Some(add_values) = &condition.additional_values {
825 if let Some(v) = add_values.get(&SALT) {
826 salt = v;
827 }
828 }
829
830 let input = format!("{salt}.{unit_id}");
831 let hash = ctx.hashing.evaluation_hash(&input).unwrap_or(1);
832 dyn_value!(hash % 1000)
833}
834
835fn evaluate_param_store_reason(
836 ctx: &mut EvaluatorContext,
837 spec_name: String,
838) -> Result<Recognition, StatsigErr> {
839 let spec_name_intern = InternedString::from_str_ref(&spec_name);
840 let has_param_store = ctx
841 .specs_data
842 .param_stores
843 .as_ref()
844 .and_then(|stores| stores.get(&spec_name_intern))
845 .is_some();
846 Ok(if has_param_store {
847 Recognition::Recognized
848 } else {
849 Recognition::Unrecognized
850 })
851}