Bug 1547062 - Experiment branch generation for Trailhead (#4980)
(cherry picked from commit be9b3bacc1
)
This commit is contained in:
Родитель
633596d511
Коммит
83131b740a
|
@ -28,7 +28,8 @@ Please note that some targeting attributes require stricter controls on the tele
|
|||
* [sync](#sync)
|
||||
* [topFrecentSites](#topfrecentsites)
|
||||
* [totalBookmarksCount](#totalbookmarkscount)
|
||||
* [trailheadCohort](#trailheadcohort)
|
||||
* [trailheadInterrupt](#trailheadinterrupt)
|
||||
* [trailheadTriplet](#trailheadtriplet)
|
||||
* [usesFirefoxSync](#usesfirefoxsync)
|
||||
* [xpinstallEnabled](#xpinstallEnabled)
|
||||
* [hasPinnedTabs](#haspinnedtabs)
|
||||
|
@ -425,10 +426,13 @@ Total number of bookmarks.
|
|||
declare const totalBookmarksCount: number;
|
||||
```
|
||||
|
||||
### `trailheadCohort`
|
||||
### `trailheadInterrupt`
|
||||
|
||||
(67+ only) Experiment cohort for special trailhead project
|
||||
(67.05+ only) Experiment branch for "interrupt" study
|
||||
|
||||
### `trailheadTriplet`
|
||||
|
||||
(67.05+ only) Experiment branch for "triplet" study
|
||||
|
||||
### `usesFirefoxSync`
|
||||
|
||||
|
|
|
@ -699,6 +699,16 @@ export class ASRouterAdminInner extends React.PureComponent {
|
|||
return <p>No errors</p>;
|
||||
}
|
||||
|
||||
renderTrailheadInfo() {
|
||||
const {trailheadInterrupt, trailheadTriplet, trailheadInitialized} = this.state;
|
||||
return trailheadInitialized ? (<table className="minimal-table">
|
||||
<tbody>
|
||||
<tr><td>Interrupt branch</td><td>{trailheadInterrupt}</td></tr>
|
||||
<tr><td>Triplet branch</td><td>{trailheadTriplet}</td></tr>
|
||||
</tbody>
|
||||
</table>) : <p>Trailhead is not initialized. To update these values, load about:welcome.</p>;
|
||||
}
|
||||
|
||||
getSection() {
|
||||
const [section] = this.props.location.routes;
|
||||
switch (section) {
|
||||
|
@ -729,6 +739,8 @@ export class ASRouterAdminInner extends React.PureComponent {
|
|||
return (<React.Fragment>
|
||||
<h2>Message Providers <button title="Restore all provider settings that ship with Firefox" className="button" onClick={this.resetPref}>Restore default prefs</button></h2>
|
||||
{this.state.providers ? this.renderProviders() : null}
|
||||
<h2>Trailhead</h2>
|
||||
{this.renderTrailheadInfo()}
|
||||
<h2>Messages</h2>
|
||||
{this.renderMessageFilter()}
|
||||
{this.renderMessages()}
|
||||
|
|
|
@ -82,6 +82,24 @@
|
|||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
||||
&.minimal-table {
|
||||
border-collapse: collapse;
|
||||
|
||||
td {
|
||||
padding: 8px;
|
||||
border: 1px solid $border-color;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td:not(:first-child) {
|
||||
font-family: $monospace;
|
||||
}
|
||||
}
|
||||
|
||||
&.errorReporting {
|
||||
tr {
|
||||
border: 1px solid var(--newtab-textbox-background-color);
|
||||
|
|
136
lib/ASRouter.jsm
136
lib/ASRouter.jsm
|
@ -19,6 +19,7 @@ const {OnboardingMessageProvider} = ChromeUtils.import("resource://activity-stre
|
|||
const {SnippetsTestMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/SnippetsTestMessageProvider.jsm");
|
||||
const {RemoteSettings} = ChromeUtils.import("resource://services-settings/remote-settings.js");
|
||||
const {CFRPageActions} = ChromeUtils.import("resource://activity-stream/lib/CFRPageActions.jsm");
|
||||
const {AttributionCode} = ChromeUtils.import("resource:///modules/AttributionCode.jsm");
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "ASRouterPreferences",
|
||||
"resource://activity-stream/lib/ASRouterPreferences.jsm");
|
||||
|
@ -28,7 +29,38 @@ ChromeUtils.defineModuleGetter(this, "QueryCache",
|
|||
"resource://activity-stream/lib/ASRouterTargeting.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "ASRouterTriggerListeners",
|
||||
"resource://activity-stream/lib/ASRouterTriggerListeners.jsm");
|
||||
const {AttributionCode} = ChromeUtils.import("resource:///modules/AttributionCode.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "TelemetryEnvironment",
|
||||
"resource://gre/modules/TelemetryEnvironment.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "ClientEnvironment",
|
||||
"resource://normandy/lib/ClientEnvironment.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "Sampling",
|
||||
"resource://gre/modules/components-utils/Sampling.jsm");
|
||||
|
||||
const TRAILHEAD_CONFIG = {
|
||||
OVERRIDE_PREF: "trailhead.firstrun.branches",
|
||||
DID_SEE_ABOUT_WELCOME_PREF: "trailhead.firstrun.didSeeAboutWelcome",
|
||||
BRANCHES: {
|
||||
interrupts: [
|
||||
["control"],
|
||||
["join"],
|
||||
["sync"],
|
||||
["nofirstrun"],
|
||||
["cards"],
|
||||
],
|
||||
triplets: [
|
||||
["supercharge"],
|
||||
["payoff"],
|
||||
["multidevice"],
|
||||
["privacy"],
|
||||
],
|
||||
},
|
||||
LOCALES: ["en-US", "en-GB", "en-CA", "de", "de-DE", "fr", "fr-FR"],
|
||||
EXPERIMENT_RATIOS: [
|
||||
["", 1],
|
||||
["interrupts", 1],
|
||||
["triplets", 1],
|
||||
],
|
||||
};
|
||||
|
||||
const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
|
||||
const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
|
||||
|
@ -46,6 +78,17 @@ const MAX_MESSAGE_LIFETIME_CAP = 100;
|
|||
const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider, CFRMessageProvider, SnippetsTestMessageProvider};
|
||||
const STARTPAGE_VERSION = "6";
|
||||
|
||||
/**
|
||||
* chooseBranch<T> - Choose an item from a list of "branches" pseudorandomly using a seed / ratio configuration
|
||||
* @param seed {string} A unique seed for the randomizer
|
||||
* @param branches {Array<[T, number?]>} A list of branches, where branch[0] is any item and branch[1] is the ratio
|
||||
* @returns {T} An randomly chosen item in a branch
|
||||
*/
|
||||
async function chooseBranch(seed, branches) {
|
||||
const ratios = branches.map(([item, ratio]) => ((typeof ratio !== "undefined") ? ratio : 1));
|
||||
return branches[await Sampling.ratioSample(seed, ratios)][0];
|
||||
}
|
||||
|
||||
const MessageLoaderUtils = {
|
||||
STARTPAGE_VERSION,
|
||||
REMOTE_LOADER_CACHE_KEY: "RemoteLoaderCache",
|
||||
|
@ -343,6 +386,9 @@ class _ASRouter {
|
|||
providerBlockList: [],
|
||||
messageImpressions: {},
|
||||
providerImpressions: {},
|
||||
trailheadInitialized: false,
|
||||
trailheadInterrupt: "",
|
||||
trailheadTriplet: "",
|
||||
messages: [],
|
||||
errors: [],
|
||||
};
|
||||
|
@ -520,6 +566,9 @@ class _ASRouter {
|
|||
ASRouterPreferences.init();
|
||||
ASRouterPreferences.addListener(this.onPrefChange);
|
||||
|
||||
// We need to check whether to set up telemetry for trailhead
|
||||
await this.setupTrailhead();
|
||||
|
||||
const messageBlockList = await this._storage.get("messageBlockList") || [];
|
||||
const providerBlockList = await this._storage.get("providerBlockList") || [];
|
||||
const messageImpressions = await this._storage.get("messageImpressions") || {};
|
||||
|
@ -618,13 +667,84 @@ class _ASRouter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* _generateTrailheadBranches - Generates and returns Trailhead configuration and chooses an experiment
|
||||
* based on clientID and locale.
|
||||
* @returns {{experiment: string, interrupt: string, triplet: string}}
|
||||
*/
|
||||
async _generateTrailheadBranches() {
|
||||
let experiment = "";
|
||||
let interrupt;
|
||||
let triplet;
|
||||
|
||||
// If a value is set in TRAILHEAD_OVERRIDE_PREF, it will be returned and no experiment will be set.
|
||||
const overrideValue = Services.prefs.getStringPref(TRAILHEAD_CONFIG.OVERRIDE_PREF, "");
|
||||
if (overrideValue) {
|
||||
[interrupt, triplet] = overrideValue.split("-");
|
||||
return {experiment, interrupt, triplet: triplet || ""};
|
||||
}
|
||||
|
||||
const locale = Services.locale.appLocaleAsLangTag;
|
||||
|
||||
if (TRAILHEAD_CONFIG.LOCALES.includes(locale)) {
|
||||
const {userId} = ClientEnvironment;
|
||||
experiment = await chooseBranch(`${userId}-trailhead-experiments`, TRAILHEAD_CONFIG.EXPERIMENT_RATIOS);
|
||||
|
||||
// For the interrupts experiment,
|
||||
// we randomly assign an interrupt and always use the "supercharge" triplet.
|
||||
if (experiment === "interrupts") {
|
||||
interrupt = await chooseBranch(`${userId}-interrupts-branch`, TRAILHEAD_CONFIG.BRANCHES.interrupts);
|
||||
if (["join", "sync", "cards"].includes(interrupt)) {
|
||||
triplet = "supercharge";
|
||||
}
|
||||
|
||||
// For the triplets experiment or non-experiment experience,
|
||||
// we randomly assign a triplet and always use the "join" interrupt.
|
||||
} else {
|
||||
interrupt = "join";
|
||||
triplet = await chooseBranch(`${userId}-triplets-branch`, TRAILHEAD_CONFIG.BRANCHES.triplets);
|
||||
}
|
||||
} else {
|
||||
// If the user is not in a trailhead-compabtible locale, return the control experience and no experiment.
|
||||
interrupt = "control";
|
||||
}
|
||||
|
||||
return {experiment, interrupt, triplet};
|
||||
}
|
||||
|
||||
async setupTrailhead() {
|
||||
// Don't initialize
|
||||
if (this.state.trailheadInitialized || !Services.prefs.getBoolPref(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {experiment, interrupt, triplet} = await this._generateTrailheadBranches();
|
||||
await this.setState({trailheadInitialized: true, trailheadInterrupt: interrupt, trailheadTriplet: triplet});
|
||||
|
||||
if (experiment) {
|
||||
TelemetryEnvironment.setExperimentActive(
|
||||
// In order for ping centre to pick this up, it MUST start with activity-stream
|
||||
`activity-stream-firstrun-trailhead-${experiment}`,
|
||||
experiment === "interrupts" ? interrupt : triplet,
|
||||
{type: "as-firstrun"}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Return an object containing targeting parameters used to select messages
|
||||
_getMessagesContext() {
|
||||
const {previousSessionEnd} = this.state;
|
||||
const {previousSessionEnd, trailheadInterrupt, trailheadTriplet} = this.state;
|
||||
|
||||
return {
|
||||
get previousSessionEnd() {
|
||||
return previousSessionEnd;
|
||||
},
|
||||
get trailheadInterrupt() {
|
||||
return trailheadInterrupt;
|
||||
},
|
||||
get trailheadTriplet() {
|
||||
return trailheadTriplet;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1146,6 +1266,7 @@ class _ASRouter {
|
|||
this.onMessage({data: action, target});
|
||||
}
|
||||
|
||||
/* eslint-disable complexity */
|
||||
async onMessage({data: action, target}) {
|
||||
switch (action.type) {
|
||||
case "USER_ACTION":
|
||||
|
@ -1160,6 +1281,13 @@ class _ASRouter {
|
|||
if (action.data && action.data.endpoint) {
|
||||
await this._addPreviewEndpoint(action.data.endpoint.url, target.portID);
|
||||
}
|
||||
|
||||
// Special experiment intialization for trailhead
|
||||
if (action.data && action.data.trigger && action.data.trigger.id === "firstRun") {
|
||||
Services.prefs.setBoolPref(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF, true);
|
||||
await this.setupTrailhead();
|
||||
}
|
||||
|
||||
// Check if any updates are needed first
|
||||
await this.loadMessagesFromAllProviders();
|
||||
await this.sendNextMessage(target, (action.data && action.data.trigger) || {});
|
||||
|
@ -1258,6 +1386,8 @@ class _ASRouter {
|
|||
}
|
||||
}
|
||||
this._ASRouter = _ASRouter;
|
||||
this.chooseBranch = chooseBranch;
|
||||
this.TRAILHEAD_CONFIG = TRAILHEAD_CONFIG;
|
||||
|
||||
/**
|
||||
* ASRouter - singleton instance of _ASRouter that controls all messages
|
||||
|
@ -1265,4 +1395,4 @@ this._ASRouter = _ASRouter;
|
|||
*/
|
||||
this.ASRouter = new _ASRouter();
|
||||
|
||||
const EXPORTED_SYMBOLS = ["_ASRouter", "ASRouter", "MessageLoaderUtils"];
|
||||
const EXPORTED_SYMBOLS = ["_ASRouter", "ASRouter", "MessageLoaderUtils", "chooseBranch", "TRAILHEAD_CONFIG"];
|
||||
|
|
|
@ -172,9 +172,6 @@ function sortMessagesByTargeting(messages) {
|
|||
}
|
||||
|
||||
const TargetingGetters = {
|
||||
get trailheadCohort() {
|
||||
return Services.prefs.getIntPref("trailhead.firstrun.cohort", 0);
|
||||
},
|
||||
get locale() {
|
||||
return Services.locale.appLocaleAsLangTag;
|
||||
},
|
||||
|
|
|
@ -99,7 +99,7 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort == 0 && attributionData.campaign != 'non-fx-button' && attributionData.source != 'addons.mozilla.org'",
|
||||
targeting: "trailheadInterrupt == 'control' && attributionData.campaign != 'non-fx-button' && attributionData.source != 'addons.mozilla.org'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
|
@ -119,7 +119,7 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort == 0 && providerCohorts.onboarding == 'ghostery'",
|
||||
targeting: "trailheadInterrupt == 'control' && providerCohorts.onboarding == 'ghostery'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
|
@ -139,13 +139,13 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort == 0 && attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
|
||||
targeting: "trailheadInterrupt == 'control' && attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_1",
|
||||
template: "trailhead",
|
||||
targeting: "trailheadCohort == 1",
|
||||
targeting: "trailheadInterrupt == 'join'",
|
||||
trigger: {id: "firstRun"},
|
||||
includeBundle: {length: 3, template: "onboarding", trigger: {id: "showOnboarding"}},
|
||||
content: {
|
||||
|
@ -174,7 +174,7 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
{
|
||||
id: "TRAILHEAD_2",
|
||||
template: "trailhead",
|
||||
targeting: "trailheadCohort == 2",
|
||||
targeting: "trailheadInterrupt == 'sync'",
|
||||
trigger: {id: "firstRun"},
|
||||
includeBundle: {length: 3, template: "onboarding", trigger: {id: "showOnboarding"}},
|
||||
content: {
|
||||
|
@ -198,20 +198,21 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
{
|
||||
id: "TRAILHEAD_3",
|
||||
template: "trailhead",
|
||||
targeting: "trailheadCohort == 3",
|
||||
targeting: "trailheadInterrupt == 'cards'",
|
||||
trigger: {id: "firstRun"},
|
||||
includeBundle: {length: 3, template: "onboarding", trigger: {id: "showOnboarding"}},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_4",
|
||||
template: "trailhead",
|
||||
targeting: "trailheadCohort == 4",
|
||||
targeting: "trailheadInterrupt == 'nofirstrun'",
|
||||
trigger: {id: "firstRun"},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_CARD_1",
|
||||
template: "onboarding",
|
||||
bundled: 3,
|
||||
order: 2,
|
||||
content: {
|
||||
title: {string_id: "onboarding-tracking-protection-title"},
|
||||
text: {string_id: "onboarding-tracking-protection-text"},
|
||||
|
@ -224,13 +225,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort > 0",
|
||||
targeting: "trailheadTriplet == 'privacy'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_CARD_2",
|
||||
template: "onboarding",
|
||||
bundled: 3,
|
||||
order: 2,
|
||||
content: {
|
||||
title: {string_id: "onboarding-data-sync-title"},
|
||||
text: {string_id: "onboarding-data-sync-text"},
|
||||
|
@ -243,13 +245,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort > 0",
|
||||
targeting: "trailheadTriplet == 'supercharge'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_CARD_3",
|
||||
template: "onboarding",
|
||||
bundled: 3,
|
||||
order: 3,
|
||||
content: {
|
||||
title: {string_id: "onboarding-firefox-monitor-title"},
|
||||
text: {string_id: "onboarding-firefox-monitor-text"},
|
||||
|
@ -262,13 +265,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort > 0",
|
||||
targeting: "trailheadTriplet in ['payoff', 'supercharge']",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_CARD_4",
|
||||
template: "onboarding",
|
||||
bundled: 3,
|
||||
order: 1,
|
||||
content: {
|
||||
title: {string_id: "onboarding-private-browsing-title"},
|
||||
text: {string_id: "onboarding-private-browsing-text"},
|
||||
|
@ -278,13 +282,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
action: {type: "OPEN_PRIVATE_BROWSER_WINDOW"},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort > 0",
|
||||
targeting: "trailheadTriplet == 'privacy'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_CARD_5",
|
||||
template: "onboarding",
|
||||
bundled: 3,
|
||||
order: 5,
|
||||
content: {
|
||||
title: {string_id: "onboarding-firefox-send-title"},
|
||||
text: {string_id: "onboarding-firefox-send-text"},
|
||||
|
@ -297,13 +302,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort > 0",
|
||||
targeting: "trailheadTriplet == 'payoff'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_CARD_6",
|
||||
template: "onboarding",
|
||||
bundled: 3,
|
||||
order: 1,
|
||||
content: {
|
||||
title: {string_id: "onboarding-mobile-phone-title"},
|
||||
text: {string_id: "onboarding-mobile-phone-text"},
|
||||
|
@ -316,7 +322,7 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort > 0",
|
||||
targeting: "trailheadTriplet in ['supercharge', 'multidevice']",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
|
@ -335,13 +341,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort > 0",
|
||||
targeting: "trailheadTriplet == 'unused'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_CARD_8",
|
||||
template: "onboarding",
|
||||
bundled: 3,
|
||||
order: 3,
|
||||
content: {
|
||||
title: {string_id: "onboarding-send-tabs-title"},
|
||||
text: {string_id: "onboarding-send-tabs-text"},
|
||||
|
@ -354,13 +361,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort > 0",
|
||||
targeting: "trailheadTriplet == 'multidevice'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_CARD_9",
|
||||
template: "onboarding",
|
||||
bundled: 3,
|
||||
order: 2,
|
||||
content: {
|
||||
title: {string_id: "onboarding-pocket-anywhere-title"},
|
||||
text: {string_id: "onboarding-pocket-anywhere-text"},
|
||||
|
@ -373,13 +381,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort > 0",
|
||||
targeting: "trailheadTriplet == 'multidevice'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_CARD_10",
|
||||
template: "onboarding",
|
||||
bundled: 3,
|
||||
order: 3,
|
||||
content: {
|
||||
title: {string_id: "onboarding-lockwise-passwords-title"},
|
||||
text: {string_id: "onboarding-lockwise-passwords-text"},
|
||||
|
@ -392,13 +401,14 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort > 0",
|
||||
targeting: "trailheadTriplet == 'privacy'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
id: "TRAILHEAD_CARD_11",
|
||||
template: "onboarding",
|
||||
bundled: 3,
|
||||
order: 4,
|
||||
content: {
|
||||
title: {string_id: "onboarding-facebook-container-title"},
|
||||
text: {string_id: "onboarding-facebook-container-text"},
|
||||
|
@ -411,7 +421,7 @@ const ONBOARDING_MESSAGES = async () => ([
|
|||
},
|
||||
},
|
||||
},
|
||||
targeting: "trailheadCohort > 0",
|
||||
targeting: "trailheadTriplet == 'payoff'",
|
||||
trigger: {id: "showOnboarding"},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import {_ASRouter, MessageLoaderUtils} from "lib/ASRouter.jsm";
|
||||
import {
|
||||
_ASRouter,
|
||||
chooseBranch,
|
||||
MessageLoaderUtils,
|
||||
TRAILHEAD_CONFIG,
|
||||
} from "lib/ASRouter.jsm";
|
||||
import {ASRouterTargeting, QueryCache} from "lib/ASRouterTargeting.jsm";
|
||||
import {
|
||||
CHILD_TO_PARENT_MESSAGE_NAME,
|
||||
|
@ -1483,4 +1488,159 @@ describe("ASRouter", () => {
|
|||
assert.equal(action.data.message_id, "foo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("trailhead", () => {
|
||||
it("should call .setupTrailhead on init", async () => {
|
||||
sandbox.spy(Router, "setupTrailhead");
|
||||
sandbox.stub(Router, "_generateTrailheadBranches").resolves({experiment: "", interrupt: "join", triplet: "privacy"});
|
||||
sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(true);
|
||||
|
||||
await Router.init(channel, createFakeStorage(), dispatchStub);
|
||||
|
||||
assert.calledOnce(Router.setupTrailhead);
|
||||
assert.propertyVal(Router.state, "trailheadInitialized", true);
|
||||
});
|
||||
it("should call .setupTrailhead on init but return early if the DID_SEE_ABOUT_WELCOME_PREF is false", async () => {
|
||||
sandbox.spy(Router, "setupTrailhead");
|
||||
sandbox.spy(Router, "_generateTrailheadBranches");
|
||||
sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(false);
|
||||
|
||||
await Router.init(channel, createFakeStorage(), dispatchStub);
|
||||
|
||||
assert.calledOnce(Router.setupTrailhead);
|
||||
assert.notCalled(Router._generateTrailheadBranches);
|
||||
assert.propertyVal(Router.state, "trailheadInitialized", false);
|
||||
});
|
||||
it("should call .setupTrailhead and set the DID_SEE_ABOUT_WELCOME_PREF on a firstRun TRIGGER message", async () => {
|
||||
sandbox.spy(Router, "setupTrailhead");
|
||||
const msg = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
|
||||
await Router.onMessage(msg);
|
||||
|
||||
assert.calledOnce(Router.setupTrailhead);
|
||||
});
|
||||
|
||||
it("should have trailheadInterrupt and trailheadTriplet in the message context", async () => {
|
||||
sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(true);
|
||||
sandbox.stub(Router, "_generateTrailheadBranches").resolves({experiment: "", interrupt: "join", triplet: "privacy"});
|
||||
await Router.setupTrailhead();
|
||||
|
||||
assert.propertyVal(Router._getMessagesContext(), "trailheadInterrupt", "join");
|
||||
assert.propertyVal(Router._getMessagesContext(), "trailheadTriplet", "privacy");
|
||||
});
|
||||
|
||||
describe(".setupTrailhead", () => {
|
||||
let getBoolPrefStub;
|
||||
|
||||
beforeEach(() => {
|
||||
getBoolPrefStub = sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(true);
|
||||
});
|
||||
|
||||
const configWithExperiment = {experiment: "interrupt", interrupt: "join", triplet: "privacy"};
|
||||
const configWithoutExperiment = {experiment: "", interrupt: "control", triplet: ""};
|
||||
|
||||
it("should generates an experiment/branch configuration and update Router.state", async () => {
|
||||
const config = configWithoutExperiment;
|
||||
sandbox.stub(Router, "_generateTrailheadBranches").resolves(config);
|
||||
|
||||
await Router.setupTrailhead();
|
||||
|
||||
assert.propertyVal(Router.state, "trailheadInitialized", true);
|
||||
assert.propertyVal(Router.state, "trailheadInterrupt", config.interrupt);
|
||||
assert.propertyVal(Router.state, "trailheadTriplet", config.triplet);
|
||||
});
|
||||
it("should only run once", async () => {
|
||||
sandbox.spy(Router, "setState");
|
||||
|
||||
await Router.setupTrailhead();
|
||||
await Router.setupTrailhead();
|
||||
await Router.setupTrailhead();
|
||||
|
||||
assert.calledOnce(Router.setState);
|
||||
});
|
||||
it("should return early if DID_SEE_ABOUT_WELCOME_PREF is false", async () => {
|
||||
getBoolPrefStub.withArgs(TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF).returns(false);
|
||||
|
||||
await Router.setupTrailhead();
|
||||
|
||||
sandbox.spy(Router, "setState");
|
||||
assert.notCalled(Router.setState);
|
||||
});
|
||||
it("should set active experiment if one is defined", async () => {
|
||||
sandbox.stub(Router, "_generateTrailheadBranches").resolves(configWithExperiment);
|
||||
sandbox.stub(global.TelemetryEnvironment, "setExperimentActive");
|
||||
|
||||
await Router.setupTrailhead();
|
||||
|
||||
assert.calledOnce(global.TelemetryEnvironment.setExperimentActive);
|
||||
});
|
||||
it("should not set an active experiment if no experiment is defined", async () => {
|
||||
sandbox.stub(Router, "_generateTrailheadBranches").resolves(configWithoutExperiment);
|
||||
sandbox.stub(global.TelemetryEnvironment, "setExperimentActive");
|
||||
|
||||
await Router.setupTrailhead();
|
||||
|
||||
assert.notCalled(global.TelemetryEnvironment.setExperimentActive);
|
||||
});
|
||||
});
|
||||
|
||||
describe("._generateTrailheadBranches", () => {
|
||||
async function checkReturnValue(expected) {
|
||||
const result = await Router._generateTrailheadBranches();
|
||||
assert.propertyVal(result, "experiment", expected.experiment);
|
||||
assert.propertyVal(result, "interrupt", expected.interrupt);
|
||||
assert.propertyVal(result, "triplet", expected.triplet);
|
||||
}
|
||||
it("should return control experience with no experiment if locale is NOT in TRAILHEAD_LOCALES", async () => {
|
||||
sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "zh-CN");
|
||||
checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
|
||||
});
|
||||
it("should use values in override pref if it is set with no experiment", async () => {
|
||||
getStringPrefStub.withArgs(TRAILHEAD_CONFIG.OVERRIDE_PREF).returns("join-privacy");
|
||||
checkReturnValue({experiment: "", interrupt: "join", triplet: "privacy"});
|
||||
|
||||
getStringPrefStub.withArgs(TRAILHEAD_CONFIG.OVERRIDE_PREF).returns("nofirstrun");
|
||||
checkReturnValue({experiment: "", interrupt: "nofirstrun", triplet: ""});
|
||||
});
|
||||
it("should return control experience with no experiment if locale is NOT in TRAILHEAD_LOCALES", async () => {
|
||||
sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "zh-CN");
|
||||
checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
|
||||
});
|
||||
it("should return control experience with no experiment if locale is NOT in TRAILHEAD_LOCALES", async () => {
|
||||
sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "zh-CN");
|
||||
checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
|
||||
});
|
||||
it("should roll for experiment if locale is in TRAILHEAD_LOCALES", async () => {
|
||||
sandbox.stub(global.Sampling, "ratioSample").resolves(1); // 1 = interrupts experiment
|
||||
sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
|
||||
checkReturnValue({experiment: "interrupts", interrupt: "join", triplet: "supercharge"});
|
||||
});
|
||||
it("should roll a triplet experiment", async () => {
|
||||
sandbox.stub(global.Sampling, "ratioSample").resolves(2); // 2 = triplets experiment
|
||||
sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
|
||||
checkReturnValue({experiment: "triplets", interrupt: "join", triplet: "multidevice"});
|
||||
});
|
||||
it("should roll no experiment", async () => {
|
||||
sandbox.stub(global.Sampling, "ratioSample").resolves(0); // 0 = no experiment
|
||||
sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
|
||||
checkReturnValue({experiment: "", interrupt: "join", triplet: "supercharge"});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("chooseBranch", () => {
|
||||
it("should call .ratioSample with the second value in each branch and return one of the first values", async () => {
|
||||
sandbox.stub(global.Sampling, "ratioSample").resolves(0);
|
||||
const result = await chooseBranch("bleep", [["foo", 14], ["bar", 42]]);
|
||||
|
||||
assert.calledWith(global.Sampling.ratioSample, "bleep", [14, 42]);
|
||||
assert.equal(result, "foo");
|
||||
});
|
||||
it("should use 1 as the default ratio", async () => {
|
||||
sandbox.stub(global.Sampling, "ratioSample").resolves(1);
|
||||
const result = await chooseBranch("bleep", [["foo"], ["bar"]]);
|
||||
|
||||
assert.calledWith(global.Sampling.ratioSample, "bleep", [1, 1]);
|
||||
assert.equal(result, "bar");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,8 @@ const SKIP_DOCS = [];
|
|||
// These are extra message context attributes via ASRouter.jsm
|
||||
const MESSAGE_CONTEXT_ATTRIBUTES = [
|
||||
"previousSessionEnd",
|
||||
"trailheadInterrupt",
|
||||
"trailheadTriplet",
|
||||
];
|
||||
|
||||
function getHeadingsFromDocs() {
|
||||
|
|
|
@ -66,7 +66,7 @@ describe("ASRouterAdmin", () => {
|
|||
});
|
||||
describe("#getSection", () => {
|
||||
it("should render a message provider section by default", () => {
|
||||
assert.equal(wrapper.find("h2").at(1).text(), "Messages");
|
||||
assert.equal(wrapper.find("h2").at(2).text(), "Messages");
|
||||
});
|
||||
it("should render a targeting section for targeting route", () => {
|
||||
wrapper = shallow(<ASRouterAdminInner location={{routes: ["targeting"]}} />);
|
||||
|
|
|
@ -40,6 +40,9 @@ const TEST_GLOBAL = {
|
|||
generateQI() { return {}; },
|
||||
import() { return global; },
|
||||
},
|
||||
ClientEnvironment: {
|
||||
get userId() { return "foo123"; },
|
||||
},
|
||||
Components: {isSuccessCode: () => true},
|
||||
// eslint-disable-next-line object-shorthand
|
||||
ContentSearchUIController: function() {}, // NB: This is a function/constructor
|
||||
|
@ -278,6 +281,14 @@ const TEST_GLOBAL = {
|
|||
return Promise.resolve(id);
|
||||
},
|
||||
},
|
||||
TelemetryEnvironment: {
|
||||
setExperimentActive() {},
|
||||
},
|
||||
Sampling: {
|
||||
ratioSample(seed, ratios) {
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
overrider.set(TEST_GLOBAL);
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче