commit 8f2ec3f7c78314af60f32c9de59c83efda1df3e7
parent eb3f9ea0ff5ee79d4998a032d2e460d011a43c4d
Author: Beth Rennie <beth@brennie.ca>
Date: Fri, 31 Oct 2025 03:39:49 +0000
Bug 1972647 - Unenroll from Firefox labs opt-ins during Nimbus initialization if Labs is disabled by policy r=nimbus-reviewers,relud
Differential Revision: https://phabricator.services.mozilla.com/D269921
Diffstat:
6 files changed, 91 insertions(+), 17 deletions(-)
diff --git a/toolkit/components/nimbus/ExperimentAPI.sys.mjs b/toolkit/components/nimbus/ExperimentAPI.sys.mjs
@@ -299,8 +299,14 @@ export const ExperimentAPI = new (class {
);
}
- Services.prefs.addObserver(STUDIES_OPT_OUT_PREF, this._onStudiesEnabledChanged);
- Services.prefs.addObserver(UPLOAD_ENABLED_PREF, this._onStudiesEnabledChanged);
+ Services.prefs.addObserver(
+ STUDIES_OPT_OUT_PREF,
+ this._onStudiesEnabledChanged
+ );
+ Services.prefs.addObserver(
+ UPLOAD_ENABLED_PREF,
+ this._onStudiesEnabledChanged
+ );
// If Nimbus was disabled between the start of this function and registering
// the pref observers we have not handled it yet.
@@ -361,21 +367,32 @@ export const ExperimentAPI = new (class {
this.#experimentManager?.store.off("update", this._annotateCrashReport);
this.#experimentManager = null;
- Services.prefs.removeObserver(STUDIES_OPT_OUT_PREF, this._onStudiesEnabledChanged);
- Services.prefs.removeObserver(UPLOAD_ENABLED_PREF, this._onStudiesEnabledChanged);
+ Services.prefs.removeObserver(
+ STUDIES_OPT_OUT_PREF,
+ this._onStudiesEnabledChanged
+ );
+ Services.prefs.removeObserver(
+ UPLOAD_ENABLED_PREF,
+ this._onStudiesEnabledChanged
+ );
this.#initialized = false;
}
#computeEnabled() {
- this.#prefValues.studiesEnabled = Services.prefs.getBoolPref(STUDIES_OPT_OUT_PREF, false);
- this.#prefValues.telemetryEnabled = Services.prefs.getBoolPref(UPLOAD_ENABLED_PREF, false)
+ this.#prefValues.studiesEnabled = Services.prefs.getBoolPref(
+ STUDIES_OPT_OUT_PREF,
+ false
+ );
+ this.#prefValues.telemetryEnabled = Services.prefs.getBoolPref(
+ UPLOAD_ENABLED_PREF,
+ false
+ );
- this.#studiesEnabled = (
+ this.#studiesEnabled =
this.#prefValues.studiesEnabled &&
this.#prefValues.telemetryEnabled &&
- Services.policies.isAllowed("Shield")
- );
+ Services.policies.isAllowed("Shield");
}
get enabled() {
diff --git a/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs b/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs
@@ -261,6 +261,10 @@ export class ExperimentManager {
this._handleStudiesOptOut();
}
+ if (!lazy.ExperimentAPI.labsEnabled) {
+ this._handleLabsDisabled();
+ }
+
lazy.NimbusFeatures.nimbusTelemetry.onUpdate(() => {
// Providing default values ensure we disable metrics when unenrolling.
const cfg = {
@@ -755,7 +759,7 @@ export class ExperimentManager {
if (result.ok) {
// Unenrollment due to studies becoming disabled is handled in
// `_handleStudiesOptOut`. Firefox Labs can only be disabled by policy and
- // thus its enabled state cannot change at runtime.
+ // thus its enabled state cannot change after Nimbus is initialized.
if (result.status === lazy.MatchStatus.DISABLED) {
return false;
}
@@ -938,9 +942,11 @@ export class ExperimentManager {
* Unenroll from all active studies if user opts out.
*/
_handleStudiesOptOut() {
- for (const enrollment of this.store
+ const enrollments = this.store
.getAll()
- .filter(e => e.active && !e.isFirefoxLabsOptIn)) {
+ .filter(e => e.active && !e.isFirefoxLabsOptIn);
+
+ for (const enrollment of enrollments) {
this._unenroll(
enrollment,
UnenrollmentCause.fromReason(
@@ -951,6 +957,26 @@ export class ExperimentManager {
}
/**
+ * Unenroll from all active Firefox Labs opt-ins if Labs becomes disabled.
+ */
+ _handleLabsDisabled() {
+ const enrollments = this.store
+ .getAll()
+ .filter(e => e.active && e.isFirefoxLabsOptIn);
+
+ for (const enrollment of enrollments) {
+ this._unenroll(
+ enrollment,
+ UnenrollmentCause.fromReason(
+ lazy.NimbusTelemetry.UnenrollReason.LABS_DISABLED
+ )
+ );
+ }
+
+ this.optinRecipes = [];
+ }
+
+ /**
* Generate Normandy UserId respective to a branch
* for a given experiment.
*
diff --git a/toolkit/components/nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs b/toolkit/components/nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs
@@ -815,7 +815,7 @@ export class RemoteSettingsExperimentLoader {
* Resolves when the RemoteSettingsExperimentLoader has updated at least once
* and is not in the middle of an update.
*
- * If studies are disabled or the RemoteSettingsExperimentLoader has been
+ * If Nimbus is disabled or the RemoteSettingsExperimentLoader has been
* disabled (i.e., during shutdown), then this will always resolve
* immediately.
*/
diff --git a/toolkit/components/nimbus/test/browser/browser_remotesettings_experiment_enroll.js b/toolkit/components/nimbus/test/browser/browser_remotesettings_experiment_enroll.js
@@ -46,7 +46,10 @@ add_task(async function test_experimentEnrollment_startup() {
});
Assert.ok(ExperimentAPI.enabled, "ExperimentAPI is still enabled");
- Assert.ok(ExperimentAPI._rsLoader._enabled, "RemoteSettingsExperimentLoader is still enabled");
+ Assert.ok(
+ ExperimentAPI._rsLoader._enabled,
+ "RemoteSettingsExperimentLoader is still enabled"
+ );
Assert.ok(!ExperimentAPI.studiesEnabled, "Studies disabled");
Assert.ok(ExperimentAPI.labsEnabled, "Firefox Labs enabled");
diff --git a/toolkit/components/nimbus/test/browser/browser_remotesettingsexperimentloader_remote_defaults.js b/toolkit/components/nimbus/test/browser/browser_remotesettingsexperimentloader_remote_defaults.js
@@ -90,8 +90,8 @@ add_task(async function test_remote_fetch_and_ready() {
await SpecialPowers.pushPrefEnv({
set: [
["datareporting.healthreport.uploadEnabled", true],
- ["app.shield.optoutstudies.enabled", true]
- ]
+ ["app.shield.optoutstudies.enabled", true],
+ ],
});
await ExperimentAPI.ready();
@@ -450,7 +450,7 @@ add_task(async function remote_defaults_active_remote_defaults() {
set: [
["datareporting.healthreport.uploadEnabled", true],
["app.shield.optoutstudies.enabled", true],
- ]
+ ],
});
await ExperimentAPI._rsLoader.updateRecipes("mochitest");
diff --git a/toolkit/components/nimbus/test/unit/test_policy.js b/toolkit/components/nimbus/test/unit/test_policy.js
@@ -40,6 +40,7 @@ async function doTest({
policies,
labsEnabled,
studiesEnabled,
+ existingEnrollments = [],
expectedEnrollments,
expectedOptIns,
}) {
@@ -53,10 +54,26 @@ async function doTest({
"Policy engine is active"
);
+ let storePath = undefined;
+ if (existingEnrollments) {
+ const store = NimbusTestUtils.stubs.store();
+ await store.init();
+
+ for (const slug of existingEnrollments) {
+ NimbusTestUtils.addEnrollmentForRecipe(
+ RECIPES.find(e => e.slug === slug),
+ { store }
+ );
+ }
+
+ storePath = await NimbusTestUtils.saveStore(store);
+ }
+
const { initExperimentAPI, cleanup, loader } =
await NimbusTestUtils.setupTest({
init: false,
experiments: RECIPES,
+ storePath,
});
sinon.spy(loader, "updateRecipes");
@@ -145,3 +162,14 @@ add_task(async function testNimbusDisabled() {
expectedOptIns: [],
});
});
+
+add_task(async function testDisableLabsPolicyCausesUnenrollments() {
+ await doTest({
+ policies: { UserMessaging: { FirefoxLabs: false } },
+ labsEnabled: false,
+ studiesEnabled: true,
+ expectedEnrollments: ["experiment", "rollout"],
+ existingEnrollments: ["optin"],
+ expectedOptIns: [],
+ });
+});