tor-browser

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

commit da83c75012824fb7a5f792ca0263028106bcaee2
parent 763a6cc91007752a38c7e6bab9ce5d2410fefac8
Author: Beth Rennie <beth@brennie.ca>
Date:   Sat,  1 Nov 2025 12:50:13 +0000

Bug 1972647 - Make ExperimentAPI a singleton instance of a class r=nimbus-reviewers,relud

We have been accruing internal state inside `ExperimentAPI.sys.mjs` that
we do not want to expose as mutable to outside of the module. That state
is now bundled into the `ExperimentAPI` object itself as private
elements.

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

Diffstat:
Mtoolkit/components/nimbus/ExperimentAPI.sys.mjs | 244++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
1 file changed, 154 insertions(+), 90 deletions(-)

diff --git a/toolkit/components/nimbus/ExperimentAPI.sys.mjs b/toolkit/components/nimbus/ExperimentAPI.sys.mjs @@ -2,6 +2,11 @@ * 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 { ExperimentManager } from "./lib/ExperimentManager.sys.mjs" + * @import { RemoteSettingsExperimentLoader } from "./lib/RemoteSettingsExperimentLoader.sys.mjs" + */ + import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; @@ -74,26 +79,6 @@ const experimentBranchAccessor = { const NIMBUS_PROFILE_ID_PREF = "nimbus.profileId"; -let cachedProfileId = null; - -/** - * Ensure the Nimbus profile ID exists. - * - * @returns {string} The profile ID. - */ -function ensureNimbusProfileId() { - if (!cachedProfileId) { - if (Services.prefs.prefHasUserValue(NIMBUS_PROFILE_ID_PREF)) { - cachedProfileId = Services.prefs.getStringPref(NIMBUS_PROFILE_ID_PREF); - } else { - cachedProfileId = Services.uuid.generateUUID().toString().slice(1, -1); - Services.prefs.setStringPref(NIMBUS_PROFILE_ID_PREF, cachedProfileId); - } - } - - return cachedProfileId; -} - /** * Metadata about an enrollment. * @@ -131,11 +116,98 @@ export const EnrollmentType = Object.freeze({ ROLLOUT: "rollout", }); -let initialized = false; -let experimentManager = null; -let experimentLoader = null; +export const ExperimentAPI = new (class { + /** + * Whether or not the ExperimentAPI has been initialized. + */ + #initialized = false; + + /** + * The current ExperimentManager. + * + * @type {ExperimentManager | null} + */ + #experimentManager = null; + + /** + * The current RemoteSettingsExperimentLoader. + * + * @type {RemoteSettingsExperimentLoader | null} + */ + #experimentLoader = null; + + /** + * The unique ID of this Profile. + * + * @type {string | null} + */ + #cachedProfileId = null; + + /** + * Cached pref values. + */ + #prefValues = { + /** + * Whether or not opt-out studies are enabled. + * + * @see {@link STUDIES_OPT_OUT_PREF} + */ + studiesEnabled: false, + + /** + * Whether or not telemetry is enabled. + * + * @see {@link UPLOAD_ENABLED_PREF} + */ + telemetryEnabled: false, + }; + + constructor() { + if (IS_MAIN_PROCESS) { + // Ensure that the profile ID is cached in a pref. + if (Services.prefs.prefHasUserValue(NIMBUS_PROFILE_ID_PREF)) { + this.#cachedProfileId = Services.prefs.getStringPref( + NIMBUS_PROFILE_ID_PREF + ); + } else { + this.#cachedProfileId = Services.uuid + .generateUUID() + .toString() + .slice(1, -1); + Services.prefs.setStringPref( + NIMBUS_PROFILE_ID_PREF, + this.#cachedProfileId + ); + } + } + + const onStudiesEnabledChanged = this.#onStudiesEnabledChanged.bind(this); + + XPCOMUtils.defineLazyPreferenceGetter( + this.#prefValues, + "studiesEnabled", + STUDIES_OPT_OUT_PREF, + false, + onStudiesEnabledChanged + ); + + XPCOMUtils.defineLazyPreferenceGetter( + this.#prefValues, + "telemetryEnabled", + UPLOAD_ENABLED_PREF, + false, + onStudiesEnabledChanged + ); + + this._annotateCrashReport = this._annotateCrashReport.bind(this); + this._removeCrashReportAnnotator = + this._removeCrashReportAnnotator.bind(this); + + ChromeUtils.defineLazyGetter(this, "_remoteSettingsClient", function () { + return lazy.RemoteSettings(lazy.COLLECTION_ID); + }); + } -export const ExperimentAPI = { /** * The topic that is notified when either the studies enabled pref or the * telemetry enabled pref changes. @@ -145,7 +217,7 @@ export const ExperimentAPI = { */ get STUDIES_ENABLED_CHANGED() { return "nimbus:studies-enabled-changed"; - }, + } /** * Initialize the ExperimentAPI. @@ -166,13 +238,11 @@ export const ExperimentAPI = { * Whether or not the ExperimentAPI was initialized. */ async init({ extraContext, forceSync = false } = {}) { - if (initialized) { + if (this.#initialized) { return false; } - ensureNimbusProfileId(); - - initialized = true; + this.#initialized = true; const studiesEnabled = this.studiesEnabled; @@ -240,94 +310,99 @@ export const ExperimentAPI = { ); } - Services.prefs.addObserver( - UPLOAD_ENABLED_PREF, - this._onStudiesEnabledChanged - ); - Services.prefs.addObserver( - STUDIES_OPT_OUT_PREF, - this._onStudiesEnabledChanged - ); - // If Nimbus was disabled between the start of this function and registering // the pref observers we have not handled it yet. if (studiesEnabled !== this.studiesEnabled) { - await this._onStudiesEnabledChanged(); + await this.#onStudiesEnabledChanged(); } return true; - }, + } /** * Return the global ExperimentManager. * * The ExperimentManager will be lazily created upon first access to this * property. + * + * @type {ExperimentManager} */ get manager() { - if (experimentManager === null) { - experimentManager = new lazy.ExperimentManager(); + if (this.#experimentManager === null) { + this.#experimentManager = new lazy.ExperimentManager(); } - return experimentManager; - }, + return this.#experimentManager; + } /** * Return the global ExperimentManager. * * @deprecated Use ExperimentAPI.Manager instead of this property. + * + * @type {ExperimentManager} */ get _manager() { return this.manager; - }, + } /** * Return the global RemoteSettingsExperimentLoader. + * + * @type {RemoteSettingsExperimentLoader} */ get _rsLoader() { - if (experimentLoader === null) { - experimentLoader = new lazy.RemoteSettingsExperimentLoader(this.manager); + if (this.#experimentLoader === null) { + this.#experimentLoader = new lazy.RemoteSettingsExperimentLoader( + this.manager + ); } - return experimentLoader; - }, + return this.#experimentLoader; + } _resetForTests() { - experimentLoader?.disable(); - experimentLoader = null; + this.#experimentLoader?.disable(); + this.#experimentLoader = null; - lazy.CleanupManager.removeCleanupHandler( - ExperimentAPI._removeCrashReportAnnotator - ); - experimentManager?.store.off("update", this._annotateCrashReport); - experimentManager = null; + lazy.CleanupManager.removeCleanupHandler(this._removeCrashReportAnnotator); + this.#experimentManager?.store.off("update", this._annotateCrashReport); + this.#experimentManager = null; - initialized = false; - }, + this.#initialized = false; + } get studiesEnabled() { return ( - Services.prefs.getBoolPref(UPLOAD_ENABLED_PREF, false) && - Services.prefs.getBoolPref(STUDIES_OPT_OUT_PREF, false) && + this.#prefValues.studiesEnabled && + this.#prefValues.telemetryEnabled && Services.policies.isAllowed("Shield") ); - }, + } /** * Return the profile ID. * * This is used to distinguish different profiles in a shared profile group * apart. Each profile has a persistent and stable profile ID. It is stored as - * a user branch pref but is locked to prevent tampering. + * a user branch pref. * * This is still susceptible to user.js editing, but there's nothing we can do * about that. * + * @throws {Error} If accessed outside the main process. + * * @returns {string} The profile ID. */ get profileId() { - return ensureNimbusProfileId(); - }, + if (!IS_MAIN_PROCESS) { + throw new Error( + "ExperimentAPI.profileId is not available outside the main process" + ); + } + + return this.#cachedProfileId; + } /** * Wait for the ExperimentAPI to become ready. @@ -342,7 +417,7 @@ export const ExperimentAPI = { */ async ready() { return this.manager.store.ready(); - }, + } /** * Annotate the current crash report with current enrollments. @@ -362,15 +437,19 @@ export const ExperimentAPI = { "NimbusEnrollments", activeEnrollments ); - }, + } _removeCrashReportAnnotator() { - if (initialized) { - experimentManager?.store.off("update", this._annotateCrashReport); + if (this.#initialized) { + this.#experimentManager?.store.off("update", this._annotateCrashReport); + } + } + + async #onStudiesEnabledChanged() { + if (!this.#initialized) { + return; } - }, - async _onStudiesEnabledChanged() { if (!this.studiesEnabled) { await this.manager._handleStudiesOptOut(); } @@ -378,7 +457,7 @@ export const ExperimentAPI = { await this._rsLoader.onEnabledPrefChange(); Services.obs.notifyObservers(null, this.STUDIES_ENABLED_CHANGED); - }, + } /** * Returns the recipe for a given experiment slug @@ -415,7 +494,7 @@ export const ExperimentAPI = { } return recipe; - }, + } /** * Returns all the branches for a given experiment slug @@ -437,7 +516,7 @@ export const ExperimentAPI = { return recipe?.branches.map( branch => new Proxy(branch, experimentBranchAccessor) ); - }, + } /** * Opt-in to the given experiment on the given branch. @@ -465,8 +544,8 @@ export const ExperimentAPI = { */ async optInToExperiment(options) { return this._rsLoader._optInToExperiment(options); - }, -}; + } +})(); /** * Singleton that holds lazy references to _ExperimentFeature instances @@ -933,21 +1012,6 @@ export class _ExperimentFeature { } } -ExperimentAPI._annotateCrashReport = - ExperimentAPI._annotateCrashReport.bind(ExperimentAPI); -ExperimentAPI._onStudiesEnabledChanged = - ExperimentAPI._onStudiesEnabledChanged.bind(ExperimentAPI); -ExperimentAPI._removeCrashReportAnnotator = - ExperimentAPI._removeCrashReportAnnotator.bind(ExperimentAPI); - -ChromeUtils.defineLazyGetter( - ExperimentAPI, - "_remoteSettingsClient", - function () { - return lazy.RemoteSettings(lazy.COLLECTION_ID); - } -); - class ExperimentLocalizationError extends Error { constructor(reason, locale) { super(`Localized experiment error (${reason})`);