Skip to content

Commit e4cca14

Browse files
Bug 2045565 - Do not automatically enroll in Firefox Labs opt-ins (#7403)
1 parent a38da93 commit e4cca14

5 files changed

Lines changed: 357 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# v153.0 (In progress)
22

3+
## ⚠️ Breaking Changes ⚠️
4+
5+
## Nimbus
6+
7+
- Enrollment change events (visible to the UDL) now include the feature IDs of the features involved when possible. ([#7391](https://github.com/mozilla/application-services/pull/7391/))
8+
39
## 🔧 What's Fixed 🔧
410

511
### Logins
@@ -8,13 +14,18 @@
814

915
### Nimbus
1016

11-
- Enrollment change events (visible to the UDL) now include the feature IDs of the features involved when possible. ([#7391](https://github.com/mozilla/application-services/pull/7391/))
1217
- Fixed a bug where enrollment change events were not emitted for rollouts that re-enrolled after previously unenrolling. ([#7391](https://github.com/mozilla/application-services/pull/7391/))
1318
- Attempting to opt-out from an experiment that is not currently enrolled is now a no-op. ([#7399](https://github.com/mozilla/application-services/pull/7399))
14-
- There are new separate DisqualifiedReason and NotEnrolledReason for global (experiment and rollout) opt-outs. ([#7400](https://github.com/mozilla/application-services/pull/7400))
1519
- All enrollment updates (opt-in, opt-out, reseting telemetry identifiers, unenrolling for pref conflicts) now trigger feature invalidation in Android clients. ([#7405](https://github.com/mozilla/application-services/pull/7405))
1620
- Changing experiment and/or rollout participation no longer triggers a double update. Enrollment change telemetry is now appropriately triggered when this occurs. ([#7401](https://github.com/mozilla/application-services/pull/7401))
21+
22+
## ✨ What's New ✨
23+
24+
### Nimbus
25+
26+
- There are new separate DisqualifiedReason and NotEnrolledReason for global (experiment and rollout) opt-outs. ([#7400](https://github.com/mozilla/application-services/pull/7400))
1727
- The Kotlin client now has per-feature update notifications. ([#7354](https://github.com/mozilla/application-services/pull/7354))
28+
- The Nimbus client now understands Firefox Labs opt-ins and will not automatically enroll in them. ([#7403](https://github.com/mozilla/application-services/pull/7403))
1829

1930
[Full Changelog](In progress)
2031

components/nimbus/src/enrollment.rs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pub enum EnrolledReason {
3030
Qualified,
3131
/// Explicit opt-in.
3232
OptIn,
33+
/// Opted-in via Firefox Labs
34+
FirefoxLabsOptIn,
3335
}
3436

3537
impl Display for EnrolledReason {
@@ -38,6 +40,7 @@ impl Display for EnrolledReason {
3840
match self {
3941
EnrolledReason::Qualified => "Qualified",
4042
EnrolledReason::OptIn => "OptIn",
43+
EnrolledReason::FirefoxLabsOptIn => "FirefoxLabsOptIn",
4144
},
4245
f,
4346
)
@@ -68,6 +71,9 @@ pub enum NotEnrolledReason {
6871
/// The user opted-out of rollouts before we ever got enrolled to this one.
6972
RolloutsOptOut,
7073

74+
/// This is a Firefox Labs opt-in and we have not opted-in.
75+
FirefoxLabs,
76+
7177
/// This state represents several cases:
7278
///
7379
/// - this is an experiment and the user globally opted out of experiments
@@ -99,6 +105,7 @@ impl Display for NotEnrolledReason {
99105
NotEnrolledReason::NotTargeted => "NotTargeted",
100106
NotEnrolledReason::ExperimentsOptOut => "ExperimentsOptOut",
101107
NotEnrolledReason::RolloutsOptOut => "RolloutsOptOut",
108+
NotEnrolledReason::FirefoxLabs => "FirefoxLabs",
102109
NotEnrolledReason::OptOut => "OptOut",
103110
},
104111
f,
@@ -230,6 +237,18 @@ impl ExperimentEnrollment {
230237
targeting_helper: &NimbusTargetingHelper,
231238
out_enrollment_events: &mut Vec<EnrollmentChangeEvent>,
232239
) -> Result<Self> {
240+
// The rollout opt-out does not apply to Firefox Labs, since the user
241+
// must consent to each opt-in.
242+
#[cfg(feature = "stateful")]
243+
if experiment.is_firefox_labs_opt_in {
244+
return Ok(Self {
245+
slug: experiment.slug.clone(),
246+
status: EnrollmentStatus::NotEnrolled {
247+
reason: NotEnrolledReason::FirefoxLabs,
248+
},
249+
});
250+
}
251+
233252
Ok(if !is_user_participating {
234253
Self {
235254
slug: experiment.slug.clone(),
@@ -306,6 +325,16 @@ impl ExperimentEnrollment {
306325
#[cfg(feature = "stateful")] gecko_pref_store: Option<&GeckoPrefStore>,
307326
out_enrollment_events: &mut Vec<EnrollmentChangeEvent>,
308327
) -> Result<Self> {
328+
#[cfg(feature = "stateful")]
329+
if updated_experiment.is_firefox_labs_opt_in && !self.status.is_enrolled() {
330+
return Ok(self.clone());
331+
}
332+
333+
// Users can globally opt out of rollouts, but not out of Firefox Labs.
334+
#[cfg(feature = "stateful")]
335+
let is_user_participating =
336+
is_user_participating || updated_experiment.is_firefox_labs_opt_in;
337+
309338
Ok(match &self.status {
310339
EnrollmentStatus::NotEnrolled { .. } | EnrollmentStatus::Error { .. } => {
311340
if !is_user_participating || updated_experiment.is_enrollment_paused {
@@ -822,9 +851,7 @@ impl EnrollmentStatus {
822851
}
823852
.into()
824853
}
825-
}
826854

827-
impl EnrollmentStatus {
828855
// Note that for now, we only support a single feature_id per experiment,
829856
// so this code is expected to shift once we start supporting multiple.
830857
pub fn new_enrolled(reason: EnrolledReason, branch: &str) -> Self {
@@ -836,8 +863,6 @@ impl EnrollmentStatus {
836863
}
837864
}
838865

839-
// This is used in examples, but not in the main dylib, and
840-
// triggers a dead code warning when building with `--release`.
841866
pub fn is_enrolled(&self) -> bool {
842867
matches!(self, EnrollmentStatus::Enrolled { .. })
843868
}
@@ -1181,11 +1206,9 @@ impl<'a> EnrollmentsEvolver<'a> {
11811206
out_enrollment_events: &mut Vec<EnrollmentChangeEvent>, // out param containing the events we'd like to emit to glean.
11821207
#[cfg(feature = "stateful")] gecko_pref_store: Option<&GeckoPrefStore>,
11831208
) -> Result<Option<ExperimentEnrollment>> {
1184-
let is_already_enrolled = if let Some(enrollment) = prev_enrollment {
1185-
enrollment.status.is_enrolled()
1186-
} else {
1187-
false
1188-
};
1209+
let is_already_enrolled = prev_enrollment
1210+
.map(|e| e.status.is_enrolled())
1211+
.unwrap_or_default();
11891212

11901213
// XXX This is not pretty, however, we need to re-write the way sticky targeting strings are generated in
11911214
// experimenter. Once https://github.com/mozilla/experimenter/issues/8661 is fixed, we can remove the calculation

components/nimbus/src/schema.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
use std::collections::BTreeSet;
5+
use std::collections::{BTreeSet, HashMap};
66

77
use serde_derive::{Deserialize, Serialize};
88
use serde_json::{Map, Value};
@@ -55,6 +55,20 @@ pub struct Experiment {
5555
pub published_date: Option<chrono::DateTime<chrono::Utc>>,
5656
// N.B. records in RemoteSettings will have `id` and `filter_expression` fields,
5757
// but we ignore them because they're for internal use by RemoteSettings.
58+
#[serde(default)]
59+
pub is_firefox_labs_opt_in: bool,
60+
61+
#[serde(default)]
62+
pub firefox_labs_title: Option<String>,
63+
64+
#[serde(default)]
65+
pub firefox_labs_description: Option<String>,
66+
67+
#[serde(default)]
68+
pub firefox_labs_description_links: Option<HashMap<String, String>>,
69+
70+
#[serde(default)]
71+
pub requires_restart: bool,
5872
}
5973

6074
#[cfg_attr(not(feature = "stateful"), allow(unused))]

components/nimbus/src/tests/helpers.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,51 @@ pub(crate) fn get_experiment_with_published_date(
733733
.unwrap()
734734
}
735735

736+
#[allow(unused)]
737+
pub(crate) fn get_firefox_labs(slug: &str) -> Experiment {
738+
serde_json::from_value(json!({
739+
"schemaVersion": "1.0.0",
740+
"appName": "fenix",
741+
"appId": "org.mozilla.fenix",
742+
"channel": "nightly",
743+
"slug": slug,
744+
"branches": [
745+
{
746+
"slug": "control",
747+
"ratio": 1,
748+
"features": [
749+
{
750+
"featureId": "labs-feature",
751+
"value": {}
752+
}
753+
]
754+
}
755+
],
756+
"featureIds": ["labs-feature"],
757+
"isRollout": true,
758+
"isEnrollmentPaused": false,
759+
"isFirefoxLabsOptIn": true,
760+
"firefoxLabsTitle": "labs-title",
761+
"firefoxLabsDescription": "labs-description",
762+
"firefoxLabsDescriptionLinks": {
763+
"connect": "https://example.com/#connect",
764+
"learn-more": "https://example.com/#learn-more",
765+
},
766+
"userFacingName": "Test Firefox Labs",
767+
"userFacingDescription": "Test Firefox Labs",
768+
"bucketConfig": {
769+
"randomizationUnit": "nimbus_id",
770+
"start": 0,
771+
"count": 10000,
772+
"total": 10000,
773+
"namespace": "firefox-labs-test"
774+
},
775+
"targeting": "true",
776+
"proposedEnrollment": 0,
777+
}))
778+
.unwrap()
779+
}
780+
736781
#[cfg(feature = "stateful")]
737782
mod detail {
738783
use super::TestMetrics;

0 commit comments

Comments
 (0)