Skip to content

Commit b3d8cc2

Browse files
Bug 1997627 - Add NotEnrolledReason::DifferentAppName and NotEnrolledReason::DifferentChannel (#7050)
1 parent df1ee1c commit b3d8cc2

File tree

5 files changed

+96
-46
lines changed

5 files changed

+96
-46
lines changed

components/nimbus/src/enrollment.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,33 @@ impl Display for EnrolledReason {
4848
// ⚠️ in `mod test_schema_bw_compat` below, and may require a DB migration. ⚠️
4949
#[derive(Deserialize, Serialize, Debug, Clone, Hash, Eq, PartialEq)]
5050
pub enum NotEnrolledReason {
51-
/// The user opted-out of experiments before we ever got enrolled to this one.
52-
OptOut,
53-
/// The evaluator bucketing did not choose us.
54-
NotSelected,
55-
/// We are not being targeted for this experiment.
56-
NotTargeted,
51+
/// The experiment targets a different application.
52+
DifferentAppName,
53+
/// The experiment targets a different channel.
54+
DifferentChannel,
5755
/// The experiment enrollment is paused.
5856
EnrollmentsPaused,
5957
/// The experiment used a feature that was already under experiment.
6058
FeatureConflict,
59+
/// The evaluator bucketing did not choose us.
60+
NotSelected,
61+
/// We are not being targeted for this experiment.
62+
NotTargeted,
63+
/// The user opted-out of experiments before we ever got enrolled to this one.
64+
OptOut,
6165
}
6266

6367
impl Display for NotEnrolledReason {
6468
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
6569
Display::fmt(
6670
match self {
67-
NotEnrolledReason::OptOut => "OptOut",
68-
NotEnrolledReason::NotSelected => "NotSelected",
69-
NotEnrolledReason::NotTargeted => "NotTargeted",
71+
NotEnrolledReason::DifferentAppName => "DifferentAppName",
72+
NotEnrolledReason::DifferentChannel => "DifferentChannel",
7073
NotEnrolledReason::EnrollmentsPaused => "EnrollmentsPaused",
7174
NotEnrolledReason::FeatureConflict => "FeatureConflict",
75+
NotEnrolledReason::NotSelected => "NotSelected",
76+
NotEnrolledReason::NotTargeted => "NotTargeted",
77+
NotEnrolledReason::OptOut => "OptOut",
7278
},
7379
f,
7480
)
@@ -273,6 +279,12 @@ impl ExperimentEnrollment {
273279
updated_enrollment
274280
}
275281
EnrollmentStatus::NotEnrolled {
282+
reason: NotEnrolledReason::DifferentAppName,
283+
}
284+
| EnrollmentStatus::NotEnrolled {
285+
reason: NotEnrolledReason::DifferentChannel,
286+
}
287+
| EnrollmentStatus::NotEnrolled {
276288
reason: NotEnrolledReason::NotTargeted,
277289
} => {
278290
debug!("Existing experiment enrollment '{}' is now disqualified (targeting change)", &self.slug);

components/nimbus/src/evaluator.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,10 @@ pub fn evaluate_enrollment(
7272
exp: &Experiment,
7373
th: &NimbusTargetingHelper,
7474
) -> Result<ExperimentEnrollment> {
75-
if !is_experiment_available(th, exp, true) {
75+
if let ExperimentAvailable::Unavailable { reason } = is_experiment_available(th, exp, true) {
7676
return Ok(ExperimentEnrollment {
7777
slug: exp.slug.clone(),
78-
status: EnrollmentStatus::NotEnrolled {
79-
reason: NotEnrolledReason::NotTargeted,
80-
},
78+
status: EnrollmentStatus::NotEnrolled { reason },
8179
});
8280
}
8381

@@ -129,6 +127,17 @@ pub fn evaluate_enrollment(
129127
})
130128
}
131129

130+
/// Whether or not an experiment is available.
131+
#[derive(Debug, Eq, PartialEq)]
132+
pub enum ExperimentAvailable {
133+
/// The experiment is available (i.e., it is for this application and channel).
134+
Available,
135+
136+
/// The experiment is not available (i.e., it is either not for this
137+
/// application or not for this channel).
138+
Unavailable { reason: NotEnrolledReason },
139+
}
140+
132141
/// Check if an experiment is available for this app defined by this `AppContext`.
133142
///
134143
/// # Arguments:
@@ -144,20 +153,22 @@ pub fn is_experiment_available(
144153
th: &NimbusTargetingHelper,
145154
exp: &Experiment,
146155
is_release: bool,
147-
) -> bool {
156+
) -> ExperimentAvailable {
148157
// Verify the app_name matches the application being targeted
149158
// by the experiment.
150159
match (&exp.app_name, th.context.get("app_name".to_string())) {
151160
(Some(exp), Some(Value::String(mine))) => {
152161
if !exp.eq(mine) {
153-
return false;
162+
return ExperimentAvailable::Unavailable {
163+
reason: NotEnrolledReason::DifferentAppName,
164+
};
154165
}
155166
}
156167
(_, _) => debug!("Experiment missing app_name, skipping it as a targeting parameter"),
157168
}
158169

159170
if !is_release {
160-
return true;
171+
return ExperimentAvailable::Available;
161172
}
162173

163174
// Verify the channel matches the application being targeted
@@ -166,12 +177,15 @@ pub fn is_experiment_available(
166177
match (&exp.channel, th.context.get("channel".to_string())) {
167178
(Some(exp), Some(Value::String(mine))) => {
168179
if !exp.to_lowercase().eq(&mine.to_lowercase()) {
169-
return false;
180+
return ExperimentAvailable::Unavailable {
181+
reason: NotEnrolledReason::DifferentChannel,
182+
};
170183
}
171184
}
172185
(_, _) => debug!("Experiment missing channel, skipping it as a targeting parameter"),
173186
}
174-
true
187+
188+
ExperimentAvailable::Available
175189
}
176190

177191
/// Chooses a branch randomly from a set of branches

components/nimbus/src/stateful/nimbus_client.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::{
1313
error::{info, warn, BehaviorError},
1414
evaluator::{
1515
get_calculated_attributes, is_experiment_available, CalculatedAttributes,
16-
TargetingAttributes,
16+
ExperimentAvailable, TargetingAttributes,
1717
},
1818
json::{JsonObject, PrefValue},
1919
metrics::{
@@ -322,7 +322,9 @@ impl NimbusClient {
322322
Ok(self
323323
.get_all_experiments()?
324324
.into_iter()
325-
.filter(|exp| is_experiment_available(&th, exp, false))
325+
.filter(|exp| {
326+
is_experiment_available(&th, exp, false) == ExperimentAvailable::Available
327+
})
326328
.map(|exp| exp.into())
327329
.collect())
328330
}
@@ -952,22 +954,20 @@ impl NimbusClient {
952954
self.event_store.clone(),
953955
self.gecko_prefs.clone(),
954956
);
955-
let experiments = self
956-
.database_cache
957-
.get_experiments()?
957+
let experiments = self.database_cache.get_experiments()?;
958+
let experiments = experiments
958959
.iter()
959-
.filter_map(
960-
|exp| match is_experiment_available(&targeting_helper, exp, true) {
961-
true => Some(exp.slug.clone()),
962-
false => None,
963-
},
964-
)
965-
.collect::<HashSet<String>>();
960+
.filter(|exp| {
961+
is_experiment_available(&targeting_helper, exp, true)
962+
== ExperimentAvailable::Available
963+
})
964+
.map(|exp| &*exp.slug)
965+
.collect::<HashSet<&str>>();
966966
self.metrics_handler.record_enrollment_statuses(
967967
self.database_cache
968968
.get_enrollments()?
969969
.into_iter()
970-
.filter_map(|e| match experiments.contains(&e.slug) {
970+
.filter_map(|e| match experiments.contains(&*e.slug) {
971971
true => Some(e.into()),
972972
false => None,
973973
})

components/nimbus/src/tests/test_enrollment.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,7 @@ fn test_evolver_experiment_update_enrolled_then_targeting_changed() -> Result<()
764764
&mut events,
765765
)?
766766
.unwrap();
767+
767768
if let EnrollmentStatus::Disqualified {
768769
reason: DisqualifiedReason::NotTargeted,
769770
branch,

components/nimbus/src/tests/test_evaluator.rs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use crate::{
77
enrollment::{EnrolledReason, EnrollmentStatus, NotEnrolledReason},
88
evaluate_enrollment,
9-
evaluator::{choose_branch, is_experiment_available, targeting},
9+
evaluator::{choose_branch, is_experiment_available, targeting, ExperimentAvailable},
1010
AppContext, AvailableRandomizationUnits, Branch, BucketConfig, Experiment, RandomizationUnit,
1111
Result, TargetingAttributes,
1212
};
@@ -411,26 +411,50 @@ fn test_is_experiment_available() {
411411
.into();
412412
// If is_release is true, we should match on the exact combination of
413413
// app_name, channel and app_id.
414-
assert!(!is_experiment_available(&th, &experiment, true));
414+
assert_eq!(
415+
is_experiment_available(&th, &experiment, true),
416+
ExperimentAvailable::Unavailable {
417+
reason: NotEnrolledReason::DifferentChannel
418+
}
419+
);
415420

416421
// If is_release is false, we only match on app_name.
417422
// As a nightly build, we want to be able to test production experiments
418-
assert!(is_experiment_available(&th, &experiment, false));
423+
assert_eq!(
424+
is_experiment_available(&th, &experiment, false),
425+
ExperimentAvailable::Available
426+
);
419427

420428
let experiment = Experiment {
421429
channel: Some("nightly".to_string()),
422430
..experiment
423431
};
424432
// channels now match, so should be available for enrollment (true) and testing (false)
425-
assert!(is_experiment_available(&th, &experiment, true));
426-
assert!(is_experiment_available(&th, &experiment, false));
433+
assert_eq!(
434+
is_experiment_available(&th, &experiment, true),
435+
ExperimentAvailable::Available
436+
);
437+
assert_eq!(
438+
is_experiment_available(&th, &experiment, false),
439+
ExperimentAvailable::Available
440+
);
427441

428442
let experiment = Experiment {
429443
app_name: Some("a_different_app".to_string()),
430444
..experiment
431445
};
432-
assert!(!is_experiment_available(&th, &experiment, false));
433-
assert!(!is_experiment_available(&th, &experiment, false));
446+
assert_eq!(
447+
is_experiment_available(&th, &experiment, true),
448+
ExperimentAvailable::Unavailable {
449+
reason: NotEnrolledReason::DifferentAppName
450+
}
451+
);
452+
assert_eq!(
453+
is_experiment_available(&th, &experiment, false),
454+
ExperimentAvailable::Unavailable {
455+
reason: NotEnrolledReason::DifferentAppName
456+
}
457+
);
434458
}
435459

436460
#[test]
@@ -635,31 +659,30 @@ fn test_not_targeted_for_enrollment() {
635659
&ctx.clone().into(),
636660
)
637661
.unwrap();
638-
assert!(matches!(
662+
assert_eq!(
639663
enrollment.status,
640664
EnrollmentStatus::NotEnrolled {
641-
reason: NotEnrolledReason::NotTargeted
665+
reason: NotEnrolledReason::DifferentAppName
642666
}
643-
));
667+
);
644668

645669
// Change the app_name back and change the channel to test when it doesn't match:
646670
ctx.app_name = "NimbusTest".to_string();
647671
ctx.channel = "Wrong".to_string();
648672

649-
// Now we won't be enrolled in the experiment because we don't have the right channel, but with the same
650-
// `NotTargeted` reason
673+
// Now we won't be enrolled in the experiment because we don't have the right channel
651674
let enrollment = evaluate_enrollment(
652675
&AvailableRandomizationUnits::with_nimbus_id(&id),
653676
&experiment,
654677
&ctx.into(),
655678
)
656679
.unwrap();
657-
assert!(matches!(
680+
assert_eq!(
658681
enrollment.status,
659682
EnrollmentStatus::NotEnrolled {
660-
reason: NotEnrolledReason::NotTargeted
683+
reason: NotEnrolledReason::DifferentChannel
661684
}
662-
));
685+
);
663686
}
664687

665688
#[test]

0 commit comments

Comments
 (0)