Bug 1844165 - Add a script to import messaging rollouts from Nimbus. r=omc-reviewers,emcminn,negin

Differential Revision: https://phabricator.services.mozilla.com/D183916
This commit is contained in:
Shane Hughes 2023-08-10 16:12:11 +00:00
parent aa8eccb5a8
commit 460a0637fd
7 changed files with 612 additions and 65 deletions

View file

@ -0,0 +1,366 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This is a script to import Nimbus experiments from a given collection into
* browser/components/newtab/test/NimbusRolloutMessageProvider.sys.mjs. By
* default, it only imports messaging rollouts. This is done so that the content
* of off-train rollouts can be easily searched. That way, when we are cleaning
* up old assets (such as Fluent strings), we don't accidentally delete strings
* that live rollouts are using because it was too difficult to find whether
* they were in use.
*
* This works by fetching the message records from the Nimbus collection and
* then writing them to the file. The messages are converted from JSON to JS.
* The file is structured like this:
* export const NimbusRolloutMessageProvider = {
* getMessages() {
* return [
* { ...message1 },
* { ...message2 },
* ];
* },
* };
*/
/* eslint-disable max-depth, no-console */
const meow = require("meow");
const chalk = require("chalk");
const https = require("https");
const path = require("path");
const fs = require("fs");
const util = require("util");
const prettier = require("prettier");
const jsonschema = require("../../../../third_party/js/cfworker/json-schema.js");
const DEFAULT_COLLECTION_ID = "nimbus-desktop-experiments";
const BASE_URL =
"https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/";
const EXPERIMENTER_URL = "https://experimenter.services.mozilla.com/nimbus/";
const OUTPUT_PATH = "./test/NimbusRolloutMessageProvider.sys.mjs";
const LICENSE_STRING = `/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */`;
const cli = meow(
`
Usage
$ node bin/import-rollouts.js [options]
Options
-c ID, --collection ID The Nimbus collection ID to import from
default: ${DEFAULT_COLLECTION_ID}
-e, --experiments Import all messaging experiments, not just rollouts
-s, --skip-validation Skip validation of experiments and messages
-h, --help Show this help message
Examples
$ node bin/import-rollouts.js --collection nimbus-preview
$ ./mach npm run import-rollouts --prefix=browser/components/newtab -- -e
`,
{
description: false,
// `pkg` is a tiny optimization. It prevents meow from looking for a package
// that doesn't technically exist. meow searches for a package and changes
// the process name to the package name. It resolves to the newtab
// package.json, which would give a confusing name and be wasteful.
pkg: {
name: "import-rollouts",
version: "1.0.0",
},
flags: {
collection: {
type: "string",
alias: "c",
default: DEFAULT_COLLECTION_ID,
},
experiments: {
type: "boolean",
alias: "e",
default: false,
},
skipValidation: {
type: "boolean",
alias: "s",
default: false,
},
},
}
);
const RECORDS_URL = `${BASE_URL}${cli.flags.collection}/records`;
function fetchJSON(url) {
return new Promise((resolve, reject) => {
https
.get(url, resp => {
let data = "";
resp.on("data", chunk => {
data += chunk;
});
resp.on("end", () => resolve(JSON.parse(data)));
})
.on("error", reject);
});
}
function isMessageValid(validator, obj) {
if (validator) {
const result = validator.validate(obj);
return result.valid && result.errors.length === 0;
}
return true;
}
async function getMessageValidators(skipValidation) {
if (skipValidation) {
return { experimentValidator: null, messageValidators: {} };
}
async function getSchema(filePath) {
const file = await util.promisify(fs.readFile)(filePath, "utf8");
return JSON.parse(file);
}
async function getValidator(filePath, { common = false } = {}) {
const schema = await getSchema(filePath);
const validator = new jsonschema.Validator(schema);
if (common) {
const commonSchema = await getSchema(
"./content-src/asrouter/schemas/FxMSCommon.schema.json"
);
validator.addSchema(commonSchema);
}
return validator;
}
const experimentValidator = await getValidator(
"./content-src/asrouter/schemas/MessagingExperiment.schema.json"
);
const messageValidators = {
cfr_doorhanger: await getValidator(
"./content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json",
{ common: true }
),
cfr_urlbar_chiclet: await getValidator(
"./content-src/asrouter/templates/CFR/templates/CFRUrlbarChiclet.schema.json",
{ common: true }
),
infobar: await getValidator(
"./content-src/asrouter/templates/CFR/templates/InfoBar.schema.json",
{ common: true }
),
pb_newtab: await getValidator(
"./content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json",
{ common: true }
),
protections_panel: await getValidator(
"./content-src/asrouter/templates/OnboardingMessage/ProtectionsPanelMessage.schema.json",
{ common: true }
),
spotlight: await getValidator(
"./content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json",
{ common: true }
),
toast_notification: await getValidator(
"./content-src/asrouter/templates/ToastNotification/ToastNotification.schema.json",
{ common: true }
),
toolbar_badge: await getValidator(
"./content-src/asrouter/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json",
{ common: true }
),
update_action: await getValidator(
"./content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json",
{ common: true }
),
whatsnew_panel_message: await getValidator(
"./content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json",
{ common: true }
),
feature_callout: await getValidator(
// For now, Feature Callout and Spotlight share a common schema
"./content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json",
{ common: true }
),
};
messageValidators.milestone_message = messageValidators.cfr_doorhanger;
return { experimentValidator, messageValidators };
}
function annotateMessage({ message, slug, minVersion, maxVersion, url }) {
const comments = [];
if (slug) {
comments.push(`// Nimbus slug: ${slug}`);
}
let versionRange = "";
if (minVersion) {
versionRange = minVersion;
if (maxVersion) {
versionRange += `-${maxVersion}`;
} else {
versionRange += "+";
}
} else if (maxVersion) {
versionRange = `0-${maxVersion}`;
}
if (versionRange) {
comments.push(`// Version range: ${versionRange}`);
}
if (url) {
comments.push(`// Recipe: ${url}`);
}
return JSON.stringify(message, null, 2).replace(
/^{/,
`{ ${comments.join("\n")}`
);
}
async function format(content) {
const config = await prettier.resolveConfig("./.prettierrc.js");
return prettier.format(content, { ...config, filepath: OUTPUT_PATH });
}
async function main() {
const { MESSAGING_EXPERIMENTS_DEFAULT_FEATURES } = await import(
"../lib/MessagingExperimentConstants.sys.mjs"
);
console.log(`Fetching records from ${chalk.underline.yellow(RECORDS_URL)}`);
const { data: records } = await fetchJSON(RECORDS_URL);
if (!Array.isArray(records)) {
throw new TypeError(
`Expected records to be an array, got ${typeof records}`
);
}
const recipes = records.filter(
record =>
record.application === "firefox-desktop" &&
record.featureIds.some(id =>
MESSAGING_EXPERIMENTS_DEFAULT_FEATURES.includes(id)
) &&
(record.isRollout || cli.flags.experiments)
);
const importItems = [];
const { experimentValidator, messageValidators } = await getMessageValidators(
cli.flags.skipValidation
);
for (const recipe of recipes) {
const { slug: experimentSlug, branches, targeting } = recipe;
if (!(experimentSlug && Array.isArray(branches) && branches.length)) {
continue;
}
console.log(
`Processing ${recipe.isRollout ? "rollout" : "experiment"}: ${chalk.blue(
experimentSlug
)}${
branches.length > 1
? ` with ${chalk.underline(`${String(branches.length)} branches`)}`
: ""
}`
);
const recipeUrl = `${EXPERIMENTER_URL}${experimentSlug}/summary`;
const [, minVersion] =
targeting?.match(/\(version\|versionCompare\(\'([0-9]+)\.!\'\) >= 0/) ||
[];
const [, maxVersion] =
targeting?.match(/\(version\|versionCompare\(\'([0-9]+)\.\*\'\) <= 0/) ||
[];
let branchIndex = branches.length > 1 ? 1 : 0;
for (const branch of branches) {
const { slug: branchSlug, features } = branch;
console.log(
` Processing branch${
branchIndex > 0 ? ` ${branchIndex} of ${branches.length}` : ""
}: ${chalk.blue(branchSlug)}`
);
branchIndex += 1;
const url = `${recipeUrl}#${branchSlug}`;
if (!Array.isArray(features)) {
continue;
}
for (const feature of features) {
if (
feature.enabled &&
MESSAGING_EXPERIMENTS_DEFAULT_FEATURES.includes(feature.featureId) &&
feature.value &&
typeof feature.value === "object" &&
feature.value.template
) {
if (!isMessageValid(experimentValidator, feature.value)) {
console.log(
` ${chalk.red(
"✗"
)} Skipping invalid value for branch: ${chalk.blue(branchSlug)}`
);
continue;
}
const messages = (
feature.value.template === "multi" &&
Array.isArray(feature.value.messages)
? feature.value.messages
: [feature.value]
).filter(m => m && m.id);
let msgIndex = messages.length > 1 ? 1 : 0;
for (const message of messages) {
let messageLogString = `message${
msgIndex > 0 ? ` ${msgIndex} of ${messages.length}` : ""
}: ${chalk.italic.green(message.id)}`;
if (!isMessageValid(messageValidators[message.template], message)) {
console.log(
` ${chalk.red("✗")} Skipping invalid ${messageLogString}`
);
continue;
}
console.log(` Importing ${messageLogString}`);
let slug = `${experimentSlug}:${branchSlug}`;
if (msgIndex > 0) {
slug += ` (message ${msgIndex} of ${messages.length})`;
}
msgIndex += 1;
importItems.push({ message, slug, minVersion, maxVersion, url });
}
}
}
}
}
const content = `${LICENSE_STRING}
/**
* This file is generated by browser/components/newtab/bin/import-rollouts.js
* Run the following from the repository root to regenerate it:
* ./mach npm run import-rollouts --prefix=browser/components/newtab
*/
export const NimbusRolloutMessageProvider = {
getMessages() {
return [${importItems.map(annotateMessage).join(",\n")}];
},
};
`;
const formattedContent = await format(content);
await util.promisify(fs.writeFile)(OUTPUT_PATH, formattedContent);
console.log(
`${chalk.green("✓")} Wrote ${chalk.underline.green(
`${String(importItems.length)} ${
importItems.length === 1 ? "message" : "messages"
}`
)} to ${chalk.underline.yellow(path.resolve(OUTPUT_PATH))}`
);
}
main();

View file

@ -52,7 +52,9 @@ XPCOMUtils.defineLazyServiceGetters(lazy, {
const { actionCreators: ac } = ChromeUtils.importESModule(
"resource://activity-stream/common/Actions.sys.mjs"
);
const { MESSAGING_EXPERIMENTS_DEFAULT_FEATURES } = ChromeUtils.importESModule(
"resource://activity-stream/lib/MessagingExperimentConstants.sys.mjs"
);
const { CFRMessageProvider } = ChromeUtils.importESModule(
"resource://activity-stream/lib/CFRMessageProvider.sys.mjs"
);
@ -105,26 +107,6 @@ const TOPIC_EXPERIMENT_ENROLLMENT_CHANGED = "nimbus:enrollments-updated";
const USE_REMOTE_L10N_PREF =
"browser.newtabpage.activity-stream.asrouter.useRemoteL10n";
const MESSAGING_EXPERIMENTS_DEFAULT_FEATURES = [
"cfr",
"fxms-message-1",
"fxms-message-2",
"fxms-message-3",
"fxms-message-4",
"fxms-message-5",
"fxms-message-6",
"fxms-message-7",
"fxms-message-8",
"fxms-message-9",
"fxms-message-10",
"fxms-message-11",
"infobar",
"moments-page",
"pbNewtab",
"spotlight",
"featureCallout",
];
// Experiment groups that need to report the reach event in Messaging-Experiments.
// If you're adding new groups to it, make sure they're also added in the
// `messaging_experiments.reach.objects` defined in "toolkit/components/telemetry/Events.yaml"

View file

@ -0,0 +1,37 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This file is used to define constants related to messaging experiments. It is
* imported by both ASRouter as well as import-rollouts.js, a node script that
* imports Nimbus rollouts into tree. It doesn't have access to any Firefox
* APIs, XPCOM, etc. and should be kept that way.
*/
/**
* These are the Nimbus feature IDs that correspond to messaging experiments.
* Other Nimbus features contain specific variables whose keys are enumerated in
* FeatureManifest.yaml. Conversely, messaging experiment features contain
* actual messages, with the usual message keys like `template` and `targeting`.
* @see FeatureManifest.yaml
*/
export const MESSAGING_EXPERIMENTS_DEFAULT_FEATURES = [
"cfr",
"fxms-message-1",
"fxms-message-2",
"fxms-message-3",
"fxms-message-4",
"fxms-message-5",
"fxms-message-6",
"fxms-message-7",
"fxms-message-8",
"fxms-message-9",
"fxms-message-10",
"fxms-message-11",
"infobar",
"moments-page",
"pbNewtab",
"spotlight",
"featureCallout",
];

View file

@ -113,6 +113,7 @@
"fix": "npm-run-all fix:*",
"fix:eslint": "npm run lint:eslint -- --fix",
"fix:stylelint": "npm run lint:stylelint -- --fix",
"import-rollouts": "node ./bin/import-rollouts.js",
"help": "yamscripts help",
"yamscripts": "yamscripts compile",
"__": "# NOTE: THESE SCRIPTS ARE COMPILED!!! EDIT yamscripts.yml instead!!!"

View file

@ -1,76 +1,234 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This file is generated by browser/components/newtab/bin/import-rollouts.js
* Run the following from the repository root to regenerate it:
* ./mach npm run import-rollouts --prefix=browser/components/newtab
*/
export const NimbusRolloutMessageProvider = {
getMessages() {
return [
{
// Nimbus slug: device-migration-existing-users-sumo-switch-device-cfr-rollout:control
// Version range: 114+
// Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-existing-users-sumo-switch-device-cfr-rollout/summary#control
id: "CFR_WINDOWS_DEVICE_MIGRATION_SUMO_SWITCH_DEVICE",
groups: ["cfr"],
content: {
backdrop: "transparent",
id: "test-message-import-infreq-make-self-at-home:treatment-c",
text: {
string_id: "fxa-sync-cfr-body",
},
layout: "icon_and_message",
buttons: {
primary: {
label: {
string_id: "fxa-sync-cfr-primary",
},
action: {
data: {
args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=panel-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=migrate-learn-more",
where: "tabshifted",
},
type: "OPEN_URL",
},
},
secondary: [
{
label: {
string_id: "fxa-sync-cfr-secondary",
},
action: {
type: "CANCEL",
},
},
],
},
anchor_id: "PanelUI-menu-button",
bucket_id: "CFR_WINDOWS_DEVICE_MIGRATION_SUMO_SWITCH_DEVICE",
heading_text: {
string_id: "fxa-sync-cfr-header",
},
skip_address_bar_notifier: true,
},
trigger: {
id: "defaultBrowserCheck",
},
template: "cfr_doorhanger",
frequency: {
custom: [
{
cap: 1,
period: 604800000,
},
],
lifetime: 3,
},
targeting: "isFxASignedIn && !usesFirefoxSync && source == 'newtab'",
},
{
// Nimbus slug: device-migration-existing-user-messaging-tour-spotlight-rollout:control
// Version range: 114+
// Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-existing-user-messaging-tour-spotlight-rollout/summary#control
id: "WINDOWS_DEVICE_MIGRATION_SUMO_SWITCH_DEVICE",
groups: ["eco"],
content: {
id: "WINDOWS_DEVICE_MIGRATION_SUMO_SWITCH_DEVICE",
modal: "tab",
screens: [
{
id: "WINDOWS_DEVICE_MIGRATION_SUMO_SWITCH_DEVICE:CONTROL_ROLLOUT",
content: {
logo: {
height: "211px",
imageURL:
"https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/f0f51715-7f5e-48de-839a-f26cc76cbb8b.svg",
},
title: {
fontSize: "24px",
string_id: "device-migration-fxa-spotlight-header",
paddingBlock: "20px 0",
letterSpacing: 0,
paddingInline: "82px",
},
subtitle: {
fontSize: "15px",
string_id: "device-migration-fxa-spotlight-body",
lineHeight: "1.4",
marginBlock: "8px 20px",
letterSpacing: 0,
paddingInline: "30px",
},
dismiss_button: {
action: {
data: {
id: "WINDOWS_DEVICE_MIGRATION_SUMO_SWITCH_DEVICE",
},
type: "BLOCK_MESSAGE",
navigate: true,
},
},
primary_button: {
label: {
string_id: "device-migration-fxa-spotlight-primary-button",
paddingBlock: "0",
},
action: {
data: {
args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=how-to-backup-data",
where: "tabshifted",
},
type: "OPEN_URL",
navigate: true,
},
},
secondary_button: {
label: {
string_id: "device-migration-fxa-spotlight-link",
marginBlock: "1px -20px",
},
action: {
navigate: true,
},
},
},
},
],
backdrop: "transparent",
template: "multistage",
transitions: true,
},
trigger: {
id: "defaultBrowserCheck",
},
template: "spotlight",
frequency: {
custom: [
{
cap: 1,
period: 4838400000,
},
],
lifetime: 3,
},
targeting:
"!isFxASignedIn && !willShowDefaultPrompt && !isMajorUpgrade && !activeNotifications",
},
{
// Nimbus slug: updated-import-infrequent-rollout-make-yourself-at-home-copy:control
// Version range: 107+
// Recipe: https://experimenter.services.mozilla.com/nimbus/updated-import-infrequent-rollout-make-yourself-at-home-copy/summary#control
id: "import-infreq-make-self-at-home:treatment-c",
groups: ["import-spotlights"],
content: {
id: "import-infreq-make-self-at-home:treatment-c",
screens: [
{
id: "IMPORT",
content: {
logo: {
height: "186px",
imageURL:
"chrome://activity-stream/content/data/content/assets/person-typing.svg",
},
primary_button: {
action: {
navigate: true,
type: "SHOW_MIGRATION_WIZARD",
},
label: {
paddingBlock: "6px",
paddingInline: "14px",
string_id: "onboarding-infrequent-import-primary-button",
},
},
secondary_button: {
action: {
navigate: true,
},
label: {
fontSize: "13px",
lineHeight: "15px",
marginBlock: "-4px -28px",
string_id: "onboarding-not-now-button-label",
},
title: {
fontSize: "26px",
string_id: "onboarding-infrequent-import-title",
fontWeight: "400",
lineHeight: "36px",
marginBlock: "6px 0",
letterSpacing: "-.01em",
},
subtitle: {
fontSize: "13px",
letterSpacing: ".05px",
string_id: "onboarding-infrequent-import-subtitle",
lineHeight: "16px",
marginBlock: "4px 12px",
letterSpacing: ".05px",
paddingInline: "48px",
string_id: "onboarding-infrequent-import-subtitle",
},
title: {
fontSize: "26px",
fontWeight: "400",
letterSpacing: "-.01em",
lineHeight: "36px",
marginBlock: "6px 0",
string_id: "onboarding-infrequent-import-title",
},
title_style: "slim",
primary_button: {
label: {
string_id: "onboarding-infrequent-import-primary-button",
paddingBlock: "6px",
paddingInline: "14px",
},
action: {
type: "SHOW_MIGRATION_WIZARD",
navigate: true,
},
},
secondary_button: {
label: {
fontSize: "13px",
string_id: "onboarding-not-now-button-label",
lineHeight: "15px",
marginBlock: "-4px -28px",
},
action: {
navigate: true,
},
},
},
id: "IMPORT",
},
],
backdrop: "transparent",
template: "multistage",
transitions: true,
},
frequency: {
lifetime: 1,
},
groups: ["import-spotlights"],
id: "test-message-import-infreq-make-self-at-home:treatment-c",
priority: 1,
targeting:
"!willShowDefaultPrompt && !('browser.migrate.content-modal.enabled'|preferenceValue) && source == 'startup' && !isMajorUpgrade && !activeNotifications && totalBookmarksCount == 5",
template: "spotlight",
trigger: {
id: "defaultBrowserCheck",
},
priority: 1,
template: "spotlight",
frequency: {
lifetime: 1,
},
targeting:
"!willShowDefaultPrompt && !('browser.migrate.content-modal.enabled'|preferenceValue) && source == 'startup' && !isMajorUpgrade && !activeNotifications && totalBookmarksCount == 5",
},
];
},

View file

@ -62,3 +62,6 @@ scripts:
# running fix:eslint will also reformat changed JS files using prettier.
eslint: =>lint:eslint -- --fix
stylelint: =>lint:stylelint -- --fix
# script to import Nimbus rollouts into NimbusRolloutMessageProvider.sys.mjs
import-rollouts: node ./bin/import-rollouts.js

View file

@ -912,7 +912,7 @@ featureCallout:
# 1) clone an existing one here
# 2) update the YAML feature id to the next unused number
# 3) update the YAML description
# 4) add the new feature id to MESSAGING_EXPERIMENTS_DEFAULT_FEATURES list in ASRouter.jsm
# 4) add the new feature id to MESSAGING_EXPERIMENTS_DEFAULT_FEATURES list in MessagingExperimentConstants.sys.mjs
# 5) add the new feature id and the version it landed to the spreadsheet tab linked to above
fxms-message-1: