commit cd1586e5cf72d8c948d8d74d66a8a696ec07eb40
parent 73fb4a228ffb514b21a86ec0391ab52a51a152ff
Author: Beth Rennie <beth@brennie.ca>
Date: Sat, 1 Nov 2025 12:50:15 +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:
3 files changed, 58 insertions(+), 4 deletions(-)
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/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: [],
+ });
+});