/* 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/. */ /** @import {OptInEntry} from "./lib/ExperimentManager.sys.mjs" */ 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 { /** * A map of experiment slugs to opt-in entries. * * @type {Map} */ #optIns; /** * Construct a new FirefoxLabs instance from the given set of recipes. * * @param {OptInEntry[]} optIns The opt-ins. * * NB: You shiould use FirefoxLabs.create() directly instead of calling this constructor. */ constructor(optIns) { this.#optIns = new Map(optIns.map(entry => [entry.recipe.slug, entry])); } /** * 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 entries = await lazy.ExperimentAPI.manager.getAvailableOptIns(); return new FirefoxLabs(entries); } /** * 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 entry = this.#optIns.get(slug); if (!entry) { lazy.log.error(`No recipe found with id ${slug}`); return; } const { recipe, source } = entry; 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, source, { 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. */ unenroll(slug) { if (!slug) { throw new TypeError("slug is required"); } if (!this.#optIns.has(slug)) { lazy.log.error(`Unknown opt-in ${slug}`); return; } try { 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.#optIns.size; } /** * Yield all available opt-ins. * * @returns {Generator} The recipes of the available opt-ins. */ *all() { for (const entry of this.#optIns.values()) { yield entry.recipe; } } /** * Return an opt-in by its slug. * * @param {string} slug The slug of the opt-in to return. * * @returns {OptInEntry} The requested opt-in, if it exists. */ get(slug) { return this.#optIns.get(slug); } }