Skip to content

Commit 3938465

Browse files
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
1 parent b1f9766 commit 3938465

1 file changed

Lines changed: 143 additions & 89 deletions

File tree

‎toolkit/components/nimbus/ExperimentAPI.sys.mjs‎

Lines changed: 143 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
/**
6+
* @import { ExperimentManager } from "./lib/ExperimentManager.sys.mjs"
7+
* @import { RemoteSettingsExperimentLoader } from "./lib/RemoteSettingsExperimentLoader.sys.mjs"
8+
*/
9+
510
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
611
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
712

@@ -74,26 +79,6 @@ const experimentBranchAccessor = {
7479

7580
const NIMBUS_PROFILE_ID_PREF = "nimbus.profileId";
7681

77-
let cachedProfileId = null;
78-
79-
/**
80-
* Ensure the Nimbus profile ID exists.
81-
*
82-
* @returns {string} The profile ID.
83-
*/
84-
function ensureNimbusProfileId() {
85-
if (!cachedProfileId) {
86-
if (Services.prefs.prefHasUserValue(NIMBUS_PROFILE_ID_PREF)) {
87-
cachedProfileId = Services.prefs.getStringPref(NIMBUS_PROFILE_ID_PREF);
88-
} else {
89-
cachedProfileId = Services.uuid.generateUUID().toString().slice(1, -1);
90-
Services.prefs.setStringPref(NIMBUS_PROFILE_ID_PREF, cachedProfileId);
91-
}
92-
}
93-
94-
return cachedProfileId;
95-
}
96-
9782
/**
9883
* Metadata about an enrollment.
9984
*
@@ -131,11 +116,96 @@ export const EnrollmentType = Object.freeze({
131116
ROLLOUT: "rollout",
132117
});
133118

134-
let initialized = false;
135-
let experimentManager = null;
136-
let experimentLoader = null;
119+
export const ExperimentAPI = new (class {
120+
/**
121+
* Whether or not the ExperimentAPI has been initialized.
122+
*/
123+
#initialized = false;
124+
125+
/**
126+
* The current ExperimentManager.
127+
*
128+
* @type {ExperimentManager | null}
129+
*/
130+
#experimentManager = null;
131+
132+
/**
133+
* The current RemoteSettingsExperimentLoader.
134+
*
135+
* @type {RemoteSettingsExperimentLoader | null}
136+
*/
137+
#experimentLoader = null;
138+
139+
/**
140+
* The unique ID of this Profile.
141+
*
142+
* @type {string | null}
143+
*/
144+
#cachedProfileId = null;
145+
146+
/**
147+
* Cached pref values.
148+
*/
149+
#prefValues = {
150+
/**
151+
* Whether or not opt-out studies are enabled.
152+
*
153+
* @see {@link STUDIES_OPT_OUT_PREF}
154+
*/
155+
studiesEnabled: false,
156+
157+
/**
158+
* Whether or not telemetry is enabled.
159+
*
160+
* @see {@link UPLOAD_ENABLED_PREF}
161+
*/
162+
telemetryEnabled: false,
163+
};
164+
165+
constructor() {
166+
// Ensure that the profile ID is cached in a pref.
167+
if (Services.prefs.prefHasUserValue(NIMBUS_PROFILE_ID_PREF)) {
168+
this.#cachedProfileId = Services.prefs.getStringPref(
169+
NIMBUS_PROFILE_ID_PREF
170+
);
171+
} else {
172+
this.#cachedProfileId = Services.uuid
173+
.generateUUID()
174+
.toString()
175+
.slice(1, -1);
176+
Services.prefs.setStringPref(
177+
NIMBUS_PROFILE_ID_PREF,
178+
this.#cachedProfileId
179+
);
180+
}
181+
182+
const onStudiesEnabledChanged = this.#onStudiesEnabledChanged.bind(this);
183+
184+
XPCOMUtils.defineLazyPreferenceGetter(
185+
this.#prefValues,
186+
"studiesEnabled",
187+
STUDIES_OPT_OUT_PREF,
188+
false,
189+
onStudiesEnabledChanged
190+
);
191+
192+
XPCOMUtils.defineLazyPreferenceGetter(
193+
this.#prefValues,
194+
"telemetryEnabled",
195+
UPLOAD_ENABLED_PREF,
196+
false,
197+
onStudiesEnabledChanged
198+
);
199+
200+
this._annotateCrashReport = this._annotateCrashReport.bind(this);
201+
this._removeCrashReportAnnotator =
202+
this._removeCrashReportAnnotator.bind(this);
203+
204+
ChromeUtils.defineLazyGetter(this, "_remoteSettingsClient", function () {
205+
return lazy.RemoteSettings(lazy.COLLECTION_ID);
206+
});
207+
}
137208

