Files
tubestation/toolkit/components/nimbus/FirefoxLabs.sys.mjs
Beth Rennie c57c8e4c7a Bug 1956080 - Make the unenroll flow (mostly) async r=nimbus-reviewers,relud,settings-reviewers,omc-reviewers,emcminn,bytesized
Writing enrollments to the SQL database is an async process, so the
entire unenroll flow needs to become async. This patch lays the
groundwork for making that happen by updating our testing helpers to use
async functions, as well as adding some new helpers for asserting the
state of the enrollments database.

For now the unenroll() (_unenroll()) functions are marked async but
otherwise have no behavioural changes -- this is just a first step to
port all the tests over before landing changes that write to the
enrollments store (which have to all be landed together).

Most callers of unenroll() have been updated so that they await the
result. There are a few callers left that do not await the result,
however, mostly because doing so causes race conditions in tests (most
notably in the pref observers in ExperimentManager and the
PrefFlipsFeature). These issues will be addressed in bug 1956082.

Differential Revision: https://phabricator.services.mozilla.com/D250504
2025-05-22 18:22:02 +00:00

142 lines
3.5 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
NimbusTelemetry: "resource://nimbus/lib/Telemetry.sys.mjs",
UnenrollmentCause: "resource://nimbus/lib/ExperimentManager.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "log", () => {
const { Logger } = ChromeUtils.importESModule(
"resource://messaging-system/lib/Logger.sys.mjs"
);
return new Logger("FirefoxLabs");
});
const IS_MAIN_PROCESS =
Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
export class FirefoxLabs {
#recipes;
/**
* Construct a new FirefoxLabs instance from the given set of recipes.
*
* @param {object[]} recipes The opt-in recipes to use.
*
* NB: You shiould use FirefoxLabs.create() directly instead of calling this constructor.
*/
constructor(recipes) {
this.#recipes = new Map(recipes.map(recipe => [recipe.slug, recipe]));
}
/**
* Create a new FirefoxLabs instance with all available opt-in recipes that match targeting and
* bucketing.
*/
static async create() {
if (!IS_MAIN_PROCESS) {
throw new Error("FirefoxLabs can only be created in the main process");
}
const recipes = await lazy.ExperimentAPI.manager.getAllOptInRecipes();
return new FirefoxLabs(recipes);
}
/**
* Enroll in an opt-in.
*
* @param {string} slug The slug of the opt-in to enroll.
* @param {string} branchSlug The slug of the branch to enroll in.
*/
async enroll(slug, branchSlug) {
if (!slug || !branchSlug) {
throw new TypeError("enroll: slug and branchSlug are required");
}
const recipe = this.#recipes.get(slug);
if (!recipe) {
lazy.log.error(`No recipe found with slug ${slug}`);
return;
}
if (!recipe.branches.find(branch => branch.slug === branchSlug)) {
lazy.log.error(
`Failed to enroll in ${slug} ${branchSlug}: branch does not exist`
);
return;
}
try {
await lazy.ExperimentAPI.manager.enroll(recipe, "rs-loader", {
branchSlug,
});
} catch (e) {
lazy.log.error(`Failed to enroll in ${slug} (branch ${branchSlug})`, e);
}
}
/**
* Unenroll from a opt-in.
*
* @param {string} slug The slug of the opt-in to unenroll.
*/
async unenroll(slug) {
if (!slug) {
throw new TypeError("slug is required");
}
if (!this.#recipes.has(slug)) {
lazy.log.error(`Unknown opt-in ${slug}`);
return;
}
try {
await lazy.ExperimentAPI.manager.unenroll(
slug,
lazy.UnenrollmentCause.fromReason(
lazy.NimbusTelemetry.UnenrollReason.LABS_OPT_OUT
)
);
} catch (e) {
lazy.log.error(`unenroll: failed to unenroll from ${slug}`, e);
}
}
/**
* Return the number of eligible opt-ins.
*
* @return {number} The number of eligible opt-ins.
*/
get count() {
return this.#recipes.size;
}
/**
* Yield all available opt-ins.
*
* @yields {object} The opt-ins.
*/
*all() {
for (const recipe of this.#recipes.values()) {
yield recipe;
}
}
/**
* Return an opt-in by its slug
*
* @param {string} slug The slug of the opt-in to return.
*
* @returns {object} The requested opt-in, if it exists.
*/
get(slug) {
return this.#recipes.get(slug);
}
}