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:
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();
+});