138-
export const ExperimentAPI = {
139209
/**
140210
* The topic that is notified when either the studies enabled pref or the
141211
* telemetry enabled pref changes.
@@ -145,7 +215,7 @@ export const ExperimentAPI = {
145215
*/
146216
get STUDIES_ENABLED_CHANGED() {
147217
return "nimbus:studies-enabled-changed";
148-
},
218+
}
149219

150220
/**
151221
* Initialize the ExperimentAPI.
@@ -166,13 +236,11 @@ export const ExperimentAPI = {
166236
* Whether or not the ExperimentAPI was initialized.
167237
*/
168238
async init({ extraContext, forceSync = false } = {}) {
169-
if (initialized) {
239+
if (this.#initialized) {
170240
return false;
171241
}
172242

173-
ensureNimbusProfileId();
174-
175-
initialized = true;
243+
this.#initialized = true;
176244

177245
const studiesEnabled = this.studiesEnabled;
178246

@@ -240,78 +308,75 @@ export const ExperimentAPI = {
240308
);
241309
}
242310

243-
Services.prefs.addObserver(
244-
UPLOAD_ENABLED_PREF,
245-
this._onStudiesEnabledChanged
246-
);
247-
Services.prefs.addObserver(
248-
STUDIES_OPT_OUT_PREF,
249-
this._onStudiesEnabledChanged
250-
);
251-
252311
// If Nimbus was disabled between the start of this function and registering
253312
// the pref observers we have not handled it yet.
254313
if (studiesEnabled !== this.studiesEnabled) {
255-
await this._onStudiesEnabledChanged();
314+
await this.#onStudiesEnabledChanged();
256315
}
257316

258317
return true;
259-
},
318+
}
260319

261320
/**
262321
* Return the global ExperimentManager.
263322
*
264323
* The ExperimentManager will be lazily created upon first access to this
265324
* property.
325+
*
326+
* @type {ExperimentManager}
266327
*/
267328
get manager() {
268-
if (experimentManager === null) {
269-
experimentManager = new lazy.ExperimentManager();
329+
if (this.#experimentManager === null) {
330+
this.#experimentManager = new lazy.ExperimentManager();
270331
}
271332

272-
return experimentManager;
273-
},
333+
return this.#experimentManager;
334+
}
274335

275336
/**
276337
* Return the global ExperimentManager.
277338
*
278339
* @deprecated Use ExperimentAPI.Manager instead of this property.
340+
*
341+
* @type {ExperimentManager}
279342
*/
280343
get _manager() {
281344
return this.manager;
282-
},
345+
}
283346

284347
/**
285348
* Return the global RemoteSettingsExperimentLoader.
349+
*
350+
* @type {RemoteSettingsExperimentLoader}
286351
*/
287352
get _rsLoader() {
288-
if (experimentLoader === null) {
289-
experimentLoader = new lazy.RemoteSettingsExperimentLoader(this.manager);
353+
if (this.#experimentLoader === null) {
354+
this.#experimentLoader = new lazy.RemoteSettingsExperimentLoader(
355+
this.manager
356+
);
290357
}
291358

292-
return experimentLoader;
293-
},
359+
return this.#experimentLoader;
360+
}
294361

295362
_resetForTests() {
296-
experimentLoader?.disable();
297-
experimentLoader = null;
363+
this.#experimentLoader?.disable();
364+
this.#experimentLoader = null;
298365

299-
lazy.CleanupManager.removeCleanupHandler(
300-
ExperimentAPI._removeCrashReportAnnotator
301-
);
302-
experimentManager?.store.off("update", this._annotateCrashReport);
303-
experimentManager = null;
366+
lazy.CleanupManager.removeCleanupHandler(this._removeCrashReportAnnotator);
367+
this.#experimentManager?.store.off("update", this._annotateCrashReport);
368+
this.#experimentManager = null;
304369

305-
initialized = false;
306-
},
370+
this.#initialized = false;
371+
}
307372

