Bug 1726190 - Implement multi-feature enrollment support for Nimbus r=k88hudson

Differential Revision: https://phabricator.services.mozilla.com/D123139
This commit is contained in:
Andrei Oprea 2021-09-06 16:04:19 +00:00
Родитель b2955e8a9d
Коммит 07703bd30d
22 изменённых файлов: 637 добавлений и 271 удалений

Просмотреть файл

@ -57,6 +57,19 @@ function parseJSON(value) {
return null; return null;
} }
function featuresCompat(branch) {
if (!branch) {
return [];
}
let { features } = branch;
// In <=v1.5.0 of the Nimbus API, experiments had single feature
if (!features) {
features = [branch.feature];
}
return features;
}
const ExperimentAPI = { const ExperimentAPI = {
/** /**
* @returns {Promise} Resolves when the API has synchronized to the main store * @returns {Promise} Resolves when the API has synchronized to the main store
@ -405,9 +418,13 @@ class _ExperimentFeature {
isEnabled({ defaultValue = null } = {}) { isEnabled({ defaultValue = null } = {}) {
const branch = ExperimentAPI.activateBranch({ featureId: this.featureId }); const branch = ExperimentAPI.activateBranch({ featureId: this.featureId });
let feature = featuresCompat(branch).find(
({ featureId }) => featureId === this.featureId
);
// First, try to return an experiment value if it exists. // First, try to return an experiment value if it exists.
if (isBooleanValueDefined(branch?.feature.enabled)) { if (isBooleanValueDefined(feature?.enabled)) {
return branch.feature.enabled; return feature.enabled;
} }
if (isBooleanValueDefined(this.getRemoteConfig()?.enabled)) { if (isBooleanValueDefined(this.getRemoteConfig()?.enabled)) {
@ -436,12 +453,15 @@ class _ExperimentFeature {
// Any user pref will override any other configuration // Any user pref will override any other configuration
let userPrefs = this._getUserPrefsValues(); let userPrefs = this._getUserPrefsValues();
const branch = ExperimentAPI.activateBranch({ featureId: this.featureId }); const branch = ExperimentAPI.activateBranch({ featureId: this.featureId });
const featureValue = featuresCompat(branch).find(
({ featureId }) => featureId === this.featureId
)?.value;
return { return {
...this.prefGetters, ...this.prefGetters,
...defaultValues, ...defaultValues,
...this.getRemoteConfig()?.variables, ...this.getRemoteConfig()?.variables,
...(branch?.feature?.value || null), ...(featureValue || null),
...userPrefs, ...userPrefs,
}; };
} }
@ -465,9 +485,12 @@ class _ExperimentFeature {
} }
// Next, check if an experiment is defined // Next, check if an experiment is defined
const experimentValue = ExperimentAPI.activateBranch({ const branch = ExperimentAPI.activateBranch({
featureId: this.featureId, featureId: this.featureId,
})?.feature?.value?.[variable]; });
const experimentValue = featuresCompat(branch).find(
({ featureId }) => featureId === this.featureId
)?.value?.[variable];
if (typeof experimentValue !== "undefined") { if (typeof experimentValue !== "undefined") {
return experimentValue; return experimentValue;

Просмотреть файл

@ -35,6 +35,19 @@ const TELEMETRY_DEFAULT_EXPERIMENT_TYPE = "nimbus";
const STUDIES_OPT_OUT_PREF = "app.shield.optoutstudies.enabled"; const STUDIES_OPT_OUT_PREF = "app.shield.optoutstudies.enabled";
function featuresCompat(branch) {
if (!branch || (!branch.feature && !branch.features)) {
return [];
}
let { features } = branch;
// In <=v1.5.0 of the Nimbus API, experiments had single feature
if (!features) {
features = [branch.feature];
}
return features;
}
/** /**
* A module for processes Experiment recipes, choosing and storing enrollment state, * A module for processes Experiment recipes, choosing and storing enrollment state,
* and sending experiment-related Telemetry. * and sending experiment-related Telemetry.
@ -184,19 +197,16 @@ class _ExperimentManager {
} }
const branch = await this.chooseBranch(slug, branches); const branch = await this.chooseBranch(slug, branches);
const features = featuresCompat(branch);
for (let feature of features) {
if (this.store.hasExperimentForFeature(feature?.featureId)) {
log.debug(
`Skipping enrollment for "${slug}" because there is an existing experiment for its feature.`
);
this.sendFailureTelemetry("enrollFailed", slug, "feature-conflict");
if ( return null;
this.store.hasExperimentForFeature( }
// Extract out only the feature names from the branch
branch.feature?.featureId
)
) {
log.debug(
`Skipping enrollment for "${slug}" because there is an existing experiment for its feature.`
);
this.sendFailureTelemetry("enrollFailed", slug, "feature-conflict");
return null;
} }
return this._enroll(recipe, branch, source); return this._enroll(recipe, branch, source);
@ -250,15 +260,16 @@ class _ExperimentManager {
* If the experiment has the same slug after unenrollment adding it to the * If the experiment has the same slug after unenrollment adding it to the
* store will overwrite the initial experiment. * store will overwrite the initial experiment.
*/ */
let experiment = this.store.getExperimentForFeature( const features = featuresCompat(branch);
branch.feature?.featureId for (let feature of features) {
); let experiment = this.store.getExperimentForFeature(feature?.featureId);
if (experiment) { if (experiment) {
log.debug( log.debug(
`Existing experiment found for the same feature ${branch?.feature.featureId}, unenrolling.` `Existing experiment found for the same feature ${feature.featureId}, unenrolling.`
); );
this.unenroll(experiment.slug, source); this.unenroll(experiment.slug, source);
}
} }
recipe.userFacingName = `${recipe.userFacingName} - Forced enrollment`; recipe.userFacingName = `${recipe.userFacingName} - Forced enrollment`;

Просмотреть файл

@ -34,14 +34,6 @@ let tryJSONParse = data => {
return null; return null;
}; };
XPCOMUtils.defineLazyGetter(this, "syncDataStore", () => { XPCOMUtils.defineLazyGetter(this, "syncDataStore", () => {
// Pref name changed in bug 1691516 and we want to clear the old pref for users
const previousPrefName = "messaging-system.syncdatastore.data";
try {
if (IS_MAIN_PROCESS) {
Services.prefs.clearUserPref(previousPrefName);
}
} catch (e) {}
let experimentsPrefBranch = Services.prefs.getBranch(SYNC_DATA_PREF_BRANCH); let experimentsPrefBranch = Services.prefs.getBranch(SYNC_DATA_PREF_BRANCH);
let defaultsPrefBranch = Services.prefs.getBranch(SYNC_DEFAULTS_PREF_BRANCH); let defaultsPrefBranch = Services.prefs.getBranch(SYNC_DEFAULTS_PREF_BRANCH);
return { return {
@ -205,6 +197,31 @@ XPCOMUtils.defineLazyGetter(this, "syncDataStore", () => {
const DEFAULT_STORE_ID = "ExperimentStoreData"; const DEFAULT_STORE_ID = "ExperimentStoreData";
/**
* Returns all feature ids associated with the branch provided.
* Fallback for when `featureIds` was not persisted to disk. Can be removed
* after bug 1725240 has reached release.
*
* @param {Branch} branch
* @returns {string[]}
*/
function getAllBranchFeatureIds(branch) {
return featuresCompat(branch).map(f => f.featureId);
}
function featuresCompat(branch) {
if (!branch || (!branch.feature && !branch.features)) {
return [];
}
let { features } = branch;
// In <=v1.5.0 of the Nimbus API, experiments had single feature
if (!features) {
features = [branch.feature];
}
return features;
}
class ExperimentStore extends SharedDataMap { class ExperimentStore extends SharedDataMap {
static SYNC_DATA_PREF_BRANCH = SYNC_DATA_PREF_BRANCH; static SYNC_DATA_PREF_BRANCH = SYNC_DATA_PREF_BRANCH;
static SYNC_DEFAULTS_PREF_BRANCH = SYNC_DEFAULTS_PREF_BRANCH; static SYNC_DEFAULTS_PREF_BRANCH = SYNC_DEFAULTS_PREF_BRANCH;
@ -216,13 +233,10 @@ class ExperimentStore extends SharedDataMap {
async init() { async init() {
await super.init(); await super.init();
this.getAllActive().forEach(({ branch }) => { this.getAllActive().forEach(({ branch, featureIds }) => {
if (branch?.feature?.featureId) { (featureIds || getAllBranchFeatureIds(branch)).forEach(featureId =>
this._emitFeatureUpdate( this._emitFeatureUpdate(featureId, "feature-experiment-loaded")
branch.feature.featureId, );
"feature-experiment-loaded"
);
}
}); });
Services.tm.idleDispatchToMainThread(() => this._cleanupOldRecipes()); Services.tm.idleDispatchToMainThread(() => this._cleanupOldRecipes());
@ -243,7 +257,7 @@ class ExperimentStore extends SharedDataMap {
experiment => experiment =>
experiment.featureIds?.includes(featureId) || experiment.featureIds?.includes(featureId) ||
// Supports <v1.3.0, which was when .featureIds was added // Supports <v1.3.0, which was when .featureIds was added
experiment.branch?.feature?.featureId === featureId getAllBranchFeatureIds(experiment.branch).includes(featureId)
// Default to the pref store if data is not yet ready // Default to the pref store if data is not yet ready
) || syncDataStore.get(featureId) ) || syncDataStore.get(featureId)
); );
@ -305,13 +319,12 @@ class ExperimentStore extends SharedDataMap {
_emitExperimentUpdates(experiment) { _emitExperimentUpdates(experiment) {
this.emit(`update:${experiment.slug}`, experiment); this.emit(`update:${experiment.slug}`, experiment);
if (experiment.branch.feature) { (
this.emit(`update:${experiment.branch.feature.featureId}`, experiment); experiment.featureIds || getAllBranchFeatureIds(experiment.branch)
this._emitFeatureUpdate( ).forEach(featureId => {
experiment.branch.feature.featureId, this.emit(`update:${featureId}`, experiment);
"experiment-updated" this._emitFeatureUpdate(featureId, "experiment-updated");
); });
}
} }
_emitFeatureUpdate(featureId, reason) { _emitFeatureUpdate(featureId, reason) {
@ -330,16 +343,26 @@ class ExperimentStore extends SharedDataMap {
* @param {Enrollment} experiment * @param {Enrollment} experiment
*/ */
_updateSyncStore(experiment) { _updateSyncStore(experiment) {
let featureId = experiment.branch.feature?.featureId; let features = featuresCompat(experiment.branch);
if ( for (let feature of features) {
FeatureManifest[featureId]?.isEarlyStartup || if (
experiment.branch.feature?.isEarlyStartup FeatureManifest[feature.featureId]?.isEarlyStartup ||
) { feature.isEarlyStartup
if (!experiment.active) { ) {
// Remove experiments on un-enroll, no need to check if it exists if (!experiment.active) {
syncDataStore.delete(featureId); // Remove experiments on un-enroll, no need to check if it exists
} else { syncDataStore.delete(feature.featureId);
syncDataStore.set(featureId, experiment); } else {
syncDataStore.set(feature.featureId, {
...experiment,
branch: {
...experiment.branch,
feature,
// Only store the early startup feature
features: null,
},
});
}
} }
} }
} }

Просмотреть файл

@ -11,35 +11,38 @@
"branch": { "branch": {
"type": "object", "type": "object",
"properties": { "properties": {
"feature": { "features": {
"type": "object", "type": "array",
"properties": { "items": {
"featureId": { "type": "object",
"type": "string", "properties": {
"description": "The identifier for the feature flag" "featureId": {
"type": "string",
"description": "The identifier for the feature flag"
},
"value": {
"anyOf": [
{
"type": "object",
"additionalProperties": {}
},
{
"type": "null"
}
],
"description": "Optional extra params for the feature (this should be validated against a schema)"
},
"enabled": {
"type": "boolean",
"description": "(deprecated)"
}
}, },
"value": { "required": ["featureId", "value"],
"anyOf": [ "additionalProperties": false
{ }
"type": "object",
"additionalProperties": {}
},
{
"type": "null"
}
],
"description": "Optional extra params for the feature (this should be validated against a schema)"
},
"enabled": {
"type": "boolean",
"description": "(deprecated)"
}
},
"required": ["featureId", "value"],
"additionalProperties": false
} }
}, },
"required": ["feature"] "required": ["features"]
}, },
"active": { "active": {
"type": "boolean", "type": "boolean",
@ -87,7 +90,8 @@
"experimentType", "experimentType",
"source", "source",
"userFacingName", "userFacingName",
"userFacingDescription" "userFacingDescription",
"featureIds"
], ],
"additionalProperties": false, "additionalProperties": false,
"description": "The experiment definition accessible to:\n1. The Nimbus SDK via Remote Settings\n2. Jetstream via the Experimenter API" "description": "The experiment definition accessible to:\n1. The Nimbus SDK via Remote Settings\n2. Jetstream via the Experimenter API"

Просмотреть файл

@ -85,28 +85,31 @@
"description": "Relative ratio of population for the branch (e.g. if branch A=1 and branch B=3,\nbranch A would get 25% of the population)", "description": "Relative ratio of population for the branch (e.g. if branch A=1 and branch B=3,\nbranch A would get 25% of the population)",
"default": 1 "default": 1
}, },
"feature": { "features": {
"type": "object", "type": "array",
"properties": { "items": {
"featureId": { "type": "object",
"type": "string", "properties": {
"description": "The identifier for the feature flag" "featureId": {
"type": "string",
"description": "The identifier for the feature flag"
},
"value": {
"anyOf": [
{
"type": "object",
"additionalProperties": {}
},
{
"type": "null"
}
],
"description": "Optional extra params for the feature (this should be validated against a schema)"
}
}, },
"value": { "required": ["featureId", "value"],
"anyOf": [ "additionalProperties": false
{ }
"type": "object",
"additionalProperties": {}
},
{
"type": "null"
}
],
"description": "Optional extra params for the feature (this should be validated against a schema)"
}
},
"required": ["featureId", "value"],
"additionalProperties": false
} }
}, },
"required": ["slug", "ratio"], "required": ["slug", "ratio"],
@ -143,6 +146,11 @@
"filter_expression": { "filter_expression": {
"type": "string", "type": "string",
"description": "This is NOT used by Nimbus, but has special functionality in Remote Settings.\nSee https://remote-settings.readthedocs.io/en/latest/target-filters.html#how" "description": "This is NOT used by Nimbus, but has special functionality in Remote Settings.\nSee https://remote-settings.readthedocs.io/en/latest/target-filters.html#how"
},
"featureIds": {
"type": "array",
"items": [{ "type": "string" }],
"description": "Array of strings corresponding to the branch features in the enrollment."
} }
}, },
"required": [ "required": [
@ -158,7 +166,8 @@
"startDate", "startDate",
"endDate", "endDate",
"proposedEnrollment", "proposedEnrollment",
"referenceBranch" "referenceBranch",
"featureIds"
], ],
"additionalProperties": true, "additionalProperties": true,
"description": "The experiment definition accessible to:\n1. The Nimbus SDK via Remote Settings\n2. Jetstream via the Experimenter API" "description": "The experiment definition accessible to:\n1. The Nimbus SDK via Remote Settings\n2. Jetstream via the Experimenter API"

Просмотреть файл

@ -53,27 +53,28 @@ const ExperimentTestUtils = {
}, },
_validateFeatureValueEnum({ branch }) { _validateFeatureValueEnum({ branch }) {
let { feature } = branch; let { features } = branch;
// If we're not using a real feature skip this check for (let feature of features) {
if (!FeatureManifest[feature.featureId]) { // If we're not using a real feature skip this check
return true; if (!FeatureManifest[feature.featureId]) {
} return true;
let { variables } = FeatureManifest[feature.featureId]; }
for (let varName of Object.keys(variables)) { let { variables } = FeatureManifest[feature.featureId];
let varValue = feature.value[varName]; for (let varName of Object.keys(variables)) {
if ( let varValue = feature.value[varName];
varValue && if (
variables[varName].enum && varValue &&
!variables[varName].enum.includes(varValue) variables[varName].enum &&
) { !variables[varName].enum.includes(varValue)
throw new Error( ) {
`${varName} should have one of the following values: ${JSON.stringify( throw new Error(
variables[varName].enum `${varName} should have one of the following values: ${JSON.stringify(
)} but has value '${varValue}'` variables[varName].enum
); )} but has value '${varValue}'`
);
}
} }
} }
return true; return true;
}, },
@ -100,6 +101,12 @@ const ExperimentTestUtils = {
) )
).NimbusExperiment; ).NimbusExperiment;
// We still have single feature experiment recipes for backwards
// compatibility testing but we don't do schema validation
if (!enrollment.branch.features && enrollment.branch.feature) {
return true;
}
return ( return (
this._validateFeatureValueEnum(enrollment) && this._validateFeatureValueEnum(enrollment) &&
this._validator( this._validator(
@ -184,7 +191,7 @@ const ExperimentFakes = {
{ {
slug: "control", slug: "control",
ratio: 1, ratio: 1,
feature: featureConfig, features: [featureConfig],
}, },
], ],
} }
@ -268,10 +275,12 @@ const ExperimentFakes = {
enrollmentId: NormandyUtils.generateUuid(), enrollmentId: NormandyUtils.generateUuid(),
branch: { branch: {
slug: "treatment", slug: "treatment",
feature: { features: [
featureId: "test-feature", {
value: { title: "hello", enabled: true }, featureId: "test-feature",
}, value: { title: "hello", enabled: true },
},
],
...props, ...props,
}, },
source: "NimbusTestUtils", source: "NimbusTestUtils",
@ -279,6 +288,9 @@ const ExperimentFakes = {
experimentType: "NimbusTestUtils", experimentType: "NimbusTestUtils",
userFacingName: "NimbusTestUtils", userFacingName: "NimbusTestUtils",
userFacingDescription: "NimbusTestUtils", userFacingDescription: "NimbusTestUtils",
featureIds: props?.branch?.features?.map(f => f.featureId) || [
"test-feature",
],
...props, ...props,
}; };
}, },
@ -298,15 +310,17 @@ const ExperimentFakes = {
{ {
slug: "control", slug: "control",
ratio: 1, ratio: 1,
feature: { featureId: "test-feature", value: { enabled: true } }, features: [{ featureId: "test-feature", value: { enabled: true } }],
}, },
{ {
slug: "treatment", slug: "treatment",
ratio: 1, ratio: 1,
feature: { features: [
featureId: "test-feature", {
value: { title: "hello", enabled: true }, featureId: "test-feature",
}, value: { title: "hello", enabled: true },
},
],
}, },
], ],
bucketConfig: { bucketConfig: {
@ -318,7 +332,9 @@ const ExperimentFakes = {
}, },
userFacingName: "Nimbus recipe", userFacingName: "Nimbus recipe",
userFacingDescription: "NimbusTestUtils recipe", userFacingDescription: "NimbusTestUtils recipe",
featureIds: ["test-feature"], featureIds: props?.branches?.[0].features?.map(f => f.featureId) || [
"test-feature",
],
...props, ...props,
}; };
}, },

