715 lines
21 KiB
JavaScript
715 lines
21 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/. */
|
|
|
|
import { SharedDataMap } from "resource://nimbus/lib/SharedDataMap.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
|
|
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
|
|
PrefUtils: "resource://normandy/lib/PrefUtils.sys.mjs",
|
|
ProfilesDatastoreService:
|
|
"moz-src:///toolkit/profile/ProfilesDatastoreService.sys.mjs",
|
|
});
|
|
|
|
// This branch is used to store experiment data
|
|
const SYNC_DATA_PREF_BRANCH = "nimbus.syncdatastore.";
|
|
// This branch is used to store remote rollouts
|
|
const SYNC_DEFAULTS_PREF_BRANCH = "nimbus.syncdefaultsstore.";
|
|
let tryJSONParse = data => {
|
|
try {
|
|
return JSON.parse(data);
|
|
} catch (e) {}
|
|
|
|
return null;
|
|
};
|
|
ChromeUtils.defineLazyGetter(lazy, "syncDataStore", () => {
|
|
let experimentsPrefBranch = Services.prefs.getBranch(SYNC_DATA_PREF_BRANCH);
|
|
let defaultsPrefBranch = Services.prefs.getBranch(SYNC_DEFAULTS_PREF_BRANCH);
|
|
return {
|
|
_tryParsePrefValue(branch, pref) {
|
|
try {
|
|
return tryJSONParse(branch.getStringPref(pref, ""));
|
|
} catch (e) {
|
|
/* This is expected if we don't have anything stored */
|
|
}
|
|
|
|
return null;
|
|
},
|
|
_trySetPrefValue(branch, pref, value) {
|
|
try {
|
|
branch.setStringPref(pref, JSON.stringify(value));
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
},
|
|
_trySetTypedPrefValue(pref, value) {
|
|
let variableType = typeof value;
|
|
switch (variableType) {
|
|
case "boolean":
|
|
Services.prefs.setBoolPref(pref, value);
|
|
break;
|
|
case "number":
|
|
Services.prefs.setIntPref(pref, value);
|
|
break;
|
|
case "string":
|
|
Services.prefs.setStringPref(pref, value);
|
|
break;
|
|
case "object":
|
|
Services.prefs.setStringPref(pref, JSON.stringify(value));
|
|
break;
|
|
}
|
|
},
|
|
_clearBranchChildValues(prefBranch) {
|
|
const variablesBranch = Services.prefs.getBranch(prefBranch);
|
|
const prefChildList = variablesBranch.getChildList("");
|
|
for (let variable of prefChildList) {
|
|
variablesBranch.clearUserPref(variable);
|
|
}
|
|
},
|
|
/**
|
|
* Given a branch pref returns all child prefs and values
|
|
* { childPref: value }
|
|
* where value is parsed to the appropriate type
|
|
*
|
|
* @returns {Object[]}
|
|
*/
|
|
_getBranchChildValues(prefBranch, featureId) {
|
|
const branch = Services.prefs.getBranch(prefBranch);
|
|
const prefChildList = branch.getChildList("");
|
|
let values = {};
|
|
if (!prefChildList.length) {
|
|
return null;
|
|
}
|
|
for (const childPref of prefChildList) {
|
|
let prefName = `${prefBranch}${childPref}`;
|
|
let value = lazy.PrefUtils.getPref(prefName);
|
|
// Try to parse string values that could be stringified objects
|
|
if (
|
|
lazy.NimbusFeatures[featureId]?.manifest?.variables?.[childPref]
|
|
?.type === "json"
|
|
) {
|
|
let parsedValue = tryJSONParse(value);
|
|
if (parsedValue) {
|
|
value = parsedValue;
|
|
}
|
|
}
|
|
values[childPref] = value;
|
|
}
|
|
|
|
return values;
|
|
},
|
|
get(featureId) {
|
|
let metadata = this._tryParsePrefValue(experimentsPrefBranch, featureId);
|
|
if (!metadata) {
|
|
return null;
|
|
}
|
|
let prefBranch = `${SYNC_DATA_PREF_BRANCH}${featureId}.`;
|
|
metadata.branch.feature.value = this._getBranchChildValues(
|
|
prefBranch,
|
|
featureId
|
|
);
|
|
// We store the enrollment in the pref in a single-feature format, but
|
|
// Nimbus only supports multi-featured experiments, so we massage the
|
|
// enrollment into a multi-featured one.
|
|
metadata.branch.features = [metadata.branch.feature];
|
|
delete metadata.branch.feature;
|
|
|
|
return metadata;
|
|
},
|
|
getDefault(featureId) {
|
|
let metadata = this._tryParsePrefValue(defaultsPrefBranch, featureId);
|
|
if (!metadata) {
|
|
return null;
|
|
}
|
|
let prefBranch = `${SYNC_DEFAULTS_PREF_BRANCH}${featureId}.`;
|
|
metadata.branch.feature.value = this._getBranchChildValues(
|
|
prefBranch,
|
|
featureId
|
|
);
|
|
// We store the enrollment in the pref in a single-feature format, but
|
|
// Nimbus only supports multi-featured experiments, so we massage the
|
|
// enrollment into a multi-featured one.
|
|
metadata.branch.features = [metadata.branch.feature];
|
|
delete metadata.branch.feature;
|
|
|
|
return metadata;
|
|
},
|
|
set(featureId, value) {
|
|
/* If the enrollment branch has variables we store those separately
|
|
* in pref branches of appropriate type:
|
|
* { featureId: "foo", value: { enabled: true } }
|
|
* gets stored as `${SYNC_DATA_PREF_BRANCH}foo.enabled=true`
|
|
*/
|
|
if (value.branch?.feature?.value) {
|
|
for (let variable of Object.keys(value.branch.feature.value)) {
|
|
let prefName = `${SYNC_DATA_PREF_BRANCH}${featureId}.${variable}`;
|
|
this._trySetTypedPrefValue(
|
|
prefName,
|
|
value.branch.feature.value[variable]
|
|
);
|
|
}
|
|
this._trySetPrefValue(experimentsPrefBranch, featureId, {
|
|
...value,
|
|
branch: {
|
|
...value.branch,
|
|
feature: {
|
|
...value.branch.feature,
|
|
value: null,
|
|
},
|
|
},
|
|
});
|
|
} else {
|
|
this._trySetPrefValue(experimentsPrefBranch, featureId, value);
|
|
}
|
|
},
|
|
setDefault(featureId, enrollment) {
|
|
/* We store configuration variables separately in pref branches of
|
|
* appropriate type:
|
|
* (feature: "foo") { variables: { enabled: true } }
|
|
* gets stored as `${SYNC_DEFAULTS_PREF_BRANCH}foo.enabled=true`
|
|
*/
|
|
let { feature } = enrollment.branch;
|
|
for (let variable of Object.keys(feature.value)) {
|
|
let prefName = `${SYNC_DEFAULTS_PREF_BRANCH}${featureId}.${variable}`;
|
|
this._trySetTypedPrefValue(prefName, feature.value[variable]);
|
|
}
|
|
this._trySetPrefValue(defaultsPrefBranch, featureId, {
|
|
...enrollment,
|
|
branch: {
|
|
...enrollment.branch,
|
|
feature: {
|
|
...enrollment.branch.feature,
|
|
value: null,
|
|
},
|
|
},
|
|
});
|
|
},
|
|
getAllDefaultBranches() {
|
|
return defaultsPrefBranch.getChildList("").filter(
|
|
// Filter out remote defaults variable prefs
|
|
pref => !pref.includes(".")
|
|
);
|
|
},
|
|
delete(featureId) {
|
|
const prefBranch = `${SYNC_DATA_PREF_BRANCH}${featureId}.`;
|
|
this._clearBranchChildValues(prefBranch);
|
|
try {
|
|
experimentsPrefBranch.clearUserPref(featureId);
|
|
} catch (e) {}
|
|
},
|
|
deleteDefault(featureId) {
|
|
let prefBranch = `${SYNC_DEFAULTS_PREF_BRANCH}${featureId}.`;
|
|
this._clearBranchChildValues(prefBranch);
|
|
try {
|
|
defaultsPrefBranch.clearUserPref(featureId);
|
|
} catch (e) {}
|
|
},
|
|
};
|
|
});
|
|
|
|
const DEFAULT_STORE_ID = "ExperimentStoreData";
|
|
|
|
export class ExperimentStore extends SharedDataMap {
|
|
static SYNC_DATA_PREF_BRANCH = SYNC_DATA_PREF_BRANCH;
|
|
static SYNC_DEFAULTS_PREF_BRANCH = SYNC_DEFAULTS_PREF_BRANCH;
|
|
|
|
constructor(sharedDataKey, options) {
|
|
super(sharedDataKey ?? DEFAULT_STORE_ID, options);
|
|
}
|
|
|
|
/**
|
|
* Initialize the ExperimentStore.
|
|
*
|
|
* @param {object} options
|
|
* @param {boolean} options.cleanupOldRecipes
|
|
* ** TEST ONLY **
|
|
*
|
|
* Whether or not to automatically remove recipes from the ExperimentStore
|
|
* after initialization. Defaults to true.
|
|
*/
|
|
async init({ cleanupOldRecipes = true } = {}) {
|
|
await super.init();
|
|
|
|
const featureIds = new Set();
|
|
for (const enrollment of this.getAll().filter(e => e.active)) {
|
|
for (const featureId of enrollment.featureIds) {
|
|
featureIds.add(featureId);
|
|
}
|
|
}
|
|
|
|
for (const featureId of featureIds) {
|
|
this._emitFeatureUpdate(featureId, "feature-enrollments-loaded");
|
|
}
|
|
|
|
await this._reportStartupDatabaseConsistency();
|
|
|
|
// Clean up the old recipes *after* we report database consistency so that
|
|
// we're not racing.
|
|
if (cleanupOldRecipes) {
|
|
Services.tm.idleDispatchToMainThread(() => this._cleanupOldRecipes());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a feature identifier, find an active experiment that matches that feature identifier.
|
|
* This assumes, for now, that there is only one active experiment per feature per browser.
|
|
* Does not activate the experiment (send an exposure event)
|
|
*
|
|
* @param {string} featureId
|
|
* @returns {Enrollment|undefined} An active experiment if it exists
|
|
* @memberof ExperimentStore
|
|
*/
|
|
getExperimentForFeature(featureId) {
|
|
if (this._isReady) {
|
|
return this.getAllActiveExperiments().find(experiment =>
|
|
experiment.featureIds.includes(featureId)
|
|
);
|
|
}
|
|
|
|
if (lazy.NimbusFeatures[featureId]?.manifest.isEarlyStartup) {
|
|
return lazy.syncDataStore.get(featureId);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Check if an active experiment already exists for a feature.
|
|
* Does not activate the experiment (send an exposure event)
|
|
*
|
|
* @param {string} featureId
|
|
* @returns {boolean} Does an active experiment exist for that feature?
|
|
* @memberof ExperimentStore
|
|
*/
|
|
hasExperimentForFeature(featureId) {
|
|
if (!featureId) {
|
|
return false;
|
|
}
|
|
return !!this.getExperimentForFeature(featureId);
|
|
}
|
|
|
|
/**
|
|
* @returns {Enrollment[]}
|
|
*/
|
|
getAll() {
|
|
let data = [];
|
|
try {
|
|
data = Object.values(this._data || {});
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Returns all active experiments
|
|
* @returns {Enrollment[]}
|
|
*/
|
|
getAllActiveExperiments() {
|
|
return this.getAll().filter(
|
|
enrollment => enrollment.active && !enrollment.isRollout
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns all active rollouts
|
|
* @returns {Enrollment[]}
|
|
*/
|
|
getAllActiveRollouts() {
|
|
return this.getAll().filter(
|
|
enrollment => enrollment.active && enrollment.isRollout
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Query the store for the remote configuration of a feature
|
|
* @param {string} featureId The feature we want to query for
|
|
* @returns {{Rollout}|undefined} Remote defaults if available
|
|
*/
|
|
getRolloutForFeature(featureId) {
|
|
if (this._isReady) {
|
|
return this.getAllActiveRollouts().find(rollout =>
|
|
rollout.featureIds.includes(featureId)
|
|
);
|
|
}
|
|
|
|
if (lazy.NimbusFeatures[featureId]?.manifest.isEarlyStartup) {
|
|
return lazy.syncDataStore.getDefault(featureId);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Check if an active rollout already exists for a feature.
|
|
* Does not active the experiment (send an exposure event).
|
|
*
|
|
* @param {string} featureId
|
|
* @returns {boolean} Does an active rollout exist for that feature?
|
|
*/
|
|
hasRolloutForFeature(featureId) {
|
|
if (!featureId) {
|
|
return false;
|
|
}
|
|
return !!this.getRolloutForFeature(featureId);
|
|
}
|
|
|
|
/**
|
|
* Remove inactive enrollments older than 12 months
|
|
*/
|
|
async _cleanupOldRecipes() {
|
|
const threshold = 365.25 * 24 * 3600 * 1000;
|
|
const nowTimestamp = new Date().getTime();
|
|
const slugsToRemove = this.getAll()
|
|
.filter(
|
|
experiment =>
|
|
!experiment.active &&
|
|
// Flip the comparison here to catch scenarios in which lastSeen is
|
|
// invalid or undefined. The result with be a comparison with NaN
|
|
// which is always false
|
|
!(nowTimestamp - new Date(experiment.lastSeen).getTime() < threshold)
|
|
)
|
|
.map(r => r.slug);
|
|
|
|
this._removeEntriesByKeys(slugsToRemove);
|
|
await this._deleteEnrollmentsBySlug(slugsToRemove);
|
|
}
|
|
|
|
_emitUpdates(enrollment) {
|
|
const updateEvent = { slug: enrollment.slug, active: enrollment.active };
|
|
if (!enrollment.active) {
|
|
updateEvent.unenrollReason = enrollment.unenrollReason;
|
|
}
|
|
this.emit("update", updateEvent);
|
|
const reason = enrollment.isRollout
|
|
? "rollout-updated"
|
|
: "experiment-updated";
|
|
|
|
for (const featureId of enrollment.featureIds) {
|
|
this._emitFeatureUpdate(featureId, reason);
|
|
}
|
|
}
|
|
|
|
_emitFeatureUpdate(featureId, reason) {
|
|
this.emit(`featureUpdate:${featureId}`, reason);
|
|
}
|
|
|
|
_onFeatureUpdate(featureId, callback) {
|
|
if (this._isReady) {
|
|
const hasExperiment = this.hasExperimentForFeature(featureId);
|
|
if (hasExperiment || this.hasRolloutForFeature(featureId)) {
|
|
callback(
|
|
`featureUpdate:${featureId}`,
|
|
hasExperiment ? "experiment-updated" : "rollout-updated"
|
|
);
|
|
}
|
|
}
|
|
|
|
this.on(`featureUpdate:${featureId}`, callback);
|
|
}
|
|
|
|
_offFeatureUpdate(featureId, callback) {
|
|
this.off(`featureUpdate:${featureId}`, callback);
|
|
}
|
|
|
|
/**
|
|
* Persists early startup experiments or rollouts
|
|
* @param {Enrollment} enrollment Experiment or rollout
|
|
*/
|
|
_updateSyncStore(enrollment) {
|
|
for (let feature of enrollment.branch.features) {
|
|
if (lazy.NimbusFeatures[feature.featureId]?.manifest.isEarlyStartup) {
|
|
if (!enrollment.active) {
|
|
// Remove experiments on un-enroll, no need to check if it exists
|
|
if (enrollment.isRollout) {
|
|
lazy.syncDataStore.deleteDefault(feature.featureId);
|
|
} else {
|
|
lazy.syncDataStore.delete(feature.featureId);
|
|
}
|
|
} else {
|
|
let updateEnrollmentSyncStore = enrollment.isRollout
|
|
? lazy.syncDataStore.setDefault.bind(lazy.syncDataStore)
|
|
: lazy.syncDataStore.set.bind(lazy.syncDataStore);
|
|
updateEnrollmentSyncStore(feature.featureId, {
|
|
...enrollment,
|
|
branch: {
|
|
...enrollment.branch,
|
|
feature,
|
|
// Only store the early startup feature
|
|
features: null,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add an enrollment and notify listeners
|
|
* @param {Enrollment} enrollment
|
|
*/
|
|
addEnrollment(enrollment) {
|
|
if (!enrollment || !enrollment.slug) {
|
|
throw new Error(
|
|
`Tried to add an experiment but it didn't have a .slug property.`
|
|
);
|
|
}
|
|
|
|
this.set(enrollment.slug, enrollment);
|
|
this._updateSyncStore(enrollment);
|
|
this._emitUpdates(enrollment);
|
|
}
|
|
|
|
/**
|
|
* Merge new properties into the properties of an existing experiment
|
|
* @param {string} slug
|
|
* @param {Partial<Enrollment>} newProperties
|
|
*/
|
|
updateExperiment(slug, newProperties) {
|
|
const oldProperties = this.get(slug);
|
|
if (!oldProperties) {
|
|
throw new Error(
|
|
`Tried to update experiment ${slug} but it doesn't exist`
|
|
);
|
|
}
|
|
const updatedExperiment = { ...oldProperties, ...newProperties };
|
|
this.set(slug, updatedExperiment);
|
|
this._updateSyncStore(updatedExperiment);
|
|
this._emitUpdates(updatedExperiment);
|
|
}
|
|
|
|
/**
|
|
* Test only helper for cleanup
|
|
*
|
|
* @param slugOrFeatureId Can be called with slug (which removes the SharedDataMap entry) or
|
|
* with featureId which removes the SyncDataStore entry for the feature
|
|
*/
|
|
_deleteForTests(slugOrFeatureId) {
|
|
super._deleteForTests(slugOrFeatureId);
|
|
lazy.syncDataStore.deleteDefault(slugOrFeatureId);
|
|
lazy.syncDataStore.delete(slugOrFeatureId);
|
|
}
|
|
|
|
async _addEnrollmentToDatabase(enrollment, recipe) {
|
|
if (
|
|
!Services.prefs.getBoolPref(
|
|
"nimbus.profilesdatastoreservice.enabled",
|
|
false
|
|
)
|
|
) {
|
|
// We are in an xpcshell test that has not initialized the
|
|
// ProfilesDatastoreService.
|
|
//
|
|
// TODO(bug 1967779): require the ProfilesDatastoreService to be initialized
|
|
// and remove this check.
|
|
return;
|
|
}
|
|
|
|
const profileId = lazy.ExperimentAPI.profileId;
|
|
|
|
let success = true;
|
|
try {
|
|
const conn = await lazy.ProfilesDatastoreService.getConnection();
|
|
await conn.execute(
|
|
`
|
|
INSERT INTO NimbusEnrollments VALUES(
|
|
null,
|
|
:profileId,
|
|
:slug,
|
|
:branchSlug,
|
|
jsonb(:recipe),
|
|
:active,
|
|
:unenrollReason,
|
|
:lastSeen,
|
|
jsonb(:setPrefs),
|
|
jsonb(:prefFlips),
|
|
:source
|
|
)
|
|
ON CONFLICT(profileId, slug)
|
|
DO UPDATE SET
|
|
branchSlug = excluded.branchSlug,
|
|
recipe = excluded.recipe,
|
|
active = excluded.active,
|
|
unenrollReason = excluded.unenrollReason,
|
|
lastSeen = excluded.lastSeen,
|
|
setPrefs = excluded.setPrefs,
|
|
prefFlips = excluded.setPrefs,
|
|
source = excluded.source;
|
|
`,
|
|
{
|
|
profileId,
|
|
slug: enrollment.slug,
|
|
branchSlug: enrollment.branch.slug,
|
|
recipe: recipe ? JSON.stringify(recipe) : null,
|
|
active: enrollment.active,
|
|
unenrollReason: enrollment.unenrollReason ?? null,
|
|
lastSeen: enrollment.lastSeen,
|
|
setPrefs: enrollment.prefs ? JSON.stringify(enrollment.prefs) : null,
|
|
prefFlips: enrollment.prefFlips
|
|
? JSON.stringify(enrollment.prefFlips)
|
|
: null,
|
|
source: enrollment.source,
|
|
}
|
|
);
|
|
} catch (e) {
|
|
console.error(
|
|
`ExperimentStore: Failed writing enrollment for ${enrollment.slug} to NimbusEnrollments`,
|
|
e
|
|
);
|
|
success = false;
|
|
}
|
|
|
|
Glean.nimbusEvents.databaseWrite.record({ success });
|
|
}
|
|
|
|
async _deactivateEnrollmentInDatabase(slug, unenrollReason = "unknown") {
|
|
if (
|
|
!Services.prefs.getBoolPref(
|
|
"nimbus.profilesdatastoreservice.enabled",
|
|
false
|
|
)
|
|
) {
|
|
// We are in an xpcshell test that has not initialized the
|
|
// ProfilesDatastoreService.
|
|
//
|
|
// TODO(bug 1967779): require the ProfilesDatastoreService to be initialized
|
|
// and remove this check.
|
|
return;
|
|
}
|
|
|
|
const profileId = lazy.ExperimentAPI.profileId;
|
|
|
|
let success = true;
|
|
try {
|
|
const conn = await lazy.ProfilesDatastoreService.getConnection();
|
|
await conn.execute(
|
|
`
|
|
UPDATE NimbusEnrollments SET
|
|
active = false,
|
|
unenrollReason = :unenrollReason,
|
|
recipe = null,
|
|
prefFlips = null,
|
|
setPrefs = null
|
|
WHERE
|
|
profileId = :profileId AND
|
|
slug = :slug;
|
|
`,
|
|
{
|
|
slug,
|
|
profileId,
|
|
unenrollReason,
|
|
}
|
|
);
|
|
} catch (e) {
|
|
console.error(
|
|
`ExperimentStore: Failed writing unenrollment for ${slug} to NimbusEnrollments`,
|
|
e
|
|
);
|
|
success = false;
|
|
}
|
|
|
|
Glean.nimbusEvents.databaseWrite.record({ success });
|
|
}
|
|
|
|
async _deleteEnrollmentsBySlug(slugsToRemove) {
|
|
if (
|
|
!Services.prefs.getBoolPref(
|
|
"nimbus.profilesdatastoreservice.enabled",
|
|
false
|
|
)
|
|
) {
|
|
// We are in an xpcshell test that has not initialized the
|
|
// ProfilesDatastoreService.
|
|
//
|
|
// TODO(bug 1967779): require the ProfilesDatastoreService to be initialized
|
|
// and remove this check.
|
|
return;
|
|
}
|
|
|
|
if (!slugsToRemove.length) {
|
|
return;
|
|
}
|
|
|
|
let success = true;
|
|
|
|
try {
|
|
const conn = await lazy.ProfilesDatastoreService.getConnection();
|
|
await conn.executeTransaction(async () => {
|
|
for (const slug of slugsToRemove) {
|
|
await conn.execute(
|
|
`
|
|
DELETE FROM NimbusEnrollments
|
|
WHERE
|
|
profileId = :profileId AND
|
|
slug = :slug;
|
|
`,
|
|
{
|
|
profileId: lazy.ExperimentAPI.profileId,
|
|
slug,
|
|
}
|
|
);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.error(
|
|
`ExperimentStore: failed to remove enrollments for ${slugsToRemove}`,
|
|
e
|
|
);
|
|
success = false;
|
|
}
|
|
|
|
Glean.nimbusEvents.databaseWrite.record({ success });
|
|
}
|
|
|
|
async _reportStartupDatabaseConsistency() {
|
|
if (
|
|
!Services.prefs.getBoolPref(
|
|
"nimbus.profilesdatastoreservice.enabled",
|
|
false
|
|
)
|
|
) {
|
|
// We are in an xpcshell test that has not initialized the
|
|
// ProfilesDatastoreService.
|
|
//
|
|
// TODO(bug 1967779): require the ProfilesDatastoreService to be initialized
|
|
// and remove this check.
|
|
return;
|
|
}
|
|
|
|
const conn = await lazy.ProfilesDatastoreService.getConnection();
|
|
const rows = await conn.execute(
|
|
`
|
|
SELECT
|
|
slug,
|
|
active
|
|
FROM NimbusEnrollments
|
|
WHERE
|
|
profileId = :profileId;
|
|
`,
|
|
{
|
|
profileId: lazy.ExperimentAPI.profileId,
|
|
}
|
|
);
|
|
|
|
const dbEnrollments = rows.map(row => row.getResultByName("active"));
|
|
const storeEnrollments = this.getAll().map(e => e.active);
|
|
|
|
function countActive(sum, active) {
|
|
return sum + Number(active);
|
|
}
|
|
|
|
const dbActiveCount = dbEnrollments.reduce(countActive, 0);
|
|
const storeActiveCount = storeEnrollments.reduce(countActive, 0);
|
|
|
|
Glean.nimbusEvents.startupDatabaseConsistency.record({
|
|
total_db_count: dbEnrollments.length,
|
|
total_store_count: storeEnrollments.length,
|
|
db_active_count: dbActiveCount,
|
|
store_active_count: storeActiveCount,
|
|
});
|
|
}
|
|
}
|