tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mtoolkit/components/nimbus/ExperimentAPI.sys.mjs | 35++++++++++++++++++++++++++---------
Mtoolkit/components/nimbus/lib/ExperimentManager.sys.mjs | 32+++++++++++++++++++++++++++++---
Mtoolkit/components/nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs | 2+-
Mtoolkit/components/nimbus/test/browser/browser_remotesettings_experiment_enroll.js | 5++++-
Mtoolkit/components/nimbus/test/browser/browser_remotesettingsexperimentloader_remote_defaults.js | 6+++---
Mtoolkit/components/nimbus/test/unit/test_policy.js | 28++++++++++++++++++++++++++++
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: [], + }); +});