Просмотреть файл

@ -5,9 +5,11 @@ prefs =
# This turns off the update interval for fetching recipes from Remote Settings # This turns off the update interval for fetching recipes from Remote Settings
app.normandy.run_interval_seconds=0 app.normandy.run_interval_seconds=0
[browser_experiment_single_feature_enrollment.js]
[browser_remotesettingsexperimentloader_remote_defaults.js] [browser_remotesettingsexperimentloader_remote_defaults.js]
[browser_remotesettingsexperimentloader_force_enrollment.js] [browser_remotesettingsexperimentloader_force_enrollment.js]
[browser_experimentstore_load.js] [browser_experimentstore_load.js]
[browser_experimentstore_load_single_feature.js]
[browser_remotesettings_experiment_enroll.js] [browser_remotesettings_experiment_enroll.js]
[browser_experiment_evaluate_jexl.js] [browser_experiment_evaluate_jexl.js]
[browser_remotesettingsexperimentloader_init.js] [browser_remotesettingsexperimentloader_init.js]

Просмотреть файл

@ -83,14 +83,15 @@ add_task(async function test_evaluate_active_experiments_activeExperiments() {
branches: [ branches: [
{ {
slug: "mochitest-active-foo", slug: "mochitest-active-foo",
feature: { features: [
enabled: true, {
featureId: "foo", enabled: true,
value: null, featureId: "foo",
}, value: null,
},
],
}, },
], ],
active: true,
}) })
); );

