commit 9b32a29ee6bd3a64aec22f1dfd399ca8a6d3cce9
parent 5a5d32606016abf413fe9091dce3d2e90777938b
Author: Beth Rennie <beth@brennie.ca>
Date: Fri, 9 Jan 2026 21:10:30 +0000
Bug 2003350 - Add a separate opt-out for rollouts r=nimbus-reviewers,relud
Differential Revision: https://phabricator.services.mozilla.com/D277367
Diffstat:
9 files changed, 373 insertions(+), 175 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
@@ -1744,6 +1744,7 @@ pref("services.sync.prefs.sync.media.eme.enabled", true);
pref("services.sync.prefs.sync-seen.media.eme.enabled", false);
pref("services.sync.prefs.sync.media.videocontrols.picture-in-picture.video-toggle.enabled", true);
pref("services.sync.prefs.sync.network.cookie.cookieBehavior", true);
+pref("services.sync.prefs.sync.nimbus.rollouts.enabled", true);
pref("services.sync.prefs.sync.permissions.default.image", true);
pref("services.sync.prefs.sync.pref.downloads.disable_button.edit_actions", true);
pref("services.sync.prefs.sync.pref.privacy.disable_button.cookie_exceptions", true);
@@ -2164,6 +2165,9 @@ pref("nimbus.profilesdatastoreservice.sync.enabled", false);
pref("nimbus.telemetry.targetingContextEnabled", true);
#endif
+// Enable Rollouts by default.
+pref("nimbus.rollouts.enabled", true);
+
// Nimbus QA prefs. Used to monitor pref-setting test experiments.
pref("nimbus.qa.pref-1", "default");
pref("nimbus.qa.pref-2", "default");
diff --git a/toolkit/components/nimbus/ExperimentAPI.sys.mjs b/toolkit/components/nimbus/ExperimentAPI.sys.mjs
@@ -37,6 +37,7 @@ const CRASHREPORTER_ENABLED =
const IS_MAIN_PROCESS =
Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
+const ROLLOUTS_ENABLED_PREF = "nimbus.rollouts.enabled";
const UPLOAD_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
const STUDIES_OPT_OUT_PREF = "app.shield.optoutstudies.enabled";
@@ -148,6 +149,13 @@ export const ExperimentAPI = new (class {
*/
#prefValues = {
/**
+ * Whether or not rollouts are enabled.
+ *
+ * @see {@link ROLLOUTS_ENABLED_PREF}
+ */
+ rolloutsEnabled: false,
+
+ /**
* Whether or not opt-out studies are enabled.
*
* @see {@link STUDIES_OPT_OUT_PREF}
@@ -162,6 +170,11 @@ export const ExperimentAPI = new (class {
telemetryEnabled: false,
};
+ /**
+ * Whether or not studies are enabled.
+ *
+ * @see {@link studiesEnabled}
+ */
#studiesEnabled = false;
constructor() {
@@ -183,7 +196,7 @@ export const ExperimentAPI = new (class {
}
}
- this._onStudiesEnabledChanged = this._onStudiesEnabledChanged.bind(this);
+ this._onEnabledPrefChange = this._onEnabledPrefChange.bind(this);
this._annotateCrashReport = this._annotateCrashReport.bind(this);
this._removeCrashReportAnnotator =
this._removeCrashReportAnnotator.bind(this);
@@ -194,8 +207,7 @@ export const ExperimentAPI = new (class {
}
/**
- * The topic that is notified when either the studies enabled pref or the
- * telemetry enabled pref changes.
+ * The topic that is notified when the Nimbus enabled state changes.
*
* Consumers can listen for notifications on this topic to react to
* Nimbus being enabled or disabled.
@@ -233,7 +245,6 @@ export const ExperimentAPI = new (class {
// state to change during ExperimentAPI initialization, but we do not
// register our observers until the end of this function.
this.#computeEnabled();
- const studiesEnabled = this.studiesEnabled;
try {
await lazy.NimbusMigrations.applyMigrations(
@@ -248,6 +259,8 @@ export const ExperimentAPI = new (class {
);
}
+ this.#computeEnabled();
+
try {
await this.manager.store.init();
} catch (e) {
@@ -300,19 +313,18 @@ export const ExperimentAPI = new (class {
}
Services.prefs.addObserver(
- STUDIES_OPT_OUT_PREF,
- this._onStudiesEnabledChanged
- );
- Services.prefs.addObserver(
- UPLOAD_ENABLED_PREF,
- this._onStudiesEnabledChanged
+ ROLLOUTS_ENABLED_PREF,
+ this._onEnabledPrefChange
);
+ Services.prefs.addObserver(STUDIES_OPT_OUT_PREF, this._onEnabledPrefChange);
+ Services.prefs.addObserver(UPLOAD_ENABLED_PREF, this._onEnabledPrefChange);
// If Nimbus was disabled between the start of this function and registering
// the pref observers we have not handled it yet.
- if (studiesEnabled !== this.studiesEnabled) {
- await this._onStudiesEnabledChanged();
- }
+ //
+ // If the enabled state hasn't actually changed, calling this function is a
+ // no-op.
+ await this._onEnabledPrefChange();
return true;
}
@@ -368,18 +380,26 @@ export const ExperimentAPI = new (class {
this.#experimentManager = null;
Services.prefs.removeObserver(
+ ROLLOUTS_ENABLED_PREF,
+ this._onEnabledPrefChange
+ );
+ Services.prefs.removeObserver(
STUDIES_OPT_OUT_PREF,
- this._onStudiesEnabledChanged
+ this._onEnabledPrefChange
);
Services.prefs.removeObserver(
UPLOAD_ENABLED_PREF,
- this._onStudiesEnabledChanged
+ this._onEnabledPrefChange
);
this.#initialized = false;
}
#computeEnabled() {
+ this.#prefValues.rolloutsEnabled = Services.prefs.getBoolPref(
+ ROLLOUTS_ENABLED_PREF,
+ false
+ );
this.#prefValues.studiesEnabled = Services.prefs.getBoolPref(
STUDIES_OPT_OUT_PREF,
false
@@ -396,13 +416,17 @@ export const ExperimentAPI = new (class {
}
get enabled() {
- return this.studiesEnabled || this.labsEnabled;
+ return this.labsEnabled || this.rolloutsEnabled || this.studiesEnabled;
}
get labsEnabled() {
return Services.policies.isAllowed("FirefoxLabs");
}
+ get rolloutsEnabled() {
+ return this.#prefValues.rolloutsEnabled;
+ }
+
get studiesEnabled() {
return this.#studiesEnabled;
}
@@ -472,31 +496,36 @@ export const ExperimentAPI = new (class {
}
}
- async _onStudiesEnabledChanged(_topic, _subject, prefName) {
- const studiesPreviouslyEnabled = this.studiesEnabled;
-
- switch (prefName) {
- case STUDIES_OPT_OUT_PREF:
- case UPLOAD_ENABLED_PREF:
- this.#computeEnabled();
- break;
-
- default:
- return;
- }
-
+ /**
+ * Handle a pref change that may result in Nimbus being enabled or disabled.
+ */
+ async _onEnabledPrefChange() {
if (!this.#initialized) {
return;
}
- if (studiesPreviouslyEnabled !== this.studiesEnabled) {
+ const studiesPreviouslyEnabled = this.studiesEnabled;
+ const rolloutsPreviouslyEnabled = this.rolloutsEnabled;
+
+ this.#computeEnabled();
+
+ const studiesEnabledChanged =
+ studiesPreviouslyEnabled !== this.studiesEnabled;
+ const rolloutsEnabledChanged =
+ rolloutsPreviouslyEnabled !== this.rolloutsEnabled;
+
+ if (studiesEnabledChanged || rolloutsEnabledChanged) {
if (!this.studiesEnabled) {
this.manager._handleStudiesOptOut();
}
+ if (!this.rolloutsEnabled) {
+ this.manager._handleRolloutsOptOut();
+ }
+
// Labs is disabled only by policy, so it cannot be disabled at runtime.
// Thus we only need to notify the RemoteSettingsExperimentLoader when
- // studies become enabled or disabled.
+ // studies or rollouts become enabled or disabled.
await this._rsLoader.onEnabledPrefChange();
Services.obs.notifyObservers(null, this.STUDIES_ENABLED_CHANGED);
diff --git a/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs b/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs
@@ -330,14 +330,18 @@ export class ExperimentManager {
this._prefFlips.init();
- if (!lazy.ExperimentAPI.studiesEnabled) {
- this._handleStudiesOptOut();
- }
-
if (!lazy.ExperimentAPI.labsEnabled) {
this._handleLabsDisabled();
}
+ if (!lazy.ExperimentAPI.rolloutsEnabled) {
+ this._handleRolloutsOptOut();
+ }
+
+ if (!lazy.ExperimentAPI.studiesEnabled) {
+ this._handleStudiesOptOut();
+ }
+
lazy.NimbusFeatures.nimbusTelemetry.onUpdate(() => {
// Providing default values ensure we disable metrics when unenrolling.
const cfg = {
@@ -971,6 +975,7 @@ export class ExperimentManager {
[
UnenrollReason.BUCKETING,
UnenrollReason.TARGETING_MISMATCH,
+ UnenrollReason.ROLLOUTS_OPT_OUT,
UnenrollReason.STUDIES_OPT_OUT,
].includes(enrollment.unenrollReason)
) {
@@ -1067,12 +1072,30 @@ export class ExperimentManager {
}
/**
+ * Unenroll from all active rollouts if user opts out.
+ */
+ _handleRolloutsOptOut() {
+ const enrollments = this.store
+ .getAll()
+ .filter(e => e.active && !e.isFirefoxLabsOptIn && e.isRollout);
+
+ for (const enrollment of enrollments) {
+ this._unenroll(
+ enrollment,
+ UnenrollmentCause.fromReason(
+ lazy.NimbusTelemetry.UnenrollReason.ROLLOUTS_OPT_OUT
+ )
+ );
+ }
+ }
+
+ /**
* Unenroll from all active studies if user opts out.
*/
_handleStudiesOptOut() {
const enrollments = this.store
.getAll()
- .filter(e => e.active && !e.isFirefoxLabsOptIn);
+ .filter(e => e.active && !e.isFirefoxLabsOptIn && !e.isRollout);
for (const enrollment of enrollments) {
this._unenroll(
diff --git a/toolkit/components/nimbus/lib/Migrations.sys.mjs b/toolkit/components/nimbus/lib/Migrations.sys.mjs
@@ -100,6 +100,15 @@ function migrateMultiphase() {
}
}
+/**
+ * Disable rollouts if the user has previously opted out of studies.
+ */
+function migrateSeparateRolloutOptOut() {
+ if (!Services.prefs.getBoolPref("app.shield.optoutstudies.enabled")) {
+ Services.prefs.setBoolPref("nimbus.rollouts.enabled", false);
+ }
+}
+
function migrateNoop() {
// This migration intentionally left blank.
//
@@ -477,6 +486,7 @@ export const NimbusMigrations = {
MIGRATIONS: {
[Phase.INIT_STARTED]: [
migration("multi-phase-migrations", migrateMultiphase),
+ migration("separate-rollout-opt-out", migrateSeparateRolloutOptOut),
],
[Phase.AFTER_STORE_INITIALIZED]: [
diff --git a/toolkit/components/nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs b/toolkit/components/nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs
@@ -119,6 +119,27 @@ export const MatchStatus = Object.freeze({
DISABLED: "DISABLED",
});
+const DeliveryKind = Object.freeze({
+ FIREFOX_LABS_OPT_IN: "firefox-labs-opt-in",
+ ROLLOUT: "rollout",
+ STUDY: "study",
+});
+
+/**
+ * @returns {DeliveryKind}
+ */
+function getDeliveryKind(recipe) {
+ if (recipe.isFirefoxLabsOptIn) {
+ return DeliveryKind.FIREFOX_LABS_OPT_IN;
+ }
+
+ if (recipe.isRollout) {
+ return DeliveryKind.ROLLOUT;
+ }
+
+ return DeliveryKind.STUDY;
+}
+
export const CheckRecipeResult = {
Ok(status) {
return {
@@ -435,6 +456,7 @@ export class RemoteSettingsExperimentLoader {
{
validationEnabled,
labsEnabled: lazy.ExperimentAPI.labsEnabled,
+ rolloutsEnabled: lazy.ExperimentAPI.rolloutsEnabled,
studiesEnabled: lazy.ExperimentAPI.studiesEnabled,
shouldCheckTargeting: true,
unenrolledExperimentSlugs,
@@ -931,16 +953,18 @@ export class EnrollmentsContext {
validationEnabled = true,
shouldCheckTargeting = true,
unenrolledExperimentSlugs,
- studiesEnabled = true,
labsEnabled = true,
+ rolloutsEnabled = true,
+ studiesEnabled = true,
} = {}
) {
this.manager = manager;
this.recipeValidator = recipeValidator;
this.validationEnabled = validationEnabled;
- this.studiesEnabled = studiesEnabled;
this.labsEnabled = labsEnabled;
+ this.rolloutsEnabled = rolloutsEnabled;
+ this.studiesEnabled = studiesEnabled;
this.validatorCache = {};
this.shouldCheckTargeting = shouldCheckTargeting;
@@ -975,9 +999,12 @@ export class EnrollmentsContext {
}
}
+ const deliveryKind = getDeliveryKind(recipe);
if (
- (recipe.isFirefoxLabsOptIn && !this.labsEnabled) ||
- (!recipe.isFirefoxLabsOptIn && !this.studiesEnabled)
+ (deliveryKind === DeliveryKind.FIREFOX_LABS_OPT_IN &&
+ !this.labsEnabled) ||
+ (deliveryKind === DeliveryKind.ROLLOUT && !this.rolloutsEnabled) ||
+ (deliveryKind === DeliveryKind.STUDY && !this.studiesEnabled)
) {
return CheckRecipeResult.Ok(MatchStatus.DISABLED);
}
diff --git a/toolkit/components/nimbus/lib/Telemetry.sys.mjs b/toolkit/components/nimbus/lib/Telemetry.sys.mjs
@@ -92,6 +92,7 @@ const UnenrollReason = Object.freeze({
PREF_VARIABLE_MISSING: "pref-variable-missing",
PREF_VARIABLE_NO_LONGER: "pref-variable-no-longer",
RECIPE_NOT_SEEN: "recipe-not-seen",
+ ROLLOUTS_OPT_OUT: "rollouts-opt-out",
STUDIES_OPT_OUT: "studies-opt-out",
TARGETING_MISMATCH: "targeting-mismatch",
UNENROLLED_IN_ANOTHER_PROFILE: "unenrolled-in-another-profile",
diff --git a/toolkit/components/nimbus/test/NimbusTestUtils.sys.mjs b/toolkit/components/nimbus/test/NimbusTestUtils.sys.mjs
@@ -563,13 +563,23 @@ export const NimbusTestUtils = {
});
},
+ get SEPARATE_ROLLOUT_OPT_OUT() {
+ const { Phase } = lazy.NimbusMigrations;
+
+ return NimbusTestUtils.makeMigrationState({
+ [Phase.INIT_STARTED]: "separate-rollout-opt-out",
+ [Phase.AFTER_STORE_INITIALIZED]: "graduate-firefox-labs-auto-pip",
+ [Phase.AFTER_REMOTE_SETTINGS_UPDATE]: "firefox-labs-enrollments",
+ });
+ },
+
/**
* A migration state that represents all migrations applied.
*
* @type {Record<Phase, number>}
*/
get LATEST() {
- return NimbusTestUtils.migrationState.GRADUATED_FIREFOX_LABS_AUTO_PIP;
+ return NimbusTestUtils.migrationState.SEPARATE_ROLLOUT_OPT_OUT;
},
},
diff --git a/toolkit/components/nimbus/test/unit/test_ExperimentManager_unenroll.js b/toolkit/components/nimbus/test/unit/test_ExperimentManager_unenroll.js
@@ -3,7 +3,12 @@
const { TelemetryEnvironment } = ChromeUtils.importESModule(
"resource://gre/modules/TelemetryEnvironment.sys.mjs"
);
-const STUDIES_OPT_OUT_PREF = "app.shield.optoutstudies.enabled";
+const { FirefoxLabs } = ChromeUtils.importESModule(
+ "resource://nimbus/FirefoxLabs.sys.mjs"
+);
+
+const ROLLOUTS_ENABLED_PREF = "nimbus.rollouts.enabled";
+const STUDIES_ENABLED_PREF = "app.shield.optoutstudies.enabled";
const UPLOAD_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
add_setup(function test_setup() {
@@ -35,14 +40,44 @@ add_task(async function test_set_inactive() {
await cleanup();
});
-add_task(async function test_unenroll_opt_out() {
- Services.prefs.setBoolPref(STUDIES_OPT_OUT_PREF, true);
+async function doOptOutTest({
+ rolloutsEnabled = true,
+ studiesEnabled = true,
+} = {}) {
+ Services.prefs.setBoolPref(ROLLOUTS_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(STUDIES_ENABLED_PREF, true);
+
+ const recipes = {
+ experiment: NimbusTestUtils.factories.recipe.withFeatureConfig(
+ "experiment",
+ { featureId: "no-feature-firefox-desktop" }
+ ),
+ rollout: NimbusTestUtils.factories.recipe.withFeatureConfig(
+ "rollout",
+ { featureId: "no-feature-firefox-desktop" },
+ { isRollout: true }
+ ),
+ optin: NimbusTestUtils.factories.recipe.withFeatureConfig(
+ "optin",
+ { featureId: "no-feature-firefox-desktop" },
+ {
+ isRollout: true,
+ isFirefoxLabsOptIn: true,
+ firefoxLabsTitle: "title",
+ firefoxLabsDescription: "description",
+ firefoxLabsDescriptionLinks: null,
+ firefoxLabsGroup: "group",
+ requiresRestart: false,
+ }
+ ),
+ };
- const { manager, cleanup } = await setupTest();
- const experiment = NimbusTestUtils.factories.recipe.withFeatureConfig("foo", {
- featureId: "testFeature",
+ const { manager, cleanup } = await setupTest({
+ experiments: Object.values(recipes),
});
- await manager.enroll(experiment, "test");
+
+ const labs = await FirefoxLabs.create();
+ await labs.enroll(recipes.optin.slug, "control");
// Check that there aren't any Glean normandy unenrollNimbusExperiment events yet
Assert.equal(
@@ -58,109 +93,88 @@ add_task(async function test_unenroll_opt_out() {
"no Glean unenrollment events before unenrollment"
);
- Services.prefs.setBoolPref(STUDIES_OPT_OUT_PREF, false);
+ Services.prefs.setBoolPref(ROLLOUTS_ENABLED_PREF, rolloutsEnabled);
+ Services.prefs.setBoolPref(STUDIES_ENABLED_PREF, studiesEnabled);
- await NimbusTestUtils.assert.enrollmentExists(experiment.slug, {
- active: false,
+ await NimbusTestUtils.assert.enrollmentExists(recipes.experiment.slug, {
+ active: studiesEnabled,
});
-
Assert.equal(
- manager.store.get(experiment.slug).active,
- false,
- "should set .active to false"
+ manager.store.get(recipes.experiment.slug).active,
+ studiesEnabled
);
- // We expect only one event and that that one event matches the expected enrolled experiment
- Assert.deepEqual(
+ await NimbusTestUtils.assert.enrollmentExists(recipes.rollout.slug, {
+ active: rolloutsEnabled,
+ });
+ Assert.equal(manager.store.get(recipes.rollout.slug).active, rolloutsEnabled);
+
+ await NimbusTestUtils.assert.enrollmentExists(recipes.optin.slug, {
+ active: true,
+ });
+ Assert.ok(manager.store.get(recipes.optin.slug).active);
+
+ const normandyEvents =
Glean.normandy.unenrollNimbusExperiment
.testGetValue("events")
- .map(ev => ev.extra),
- [
- {
- value: experiment.slug,
- branch: experiment.branches[0].slug,
- reason: "studies-opt-out",
- },
- ]
- );
+ ?.map(ev => ev.extra) ?? [];
+ const nimbusEvents =
+ Glean.nimbusEvents.unenrollment
+ .testGetValue("events")
+ ?.map(ev => ev.extra) ?? [];
+
+ const unenrolledSlugs = [];
+ if (!rolloutsEnabled) {
+ unenrolledSlugs.push(recipes.rollout.slug);
+ }
+ if (!studiesEnabled) {
+ unenrolledSlugs.push(recipes.experiment.slug);
+ }
- // We expect only one event and that that one event matches the expected enrolled experiment
Assert.deepEqual(
- Glean.nimbusEvents.unenrollment.testGetValue("events").map(ev => ev.extra),
- [
- {
- experiment: experiment.slug,
- branch: experiment.branches[0].slug,
- reason: "studies-opt-out",
- },
- ]
+ normandyEvents,
+ unenrolledSlugs.map(slug => ({
+ value: slug,
+ branch: "control",
+ reason: recipes[slug].isRollout ? "rollouts-opt-out" : "studies-opt-out",
+ })),
+ "Expected normandy unenroll events"
);
+ Assert.deepEqual(
+ nimbusEvents,
+ unenrolledSlugs.map(slug => ({
+ experiment: slug,
+ branch: "control",
+ reason: recipes[slug].isRollout ? "rollouts-opt-out" : "studies-opt-out",
+ })),
+ "Expected nimbus unenroll events"
+ );
+
+ if (rolloutsEnabled) {
+ manager.unenroll(recipes.rollout.slug, { reason: "test" });
+ }
+ if (studiesEnabled) {
+ manager.unenroll(recipes.experiment.slug, { reason: "test" });
+ }
+
+ labs.unenroll(recipes.optin.slug);
await cleanup();
- Services.prefs.clearUserPref(STUDIES_OPT_OUT_PREF);
-});
-
-add_task(async function test_unenroll_rollout_opt_out() {
- Services.prefs.setBoolPref(STUDIES_OPT_OUT_PREF, true);
-
- const { manager, cleanup } = await setupTest();
- const rollout = NimbusTestUtils.factories.recipe("foo", { isRollout: true });
- await manager.enroll(rollout, "test");
- // Check that there aren't any Glean normandy unenrollNimbusExperiment events yet
- Assert.equal(
- Glean.normandy.unenrollNimbusExperiment.testGetValue("events"),
- undefined,
- "no Glean normandy unenrollNimbusExperiment events before unenrollment"
- );
-
- // Check that there aren't any Glean unenrollment events yet
- Assert.equal(
- Glean.nimbusEvents.unenrollment.testGetValue("events"),
- undefined,
- "no Glean unenrollment events before unenrollment"
- );
-
- Services.prefs.setBoolPref(STUDIES_OPT_OUT_PREF, false);
-
- await NimbusTestUtils.assert.enrollmentExists(rollout.slug, {
- active: false,
- });
-
- Assert.equal(
- manager.store.get(rollout.slug).active,
- false,
- "should set .active to false"
- );
+ Services.prefs.clearUserPref(ROLLOUTS_ENABLED_PREF);
+ Services.prefs.clearUserPref(STUDIES_ENABLED_PREF);
+}
- // We expect only one event and that that one event matches the expected enrolled experiment
- Assert.deepEqual(
- Glean.normandy.unenrollNimbusExperiment
- .testGetValue("events")
- .map(ev => ev.extra),
- [
- {
- value: rollout.slug,
- branch: rollout.branches[0].slug,
- reason: "studies-opt-out",
- },
- ]
- );
+add_task(async function testUnenrollStudiesOptOut() {
+ await doOptOutTest({ studiesEnabled: false });
+});
- // We expect only one event and that that one event matches the expected enrolled experiment
- Assert.deepEqual(
- Glean.nimbusEvents.unenrollment.testGetValue("events").map(ev => ev.extra),
- [
- {
- experiment: rollout.slug,
- branch: rollout.branches[0].slug,
- reason: "studies-opt-out",
- },
- ]
- );
+add_task(async function testUnenrollRolloutOptOut() {
+ await doOptOutTest({ rolloutsEnabled: false });
+});
- await cleanup();
- Services.prefs.clearUserPref(STUDIES_OPT_OUT_PREF);
+add_task(async function testUnenrollAllOptOut() {
+ await doOptOutTest({ rolloutsEnabled: false, studiesEnabled: false });
});
add_task(async function test_unenroll_uploadPref() {
diff --git a/toolkit/components/nimbus/test/unit/test_Migrations.js b/toolkit/components/nimbus/test/unit/test_Migrations.js
@@ -1649,46 +1649,46 @@ add_task(async function testGraduateFirefoxLabsAutoPip() {
Services.prefs.setBoolPref(ENABLED_PREF, true);
- let cleanup, manager;
- await GleanPings.nimbusTargetingContext.testSubmission(
- () => {
- Assert.deepEqual(
- Glean.nimbusEvents.enrollmentStatus
- .testGetValue("nimbus-targeting-context")
- .map(event => event.extra),
- [
- {
- slug: "firefox-labs-auto-pip",
- branch: "control",
- status: "WasEnrolled",
- reason: "Migration",
- migration: "graduate-firefox-labs-auto-pip",
+ const { cleanup, initExperimentAPI, manager } =
+ await NimbusTestUtils.setupTest({
+ clearTelemetry: true,
+ init: false,
+ storePath: await NimbusTestUtils.createStoreWith(store => {
+ NimbusTestUtils.addEnrollmentForRecipe(recipe, {
+ store,
+ extra: {
+ prefs: [
+ {
+ name: ENABLED_PREF,
+ featureId: "auto-pip",
+ variable: "enabled",
+ branch: "user",
+ originalValue: false,
+ },
+ ],
},
- ]
- );
- },
- async () =>
- ({ cleanup, manager } = await NimbusTestUtils.setupTest({
- storePath: await NimbusTestUtils.createStoreWith(store => {
- NimbusTestUtils.addEnrollmentForRecipe(recipe, {
- store,
- extra: {
- prefs: [
- {
- name: ENABLED_PREF,
- featureId: "auto-pip",
- variable: "enabled",
- branch: "user",
- originalValue: false,
- },
- ],
- },
- });
- }),
- migrationState:
- NimbusTestUtils.migrationState.IMPORTED_ENROLLMENTS_TO_SQL,
- }))
- );
+ });
+ }),
+ migrationState:
+ NimbusTestUtils.migrationState.IMPORTED_ENROLLMENTS_TO_SQL,
+ });
+
+ await GleanPings.nimbusTargetingContext.testSubmission(() => {
+ Assert.deepEqual(
+ Glean.nimbusEvents.enrollmentStatus
+ .testGetValue("nimbus-targeting-context")
+ .map(event => event.extra),
+ [
+ {
+ slug: "firefox-labs-auto-pip",
+ branch: "control",
+ status: "WasEnrolled",
+ reason: "Migration",
+ migration: "graduate-firefox-labs-auto-pip",
+ },
+ ]
+ );
+ }, initExperimentAPI);
const enrollment = manager.store.get(SLUG);
@@ -1706,8 +1706,12 @@ add_task(async function testGraduateFirefoxLabsAutoPip() {
Glean.nimbusEvents.migration.testGetValue().map(event => event.extra),
[
{
+ migration_id: "separate-rollout-opt-out",
success: "true",
+ },
+ {
migration_id: "graduate-firefox-labs-auto-pip",
+ success: "true",
},
]
);
@@ -1728,6 +1732,82 @@ add_task(async function testGraduateFirefoxLabsAutoPip() {
Services.prefs.setBoolPref(ENABLED_PREF, false);
await cleanup();
+});
+
+add_task(async function testSeparateRolloutOptOut() {
+ const STUDIES_PREF = "app.shield.optoutstudies.enabled";
+ const TELEMETRY_PREF = "datareporting.healthreport.uploadEnabled";
+ const ROLLOUT_PREF = "nimbus.rollouts.enabled";
+
+ const rollout = NimbusTestUtils.factories.recipe.withFeatureConfig(
+ "test-rollout",
+ { featureId: "no-feature-firefox-desktop" },
+ { isRollout: true }
+ );
+
+ for (const studiesEnabled of [true, false]) {
+ for (const telemetryEnabled of [true, false]) {
+ info(
+ `testSeparateRolloutOptOut: studiesEnabled=${studiesEnabled} telemetryEnabled=${telemetryEnabled}\n`
+ );
+ Services.prefs.setBoolPref(STUDIES_PREF, studiesEnabled);
+ Services.prefs.setBoolPref(TELEMETRY_PREF, telemetryEnabled);
- Services.fog.testResetFOG();
+ const { manager, cleanup } = await NimbusTestUtils.setupTest({
+ migrationState:
+ NimbusTestUtils.migrationState.GRADUATED_FIREFOX_LABS_AUTO_PIP,
+ experiments: [rollout],
+ clearTelemetry: true,
+ });
+
+ Assert.deepEqual(
+ Glean.nimbusEvents.migration
+ .testGetValue("events")
+ .map(event => event.extra),
+ [
+ {
+ migration_id: "separate-rollout-opt-out",
+ success: "true",
+ },
+ ]
+ );
+
+ Assert.equal(
+ Services.prefs.getBoolPref(ROLLOUT_PREF),
+ studiesEnabled,
+ "Rollout pref matches expected value"
+ );
+ Assert.equal(
+ ExperimentAPI.rolloutsEnabled,
+ studiesEnabled,
+ "Rollouts enable status correct"
+ );
+
+ const activeEnrollments = manager.store
+ .getAll()
+ .filter(e => e.active)
+ .map(e => e.slug);
+
+ if (studiesEnabled) {
+ Assert.deepEqual(
+ activeEnrollments,
+ ["test-rollout"],
+ "Should have enrolled in rollout"
+ );
+ manager.unenroll("test-rollout", "test-cleanup");
+ } else {
+ Assert.deepEqual(
+ activeEnrollments,
+ [],
+ "Should not have enrolled in rollout"
+ );
+ }
+
+ await cleanup();
+ }
+
+ Services.prefs.clearUserPref(STUDIES_PREF);
+ Services.prefs.clearUserPref(TELEMETRY_PREF);
+ Services.prefs.clearUserPref(ROLLOUT_PREF);
+ }
});