Bug 1547062 - Experiment branch generation for Trailhead (#4980)

(cherry picked from commit be9b3bacc1)
This commit is contained in:
Kate Hudson 2019-05-03 17:23:36 -04:00 коммит произвёл Ed Lee
Родитель 633596d511
Коммит 83131b740a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 40B7250312F03605
10 изменённых файлов: 373 добавлений и 29 удалений

Просмотреть файл

@ -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);

Просмотреть файл

@ -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);