Просмотреть файл

@ -0,0 +1,131 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const { ExperimentAPI, NimbusFeatures } = ChromeUtils.import(
"resource://nimbus/ExperimentAPI.jsm"
);
const SINGLE_FEATURE_RECIPE = {
appId: "firefox-desktop",
appName: "firefox_desktop",
application: "firefox-desktop",
arguments: {},
branches: [
{
feature: {
enabled: true,
featureId: "urlbar",
isEarlyStartup: true,
value: {
quickSuggestEnabled: false,
quickSuggestNonSponsoredIndex: -1,
quickSuggestShouldShowOnboardingDialog: true,
quickSuggestShowOnboardingDialogAfterNRestarts: 2,
quickSuggestSponsoredIndex: -1,
},
},
ratio: 1,
slug: "control",
},
{
feature: {
enabled: true,
featureId: "urlbar",
isEarlyStartup: true,
value: {
quickSuggestEnabled: true,
quickSuggestNonSponsoredIndex: -1,
quickSuggestShouldShowOnboardingDialog: false,
quickSuggestShowOnboardingDialogAfterNRestarts: 2,
quickSuggestSponsoredIndex: -1,
},
},
ratio: 1,
slug: "treatment",
},
],
bucketConfig: {
count: 10000,
namespace: "urlbar-9",
randomizationUnit: "normandy_id",
start: 0,
total: 10000,
},
channel: "release",
endDate: null,
featureIds: ["urlbar"],
id: "firefox-suggest-history-vs-offline",
isEnrollmentPaused: false,
outcomes: [],
probeSets: [],
proposedDuration: 28,
proposedEnrollment: 7,
referenceBranch: "control",
schemaVersion: "1.5.0",
slug: "firefox-suggest-history-vs-offline",
startDate: "2021-07-21",
targeting: "true",
userFacingDescription: "Smarter suggestions in the AwesomeBar",
userFacingName: "Firefox Suggest - History vs Offline",
};
const SYNC_DATA_PREF_BRANCH = "nimbus.syncdatastore.";
add_task(async function test_TODO() {
let {
enrollmentPromise,
doExperimentCleanup,
} = ExperimentFakes.enrollmentHelper(SINGLE_FEATURE_RECIPE);
let sandbox = sinon.createSandbox();
let stub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
await enrollmentPromise;
Assert.ok(
ExperimentAPI.getExperiment({ featureId: "urlbar" }),
"Should enroll in single feature experiment"
);
Assert.ok(
Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}urlbar`),
"Should store early startup feature for sync access"
);
Assert.equal(
Services.prefs.getIntPref(
`${SYNC_DATA_PREF_BRANCH}urlbar.quickSuggestSponsoredIndex`
),
-1,
"Should store early startup variable for sync access"
);
Assert.equal(
NimbusFeatures.urlbar.getVariable(
"quickSuggestShowOnboardingDialogAfterNRestarts"
),
2,
"Should return value"
);
NimbusFeatures.urlbar.recordExposureEvent();
Assert.ok(stub.calledOnce, "Should be called once by urlbar");
Assert.equal(
stub.firstCall.args[0].experimentSlug,
"firefox-suggest-history-vs-offline",
"Should have expected slug"
);
Assert.equal(
stub.firstCall.args[0].featureId,
"urlbar",
"Should have expected featureId"
);
await doExperimentCleanup();
sandbox.restore();
NimbusFeatures.urlbar._sendExposureEventOnce = true;
});

Просмотреть файл

@ -61,7 +61,7 @@ add_task(async function test_load_from_disk_event() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "green", enabled: true }, features: [{ featureId: "green", enabled: true }],
}, },
lastSeen: Date.now(), lastSeen: Date.now(),
}); });

Просмотреть файл

@ -0,0 +1,93 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { ExperimentStore } = ChromeUtils.import(
"resource://nimbus/lib/ExperimentStore.jsm"
);
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const { NimbusFeatures, ExperimentAPI } = ChromeUtils.import(
"resource://nimbus/ExperimentAPI.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"JSONFile",
"resource://gre/modules/JSONFile.jsm"
);
const SINGLE_FEATURE_RECIPE = {
...ExperimentFakes.experiment(),
branch: {
feature: {
enabled: true,
featureId: "urlbar",
value: {
valueThatWillDefinitelyShowUp: 42,
quickSuggestNonSponsoredIndex: 2021,
},
},
ratio: 1,
slug: "control",
},
featureIds: ["urlbar"],
slug: "browser_experimentstore_load_single_feature",
userFacingDescription: "Smarter suggestions in the AwesomeBar",
userFacingName: "Firefox Suggest - History vs Offline",
};
function getPath() {
const profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
// NOTE: If this test is failing because you have updated this path in `ExperimentStore`,
// users will lose their old experiment data. You should do something to migrate that data.
return PathUtils.join(profileDir, "ExperimentStoreData.json");
}
add_task(async function test_load_from_disk_event() {
Services.prefs.setStringPref("messaging-system.log", "all");
const stub = sinon.stub();
const previousSession = new JSONFile({ path: getPath() });
await previousSession.load();
previousSession.data[SINGLE_FEATURE_RECIPE.slug] = SINGLE_FEATURE_RECIPE;
previousSession.saveSoon();
await previousSession.finalize();
// Create a store and expect to load data from previous session
const store = new ExperimentStore();
let apiStoreStub = sinon.stub(ExperimentAPI, "_store").get(() => store);
store._onFeatureUpdate("urlbar", stub);
await store.init();
await store.ready();
await TestUtils.waitForCondition(() => stub.called, "Stub was called");
Assert.ok(
store.get(SINGLE_FEATURE_RECIPE.slug)?.slug,
"Experiment is loaded from disk"
);
Assert.ok(stub.firstCall.args[1], "feature-experiment-loaded");
Assert.equal(
NimbusFeatures.urlbar.getAllVariables().valueThatWillDefinitelyShowUp,
SINGLE_FEATURE_RECIPE.branch.feature.value.valueThatWillDefinitelyShowUp,
"Should match getAllVariables"
);
Assert.equal(
NimbusFeatures.urlbar.getVariable("quickSuggestNonSponsoredIndex"),
SINGLE_FEATURE_RECIPE.branch.feature.value.quickSuggestNonSponsoredIndex,
"Should match getVariable"
);
registerCleanupFunction(async () => {
// Remove the experiment from disk
const fileStore = new JSONFile({ path: getPath() });
await fileStore.load();
fileStore.data = {};
fileStore.saveSoon();
await fileStore.finalize();
apiStoreStub.restore();
});
});

Просмотреть файл

@ -91,7 +91,7 @@ add_task(async function test_experiment_expose_Telemetry() {
featureId: "test-feature", featureId: "test-feature",
}); });
const { featureId } = experiment.branch.feature; const { featureId } = experiment.branch.features[0];
const feature = new ExperimentFeature(featureId); const feature = new ExperimentFeature(featureId);
Services.telemetry.clearEvents(); Services.telemetry.clearEvents();

Просмотреть файл

@ -530,11 +530,13 @@ add_task(async function remote_defaults_active_experiments_check() {
branches: [ branches: [
{ {
slug: "mochitest-active-foo", slug: "mochitest-active-foo",
feature: { features: [
enabled: true, {
featureId: "foo", enabled: true,
value: null, featureId: "foo",
}, value: null,
},
],
}, },
], ],
active: true, active: true,

Просмотреть файл

@ -140,7 +140,7 @@ add_task(async function test_getExperiment_feature() {
branch: { branch: {
slug: "treatment", slug: "treatment",
value: { title: "hi" }, value: { title: "hi" },
feature: { featureId: "cfr", enabled: true, value: null }, features: [{ featureId: "cfr", enabled: true, value: null }],
}, },
}); });
@ -264,7 +264,7 @@ add_task(async function test_addExperiment_eventEmit_add() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "purple", enabled: true, value: null }, features: [{ featureId: "purple", enabled: true, value: null }],
}, },
}); });
const store = ExperimentFakes.store(); const store = ExperimentFakes.store();
@ -299,7 +299,7 @@ add_task(async function test_updateExperiment_eventEmit_add_and_update() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "purple", enabled: true, value: null }, features: [{ featureId: "purple", enabled: true, value: null }],
}, },
}); });
const store = ExperimentFakes.store(); const store = ExperimentFakes.store();
@ -333,7 +333,7 @@ add_task(async function test_updateExperiment_eventEmit_off() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "purple", enabled: true, value: null }, features: [{ featureId: "purple", enabled: true, value: null }],
}, },
}); });
const store = ExperimentFakes.store(); const store = ExperimentFakes.store();
@ -363,7 +363,7 @@ add_task(async function test_activateBranch() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "green", enabled: true, value: null }, features: [{ featureId: "green", enabled: true, value: null }],
}, },
}); });
@ -403,7 +403,7 @@ add_task(async function test_activateBranch_storeFailure() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "green", enabled: true }, features: [{ featureId: "green", enabled: true }],
}, },
}); });
@ -430,7 +430,7 @@ add_task(async function test_activateBranch_noActivationEvent() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "green", enabled: true }, features: [{ featureId: "green", enabled: true }],
}, },
}); });

Просмотреть файл

@ -163,10 +163,12 @@ add_task(
const expected = ExperimentFakes.experiment("foo", { const expected = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "treatment", slug: "treatment",
feature: { features: [
featureId: "foo", {
value: { enabled: true }, featureId: "foo",
}, value: { enabled: true },
},
],
}, },
}); });
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST); const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
@ -229,10 +231,12 @@ add_task(async function test_record_exposure_event() {
ExperimentFakes.experiment("blah", { ExperimentFakes.experiment("blah", {
branch: { branch: {
slug: "treatment", slug: "treatment",
feature: { features: [
featureId: "foo", {
value: { enabled: false }, featureId: "foo",
}, value: { enabled: false },
},
],
}, },
}) })
); );
@ -259,10 +263,12 @@ add_task(async function test_record_exposure_event_once() {
ExperimentFakes.experiment("blah", { ExperimentFakes.experiment("blah", {
branch: { branch: {
slug: "treatment", slug: "treatment",
feature: { features: [
featureId: "foo", {
value: { enabled: false }, featureId: "foo",
}, value: { enabled: false },
},
],
}, },
}) })
); );
@ -365,11 +371,13 @@ add_task(async function test_isEnabled_backwards_compatible() {
ExperimentFakes.experiment("blah", { ExperimentFakes.experiment("blah", {
branch: { branch: {
slug: "treatment", slug: "treatment",
feature: { features: [
featureId: "foo", {
enabled: true, featureId: "foo",
value: {}, enabled: true,
}, value: {},
},
],
}, },
}) })
); );
@ -386,7 +394,15 @@ add_task(async function test_onUpdate_before_store_ready() {
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store); sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
sandbox.stub(manager.store, "getAllActive").returns([ sandbox.stub(manager.store, "getAllActive").returns([
ExperimentFakes.experiment("foo-experiment", { ExperimentFakes.experiment("foo-experiment", {
branch: { slug: "control", feature: { featureId: "foo", value: null } }, branch: {
slug: "control",
features: [
{
featureId: "foo",
value: null,
},
],
},
}), }),
]); ]);

Просмотреть файл

@ -76,11 +76,13 @@ add_task(
const recipe = ExperimentFakes.experiment("awexperiment", { const recipe = ExperimentFakes.experiment("awexperiment", {
branch: { branch: {
slug: "treatment", slug: "treatment",
feature: { features: [
featureId: "aboutwelcome", {
enabled: true, featureId: "aboutwelcome",
value: { screens: ["test-value"] }, enabled: true,
}, value: { screens: ["test-value"] },
},
],
}, },
}); });

Просмотреть файл

@ -218,7 +218,7 @@ add_task(async function test_getVariable_no_mutation() {
Cu.cloneInto( Cu.cloneInto(
{ {
branch: { branch: {
feature: { value: { mochitest: true } }, features: [{ featureId: "aboutwelcome", value: { mochitest: true } }],
}, },
}, },
{}, {},

Просмотреть файл

@ -129,11 +129,11 @@ add_task(async function test_failure_group_conflict() {
// These should not be allowed to exist simultaneously. // These should not be allowed to exist simultaneously.
const existingBranch = { const existingBranch = {
slug: "treatment", slug: "treatment",
feature: { featureId: "pink", enabled: true, value: {} }, features: [{ featureId: "pink", enabled: true, value: {} }],
}; };
const newBranch = { const newBranch = {
slug: "treatment", slug: "treatment",
feature: { featureId: "pink", enabled: true, value: {} }, features: [{ featureId: "pink", enabled: true, value: {} }],
}; };
// simulate adding an experiment with a conflicting group "pink" // simulate adding an experiment with a conflicting group "pink"
@ -234,7 +234,7 @@ add_task(async function enroll_in_reference_aw_experiment() {
const branches = ["treatment-a", "treatment-b"].map(slug => ({ const branches = ["treatment-a", "treatment-b"].map(slug => ({
slug, slug,
ratio: 1, ratio: 1,
feature: { value: content, enabled: true, featureId: "aboutwelcome" }, features: [{ value: content, enabled: true, featureId: "aboutwelcome" }],
})); }));
let recipe = ExperimentFakes.recipe("reference-aw", { branches }); let recipe = ExperimentFakes.recipe("reference-aw", { branches });
// Ensure we get enrolled // Ensure we get enrolled
@ -276,7 +276,7 @@ add_task(async function test_forceEnroll_cleanup() {
{ {
slug: "treatment", slug: "treatment",
ratio: 1, ratio: 1,
feature: { featureId: "force-enrollment", enabled: true, value: {} }, features: [{ featureId: "force-enrollment", enabled: true, value: {} }],
}, },
], ],
}); });
@ -285,7 +285,7 @@ add_task(async function test_forceEnroll_cleanup() {
{ {
slug: "treatment", slug: "treatment",
ratio: 1, ratio: 1,
feature: { featureId: "force-enrollment", enabled: true, value: {} }, features: [{ featureId: "force-enrollment", enabled: true, value: {} }],
}, },
], ],
}); });
@ -325,10 +325,12 @@ add_task(async function test_featuremanifest_enum() {
{ {
slug: "control", slug: "control",
ratio: 1, ratio: 1,
feature: { features: [
featureId: "privatebrowsing", {
value: { promoLinkType: "link" }, featureId: "privatebrowsing",
}, value: { promoLinkType: "link" },
},
],
}, },
], ],
}); });
@ -356,10 +358,12 @@ add_task(async function test_featuremanifest_enum() {
branch: { branch: {
slug: "control", slug: "control",
ratio: 1, ratio: 1,
feature: { features: [
featureId: "privatebrowsing", {
value: { promoLinkType: "bar" }, featureId: "privatebrowsing",
}, value: { promoLinkType: "bar" },
},
],
}, },
}); });

Просмотреть файл

@ -166,7 +166,6 @@ add_task(async function test_onRecipe_isEnrollmentPaused() {
await manager.enroll(fooRecipe, "test"); await manager.enroll(fooRecipe, "test");
await enrollmentPromise; await enrollmentPromise;
await manager.onRecipe(updatedRecipe, "test"); await manager.onRecipe(updatedRecipe, "test");
console.log("XXX", manager.updateEnrollment.callCount);
Assert.equal( Assert.equal(
manager.updateEnrollment.calledWith(updatedRecipe), manager.updateEnrollment.calledWith(updatedRecipe),
true, true,
@ -199,12 +198,12 @@ add_task(async function test_onFinalize_unenroll() {
const recipe1 = ExperimentFakes.recipe("bar"); const recipe1 = ExperimentFakes.recipe("bar");
// Unique features to prevent overlap // Unique features to prevent overlap
recipe1.branches[0].feature.featureId = "red"; recipe1.branches[0].features[0].featureId = "red";
recipe1.branches[1].feature.featureId = "red"; recipe1.branches[1].features[0].featureId = "red";
await manager.onRecipe(recipe1, "test"); await manager.onRecipe(recipe1, "test");
const recipe2 = ExperimentFakes.recipe("baz"); const recipe2 = ExperimentFakes.recipe("baz");
recipe2.branches[0].feature.featureId = "green"; recipe2.branches[0].features[0].featureId = "green";
recipe2.branches[1].feature.featureId = "green"; recipe2.branches[1].features[0].featureId = "green";
await manager.onRecipe(recipe2, "test"); await manager.onRecipe(recipe2, "test");
// Finalize // Finalize

Просмотреть файл

@ -23,7 +23,7 @@ add_task(async function test_usageBeforeInitialization() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "purple", enabled: true }, features: [{ featureId: "purple", enabled: true }],
}, },
}); });
@ -69,7 +69,10 @@ add_task(async function test_event_updates_main() {
await store.init(); await store.init();
// Set update cb // Set update cb
store.on(`update:${experiment.branch.feature.featureId}`, updateEventCbStub); store.on(
`update:${experiment.branch.features[0].featureId}`,
updateEventCbStub
);
store.addExperiment(experiment); store.addExperiment(experiment);
store.updateExperiment("foo", { active: false }); store.updateExperiment("foo", { active: false });
@ -85,7 +88,10 @@ add_task(async function test_event_updates_main() {
"Should be called with updated experiment status" "Should be called with updated experiment status"
); );
store.off(`update:${experiment.branch.feature.featureId}`, updateEventCbStub); store.off(
`update:${experiment.branch.features[0].featureId}`,
updateEventCbStub
);
}); });
add_task(async function test_getExperimentForGroup() { add_task(async function test_getExperimentForGroup() {
@ -93,7 +99,7 @@ add_task(async function test_getExperimentForGroup() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "purple", enabled: true }, features: [{ featureId: "purple", enabled: true }],
}, },
}); });
@ -194,9 +200,9 @@ add_task(async function test_addExperiment() {
}); });
add_task(async function test_updateExperiment() { add_task(async function test_updateExperiment() {
const feature = { featureId: "cfr", enabled: true }; const features = [{ featureId: "cfr", enabled: true }];
const experiment = Object.freeze( const experiment = Object.freeze(
ExperimentFakes.experiment("foo", { feature, active: true }) ExperimentFakes.experiment("foo", { features, active: true })
); );
const store = ExperimentFakes.store(); const store = ExperimentFakes.store();
@ -207,8 +213,8 @@ add_task(async function test_updateExperiment() {
const actual = store.get("foo"); const actual = store.get("foo");
Assert.equal(actual.active, false, "should change updated props"); Assert.equal(actual.active, false, "should change updated props");
Assert.deepEqual( Assert.deepEqual(
actual.branch.feature, actual.branch.features,
feature, features,
"should not update other props" "should not update other props"
); );
}); });
@ -221,7 +227,7 @@ add_task(async function test_sync_access_before_init() {
Assert.equal(store.getAll().length, 0, "Start with an empty store"); Assert.equal(store.getAll().length, 0, "Start with an empty store");
const syncAccessExp = ExperimentFakes.experiment("foo", { const syncAccessExp = ExperimentFakes.experiment("foo", {
feature: { featureId: "newtab", enabled: "true" }, features: [{ featureId: "newtab", enabled: "true" }],
}); });
await store.init(); await store.init();
store.addExperiment(syncAccessExp); store.addExperiment(syncAccessExp);
@ -253,7 +259,7 @@ add_task(async function test_sync_access_update() {
let store = ExperimentFakes.store(); let store = ExperimentFakes.store();
let experiment = ExperimentFakes.experiment("foo", { let experiment = ExperimentFakes.experiment("foo", {
feature: { featureId: "aboutwelcome", enabled: true }, features: [{ featureId: "aboutwelcome", enabled: true }],
}); });
await store.init(); await store.init();
@ -262,11 +268,13 @@ add_task(async function test_sync_access_update() {
store.updateExperiment("foo", { store.updateExperiment("foo", {
branch: { branch: {
...experiment.branch, ...experiment.branch,
feature: { features: [
featureId: "aboutwelcome", {
enabled: true, featureId: "aboutwelcome",
value: { bar: "bar" }, enabled: true,
}, value: { bar: "bar" },
},
],
}, },
}); });
@ -275,6 +283,8 @@ add_task(async function test_sync_access_update() {
Assert.ok(cachedExperiment, "Got back 1 experiment"); Assert.ok(cachedExperiment, "Got back 1 experiment");
Assert.deepEqual( Assert.deepEqual(
// `branch.feature` and not `features` because for sync access (early startup)
// experiments we only store the `isEarlyStartup` feature
cachedExperiment.branch.feature.value, cachedExperiment.branch.feature.value,
{ bar: "bar" }, { bar: "bar" },
"Got updated value" "Got updated value"
@ -286,7 +296,7 @@ add_task(async function test_sync_features_only() {
let store = ExperimentFakes.store(); let store = ExperimentFakes.store();
let experiment = ExperimentFakes.experiment("foo", { let experiment = ExperimentFakes.experiment("foo", {
feature: { featureId: "cfr", enabled: true }, features: [{ featureId: "cfr", enabled: true }],
}); });
await store.init(); await store.init();
@ -302,7 +312,7 @@ add_task(async function test_sync_features_remotely() {
let store = ExperimentFakes.store(); let store = ExperimentFakes.store();
let experiment = ExperimentFakes.experiment("foo", { let experiment = ExperimentFakes.experiment("foo", {
feature: { featureId: "cfr", enabled: true, isEarlyStartup: true }, features: [{ featureId: "cfr", enabled: true, isEarlyStartup: true }],
}); });
await store.init(); await store.init();
@ -322,7 +332,7 @@ add_task(async function test_sync_access_unenroll() {
let store = ExperimentFakes.store(); let store = ExperimentFakes.store();
let experiment = ExperimentFakes.experiment("foo", { let experiment = ExperimentFakes.experiment("foo", {
feature: { featureId: "aboutwelcome", enabled: true }, features: [{ featureId: "aboutwelcome", enabled: true }],
active: true, active: true,
}); });
@ -342,10 +352,10 @@ add_task(async function test_sync_access_unenroll_2() {
let store = ExperimentFakes.store(); let store = ExperimentFakes.store();
let experiment1 = ExperimentFakes.experiment("foo", { let experiment1 = ExperimentFakes.experiment("foo", {
feature: { featureId: "newtab", enabled: true }, features: [{ featureId: "newtab", enabled: true }],
}); });
let experiment2 = ExperimentFakes.experiment("bar", { let experiment2 = ExperimentFakes.experiment("bar", {
feature: { featureId: "aboutwelcome", enabled: true }, features: [{ featureId: "aboutwelcome", enabled: true }],
}); });
await store.init(); await store.init();
@ -529,12 +539,14 @@ add_task(async function test_storeValuePerPref_noVariables() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { features: [
// Ensure it gets saved to prefs {
isEarlyStartup: true, // Ensure it gets saved to prefs
featureId: "purple", isEarlyStartup: true,
enabled: true, featureId: "purple",
}, enabled: true,
},
],
}, },
}); });
@ -562,12 +574,14 @@ add_task(async function test_storeValuePerPref_withVariables() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { features: [
// Ensure it gets saved to prefs {
isEarlyStartup: true, // Ensure it gets saved to prefs
featureId: "purple", isEarlyStartup: true,
value: { color: "purple", enabled: true }, featureId: "purple",
}, value: { color: "purple", enabled: true },
},
],
}, },
}); });
@ -576,12 +590,11 @@ add_task(async function test_storeValuePerPref_withVariables() {
let branch = Services.prefs.getBranch(`${SYNC_DATA_PREF_BRANCH}purple.`); let branch = Services.prefs.getBranch(`${SYNC_DATA_PREF_BRANCH}purple.`);
let val = Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}purple`);
Assert.equal( Assert.equal(
Services.prefs val.indexOf("color"),
.getStringPref(`${SYNC_DATA_PREF_BRANCH}purple`)
.indexOf("color"),
-1, -1,
"Experiment metadata does not contain variables" `Experiment metadata does not contain variables ${val}`
); );
Assert.equal(branch.getChildList("").length, 2, "Enabled and color"); Assert.equal(branch.getChildList("").length, 2, "Enabled and color");
@ -599,12 +612,14 @@ add_task(async function test_storeValuePerPref_returnsSameValue() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { features: [
// Ensure it gets saved to prefs {
isEarlyStartup: true, // Ensure it gets saved to prefs
featureId: "purple", isEarlyStartup: true,
value: { color: "purple", enabled: true }, featureId: "purple",
}, value: { color: "purple", enabled: true },
},
],
}, },
}); });
@ -613,11 +628,11 @@ add_task(async function test_storeValuePerPref_returnsSameValue() {
let branch = Services.prefs.getBranch(`${SYNC_DATA_PREF_BRANCH}purple.`); let branch = Services.prefs.getBranch(`${SYNC_DATA_PREF_BRANCH}purple.`);
store = ExperimentFakes.store(); store = ExperimentFakes.store();
Assert.deepEqual( const cachedExperiment = store.getExperimentForFeature("purple");
store.getExperimentForFeature("purple"), // Cached experiment format only stores early access feature
experiment, cachedExperiment.branch.features = [cachedExperiment.branch.feature];
"Returns the same value" delete cachedExperiment.branch.feature;
); Assert.deepEqual(cachedExperiment, experiment, "Returns the same value");
// Cleanup // Cleanup
store._updateSyncStore({ ...experiment, active: false }); store._updateSyncStore({ ...experiment, active: false });
@ -645,20 +660,22 @@ add_task(async function test_storeValuePerPref_returnsSameValue_allTypes() {
const experiment = ExperimentFakes.experiment("foo", { const experiment = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { features: [
// Ensure it gets saved to prefs {
isEarlyStartup: true, // Ensure it gets saved to prefs
featureId: "purple", isEarlyStartup: true,
value: { featureId: "purple",
string: "string", value: {
bool: true, string: "string",
array: [1, 2, 3], bool: true,
number1: 42, array: [1, 2, 3],
number2: 0, number1: 42,
number3: -5, number2: 0,
json: { jsonValue: true }, number3: -5,
json: { jsonValue: true },
},
}, },
}, ],
}, },
}); });
@ -669,7 +686,7 @@ add_task(async function test_storeValuePerPref_returnsSameValue_allTypes() {
store = ExperimentFakes.store(); store = ExperimentFakes.store();
Assert.deepEqual( Assert.deepEqual(
store.getExperimentForFeature("purple").branch.feature.value, store.getExperimentForFeature("purple").branch.feature.value,
experiment.branch.feature.value, experiment.branch.features[0].value,
"Returns the same value" "Returns the same value"
); );
@ -690,25 +707,25 @@ add_task(async function test_cleanupOldRecipes() {
const experiment1 = ExperimentFakes.experiment("foo", { const experiment1 = ExperimentFakes.experiment("foo", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "purple", enabled: true }, features: [{ featureId: "purple", enabled: true }],
}, },
}); });
const experiment2 = ExperimentFakes.experiment("bar", { const experiment2 = ExperimentFakes.experiment("bar", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "purple", enabled: true }, features: [{ featureId: "purple", enabled: true }],
}, },
}); });
const experiment3 = ExperimentFakes.experiment("baz", { const experiment3 = ExperimentFakes.experiment("baz", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "purple", enabled: true }, features: [{ featureId: "purple", enabled: true }],
}, },
}); });
const experiment4 = ExperimentFakes.experiment("faz", { const experiment4 = ExperimentFakes.experiment("faz", {
branch: { branch: {
slug: "variant", slug: "variant",
feature: { featureId: "purple", enabled: true }, features: [{ featureId: "purple", enabled: true }],
}, },
}); });
// Exp 2 is kept because it's recent (even though it's not active) // Exp 2 is kept because it's recent (even though it's not active)

