tor-browser

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

commit 4b5cf1138eb0e074690b9fae0ffbfb87e26fd1db
parent 138ff16dbfd579ff617ef328cc97d3f508fb7f7a
Author: Beth Rennie <beth@brennie.ca>
Date:   Mon,  1 Dec 2025 21:40:58 +0000

Bug 1997467 - Graduate users enrolled in firefox-labs-auto-pip r=nimbus-reviewers,relud

This Firefox Labs feature is graduating to become a setting in
about:preferences. We need to unenroll users without resetting their
pref state.

Differential Revision: https://phabricator.services.mozilla.com/D273901

Diffstat:
Mtoolkit/components/nimbus/lib/ExperimentManager.sys.mjs | 32++++++++++++++++++++++++++++++--
Mtoolkit/components/nimbus/lib/Migrations.sys.mjs | 58++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mtoolkit/components/nimbus/lib/Telemetry.sys.mjs | 14++++++++++++++
Mtoolkit/components/nimbus/metrics.yaml | 15+++++++++++++--
Mtoolkit/components/nimbus/test/NimbusTestUtils.sys.mjs | 12+++++++++++-
Mtoolkit/components/nimbus/test/unit/test_Migrations.js | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 225 insertions(+), 13 deletions(-)

diff --git a/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs b/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs @@ -162,6 +162,13 @@ export const UnenrollmentCause = { }; }, + Migration(migration) { + return { + reason: lazy.NimbusTelemetry.UnenrollReason.MIGRATION, + migration, + }; + }, + Unknown() { return { reason: lazy.NimbusTelemetry.UnenrollReason.UNKNOWN, @@ -291,6 +298,8 @@ export class ExperimentManager { ); } + lazy.log.debug("onStartup"); + this._prefs = new Map(); this._prefsBySlug = new Map(); this._prefFlips = new PrefFlipsFeature({ manager: this }); @@ -1011,8 +1020,15 @@ export class ExperimentManager { * @param {boolean} options.duringRestore * If true, this indicates that this was during the call to * `_restoreEnrollmentPrefs`. + * + * @param {boolean} options.unsetEnrollmentPrefs + * Whether or not to unset the prefs set by enrollment. */ - _unenroll(enrollment, cause, { duringRestore = false } = {}) { + _unenroll( + enrollment, + cause, + { duringRestore = false, unsetEnrollmentPrefs = true } = {} + ) { const { slug } = enrollment; if (!enrollment.active) { @@ -1029,7 +1045,14 @@ export class ExperimentManager { lazy.NimbusTelemetry.recordUnenrollment(enrollment, cause); - this._unsetEnrollmentPrefs(enrollment, cause, { duringRestore }); + if (unsetEnrollmentPrefs) { + this._unsetEnrollmentPrefs(enrollment, cause, { duringRestore }); + } else if (enrollment.prefs) { + // If we're not unsetting enrollment prefs, we must remove our listeners. + for (const pref of enrollment.prefs) { + this._removePrefObserver(pref.name, enrollment.slug); + } + } lazy.log.debug(`Recipe unenrolled: ${slug} (${cause.reason})`); } @@ -1592,6 +1615,11 @@ export class ExperimentManager { * @param {string} slug The slug of the enrollment that is being unenrolled. */ _removePrefObserver(name, slug) { + /// This may be called before the ExperimentManager has finished initializing. + if (!this._prefs) { + return; + } + // Update the pref observer that the current enrollment is no longer // involved in the pref. // diff --git a/toolkit/components/nimbus/lib/Migrations.sys.mjs b/toolkit/components/nimbus/lib/Migrations.sys.mjs @@ -14,6 +14,7 @@ ChromeUtils.defineESModuleGetters(lazy, { "moz-src:///toolkit/profile/ProfilesDatastoreService.sys.mjs", RemoteSettingsSyncError: "resource://nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs", + UnenrollmentCause: "resource://nimbus/lib/ExperimentManager.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "log", () => { @@ -23,6 +24,14 @@ ChromeUtils.defineLazyGetter(lazy, "log", () => { return new Logger("NimbusMigrations"); }); +function isBackgroundTaskMode() { + const bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( + Ci.nsIBackgroundTasks + ); + + return bts?.isBackgroundTaskMode ?? false; +} + /** * A named migration. * @@ -31,14 +40,14 @@ ChromeUtils.defineLazyGetter(lazy, "log", () => { * @property {string} name The name of the migration. This will be reported in * telemetry. * - * @property {function(): void} fn The migration implementation. + * @property {function(string): void} fn The migration implementation. */ /** * Construct a {@link Migration} with a specific name. * * @param {string} name The name of the migration. - * @param {function(): void} fn The migration function. + * @param {function(name: string): void} fn The migration function. * * @returns {Migration} The migration. */ @@ -246,6 +255,39 @@ async function migrateEnrollmentsToSql() { } /** + * Migrate enrollment from the firefox-labs-auto-pip rolloutl. + * + * This feature is becoming a regular setting in about:preferences in Firefox + * 147. We need to unenroll users without resetting the prefs controlled by the + * feature. + * + * This migration must run before the `ExperimentManager.onStartup` has run + * because this feature might be removed and we cannot guarantee that users will + * update to exactly 145. + */ +function migrateGraduateFirefoxLabsAutoPip(migration) { + if (isBackgroundTaskMode()) { + // This migration does not apply to background task mode. + lazy.log.debug(`${migration}: skipping (is background task mode)`); + return; + } + + const enrollment = lazy.ExperimentAPI.manager.store.get( + "firefox-labs-auto-pip" + ); + if (!enrollment?.active) { + lazy.log.debug(`${migration}: skipping (no or inactive enrollment)`); + return; + } + + lazy.ExperimentAPI.manager._unenroll( + enrollment, + lazy.UnenrollmentCause.Migration(migration), + { unsetEnrollmentPrefs: false } + ); +} + +/** * Migrate the pre-Nimbus Firefox Labs experiences into Nimbus enrollments. * * Previously Firefox Labs had a one-to-one correlation between Labs Experiments @@ -258,11 +300,7 @@ async function migrateEnrollmentsToSql() { * replaced with a no-op. */ async function migrateFirefoxLabsEnrollments() { - const bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( - Ci.nsIBackgroundTasks - ); - - if (bts?.isBackgroundTaskMode) { + if (isBackgroundTaskMode()) { // This migration does not apply to background task mode. return; } @@ -373,7 +411,7 @@ export const NimbusMigrations = { ); try { - await migration.fn(); + await migration.fn(migration.name); } catch (e) { lazy.log.error( `applyMigrations: error running migration ${i} (${migration.name}): ${e}` @@ -451,6 +489,10 @@ export const NimbusMigrations = { // the migration to ensure recipe was never null. migration("noop", migrateNoop), migration("import-enrollments-to-sql", migrateEnrollmentsToSql), + migration( + "graduate-firefox-labs-auto-pip", + migrateGraduateFirefoxLabsAutoPip + ), ], [Phase.AFTER_REMOTE_SETTINGS_UPDATE]: [ diff --git a/toolkit/components/nimbus/lib/Telemetry.sys.mjs b/toolkit/components/nimbus/lib/Telemetry.sys.mjs @@ -32,6 +32,7 @@ const EnrollmentStatusReason = Object.freeze({ PREF_FLIPS_CONFLICT: "PrefFlipsConflict", ERROR: "Error", UNENROLLED_IN_ANOTHER_PROFILE: "UnenrolledInAnotherProfile", + MIGRATION: "Migration", }); const EnrollmentFailureReason = Object.freeze({ @@ -84,6 +85,7 @@ const UnenrollReason = Object.freeze({ INDIVIDUAL_OPT_OUT: "individual-opt-out", LABS_DIABLED: "labs-disabled", LABS_OPT_OUT: "labs-opt-out", + MIGRATION: "migration", PREF_FLIPS_CONFLICT: "prefFlips-conflict", PREF_FLIPS_FAILED: "prefFlips-failed", PREF_VARIABLE_CHANGED: "pref-variable-changed", @@ -160,6 +162,7 @@ export const NimbusTelemetry = { branch, error_string, conflict_slug, + migration, }) { Glean.nimbusEvents.enrollmentStatus.record({ slug, @@ -168,6 +171,7 @@ export const NimbusTelemetry = { branch, error_string, conflict_slug, + migration, }); }, @@ -265,6 +269,10 @@ export const NimbusTelemetry = { case UnenrollReason.L10N_MISSING_LOCALE: gleanEvent.locale = cause.locale; break; + + case UnenrollReason.MIGRATION: + gleanEvent.migration = cause.migration; + break; } Glean.normandy.unenrollNimbusExperiment.record(legacyEvent); @@ -319,6 +327,12 @@ export const NimbusTelemetry = { EnrollmentStatusReason.UNENROLLED_IN_ANOTHER_PROFILE; break; + case UnenrollReason.MIGRATION: + enrollmentStatus.status = EnrollmentStatus.WAS_ENROLLED; + enrollmentStatus.reason = EnrollmentStatusReason.MIGRATION; + enrollmentStatus.migration = cause.migration; + break; + default: enrollmentStatus.status = EnrollmentStatus.DISQUALIFIED; enrollmentStatus.reason = EnrollmentStatusReason.ERROR; diff --git a/toolkit/components/nimbus/metrics.yaml b/toolkit/components/nimbus/metrics.yaml @@ -794,9 +794,11 @@ nimbus_events: bugs: - https://bugzilla.mozilla.org/show_bug.cgi?id=1773563 - https://bugzilla.mozilla.org/show_bug.cgi?id=1781953 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1997467 data_reviews: - https://bugzilla.mozilla.org/show_bug.cgi?id=1773563 - https://bugzilla.mozilla.org/show_bug.cgi?id=1781953 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1997467 data_sensitivity: - technical notification_emails: @@ -848,8 +850,12 @@ nimbus_events: locale we failed to find in the set of available translations. about_config_change: description: > - For nimbus_experiment, if reason = "changed-pref", whether or not the - change was triggerd via about:config. + If reason == "changed-pref", whether or not the change was triggerd + via about:config. + type: string + migration: + description: > + If reason == "migration", the migration that triggered the unenrollment. type: string bugs: - https://bugzilla.mozilla.org/show_bug.cgi?id=1773563 @@ -1030,12 +1036,17 @@ nimbus_events: conflict_slug: type: string description: If the enrollment hit a feature conflict, the slug of the conflicting experiment/rollout + migration: + type: string + description: If the unenrollment was caused by a migration, the name of the migration. bugs: - https://bugzilla.mozilla.org/show_bug.cgi?id=1817481 - https://bugzilla.mozilla.org/show_bug.cgi?id=1955169 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1997467 data_reviews: - https://bugzilla.mozilla.org/show_bug.cgi?id=1817481 - https://bugzilla.mozilla.org/show_bug.cgi?id=1955169 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1997467 data_sensitivity: - technical notification_emails: diff --git a/toolkit/components/nimbus/test/NimbusTestUtils.sys.mjs b/toolkit/components/nimbus/test/NimbusTestUtils.sys.mjs @@ -521,13 +521,23 @@ export const NimbusTestUtils = { }); }, + get GRADUATED_FIREFOX_LABS_AUTO_PIP() { + const { Phase } = lazy.NimbusMigrations; + + return NimbusTestUtils.makeMigrationState({ + [Phase.INIT_STARTED]: "multi-phase-migrations", + [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.IMPORTED_ENROLLMENTS_TO_SQL; + return NimbusTestUtils.migrationState.GRADUATED_FIREFOX_LABS_AUTO_PIP; }, }, diff --git a/toolkit/components/nimbus/test/unit/test_Migrations.js b/toolkit/components/nimbus/test/unit/test_Migrations.js @@ -1617,3 +1617,110 @@ add_task(async function testMigrateEnrollmentsToSqlDb() { await testMigrateEnrollmentsToSql("database"); resetNimbusEnrollmentPrefs(); }); + +add_task(async function testGraduateFirefoxLabsAutoPip() { + const SLUG = "firefox-labs-auto-pip"; + + const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig( + SLUG, + { + featureId: "auto-pip", + value: { enabled: true }, + }, + { + isFirefoxLabsOptIn: true, + firefoxLabsTitle: "experimental-features-auto-pip", + firefoxLabsDescription: "experimental-features-auto-pip-description", + firefoxLabsDescriptionLink: null, + firefoxLabsGroup: "experimental-features-group-productivity", + requiresRestart: false, + } + ); + + const ENABLED_PREF = getEnabledPrefForFeature("auto-pip"); + + Services.fog.applyServerKnobsConfig( + JSON.stringify({ + metrics_enabled: { + "nimbus_events.enrollment_status": true, + }, + }) + ); + + Services.prefs.setBoolPref(ENABLED_PREF, true); + const { 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, + }); + + const enrollment = manager.store.get(SLUG); + + Assert.ok(!enrollment.active, "Enrollment is not active"); + Assert.deepEqual(enrollment.featureIds, ["auto-pip"]); + Assert.equal(enrollment.unenrollReason, "migration"); + + Assert.equal( + Services.prefs.getBoolPref(ENABLED_PREF), + true, + "Pref is still set" + ); + + Assert.deepEqual( + Glean.nimbusEvents.migration.testGetValue().map(event => event.extra), + [ + { + success: "true", + migration_id: "graduate-firefox-labs-auto-pip", + }, + ] + ); + + Assert.deepEqual( + Glean.nimbusEvents.unenrollment + .testGetValue("events") + .map(event => event.extra), + [ + { + experiment: "firefox-labs-auto-pip", + branch: "control", + reason: "migration", + migration: "graduate-firefox-labs-auto-pip", + }, + ] + ); + + Assert.deepEqual( + Glean.nimbusEvents.enrollmentStatus + .testGetValue("events") + .map(event => event.extra), + [ + { + slug: "firefox-labs-auto-pip", + branch: "control", + status: "WasEnrolled", + reason: "Migration", + migration: "graduate-firefox-labs-auto-pip", + }, + ] + ); + + Services.prefs.setBoolPref(ENABLED_PREF, false); + await cleanup(); + + Services.fog.testResetFOG(); +});