forked from mirrors/gecko-dev
Backed out 3 changesets (bug 1821092) for causing failures in browser_autocomplete_import.js CLOSED TREE
Backed out changeset 142e58fc3ee1 (bug 1821092) Backed out changeset ff07a826e81d (bug 1821092) Backed out changeset b70d97b262ed (bug 1821092)
This commit is contained in:
parent
ff02e04b0e
commit
f91604fab5
13 changed files with 123 additions and 1899 deletions
|
|
@ -56,12 +56,6 @@ function featuresCompat(branch) {
|
|||
return features;
|
||||
}
|
||||
|
||||
function getBranchFeature(enrollment, targetFeatureId) {
|
||||
return featuresCompat(enrollment.branch).find(
|
||||
({ featureId }) => featureId === targetFeatureId
|
||||
);
|
||||
}
|
||||
|
||||
const experimentBranchAccessor = {
|
||||
get: (target, prop) => {
|
||||
// Offer an API where we can access `branch.feature.*`.
|
||||
|
|
@ -357,32 +351,20 @@ export class _ExperimentFeature {
|
|||
}
|
||||
|
||||
/**
|
||||
* Lookup feature variables in experiments, rollouts, and fallback prefs.
|
||||
* Lookup feature variables in experiments, prefs, and remote defaults.
|
||||
* @param {{defaultValues?: {[variableName: string]: any}}} options
|
||||
* @returns {{[variableName: string]: any}} The feature value
|
||||
*/
|
||||
getAllVariables({ defaultValues = null } = {}) {
|
||||
let enrollment = null;
|
||||
try {
|
||||
enrollment = ExperimentAPI._store.getExperimentForFeature(this.featureId);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
let featureValue = this._getLocalizedValue(enrollment);
|
||||
|
||||
if (typeof featureValue === "undefined") {
|
||||
try {
|
||||
enrollment = ExperimentAPI._store.getRolloutForFeature(this.featureId);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
featureValue = this._getLocalizedValue(enrollment);
|
||||
}
|
||||
const branch = ExperimentAPI.getActiveBranch({ featureId: this.featureId });
|
||||
const featureValue = featuresCompat(branch).find(
|
||||
({ featureId }) => featureId === this.featureId
|
||||
)?.value;
|
||||
|
||||
return {
|
||||
...this.prefGetters,
|
||||
...defaultValues,
|
||||
...featureValue,
|
||||
...(featureValue ? featureValue : this.getRollout()?.value),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -397,26 +379,21 @@ export class _ExperimentFeature {
|
|||
}
|
||||
|
||||
// Next, check if an experiment is defined
|
||||
let enrollment = null;
|
||||
try {
|
||||
enrollment = ExperimentAPI._store.getExperimentForFeature(this.featureId);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
let value = this._getLocalizedValue(enrollment, variable);
|
||||
if (typeof value !== "undefined") {
|
||||
return value;
|
||||
const branch = ExperimentAPI.getActiveBranch({
|
||||
featureId: this.featureId,
|
||||
});
|
||||
const experimentValue = featuresCompat(branch).find(
|
||||
({ featureId }) => featureId === this.featureId
|
||||
)?.value?.[variable];
|
||||
|
||||
if (typeof experimentValue !== "undefined") {
|
||||
return experimentValue;
|
||||
}
|
||||
|
||||
// Next, check for a rollout.
|
||||
try {
|
||||
enrollment = ExperimentAPI._store.getRolloutForFeature(this.featureId);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
value = this._getLocalizedValue(enrollment, variable);
|
||||
if (typeof value !== "undefined") {
|
||||
return value;
|
||||
// Next, check remote defaults
|
||||
const remoteValue = this.getRollout()?.value?.[variable];
|
||||
if (typeof remoteValue !== "undefined") {
|
||||
return remoteValue;
|
||||
}
|
||||
|
||||
// Return the default preference value
|
||||
|
|
@ -500,159 +477,8 @@ export class _ExperimentFeature {
|
|||
rollouts: this.getRollout(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Do recursive locale substitution on the values, if applicable.
|
||||
*
|
||||
* If there are no localizations provided, the value will be returned as-is.
|
||||
*
|
||||
* If the value is an object containing an $l10n key, its substitution will be
|
||||
* returned.
|
||||
*
|
||||
* Otherwise, the value will be recursively substituted.
|
||||
*
|
||||
* @param {unknown} values The values to perform substitutions upon.
|
||||
* @param {Record<string, string>} localizations The localization
|
||||
* substitutions for a specific locale.
|
||||
* @param {Set<string>?} missingIds An optional set to collect all the IDs of
|
||||
* all missing l10n entries.
|
||||
*
|
||||
* @returns {any} The values, potentially locale substituted.
|
||||
*/
|
||||
static substituteLocalizations(
|
||||
values,
|
||||
localizations,
|
||||
missingIds = undefined
|
||||
) {
|
||||
const result = _ExperimentFeature._substituteLocalizations(
|
||||
values,
|
||||
localizations,
|
||||
missingIds
|
||||
);
|
||||
|
||||
if (missingIds?.size) {
|
||||
throw new ExperimentLocalizationError("l10n-missing-entry");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The implementation of localization substitution.
|
||||
*
|
||||
* @param {unknown} values The values to perform substitutions upon.
|
||||
* @param {Record<string, string>} localizations The localization
|
||||
* substitutions for a specific locale.
|
||||
* @param {Set<string>?} missingIds An optional set to collect all the IDs of
|
||||
* all missing l10n entries.
|
||||
*
|
||||
* @returns {any} The values, potentially locale substituted.
|
||||
*/
|
||||
static _substituteLocalizations(values, localizations, missingIds) {
|
||||
// If the recipe is not localized, we don't need to do anything.
|
||||
// Likewise, if the value we are attempting to localize is not an object,
|
||||
// there is nothing to localize.
|
||||
if (
|
||||
typeof localizations === "undefined" ||
|
||||
typeof values !== "object" ||
|
||||
values === null
|
||||
) {
|
||||
return values;
|
||||
}
|
||||
|
||||
if (Array.isArray(values)) {
|
||||
return values.map(value =>
|
||||
_ExperimentFeature._substituteLocalizations(
|
||||
value,
|
||||
localizations,
|
||||
missingIds
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const substituted = Object.assign({}, values);
|
||||
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (
|
||||
key === "$l10n" &&
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
value?.id
|
||||
) {
|
||||
if (!Object.hasOwn(localizations, value.id)) {
|
||||
if (missingIds) {
|
||||
missingIds.add(value.id);
|
||||
break;
|
||||
} else {
|
||||
throw new ExperimentLocalizationError("l10n-missing-entry");
|
||||
}
|
||||
}
|
||||
|
||||
return localizations[value.id];
|
||||
}
|
||||
|
||||
substituted[key] = _ExperimentFeature._substituteLocalizations(
|
||||
value,
|
||||
localizations,
|
||||
missingIds
|
||||
);
|
||||
}
|
||||
|
||||
return substituted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value (or all values) from an enrollment, potentially localized.
|
||||
*
|
||||
* @param {Enrollment} enrollment - The enrollment to query for the value or values.
|
||||
* @param {string?} variable - The name of the variable to query for. If not
|
||||
* provided, all variables will be returned.
|
||||
*
|
||||
* @returns {any} The value for the variable(s) in question.
|
||||
*/
|
||||
_getLocalizedValue(enrollment, variable = undefined) {
|
||||
if (enrollment) {
|
||||
const locale = Services.locale.appLocaleAsBCP47;
|
||||
|
||||
if (
|
||||
typeof enrollment.localizations === "object" &&
|
||||
enrollment.localizations !== null &&
|
||||
(typeof enrollment.localizations[locale] !== "object" ||
|
||||
enrollment.localizations[locale] === null)
|
||||
) {
|
||||
ExperimentAPI._manager.unenroll(enrollment.slug, "l10n-missing-locale");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const allValues = getBranchFeature(enrollment, this.featureId)?.value;
|
||||
const value =
|
||||
typeof variable === "undefined" ? allValues : allValues?.[variable];
|
||||
|
||||
if (typeof value !== "undefined") {
|
||||
try {
|
||||
return _ExperimentFeature.substituteLocalizations(
|
||||
value,
|
||||
enrollment.localizations?.[locale]
|
||||
);
|
||||
} catch (e) {
|
||||
// This should never happen.
|
||||
if (e instanceof ExperimentLocalizationError) {
|
||||
ExperimentAPI._manager.unenroll(enrollment.slug, e.reason);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
XPCOMUtils.defineLazyGetter(ExperimentAPI, "_manager", function() {
|
||||
return lazy.ExperimentManager;
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(ExperimentAPI, "_store", function() {
|
||||
return IS_MAIN_PROCESS
|
||||
? lazy.ExperimentManager.store
|
||||
|
|
@ -662,10 +488,3 @@ XPCOMUtils.defineLazyGetter(ExperimentAPI, "_store", function() {
|
|||
XPCOMUtils.defineLazyGetter(ExperimentAPI, "_remoteSettingsClient", function() {
|
||||
return lazy.RemoteSettings(lazy.COLLECTION_ID);
|
||||
});
|
||||
|
||||
class ExperimentLocalizationError extends Error {
|
||||
constructor(reason) {
|
||||
super(`Localized experiment error (${reason})`);
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,9 +179,7 @@ export class _ExperimentManager {
|
|||
recipeMismatches,
|
||||
invalidRecipes,
|
||||
invalidBranches,
|
||||
invalidFeatures,
|
||||
missingLocale,
|
||||
missingL10nIds
|
||||
invalidFeatures
|
||||
) {
|
||||
for (const enrollment of enrollments) {
|
||||
const { slug, source } = enrollment;
|
||||
|
|
@ -198,10 +196,6 @@ export class _ExperimentManager {
|
|||
reason = "invalid-recipe";
|
||||
} else if (invalidBranches.has(slug) || invalidFeatures.has(slug)) {
|
||||
reason = "invalid-branch";
|
||||
} else if (missingLocale.includes(slug)) {
|
||||
reason = "l10n-missing-locale";
|
||||
} else if (missingL10nIds.has(slug)) {
|
||||
reason = "l10n-missing-entry";
|
||||
} else {
|
||||
reason = "recipe-not-seen";
|
||||
}
|
||||
|
|
@ -227,16 +221,6 @@ export class _ExperimentManager {
|
|||
* feature validation.
|
||||
* @param {Map<string, string[]>} options.invalidFeatures
|
||||
* The mapping of experiment slugs to a list of invalid feature IDs.
|
||||
* @param {string[]} options.missingLocale
|
||||
* The list of experiment slugs missing an entry in the localization
|
||||
* table for the current locale.
|
||||
* @param {Map<string, string[]>} options.missingL10nIds
|
||||
* The mapping of experiment slugs to the IDs of localization entries
|
||||
* missing from the current locale.
|
||||
* @param {string | null} options.locale
|
||||
* The current locale.
|
||||
* @param {boolean} options.validationEnabled
|
||||
* Whether or not schema validation was enabled.
|
||||
*/
|
||||
onFinalize(
|
||||
sourceToCheck,
|
||||
|
|
@ -245,9 +229,6 @@ export class _ExperimentManager {
|
|||
invalidRecipes = [],
|
||||
invalidBranches = new Map(),
|
||||
invalidFeatures = new Map(),
|
||||
missingLocale = [],
|
||||
missingL10nIds = new Map(),
|
||||
locale = null,
|
||||
validationEnabled = true,
|
||||
} = {}
|
||||
) {
|
||||
|
|
@ -262,9 +243,7 @@ export class _ExperimentManager {
|
|||
recipeMismatches,
|
||||
invalidRecipes,
|
||||
invalidBranches,
|
||||
invalidFeatures,
|
||||
missingLocale,
|
||||
missingL10nIds
|
||||
invalidFeatures
|
||||
);
|
||||
this._checkUnseenEnrollments(
|
||||
activeRollouts,
|
||||
|
|
@ -272,13 +251,11 @@ export class _ExperimentManager {
|
|||
recipeMismatches,
|
||||
invalidRecipes,
|
||||
invalidBranches,
|
||||
invalidFeatures,
|
||||
missingLocale,
|
||||
missingL10nIds
|
||||
invalidFeatures
|
||||
);
|
||||
|
||||
// If schema validation is disabled, then we will never send these
|
||||
// validation failed telemetry events
|
||||
// If validation is disabled, then we will never send validation failed
|
||||
// telemetry.
|
||||
if (validationEnabled) {
|
||||
for (const slug of invalidRecipes) {
|
||||
this.sendValidationFailedTelemetry(slug, "invalid-recipe");
|
||||
|
|
@ -299,21 +276,6 @@ export class _ExperimentManager {
|
|||
}
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
for (const slug of missingLocale.values()) {
|
||||
this.sendValidationFailedTelemetry(slug, "l10n-missing-locale", {
|
||||
locale,
|
||||
});
|
||||
}
|
||||
|
||||
for (const [slug, ids] of missingL10nIds.entries()) {
|
||||
this.sendValidationFailedTelemetry(slug, "l10n-missing-entry", {
|
||||
l10n_ids: ids.join(","),
|
||||
locale,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.sessions.delete(sourceToCheck);
|
||||
}
|
||||
|
||||
|
|
@ -393,7 +355,6 @@ export class _ExperimentManager {
|
|||
userFacingDescription,
|
||||
featureIds,
|
||||
isRollout,
|
||||
localizations,
|
||||
},
|
||||
branch,
|
||||
source,
|
||||
|
|
@ -414,7 +375,6 @@ export class _ExperimentManager {
|
|||
lastSeen: new Date().toJSON(),
|
||||
featureIds,
|
||||
prefs,
|
||||
localizations,
|
||||
};
|
||||
|
||||
if (typeof isRollout !== "undefined") {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
_ExperimentFeature: "resource://nimbus/ExperimentAPI.sys.mjs",
|
||||
ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs",
|
||||
JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
|
||||
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
|
||||
|
|
@ -377,10 +376,6 @@ export class EnrollmentsContext {
|
|||
this.invalidBranches = new Map();
|
||||
this.invalidFeatures = new Map();
|
||||
this.validatorCache = {};
|
||||
this.missingLocale = [];
|
||||
this.missingL10nIds = new Map();
|
||||
|
||||
this.locale = Services.locale.appLocaleAsBCP47;
|
||||
}
|
||||
|
||||
getResults() {
|
||||
|
|
@ -389,9 +384,6 @@ export class EnrollmentsContext {
|
|||
invalidRecipes: this.invalidRecipes,
|
||||
invalidBranches: this.invalidBranches,
|
||||
invalidFeatures: this.invalidFeatures,
|
||||
missingLocale: this.missingLocale,
|
||||
missingL10nIds: this.missingL10nIds,
|
||||
locale: this.locale,
|
||||
validationEnabled: this.validationEnabled,
|
||||
};
|
||||
}
|
||||
|
|
@ -409,7 +401,7 @@ export class EnrollmentsContext {
|
|||
return false;
|
||||
}
|
||||
|
||||
const validateFeatureSchemas =
|
||||
const validateFeatures =
|
||||
this.validationEnabled && !recipe.featureValidationOptOut;
|
||||
|
||||
if (this.validationEnabled) {
|
||||
|
|
@ -474,23 +466,8 @@ export class EnrollmentsContext {
|
|||
|
||||
this.matches++;
|
||||
|
||||
if (
|
||||
typeof recipe.localizations === "object" &&
|
||||
recipe.localizations !== null
|
||||
) {
|
||||
if (
|
||||
typeof recipe.localizations[this.locale] !== "object" ||
|
||||
recipe.localizations[this.locale] === null
|
||||
) {
|
||||
this.missingLocale.push(recipe.slug);
|
||||
lazy.log.debug(
|
||||
`${recipe.id} is localized but missing locale ${this.locale}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this._validateBranches(recipe, validateFeatureSchemas);
|
||||
if (validateFeatures) {
|
||||
const result = await this._validateBranches(recipe);
|
||||
if (!result.valid) {
|
||||
if (result.invalidBranchSlugs.length) {
|
||||
this.invalidBranches.set(recipe.slug, result.invalidBranchSlugs);
|
||||
|
|
@ -498,12 +475,10 @@ export class EnrollmentsContext {
|
|||
if (result.invalidFeatureIds.length) {
|
||||
this.invalidFeatures.set(recipe.slug, result.invalidFeatureIds);
|
||||
}
|
||||
if (result.missingL10nIds.length) {
|
||||
this.missingL10nIds.set(recipe.slug, result.missingL10nIds);
|
||||
}
|
||||
lazy.log.debug(`${recipe.id} did not validate`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -563,64 +538,39 @@ export class EnrollmentsContext {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validate the branches of an experiment.
|
||||
* Validate the branches of an experiment using schemas
|
||||
*
|
||||
* @param {object} recipe The recipe object.
|
||||
* @param {boolean} validateSchema Whether to validate the feature values
|
||||
* using JSON schemas.
|
||||
* @param {object} validatorCache A cache of JSON Schema validators keyed by feature
|
||||
* ID.
|
||||
*
|
||||
* @returns {object} The lists of invalid branch slugs and invalid feature
|
||||
* IDs.
|
||||
*/
|
||||
async _validateBranches({ id, branches, localizations }, validateSchema) {
|
||||
async _validateBranches({ id, branches }) {
|
||||
const invalidBranchSlugs = [];
|
||||
const invalidFeatureIds = new Set();
|
||||
const missingL10nIds = new Set();
|
||||
|
||||
if (validateSchema || typeof localizations !== "undefined") {
|
||||
for (const [branchIdx, branch] of branches.entries()) {
|
||||
const features = branch.features ?? [branch.feature];
|
||||
for (const feature of features) {
|
||||
const { featureId, value } = feature;
|
||||
if (!lazy.NimbusFeatures[featureId]) {
|
||||
console.error(
|
||||
`Experiment ${id} has unknown featureId: ${featureId}`
|
||||
);
|
||||
console.error(`Experiment ${id} has unknown featureId: ${featureId}`);
|
||||
|
||||
invalidFeatureIds.add(featureId);
|
||||
continue;
|
||||
}
|
||||
|
||||
let substitutedValue = value;
|
||||
|
||||
if (localizations) {
|
||||
// We already know that we have a localization table for this locale
|
||||
// because we checked in `checkRecipe`.
|
||||
try {
|
||||
substitutedValue = lazy._ExperimentFeature.substituteLocalizations(
|
||||
value,
|
||||
localizations[Services.locale.appLocaleAsBCP47],
|
||||
missingL10nIds
|
||||
);
|
||||
} catch (e) {
|
||||
if (e?.reason === "l10n-missing-entry") {
|
||||
// Skip validation because it *will* fail.
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (validateSchema) {
|
||||
let validator;
|
||||
if (this.validatorCache[featureId]) {
|
||||
validator = this.validatorCache[featureId];
|
||||
} else if (lazy.NimbusFeatures[featureId].manifest.schema?.uri) {
|
||||
const uri = lazy.NimbusFeatures[featureId].manifest.schema.uri;
|
||||
try {
|
||||
const schema = await fetch(uri, {
|
||||
credentials: "omit",
|
||||
}).then(rsp => rsp.json());
|
||||
const schema = await fetch(uri, { credentials: "omit" }).then(rsp =>
|
||||
rsp.json()
|
||||
);
|
||||
|
||||
validator = this.validatorCache[
|
||||
featureId
|
||||
|
|
@ -639,7 +589,7 @@ export class EnrollmentsContext {
|
|||
] = new lazy.JsonSchema.Validator(schema);
|
||||
}
|
||||
|
||||
const result = validator.validate(substitutedValue);
|
||||
const result = validator.validate(value);
|
||||
if (!result.valid) {
|
||||
console.error(
|
||||
`Experiment ${id} branch ${branchIdx} feature ${featureId} does not validate: ${JSON.stringify(
|
||||
|
|
@ -652,17 +602,11 @@ export class EnrollmentsContext {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
invalidBranchSlugs,
|
||||
invalidFeatureIds: Array.from(invalidFeatureIds),
|
||||
missingL10nIds: Array.from(missingL10nIds),
|
||||
valid:
|
||||
invalidBranchSlugs.length === 0 &&
|
||||
invalidFeatureIds.size === 0 &&
|
||||
missingL10nIds.size === 0,
|
||||
valid: invalidBranchSlugs.length === 0 && invalidFeatureIds.size === 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -177,8 +177,8 @@ nimbus_events:
|
|||
reason:
|
||||
type: string
|
||||
description: >
|
||||
Why validation failed (one of "invalid-recipe", "invalid-branch",
|
||||
"invalid-reason", "missing-locale", or "missing-l10n-entry").
|
||||
Why validation failed (one of "invalid-recipe", "invalid-branch", or
|
||||
"invalid-reason").
|
||||
branch:
|
||||
type: string
|
||||
description: >
|
||||
|
|
@ -186,27 +186,12 @@ nimbus_events:
|
|||
feature:
|
||||
type: string
|
||||
description: If reason == invalid-feature, the invalid feature ID.
|
||||
locale:
|
||||
type: string
|
||||
description: >
|
||||
If reason == missing-locale, the locale that was missing from the
|
||||
localization table.
|
||||
If reason == missing-l10n-entry, the locale that was missing the
|
||||
localization entries.
|
||||
l10n_ids:
|
||||
type: string
|
||||
description: >
|
||||
If reason == missing-l10n-entry, a comma-sparated list of missing
|
||||
localization entries.
|
||||
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1762652
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1781953
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821092
|
||||
data_reviews:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1762652
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1781953
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1821092
|
||||
data_sensitivity:
|
||||
- technical
|
||||
notification_emails:
|
||||
|
|
|
|||
|
|
@ -314,16 +314,6 @@
|
|||
"featureValidationOptOut": {
|
||||
"type": "boolean",
|
||||
"description": "Opt out of feature schema validation. Only supported on desktop."
|
||||
},
|
||||
"localizations": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Per-locale localization substitutions.\n\nThe top level key is the locale (e.g., \"en-US\" or \"fr\"). Each entry is a mapping of string IDs to their localized equivalents.\n\nOnly supported on desktop."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ origin:
|
|||
description: "Shared data and schemas for Project Nimbus"
|
||||
url: "https://github.com/mozilla/nimbus-shared"
|
||||
license: "MPL-2.0"
|
||||
release: "version 2.0.0"
|
||||
revision: "v2.0.0"
|
||||
release: "version 1.10.0"
|
||||
revision: "v1.10.0"
|
||||
|
||||
vendoring:
|
||||
url: "https://github.com/mozilla/nimbus-shared"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
|
||||
ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs",
|
||||
JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
|
||||
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
|
||||
_ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs",
|
||||
|
|
@ -186,7 +186,7 @@ export const ExperimentFakes = {
|
|||
},
|
||||
async enrollWithRollout(
|
||||
featureConfig,
|
||||
{ manager = lazy.ExperimentAPI._manager, source } = {}
|
||||
{ manager = lazy.ExperimentManager, source } = {}
|
||||
) {
|
||||
await manager.store.init();
|
||||
const rollout = this.rollout(`${featureConfig.featureId}-rollout`, {
|
||||
|
|
@ -223,7 +223,7 @@ export const ExperimentFakes = {
|
|||
},
|
||||
async enrollWithFeatureConfig(
|
||||
featureConfig,
|
||||
{ manager = lazy.ExperimentAPI._manager, isRollout = false } = {}
|
||||
{ manager = lazy.ExperimentManager, isRollout = false } = {}
|
||||
) {
|
||||
await manager.store.ready();
|
||||
// Use id passed in featureConfig value to compute experimentId
|
||||
|
|
@ -259,10 +259,7 @@ export const ExperimentFakes = {
|
|||
|
||||
return doExperimentCleanup;
|
||||
},
|
||||
enrollmentHelper(
|
||||
recipe,
|
||||
{ manager = lazy.ExperimentAPI._manager, source = "enrollmentHelper" } = {}
|
||||
) {
|
||||
enrollmentHelper(recipe, { manager = lazy.ExperimentManager } = {}) {
|
||||
if (!recipe?.slug) {
|
||||
throw new Error("Enrollment helper expects a recipe");
|
||||
}
|
||||
|
|
@ -296,11 +293,11 @@ export const ExperimentFakes = {
|
|||
if (!manager.store._isReady) {
|
||||
throw new Error("Manager store not ready, call `manager.onStartup`");
|
||||
}
|
||||
manager.enroll(recipe, source);
|
||||
manager.enroll(recipe, "enrollmentHelper");
|
||||
|
||||
return { enrollmentPromise, doExperimentCleanup };
|
||||
},
|
||||
async cleanupAll(slugs, { manager = lazy.ExperimentAPI._manager } = {}) {
|
||||
async cleanupAll(slugs, { manager = lazy.ExperimentManager } = {}) {
|
||||
function unenrollCompleted(slug) {
|
||||
return new Promise(resolve =>
|
||||
manager.store.on(`update:${slug}`, (event, experiment) => {
|
||||
|
|
|
|||
|
|
@ -7,49 +7,7 @@ const { sinon } = ChromeUtils.importESModule(
|
|||
const { XPCOMUtils } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"ObjectUtils",
|
||||
"resource://gre/modules/ObjectUtils.jsm"
|
||||
);
|
||||
ChromeUtils.defineESModuleGetters(this, {
|
||||
ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
|
||||
ExperimentTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs",
|
||||
});
|
||||
|
||||
// Sinon does not support Set or Map in spy.calledWith()
|
||||
function onFinalizeCalled(spyOrCallArgs, ...expectedArgs) {
|
||||
function mapToObject(map) {
|
||||
return Object.assign(
|
||||
{},
|
||||
...Array.from(map.entries()).map(([k, v]) => ({ [k]: v }))
|
||||
);
|
||||
}
|
||||
|
||||
function toPlainObjects(args) {
|
||||
return [
|
||||
args[0],
|
||||
{
|
||||
...args[1],
|
||||
invalidBranches: mapToObject(args[1].invalidBranches),
|
||||
invalidFeatures: mapToObject(args[1].invalidFeatures),
|
||||
missingLocale: Array.from(args[1].missingLocale),
|
||||
missingL10nIds: mapToObject(args[1].missingL10nIds),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const plainExpected = toPlainObjects(expectedArgs);
|
||||
|
||||
if (Array.isArray(spyOrCallArgs)) {
|
||||
return ObjectUtils.deepEqual(toPlainObjects(spyOrCallArgs), plainExpected);
|
||||
}
|
||||
|
||||
for (const args of spyOrCallArgs.args) {
|
||||
if (ObjectUtils.deepEqual(toPlainObjects(args), plainExpected)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,14 +161,11 @@ add_task(async function test_updateRecipes_someMismatch() {
|
|||
);
|
||||
ok(loader.manager.onFinalize.calledOnce, "Should call onFinalize.");
|
||||
ok(
|
||||
onFinalizeCalled(loader.manager.onFinalize, "rs-loader", {
|
||||
loader.manager.onFinalize.calledWith("rs-loader", {
|
||||
recipeMismatches: [FAIL_FILTER_RECIPE.slug],
|
||||
invalidRecipes: [],
|
||||
invalidBranches: new Map(),
|
||||
invalidFeatures: new Map(),
|
||||
missingL10nIds: new Map(),
|
||||
missingLocale: [],
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: true,
|
||||
}),
|
||||
"should call .onFinalize with the recipes that failed targeting"
|
||||
|
|
|
|||
|
|
@ -195,16 +195,12 @@ add_task(async function test_updateRecipes_invalidRecipeAfterUpdate() {
|
|||
"should call .onRecipe with argument data"
|
||||
);
|
||||
equal(loader.manager.onFinalize.callCount, 1, "should call .onFinalize once");
|
||||
|
||||
ok(
|
||||
onFinalizeCalled(loader.manager.onFinalize, "rs-loader", {
|
||||
loader.manager.onFinalize.calledWith("rs-loader", {
|
||||
recipeMismatches: [],
|
||||
invalidRecipes: [],
|
||||
invalidBranches: new Map(),
|
||||
invalidFeatures: new Map(),
|
||||
missingLocale: [],
|
||||
missingL10nIds: new Map(),
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: true,
|
||||
}),
|
||||
"should call .onFinalize with no mismatches or invalid recipes"
|
||||
|
|
@ -227,14 +223,11 @@ add_task(async function test_updateRecipes_invalidRecipeAfterUpdate() {
|
|||
);
|
||||
|
||||
ok(
|
||||
onFinalizeCalled(loader.manager.onFinalize.secondCall.args, "rs-loader", {
|
||||
loader.manager.onFinalize.secondCall.calledWith("rs-loader", {
|
||||
recipeMismatches: [],
|
||||
invalidRecipes: ["foo"],
|
||||
invalidBranches: new Map(),
|
||||
invalidFeatures: new Map(),
|
||||
missingLocale: [],
|
||||
missingL10nIds: new Map(),
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: true,
|
||||
}),
|
||||
"should call .onFinalize with an invalid recipe"
|
||||
|
|
@ -310,14 +303,11 @@ add_task(async function test_updateRecipes_invalidBranchAfterUpdate() {
|
|||
);
|
||||
equal(loader.manager.onFinalize.callCount, 1, "should call .onFinalize once");
|
||||
ok(
|
||||
onFinalizeCalled(loader.manager.onFinalize, "rs-loader", {
|
||||
loader.manager.onFinalize.calledWith("rs-loader", {
|
||||
recipeMismatches: [],
|
||||
invalidRecipes: [],
|
||||
invalidBranches: new Map(),
|
||||
invalidFeatures: new Map(),
|
||||
missingLocale: [],
|
||||
missingL10nIds: new Map(),
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: true,
|
||||
}),
|
||||
"should call .onFinalize with no mismatches or invalid recipes"
|
||||
|
|
@ -340,14 +330,11 @@ add_task(async function test_updateRecipes_invalidBranchAfterUpdate() {
|
|||
);
|
||||
|
||||
ok(
|
||||
onFinalizeCalled(loader.manager.onFinalize.secondCall.args, "rs-loader", {
|
||||
loader.manager.onFinalize.secondCall.calledWith("rs-loader", {
|
||||
recipeMismatches: [],
|
||||
invalidRecipes: [],
|
||||
invalidBranches: new Map([["foo", [badRecipe.branches[1].slug]]]),
|
||||
invalidBranches: new Map([["foo", [badRecipe.branches[0].slug]]]),
|
||||
invalidFeatures: new Map(),
|
||||
missingLocale: [],
|
||||
missingL10nIds: new Map(),
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: true,
|
||||
}),
|
||||
"should call .onFinalize with an invalid branch"
|
||||
|
|
@ -419,14 +406,11 @@ add_task(async function test_updateRecipes_simpleFeatureInvalidAfterUpdate() {
|
|||
);
|
||||
equal(loader.manager.onFinalize.callCount, 1, "should call .onFinalize once");
|
||||
ok(
|
||||
onFinalizeCalled(loader.manager.onFinalize, "rs-loader", {
|
||||
loader.manager.onFinalize.calledWith("rs-loader", {
|
||||
recipeMismatches: [],
|
||||
invalidRecipes: [],
|
||||
invalidBranches: new Map(),
|
||||
invalidFeatures: new Map(),
|
||||
missingLocale: [],
|
||||
missingL10nIds: new Map(),
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: true,
|
||||
}),
|
||||
"should call .onFinalize with nomismatches or invalid recipes"
|
||||
|
|
@ -460,14 +444,11 @@ add_task(async function test_updateRecipes_simpleFeatureInvalidAfterUpdate() {
|
|||
);
|
||||
|
||||
ok(
|
||||
onFinalizeCalled(loader.manager.onFinalize.secondCall.args, "rs-loader", {
|
||||
loader.manager.onFinalize.secondCall.calledWith("rs-loader", {
|
||||
recipeMismatches: [],
|
||||
invalidRecipes: [],
|
||||
invalidBranches: new Map([["foo", [badRecipe.branches[0].slug]]]),
|
||||
invalidFeatures: new Map(),
|
||||
missingLocale: [],
|
||||
missingL10nIds: new Map(),
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: true,
|
||||
}),
|
||||
"should call .onFinalize with an invalid branch"
|
||||
|
|
@ -657,14 +638,11 @@ add_task(async function test_updateRecipes_validationDisabled() {
|
|||
"Should not send validation failed telemetry"
|
||||
);
|
||||
Assert.ok(
|
||||
onFinalizeCalled(finalizeStub, "rs-loader", {
|
||||
finalizeStub.calledWith("rs-loader", {
|
||||
recipeMismatches: [],
|
||||
invalidRecipes: [],
|
||||
invalidBranches: new Map(),
|
||||
invalidFeatures: new Map(),
|
||||
missingLocale: [],
|
||||
missingL10nIds: new Map(),
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: false,
|
||||
}),
|
||||
"should call .onFinalize with no validation issues"
|
||||
|
|
@ -704,14 +682,11 @@ add_task(async function test_updateRecipes_appId() {
|
|||
|
||||
Assert.equal(manager.onRecipe.callCount, 0, ".onRecipe was never called");
|
||||
Assert.ok(
|
||||
onFinalizeCalled(manager.onFinalize, "rs-loader", {
|
||||
manager.onFinalize.calledWith("rs-loader", {
|
||||
recipeMismatches: [],
|
||||
invalidRecipes: [],
|
||||
invalidBranches: new Map(),
|
||||
invalidFeatures: new Map(),
|
||||
missingLocale: [],
|
||||
missingL10nIds: new Map(),
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: true,
|
||||
}),
|
||||
"Should call .onFinalize with no validation issues"
|
||||
|
|
@ -731,14 +706,11 @@ add_task(async function test_updateRecipes_appId() {
|
|||
);
|
||||
|
||||
Assert.ok(
|
||||
onFinalizeCalled(manager.onFinalize, "rs-loader", {
|
||||
manager.onFinalize.calledWith("rs-loader", {
|
||||
recipeMismatches: [],
|
||||
invalidRecipes: [],
|
||||
invalidBranches: new Map(),
|
||||
invalidFeatures: new Map(),
|
||||
missingLocale: [],
|
||||
missingL10nIds: new Map(),
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: true,
|
||||
}),
|
||||
"Should call .onFinalize with no validation issues"
|
||||
|
|
@ -822,14 +794,11 @@ add_task(async function test_updateRecipes_recipeAppId() {
|
|||
|
||||
Assert.equal(manager.onRecipe.callCount, 0, ".onRecipe was never called");
|
||||
Assert.ok(
|
||||
onFinalizeCalled(manager.onFinalize, "rs-loader", {
|
||||
manager.onFinalize.calledWith("rs-loader", {
|
||||
recipeMismatches: [],
|
||||
invalidRecipes: [],
|
||||
invalidBranches: new Map(),
|
||||
invalidFeatures: new Map(),
|
||||
missingLocale: [],
|
||||
missingL10nIds: new Map(),
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: true,
|
||||
}),
|
||||
"Should call .onFinalize with no validation issues"
|
||||
|
|
@ -901,17 +870,12 @@ add_task(async function test_updateRecipes_featureValidationOptOut() {
|
|||
manager.onRecipe.calledOnceWith(optOutRecipe, "rs-loader"),
|
||||
"should call .onRecipe for the opt-out recipe"
|
||||
);
|
||||
|
||||
ok(
|
||||
manager.onFinalize.calledOnce &&
|
||||
onFinalizeCalled(manager.onFinalize, "rs-loader", {
|
||||
manager.onFinalize.calledOnceWith("rs-loader", {
|
||||
recipeMismatches: [],
|
||||
invalidRecipes: [],
|
||||
invalidBranches: new Map([[invalidRecipe.slug, ["control"]]]),
|
||||
invalidFeatures: new Map(),
|
||||
missingLocale: [],
|
||||
missingL10nIds: new Map(),
|
||||
locale: Services.locale.appLocaleAsBCP47,
|
||||
validationEnabled: true,
|
||||
}),
|
||||
"should call .onFinalize with only one invalid recipe"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,20 +9,19 @@ skip-if =
|
|||
appname == "thunderbird"
|
||||
run-sequentially = very high failure rate in parallel
|
||||
|
||||
[test_ExperimentManager_context.js]
|
||||
[test_ExperimentManager_enroll.js]
|
||||
[test_ExperimentManager_lifecycle.js]
|
||||
[test_ExperimentManager_unenroll.js]
|
||||
[test_ExperimentManager_generateTestIds.js]
|
||||
[test_ExperimentManager_prefs.js]
|
||||
[test_ExperimentStore.js]
|
||||
[test_NimbusTestUtils.js]
|
||||
[test_SharedDataMap.js]
|
||||
[test_ExperimentAPI.js]
|
||||
[test_ExperimentAPI_ExperimentFeature.js]
|
||||
[test_ExperimentAPI_ExperimentFeature_getAllVariables.js]
|
||||
[test_ExperimentAPI_ExperimentFeature_getVariable.js]
|
||||
[test_ExperimentAPI_NimbusFeatures.js]
|
||||
[test_ExperimentManager_context.js]
|
||||
[test_ExperimentManager_enroll.js]
|
||||
[test_ExperimentManager_generateTestIds.js]
|
||||
[test_ExperimentManager_lifecycle.js]
|
||||
[test_ExperimentManager_prefs.js]
|
||||
[test_ExperimentManager_unenroll.js]
|
||||
[test_ExperimentStore.js]
|
||||
[test_NimbusTestUtils.js]
|
||||
[test_RemoteSettingsExperimentLoader.js]
|
||||
[test_RemoteSettingsExperimentLoader_updateRecipes.js]
|
||||
[test_SharedDataMap.js]
|
||||
[test_localization.js]
|
||||
[test_ExperimentAPI_NimbusFeatures.js]
|
||||
|
|
|
|||
|
|
@ -1034,14 +1034,6 @@ normandy:
|
|||
reason: Why validation failed (one of "invalid-recipe", "invalid-branch", or "invalid-reason").
|
||||
branch: If reason == invalid-branch, the branch that failed validation.
|
||||
feature: If reason == invalid-feature, the invalid feature ID.
|
||||
locale: >
|
||||
If reason == missing-locale, the locale that was missing from the
|
||||
localization table.
|
||||
If reason == missing-l10n-entry, the locale that was missing the
|
||||
localization entries.
|
||||
l10n_ids: >
|
||||
If reason == missing-l10n-entry, a comma-separated list of missing
|
||||
localization entries.
|
||||
|
||||
browser.launched_to_handle:
|
||||
system_notification:
|
||||
|
|
|
|||
Loading…
Reference in a new issue