Bug 1547034: Add userFacingName and userFacingDescription to schema r=mythmon

Display these when available instead of generating one.

We play some games here to let SinglePreferenceExperiment continue to
validate according to the PreferenceExperiment schema. This is kind of
ugly. Another approach might be to move the about-studies code that
generates a description. I was hesitant to do this because it would
mean losing the formatting.

Depends on D29873

Differential Revision: https://phabricator.services.mozilla.com/D30969

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Ethan Glasser-Camp 2019-05-16 15:04:25 +00:00
parent 7505d46d34
commit f589fd6d1b
6 changed files with 60 additions and 15 deletions

View File

@ -29,6 +29,12 @@ class SinglePreferenceExperimentAction extends PreferenceExperimentAction {
} = recipe.arguments;
const newArguments = {
// The multiple-preference-experiment schema requires a string
// name/description, which are necessary in the wire format, but
// experiment objects can have null for these fields. Add some
// filler fields here and remove them after validation.
userFacingName: "temp-name",
userFacingDescription: "temp-description",
...remainingArguments,
branches: branches.map(branch => {
const { value, ...branchProps } = branch;
@ -52,6 +58,9 @@ class SinglePreferenceExperimentAction extends PreferenceExperimentAction {
throw new Error(`Transformed arguments do not match schema. Original arguments: ${JSON.stringify(recipe.arguments)}, new arguments: ${JSON.stringify(newArguments)}, schema: ${JSON.stringify(multiprefSchema)}`);
}
validatedArguments.userFacingName = null;
validatedArguments.userFacingDescription = null;
recipe.arguments = validatedArguments;
const newRecipe = {

View File

@ -173,6 +173,8 @@ const ActionSchemas = {
type: "object",
required: [
"slug",
"userFacingName",
"userFacingDescription",
"branches",
],
properties: {
@ -181,6 +183,16 @@ const ActionSchemas = {
type: "string",
pattern: "^[A-Za-z0-9\\-_]+$",
},
userFacingName: {
description: "User-facing name of the experiment",
type: "string",
minLength: 1,
},
userFacingDescription: {
description: "User-facing description of the experiment",
type: "string",
minLength: 1,
},
experimentDocumentUrl: {
description: "URL of a document describing the experiment",
type: "string",

View File

@ -251,20 +251,28 @@ class PreferenceStudyListItem extends React.Component {
render() {
const { study, translations } = this.props;
// Assume there is exactly one preference (old-style preference experiment).
const [preferenceName, { preferenceValue }] = Object.entries(study.preferences)[0];
// Sanitize the values by setting them as the text content of an element,
// and then getting the HTML representation of that text. This will have the
// browser safely sanitize them. Use outerHTML to also include the <code>
// element in the string.
const sanitizer = document.createElement("code");
sanitizer.textContent = preferenceName;
const sanitizedPreferenceName = sanitizer.outerHTML;
sanitizer.textContent = preferenceValue;
const sanitizedPreferenceValue = sanitizer.outerHTML;
const description = translations.preferenceStudyDescription
.replace(/%(?:1\$)?S/, sanitizedPreferenceName)
.replace(/%(?:2\$)?S/, sanitizedPreferenceValue);
let userFacingName = study.userFacingName;
if (!userFacingName) {
userFacingName = study.name.replace(/-?pref-?(flip|study)-?/, "").replace(/-?study-?/, "").slice(0, 1);
}
let description = study.userFacingDescription;
if (!description) {
// Assume there is exactly one preference (old-style preference experiment).
const [preferenceName, { preferenceValue }] = Object.entries(study.preferences)[0];
// Sanitize the values by setting them as the text content of an element,
// and then getting the HTML representation of that text. This will have the
// browser safely sanitize them. Use outerHTML to also include the <code>
// element in the string.
const sanitizer = document.createElement("code");
sanitizer.textContent = preferenceName;
const sanitizedPreferenceName = sanitizer.outerHTML;
sanitizer.textContent = preferenceValue;
const sanitizedPreferenceValue = sanitizer.outerHTML;
description = translations.preferenceStudyDescription
.replace(/%(?:1\$)?S/, sanitizedPreferenceName)
.replace(/%(?:2\$)?S/, sanitizedPreferenceValue);
}
return (
r("li", {
@ -272,7 +280,7 @@ class PreferenceStudyListItem extends React.Component {
"data-study-name": study.name,
},
r("div", { className: "study-icon" },
study.name.replace(/-?pref-?(flip|study)-?/, "").replace(/-?study-?/, "").slice(0, 1)
userFacingName,
),
r("div", { className: "study-details" },
r("div", { className: "study-header" },

View File

@ -22,6 +22,14 @@
* @typedef {Object} Experiment
* @property {string} name
* Unique name of the experiment
* @property {string|null} userFacingName
* A user-friendly name for the experiment. Null on old-style
* single-preference experiments, which do not have a
* userFacingName.
* @property {string|null} userFacingDescription
* A user-friendly description of the experiment. Null on old-style
* single-preference experiments, which do not have a
* userFacingDescription.
* @property {string} branch
* Experiment branch that the user was matched to
* @property {boolean} expired
@ -383,6 +391,8 @@ var PreferenceExperiments = {
branch,
preferences,
experimentType = "exp",
userFacingName = null,
userFacingDescription = null,
}) {
log.debug(`PreferenceExperiments.start(${name}, ${branch})`);
@ -457,6 +467,8 @@ var PreferenceExperiments = {
lastSeen: new Date().toJSON(),
preferences,
experimentType,
userFacingName,
userFacingDescription,
};
store.data.experiments[name] = experiment;

View File

@ -38,6 +38,8 @@ function argumentsFactory(args) {
const branches = defaultBranches.map(branchFactory);
return {
slug: "test",
userFacingName: "Super Cool Test Experiment",
userFacingDescription: "Test experiment from browser_actions_PreferenceExperimentAction.",
isHighPopulation: false,
...args,
branches,

View File

@ -68,6 +68,8 @@ decorate_task(
name: "preference-experiment",
arguments: {
slug: "test",
userFacingName: null,
userFacingDescription: null,
isHighPopulation: false,
branches: [{
slug: "branch1",