308373
get studiesEnabled() {
309374
return (
310-
Services.prefs.getBoolPref(UPLOAD_ENABLED_PREF, false) &&
311-
Services.prefs.getBoolPref(STUDIES_OPT_OUT_PREF, false) &&
375+
this.#prefValues.studiesEnabled &&
376+
this.#prefValues.telemetryEnabled &&
312377
Services.policies.isAllowed("Shield")
313378
);
314-
},
379+
}
315380

316381
/**
317382
* Return the profile ID.
@@ -326,8 +391,8 @@ export const ExperimentAPI = {
326391
* @returns {string} The profile ID.
327392
*/
328393
get profileId() {
329-
return ensureNimbusProfileId();
330-
},
394+
return this.#cachedProfileId;
395+
}
331396

332397
/**
333398
* Wait for the ExperimentAPI to become ready.
@@ -342,7 +407,7 @@ export const ExperimentAPI = {
342407
*/
343408
async ready() {
344409
return this.manager.store.ready();
345-
},
410+
}
346411

347412
/**
348413
* Annotate the current crash report with current enrollments.
@@ -362,23 +427,27 @@ export const ExperimentAPI = {
362427
"NimbusEnrollments",
363428
activeEnrollments
364429
);
365-
},
430+
}
366431

367432
_removeCrashReportAnnotator() {
368-
if (initialized) {
369-
experimentManager?.store.off("update", this._annotateCrashReport);
433+
if (this.#initialized) {
434+
this.#experimentManager?.store.off("update", this._annotateCrashReport);
435+
}
436+
}
437+
438+
async #onStudiesEnabledChanged() {
439+
if (!this.#initialized) {
440+
return;
370441
}
371-
},
372442

373-
async _onStudiesEnabledChanged() {
374443
if (!this.studiesEnabled) {
375444
await this.manager._handleStudiesOptOut();
376445
}
377446

378447
await this._rsLoader.onEnabledPrefChange();
379448

380449
Services.obs.notifyObservers(null, this.STUDIES_ENABLED_CHANGED);
381-
},
450+
}
382451

383452
/**
384453
* Returns the recipe for a given experiment slug
@@ -415,7 +484,7 @@ export const ExperimentAPI = {
415484
}
416485

417486
return recipe;
418-
},
487+
}
419488

420489
/**
421490
* Returns all the branches for a given experiment slug
@@ -437,7 +506,7 @@ export const ExperimentAPI = {
437506
return recipe?.branches.map(
438507
branch => new Proxy(branch, experimentBranchAccessor)
439508
);
440-
},
509+
}
441510

442511
/**
443512
* Opt-in to the given experiment on the given branch.
@@ -465,8 +534,8 @@ export const ExperimentAPI = {
465534
*/
466535
async optInToExperiment(options) {
467536
return this._rsLoader._optInToExperiment(options);
468-
},
469-
};
537+
}
538+
})();
470539

471540
/**
472541
* Singleton that holds lazy references to _ExperimentFeature instances
@@ -933,21 +1002,6 @@ export class _ExperimentFeature {
9331002
}
9341003
}
9351004

936-
ExperimentAPI._annotateCrashReport =
937-
ExperimentAPI._annotateCrashReport.bind(ExperimentAPI);
938-
ExperimentAPI._onStudiesEnabledChanged =
939-
ExperimentAPI._onStudiesEnabledChanged.bind(ExperimentAPI);
940-
ExperimentAPI._removeCrashReportAnnotator =
941-
ExperimentAPI._removeCrashReportAnnotator.bind(ExperimentAPI);
942-
943-
ChromeUtils.defineLazyGetter(
944-
ExperimentAPI,
945-
"_remoteSettingsClient",
946-
function () {
947-
return lazy.RemoteSettings(lazy.COLLECTION_ID);
948-
}
949-
);
950-
9511005
class ExperimentLocalizationError extends Error {
9521006
constructor(reason, locale) {
9531007
super(`Localized experiment error (${reason})`);

0 commit comments

Comments
 (0)