Просмотреть файл

@ -13,13 +13,23 @@ add_task(async function test_recipe_fake_validates() {
}); });
add_task(async function test_enrollmentHelper() { add_task(async function test_enrollmentHelper() {
let recipe = ExperimentFakes.recipe("bar"); let recipe = ExperimentFakes.recipe("bar", {
recipe.branches.forEach(branch => { branches: [
// Use a feature that will set the sync pref cache {
branch.feature.featureId = "aboutwelcome"; slug: "control",
ratio: 1,
features: [{ featureId: "aboutwelcome", value: {} }],
},
],
}); });
let manager = ExperimentFakes.manager(); let manager = ExperimentFakes.manager();
Assert.deepEqual(
recipe.featureIds,
["aboutwelcome"],
"Helper sets correct featureIds"
);
await manager.onStartup(); await manager.onStartup();
let { let {

Просмотреть файл

@ -53,9 +53,12 @@ add_task(async function test_initialize() {
const experiment = sinon.stub(ExperimentAPI, "activateBranch").returns({ const experiment = sinon.stub(ExperimentAPI, "activateBranch").returns({
slug: "foo", slug: "foo",
ratio: 1, ratio: 1,
feature: { features: [
value: { directMigrateSingleProfile: true }, {
}, featureId: "password-autocomplete",
value: { directMigrateSingleProfile: true },
},
],
}); });
// This makes the last autocomplete test *not* show import suggestions. // This makes the last autocomplete test *not* show import suggestions.