зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1726190 - Implement multi-feature enrollment support for Nimbus r=k88hudson
Differential Revision: https://phabricator.services.mozilla.com/D123139
This commit is contained in:
Родитель
b2955e8a9d
Коммит
07703bd30d
|
@ -57,6 +57,19 @@ function parseJSON(value) {
|
|||
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 = {
|
||||
/**
|
||||
* @returns {Promise} Resolves when the API has synchronized to the main store
|
||||
|
@ -405,9 +418,13 @@ class _ExperimentFeature {
|
|||
isEnabled({ defaultValue = null } = {}) {
|
||||
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.
|
||||
if (isBooleanValueDefined(branch?.feature.enabled)) {
|
||||
return branch.feature.enabled;
|
||||
if (isBooleanValueDefined(feature?.enabled)) {
|
||||
return feature.enabled;
|
||||
}
|
||||
|
||||
if (isBooleanValueDefined(this.getRemoteConfig()?.enabled)) {
|
||||
|
@ -436,12 +453,15 @@ class _ExperimentFeature {
|
|||
// Any user pref will override any other configuration
|
||||
let userPrefs = this._getUserPrefsValues();
|
||||
const branch = ExperimentAPI.activateBranch({ featureId: this.featureId });
|
||||
const featureValue = featuresCompat(branch).find(
|
||||
({ featureId }) => featureId === this.featureId
|
||||
)?.value;
|
||||
|
||||
return {
|
||||
...this.prefGetters,
|
||||
...defaultValues,
|
||||
...this.getRemoteConfig()?.variables,
|
||||
...(branch?.feature?.value || null),
|
||||
...(featureValue || null),
|
||||
...userPrefs,
|
||||
};
|
||||
}
|
||||
|
@ -465,9 +485,12 @@ class _ExperimentFeature {
|
|||
}
|
||||
|
||||
// Next, check if an experiment is defined
|
||||
const experimentValue = ExperimentAPI.activateBranch({
|
||||
const branch = ExperimentAPI.activateBranch({
|
||||
featureId: this.featureId,
|
||||
})?.feature?.value?.[variable];
|
||||
});
|
||||
const experimentValue = featuresCompat(branch).find(
|
||||
({ featureId }) => featureId === this.featureId
|
||||
)?.value?.[variable];
|
||||
|
||||
if (typeof experimentValue !== "undefined") {
|
||||
return experimentValue;
|
||||
|
|
|
@ -35,6 +35,19 @@ const TELEMETRY_DEFAULT_EXPERIMENT_TYPE = "nimbus";
|
|||
|
||||
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,
|
||||
* and sending experiment-related Telemetry.
|
||||
|
@ -184,19 +197,16 @@ class _ExperimentManager {
|
|||
}
|
||||
|
||||
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 (
|
||||
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 null;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
* store will overwrite the initial experiment.
|
||||
*/
|
||||
let experiment = this.store.getExperimentForFeature(
|
||||
branch.feature?.featureId
|
||||
);
|
||||
if (experiment) {
|
||||
log.debug(
|
||||
`Existing experiment found for the same feature ${branch?.feature.featureId}, unenrolling.`
|
||||
);
|
||||
const features = featuresCompat(branch);
|
||||
for (let feature of features) {
|
||||
let experiment = this.store.getExperimentForFeature(feature?.featureId);
|
||||
if (experiment) {
|
||||
log.debug(
|
||||
`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`;
|
||||
|
|
|
@ -34,14 +34,6 @@ let tryJSONParse = data => {
|
|||
return null;
|
||||
};
|
||||
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 defaultsPrefBranch = Services.prefs.getBranch(SYNC_DEFAULTS_PREF_BRANCH);
|
||||
return {
|
||||
|
@ -205,6 +197,31 @@ XPCOMUtils.defineLazyGetter(this, "syncDataStore", () => {
|
|||
|
||||
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 {
|
||||
static SYNC_DATA_PREF_BRANCH = SYNC_DATA_PREF_BRANCH;
|
||||
static SYNC_DEFAULTS_PREF_BRANCH = SYNC_DEFAULTS_PREF_BRANCH;
|
||||
|
@ -216,13 +233,10 @@ class ExperimentStore extends SharedDataMap {
|
|||
async init() {
|
||||
await super.init();
|
||||
|
||||
this.getAllActive().forEach(({ branch }) => {
|
||||
if (branch?.feature?.featureId) {
|
||||
this._emitFeatureUpdate(
|
||||
branch.feature.featureId,
|
||||
"feature-experiment-loaded"
|
||||
);
|
||||
}
|
||||
this.getAllActive().forEach(({ branch, featureIds }) => {
|
||||
(featureIds || getAllBranchFeatureIds(branch)).forEach(featureId =>
|
||||
this._emitFeatureUpdate(featureId, "feature-experiment-loaded")
|
||||
);
|
||||
});
|
||||
|
||||
Services.tm.idleDispatchToMainThread(() => this._cleanupOldRecipes());
|
||||
|
@ -243,7 +257,7 @@ class ExperimentStore extends SharedDataMap {
|
|||
experiment =>
|
||||
experiment.featureIds?.includes(featureId) ||
|
||||
// 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
|
||||
) || syncDataStore.get(featureId)
|
||||
);
|
||||
|
@ -305,13 +319,12 @@ class ExperimentStore extends SharedDataMap {
|
|||
|
||||
_emitExperimentUpdates(experiment) {
|
||||
this.emit(`update:${experiment.slug}`, experiment);
|
||||
if (experiment.branch.feature) {
|
||||
this.emit(`update:${experiment.branch.feature.featureId}`, experiment);
|
||||
this._emitFeatureUpdate(
|
||||
experiment.branch.feature.featureId,
|
||||
"experiment-updated"
|
||||
);
|
||||
}
|
||||
(
|
||||
experiment.featureIds || getAllBranchFeatureIds(experiment.branch)
|
||||
).forEach(featureId => {
|
||||
this.emit(`update:${featureId}`, experiment);
|
||||
this._emitFeatureUpdate(featureId, "experiment-updated");
|
||||
});
|
||||
}
|
||||
|
||||
_emitFeatureUpdate(featureId, reason) {
|
||||
|
@ -330,16 +343,26 @@ class ExperimentStore extends SharedDataMap {
|
|||
* @param {Enrollment} experiment
|
||||
*/
|
||||
_updateSyncStore(experiment) {
|
||||
let featureId = experiment.branch.feature?.featureId;
|
||||
if (
|
||||
FeatureManifest[featureId]?.isEarlyStartup ||
|
||||
experiment.branch.feature?.isEarlyStartup
|
||||
) {
|
||||
if (!experiment.active) {
|
||||
// Remove experiments on un-enroll, no need to check if it exists
|
||||
syncDataStore.delete(featureId);
|
||||
} else {
|
||||
syncDataStore.set(featureId, experiment);
|
||||
let features = featuresCompat(experiment.branch);
|
||||
for (let feature of features) {
|
||||
if (
|
||||
FeatureManifest[feature.featureId]?.isEarlyStartup ||
|
||||
feature.isEarlyStartup
|
||||
) {
|
||||
if (!experiment.active) {
|
||||
// Remove experiments on un-enroll, no need to check if it exists
|
||||
syncDataStore.delete(feature.featureId);
|
||||
} else {
|
||||
syncDataStore.set(feature.featureId, {
|
||||
...experiment,
|
||||
branch: {
|
||||
...experiment.branch,
|
||||
feature,
|
||||
// Only store the early startup feature
|
||||
features: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,35 +11,38 @@
|
|||
"branch": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"feature": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"featureId": {
|
||||
"type": "string",
|
||||
"description": "The identifier for the feature flag"
|
||||
"features": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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": {
|
||||
"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)"
|
||||
}
|
||||
},
|
||||
"required": ["featureId", "value"],
|
||||
"additionalProperties": false
|
||||
"required": ["featureId", "value"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["feature"]
|
||||
"required": ["features"]
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean",
|
||||
|
@ -87,7 +90,8 @@
|
|||
"experimentType",
|
||||
"source",
|
||||
"userFacingName",
|
||||
"userFacingDescription"
|
||||
"userFacingDescription",
|
||||
"featureIds"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"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)",
|
||||
"default": 1
|
||||
},
|
||||
"feature": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"featureId": {
|
||||
"type": "string",
|
||||
"description": "The identifier for the feature flag"
|
||||
"features": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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": {
|
||||
"anyOf": [
|
||||
{
|
||||
"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": ["featureId", "value"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["slug", "ratio"],
|
||||
|
@ -143,6 +146,11 @@
|
|||
"filter_expression": {
|
||||
"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"
|
||||
},
|
||||
"featureIds": {
|
||||
"type": "array",
|
||||
"items": [{ "type": "string" }],
|
||||
"description": "Array of strings corresponding to the branch features in the enrollment."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -158,7 +166,8 @@
|
|||
"startDate",
|
||||
"endDate",
|
||||
"proposedEnrollment",
|
||||
"referenceBranch"
|
||||
"referenceBranch",
|
||||
"featureIds"
|
||||
],
|
||||
"additionalProperties": true,
|
||||
"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 }) {
|
||||
let { feature } = branch;
|
||||
// If we're not using a real feature skip this check
|
||||
if (!FeatureManifest[feature.featureId]) {
|
||||
return true;
|
||||
}
|
||||
let { variables } = FeatureManifest[feature.featureId];
|
||||
for (let varName of Object.keys(variables)) {
|
||||
let varValue = feature.value[varName];
|
||||
if (
|
||||
varValue &&
|
||||
variables[varName].enum &&
|
||||
!variables[varName].enum.includes(varValue)
|
||||
) {
|
||||
throw new Error(
|
||||
`${varName} should have one of the following values: ${JSON.stringify(
|
||||
variables[varName].enum
|
||||
)} but has value '${varValue}'`
|
||||
);
|
||||
let { features } = branch;
|
||||
for (let feature of features) {
|
||||
// If we're not using a real feature skip this check
|
||||
if (!FeatureManifest[feature.featureId]) {
|
||||
return true;
|
||||
}
|
||||
let { variables } = FeatureManifest[feature.featureId];
|
||||
for (let varName of Object.keys(variables)) {
|
||||
let varValue = feature.value[varName];
|
||||
if (
|
||||
varValue &&
|
||||
variables[varName].enum &&
|
||||
!variables[varName].enum.includes(varValue)
|
||||
) {
|
||||
throw new Error(
|
||||
`${varName} should have one of the following values: ${JSON.stringify(
|
||||
variables[varName].enum
|
||||
)} but has value '${varValue}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
|
@ -100,6 +101,12 @@ const ExperimentTestUtils = {
|
|||
)
|
||||
).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 (
|
||||
this._validateFeatureValueEnum(enrollment) &&
|
||||
this._validator(
|
||||
|
@ -184,7 +191,7 @@ const ExperimentFakes = {
|
|||
{
|
||||
slug: "control",
|
||||
ratio: 1,
|
||||
feature: featureConfig,
|
||||
features: [featureConfig],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -268,10 +275,12 @@ const ExperimentFakes = {
|
|||
enrollmentId: NormandyUtils.generateUuid(),
|
||||
branch: {
|
||||
slug: "treatment",
|
||||
feature: {
|
||||
featureId: "test-feature",
|
||||
value: { title: "hello", enabled: true },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
featureId: "test-feature",
|
||||
value: { title: "hello", enabled: true },
|
||||
},
|
||||
],
|
||||
...props,
|
||||
},
|
||||
source: "NimbusTestUtils",
|
||||
|
@ -279,6 +288,9 @@ const ExperimentFakes = {
|
|||
experimentType: "NimbusTestUtils",
|
||||
userFacingName: "NimbusTestUtils",
|
||||
userFacingDescription: "NimbusTestUtils",
|
||||
featureIds: props?.branch?.features?.map(f => f.featureId) || [
|
||||
"test-feature",
|
||||
],
|
||||
...props,
|
||||
};
|
||||
},
|
||||
|
@ -298,15 +310,17 @@ const ExperimentFakes = {
|
|||
{
|
||||
slug: "control",
|
||||
ratio: 1,
|
||||
feature: { featureId: "test-feature", value: { enabled: true } },
|
||||
features: [{ featureId: "test-feature", value: { enabled: true } }],
|
||||
},
|
||||
{
|
||||
slug: "treatment",
|
||||
ratio: 1,
|
||||
feature: {
|
||||
featureId: "test-feature",
|
||||
value: { title: "hello", enabled: true },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
featureId: "test-feature",
|
||||
value: { title: "hello", enabled: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
bucketConfig: {
|
||||
|
@ -318,7 +332,9 @@ const ExperimentFakes = {
|
|||
},
|
||||
userFacingName: "Nimbus recipe",
|
||||
userFacingDescription: "NimbusTestUtils recipe",
|
||||
featureIds: ["test-feature"],
|
||||
featureIds: props?.branches?.[0].features?.map(f => f.featureId) || [
|
||||
"test-feature",
|
||||
],
|
||||
...props,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -5,9 +5,11 @@ prefs =
|
|||
# This turns off the update interval for fetching recipes from Remote Settings
|
||||
app.normandy.run_interval_seconds=0
|
||||
|
||||
[browser_experiment_single_feature_enrollment.js]
|
||||
[browser_remotesettingsexperimentloader_remote_defaults.js]
|
||||
[browser_remotesettingsexperimentloader_force_enrollment.js]
|
||||
[browser_experimentstore_load.js]
|
||||
[browser_experimentstore_load_single_feature.js]
|
||||
[browser_remotesettings_experiment_enroll.js]
|
||||
[browser_experiment_evaluate_jexl.js]
|
||||
[browser_remotesettingsexperimentloader_init.js]
|
||||
|
|
|
@ -83,14 +83,15 @@ add_task(async function test_evaluate_active_experiments_activeExperiments() {
|
|||
branches: [
|
||||
{
|
||||
slug: "mochitest-active-foo",
|
||||
feature: {
|
||||
enabled: true,
|
||||
featureId: "foo",
|
||||
value: null,
|
||||
},
|
||||
features: [
|
||||
{
|
||||
enabled: true,
|
||||
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", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: { featureId: "green", enabled: true },
|
||||
features: [{ featureId: "green", enabled: true }],
|
||||
},
|
||||
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",
|
||||
});
|
||||
|
||||
const { featureId } = experiment.branch.feature;
|
||||
const { featureId } = experiment.branch.features[0];
|
||||
const feature = new ExperimentFeature(featureId);
|
||||
|
||||
Services.telemetry.clearEvents();
|
||||
|
|
|
@ -530,11 +530,13 @@ add_task(async function remote_defaults_active_experiments_check() {
|
|||
branches: [
|
||||
{
|
||||
slug: "mochitest-active-foo",
|
||||
feature: {
|
||||
enabled: true,
|
||||
featureId: "foo",
|
||||
value: null,
|
||||
},
|
||||
features: [
|
||||
{
|
||||
enabled: true,
|
||||
featureId: "foo",
|
||||
value: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
active: true,
|
||||
|
|
|
@ -140,7 +140,7 @@ add_task(async function test_getExperiment_feature() {
|
|||
branch: {
|
||||
slug: "treatment",
|
||||
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", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: { featureId: "purple", enabled: true, value: null },
|
||||
features: [{ featureId: "purple", enabled: true, value: null }],
|
||||
},
|
||||
});
|
||||
const store = ExperimentFakes.store();
|
||||
|
@ -299,7 +299,7 @@ add_task(async function test_updateExperiment_eventEmit_add_and_update() {
|
|||
const experiment = ExperimentFakes.experiment("foo", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: { featureId: "purple", enabled: true, value: null },
|
||||
features: [{ featureId: "purple", enabled: true, value: null }],
|
||||
},
|
||||
});
|
||||
const store = ExperimentFakes.store();
|
||||
|
@ -333,7 +333,7 @@ add_task(async function test_updateExperiment_eventEmit_off() {
|
|||
const experiment = ExperimentFakes.experiment("foo", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: { featureId: "purple", enabled: true, value: null },
|
||||
features: [{ featureId: "purple", enabled: true, value: null }],
|
||||
},
|
||||
});
|
||||
const store = ExperimentFakes.store();
|
||||
|
@ -363,7 +363,7 @@ add_task(async function test_activateBranch() {
|
|||
const experiment = ExperimentFakes.experiment("foo", {
|
||||
branch: {
|
||||
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", {
|
||||
branch: {
|
||||
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", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: { featureId: "green", enabled: true },
|
||||
features: [{ featureId: "green", enabled: true }],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -163,10 +163,12 @@ add_task(
|
|||
const expected = ExperimentFakes.experiment("foo", {
|
||||
branch: {
|
||||
slug: "treatment",
|
||||
feature: {
|
||||
featureId: "foo",
|
||||
value: { enabled: true },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
featureId: "foo",
|
||||
value: { enabled: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
|
||||
|
@ -229,10 +231,12 @@ add_task(async function test_record_exposure_event() {
|
|||
ExperimentFakes.experiment("blah", {
|
||||
branch: {
|
||||
slug: "treatment",
|
||||
feature: {
|
||||
featureId: "foo",
|
||||
value: { enabled: false },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
featureId: "foo",
|
||||
value: { enabled: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -259,10 +263,12 @@ add_task(async function test_record_exposure_event_once() {
|
|||
ExperimentFakes.experiment("blah", {
|
||||
branch: {
|
||||
slug: "treatment",
|
||||
feature: {
|
||||
featureId: "foo",
|
||||
value: { enabled: false },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
featureId: "foo",
|
||||
value: { enabled: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -365,11 +371,13 @@ add_task(async function test_isEnabled_backwards_compatible() {
|
|||
ExperimentFakes.experiment("blah", {
|
||||
branch: {
|
||||
slug: "treatment",
|
||||
feature: {
|
||||
featureId: "foo",
|
||||
enabled: true,
|
||||
value: {},
|
||||
},
|
||||
features: [
|
||||
{
|
||||
featureId: "foo",
|
||||
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(manager.store, "getAllActive").returns([
|
||||
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", {
|
||||
branch: {
|
||||
slug: "treatment",
|
||||
feature: {
|
||||
featureId: "aboutwelcome",
|
||||
enabled: true,
|
||||
value: { screens: ["test-value"] },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
featureId: "aboutwelcome",
|
||||
enabled: true,
|
||||
value: { screens: ["test-value"] },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -218,7 +218,7 @@ add_task(async function test_getVariable_no_mutation() {
|
|||
Cu.cloneInto(
|
||||
{
|
||||
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.
|
||||
const existingBranch = {
|
||||
slug: "treatment",
|
||||
feature: { featureId: "pink", enabled: true, value: {} },
|
||||
features: [{ featureId: "pink", enabled: true, value: {} }],
|
||||
};
|
||||
const newBranch = {
|
||||
slug: "treatment",
|
||||
feature: { featureId: "pink", enabled: true, value: {} },
|
||||
features: [{ featureId: "pink", enabled: true, value: {} }],
|
||||
};
|
||||
|
||||
// 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 => ({
|
||||
slug,
|
||||
ratio: 1,
|
||||
feature: { value: content, enabled: true, featureId: "aboutwelcome" },
|
||||
features: [{ value: content, enabled: true, featureId: "aboutwelcome" }],
|
||||
}));
|
||||
let recipe = ExperimentFakes.recipe("reference-aw", { branches });
|
||||
// Ensure we get enrolled
|
||||
|
@ -276,7 +276,7 @@ add_task(async function test_forceEnroll_cleanup() {
|
|||
{
|
||||
slug: "treatment",
|
||||
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",
|
||||
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",
|
||||
ratio: 1,
|
||||
feature: {
|
||||
featureId: "privatebrowsing",
|
||||
value: { promoLinkType: "link" },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
featureId: "privatebrowsing",
|
||||
value: { promoLinkType: "link" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -356,10 +358,12 @@ add_task(async function test_featuremanifest_enum() {
|
|||
branch: {
|
||||
slug: "control",
|
||||
ratio: 1,
|
||||
feature: {
|
||||
featureId: "privatebrowsing",
|
||||
value: { promoLinkType: "bar" },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
featureId: "privatebrowsing",
|
||||
value: { promoLinkType: "bar" },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -166,7 +166,6 @@ add_task(async function test_onRecipe_isEnrollmentPaused() {
|
|||
await manager.enroll(fooRecipe, "test");
|
||||
await enrollmentPromise;
|
||||
await manager.onRecipe(updatedRecipe, "test");
|
||||
console.log("XXX", manager.updateEnrollment.callCount);
|
||||
Assert.equal(
|
||||
manager.updateEnrollment.calledWith(updatedRecipe),
|
||||
true,
|
||||
|
@ -199,12 +198,12 @@ add_task(async function test_onFinalize_unenroll() {
|
|||
|
||||
const recipe1 = ExperimentFakes.recipe("bar");
|
||||
// Unique features to prevent overlap
|
||||
recipe1.branches[0].feature.featureId = "red";
|
||||
recipe1.branches[1].feature.featureId = "red";
|
||||
recipe1.branches[0].features[0].featureId = "red";
|
||||
recipe1.branches[1].features[0].featureId = "red";
|
||||
await manager.onRecipe(recipe1, "test");
|
||||
const recipe2 = ExperimentFakes.recipe("baz");
|
||||
recipe2.branches[0].feature.featureId = "green";
|
||||
recipe2.branches[1].feature.featureId = "green";
|
||||
recipe2.branches[0].features[0].featureId = "green";
|
||||
recipe2.branches[1].features[0].featureId = "green";
|
||||
await manager.onRecipe(recipe2, "test");
|
||||
|
||||
// Finalize
|
||||
|
|
|
@ -23,7 +23,7 @@ add_task(async function test_usageBeforeInitialization() {
|
|||
const experiment = ExperimentFakes.experiment("foo", {
|
||||
branch: {
|
||||
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();
|
||||
|
||||
// Set update cb
|
||||
store.on(`update:${experiment.branch.feature.featureId}`, updateEventCbStub);
|
||||
store.on(
|
||||
`update:${experiment.branch.features[0].featureId}`,
|
||||
updateEventCbStub
|
||||
);
|
||||
|
||||
store.addExperiment(experiment);
|
||||
store.updateExperiment("foo", { active: false });
|
||||
|
@ -85,7 +88,10 @@ add_task(async function test_event_updates_main() {
|
|||
"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() {
|
||||
|
@ -93,7 +99,7 @@ add_task(async function test_getExperimentForGroup() {
|
|||
const experiment = ExperimentFakes.experiment("foo", {
|
||||
branch: {
|
||||
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() {
|
||||
const feature = { featureId: "cfr", enabled: true };
|
||||
const features = [{ featureId: "cfr", enabled: true }];
|
||||
const experiment = Object.freeze(
|
||||
ExperimentFakes.experiment("foo", { feature, active: true })
|
||||
ExperimentFakes.experiment("foo", { features, active: true })
|
||||
);
|
||||
const store = ExperimentFakes.store();
|
||||
|
||||
|
@ -207,8 +213,8 @@ add_task(async function test_updateExperiment() {
|
|||
const actual = store.get("foo");
|
||||
Assert.equal(actual.active, false, "should change updated props");
|
||||
Assert.deepEqual(
|
||||
actual.branch.feature,
|
||||
feature,
|
||||
actual.branch.features,
|
||||
features,
|
||||
"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");
|
||||
|
||||
const syncAccessExp = ExperimentFakes.experiment("foo", {
|
||||
feature: { featureId: "newtab", enabled: "true" },
|
||||
features: [{ featureId: "newtab", enabled: "true" }],
|
||||
});
|
||||
await store.init();
|
||||
store.addExperiment(syncAccessExp);
|
||||
|
@ -253,7 +259,7 @@ add_task(async function test_sync_access_update() {
|
|||
|
||||
let store = ExperimentFakes.store();
|
||||
let experiment = ExperimentFakes.experiment("foo", {
|
||||
feature: { featureId: "aboutwelcome", enabled: true },
|
||||
features: [{ featureId: "aboutwelcome", enabled: true }],
|
||||
});
|
||||
|
||||
await store.init();
|
||||
|
@ -262,11 +268,13 @@ add_task(async function test_sync_access_update() {
|
|||
store.updateExperiment("foo", {
|
||||
branch: {
|
||||
...experiment.branch,
|
||||
feature: {
|
||||
featureId: "aboutwelcome",
|
||||
enabled: true,
|
||||
value: { bar: "bar" },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
featureId: "aboutwelcome",
|
||||
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.deepEqual(
|
||||
// `branch.feature` and not `features` because for sync access (early startup)
|
||||
// experiments we only store the `isEarlyStartup` feature
|
||||
cachedExperiment.branch.feature.value,
|
||||
{ bar: "bar" },
|
||||
"Got updated value"
|
||||
|
@ -286,7 +296,7 @@ add_task(async function test_sync_features_only() {
|
|||
|
||||
let store = ExperimentFakes.store();
|
||||
let experiment = ExperimentFakes.experiment("foo", {
|
||||
feature: { featureId: "cfr", enabled: true },
|
||||
features: [{ featureId: "cfr", enabled: true }],
|
||||
});
|
||||
|
||||
await store.init();
|
||||
|
@ -302,7 +312,7 @@ add_task(async function test_sync_features_remotely() {
|
|||
|
||||
let store = ExperimentFakes.store();
|
||||
let experiment = ExperimentFakes.experiment("foo", {
|
||||
feature: { featureId: "cfr", enabled: true, isEarlyStartup: true },
|
||||
features: [{ featureId: "cfr", enabled: true, isEarlyStartup: true }],
|
||||
});
|
||||
|
||||
await store.init();
|
||||
|
@ -322,7 +332,7 @@ add_task(async function test_sync_access_unenroll() {
|
|||
|
||||
let store = ExperimentFakes.store();
|
||||
let experiment = ExperimentFakes.experiment("foo", {
|
||||
feature: { featureId: "aboutwelcome", enabled: true },
|
||||
features: [{ featureId: "aboutwelcome", enabled: true }],
|
||||
active: true,
|
||||
});
|
||||
|
||||
|
@ -342,10 +352,10 @@ add_task(async function test_sync_access_unenroll_2() {
|
|||
|
||||
let store = ExperimentFakes.store();
|
||||
let experiment1 = ExperimentFakes.experiment("foo", {
|
||||
feature: { featureId: "newtab", enabled: true },
|
||||
features: [{ featureId: "newtab", enabled: true }],
|
||||
});
|
||||
let experiment2 = ExperimentFakes.experiment("bar", {
|
||||
feature: { featureId: "aboutwelcome", enabled: true },
|
||||
features: [{ featureId: "aboutwelcome", enabled: true }],
|
||||
});
|
||||
|
||||
await store.init();
|
||||
|
@ -529,12 +539,14 @@ add_task(async function test_storeValuePerPref_noVariables() {
|
|||
const experiment = ExperimentFakes.experiment("foo", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: {
|
||||
// Ensure it gets saved to prefs
|
||||
isEarlyStartup: true,
|
||||
featureId: "purple",
|
||||
enabled: true,
|
||||
},
|
||||
features: [
|
||||
{
|
||||
// Ensure it gets saved to prefs
|
||||
isEarlyStartup: true,
|
||||
featureId: "purple",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -562,12 +574,14 @@ add_task(async function test_storeValuePerPref_withVariables() {
|
|||
const experiment = ExperimentFakes.experiment("foo", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: {
|
||||
// Ensure it gets saved to prefs
|
||||
isEarlyStartup: true,
|
||||
featureId: "purple",
|
||||
value: { color: "purple", enabled: true },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
// Ensure it gets saved to prefs
|
||||
isEarlyStartup: 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 val = Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}purple`);
|
||||
Assert.equal(
|
||||
Services.prefs
|
||||
.getStringPref(`${SYNC_DATA_PREF_BRANCH}purple`)
|
||||
.indexOf("color"),
|
||||
val.indexOf("color"),
|
||||
-1,
|
||||
"Experiment metadata does not contain variables"
|
||||
`Experiment metadata does not contain variables ${val}`
|
||||
);
|
||||
|
||||
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", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: {
|
||||
// Ensure it gets saved to prefs
|
||||
isEarlyStartup: true,
|
||||
featureId: "purple",
|
||||
value: { color: "purple", enabled: true },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
// Ensure it gets saved to prefs
|
||||
isEarlyStartup: 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.`);
|
||||
|
||||
store = ExperimentFakes.store();
|
||||
Assert.deepEqual(
|
||||
store.getExperimentForFeature("purple"),
|
||||
experiment,
|
||||
"Returns the same value"
|
||||
);
|
||||
const cachedExperiment = store.getExperimentForFeature("purple");
|
||||
// Cached experiment format only stores early access feature
|
||||
cachedExperiment.branch.features = [cachedExperiment.branch.feature];
|
||||
delete cachedExperiment.branch.feature;
|
||||
Assert.deepEqual(cachedExperiment, experiment, "Returns the same value");
|
||||
|
||||
// Cleanup
|
||||
store._updateSyncStore({ ...experiment, active: false });
|
||||
|
@ -645,20 +660,22 @@ add_task(async function test_storeValuePerPref_returnsSameValue_allTypes() {
|
|||
const experiment = ExperimentFakes.experiment("foo", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: {
|
||||
// Ensure it gets saved to prefs
|
||||
isEarlyStartup: true,
|
||||
featureId: "purple",
|
||||
value: {
|
||||
string: "string",
|
||||
bool: true,
|
||||
array: [1, 2, 3],
|
||||
number1: 42,
|
||||
number2: 0,
|
||||
number3: -5,
|
||||
json: { jsonValue: true },
|
||||
features: [
|
||||
{
|
||||
// Ensure it gets saved to prefs
|
||||
isEarlyStartup: true,
|
||||
featureId: "purple",
|
||||
value: {
|
||||
string: "string",
|
||||
bool: true,
|
||||
array: [1, 2, 3],
|
||||
number1: 42,
|
||||
number2: 0,
|
||||
number3: -5,
|
||||
json: { jsonValue: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -669,7 +686,7 @@ add_task(async function test_storeValuePerPref_returnsSameValue_allTypes() {
|
|||
store = ExperimentFakes.store();
|
||||
Assert.deepEqual(
|
||||
store.getExperimentForFeature("purple").branch.feature.value,
|
||||
experiment.branch.feature.value,
|
||||
experiment.branch.features[0].value,
|
||||
"Returns the same value"
|
||||
);
|
||||
|
||||
|
@ -690,25 +707,25 @@ add_task(async function test_cleanupOldRecipes() {
|
|||
const experiment1 = ExperimentFakes.experiment("foo", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: { featureId: "purple", enabled: true },
|
||||
features: [{ featureId: "purple", enabled: true }],
|
||||
},
|
||||
});
|
||||
const experiment2 = ExperimentFakes.experiment("bar", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: { featureId: "purple", enabled: true },
|
||||
features: [{ featureId: "purple", enabled: true }],
|
||||
},
|
||||
});
|
||||
const experiment3 = ExperimentFakes.experiment("baz", {
|
||||
branch: {
|
||||
slug: "variant",
|
||||
feature: { featureId: "purple", enabled: true },
|
||||
features: [{ featureId: "purple", enabled: true }],
|
||||
},
|
||||
});
|
||||
const experiment4 = ExperimentFakes.experiment("faz", {
|
||||
branch: {
|
||||
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)
|
||||
|
|
|
@ -13,13 +13,23 @@ add_task(async function test_recipe_fake_validates() {
|
|||
});
|
||||
|
||||
add_task(async function test_enrollmentHelper() {
|
||||
let recipe = ExperimentFakes.recipe("bar");
|
||||
recipe.branches.forEach(branch => {
|
||||
// Use a feature that will set the sync pref cache
|
||||
branch.feature.featureId = "aboutwelcome";
|
||||
let recipe = ExperimentFakes.recipe("bar", {
|
||||
branches: [
|
||||
{
|
||||
slug: "control",
|
||||
ratio: 1,
|
||||
features: [{ featureId: "aboutwelcome", value: {} }],
|
||||
},
|
||||
],
|
||||
});
|
||||
let manager = ExperimentFakes.manager();
|
||||
|
||||
Assert.deepEqual(
|
||||
recipe.featureIds,
|
||||
["aboutwelcome"],
|
||||
"Helper sets correct featureIds"
|
||||
);
|
||||
|
||||
await manager.onStartup();
|
||||
|
||||
let {
|
||||
|
|
|
@ -53,9 +53,12 @@ add_task(async function test_initialize() {
|
|||
const experiment = sinon.stub(ExperimentAPI, "activateBranch").returns({
|
||||
slug: "foo",
|
||||
ratio: 1,
|
||||
feature: {
|
||||
value: { directMigrateSingleProfile: true },
|
||||
},
|
||||
features: [
|
||||
{
|
||||
featureId: "password-autocomplete",
|
||||
value: { directMigrateSingleProfile: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// This makes the last autocomplete test *not* show import suggestions.
|
||||
|
|
Загрузка…
Ссылка в новой задаче