Bug 1634481 - Add trigger to the reach ping r=andreio

Differential Revision: https://phabricator.services.mozilla.com/D76610
This commit is contained in:
Nan Jiang 2020-05-26 15:54:39 +00:00
Родитель a769706988
Коммит 082a6cd662
6 изменённых файлов: 248 добавлений и 443 удалений

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

@ -102,6 +102,13 @@ const TOPIC_INTL_LOCALE_CHANGED = "intl:app-locales-changed";
const USE_REMOTE_L10N_PREF =
"browser.newtabpage.activity-stream.asrouter.useRemoteL10n";
// 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"
const REACH_EVENT_GROUPS = ["cfr"];
const REACH_EVENT_CATEGORY = "messaging_experiments";
const REACH_EVENT_METHOD = "reach";
const MessageLoaderUtils = {
STARTPAGE_VERSION,
REMOTE_LOADER_CACHE_KEY: "RemoteLoaderCache",
@ -340,22 +347,47 @@ const MessageLoaderUtils = {
MessageLoaderUtils.reportError(e);
return [];
}
return provider.messageGroups
.map(group => {
let experimentData;
try {
experimentData = ExperimentAPI.getExperiment({ group });
} catch (e) {
MessageLoaderUtils.reportError(e);
return [];
}
if (experimentData && experimentData.branch) {
return experimentData.branch.value;
}
return [];
})
.flat();
let experiments = [];
for (const group of provider.messageGroups) {
let experimentData;
try {
experimentData = ExperimentAPI.getExperiment({ group });
} catch (e) {
MessageLoaderUtils.reportError(e);
continue;
}
if (experimentData && experimentData.branch) {
experiments.push(experimentData.branch.value);
if (!REACH_EVENT_GROUPS.includes(group)) {
continue;
}
// Check other sibling branches for triggers, add them to the return
// array if found any. The `forReachEvent` label is used to identify
// those branches so that they would only used to record the Reach
// event.
const branches =
(await ExperimentAPI.getAllBranches(experimentData.slug)) || [];
for (const branch of branches) {
if (
branch.slug !== experimentData.branch.slug &&
branch.value.trigger
) {
experiments.push({
group,
forReachEvent: true,
experimentSlug: experimentData.slug,
branchSlug: branch.slug,
...branch.value,
});
}
}
}
}
return experiments;
},
_handleRemoteSettingsUndesiredEvent(event, providerId, dispatchToAS) {
@ -1962,6 +1994,19 @@ class _ASRouter {
await this._sendMessageToTarget(message, target);
}
_recordReachEvent(message) {
// Events telemetry only accepts understores for the event `object`
const underscored = message.group.split("-").join("_");
const extra = { branches: message.branchSlug };
Services.telemetry.recordEvent(
REACH_EVENT_CATEGORY,
REACH_EVENT_METHOD,
underscored,
message.experimentSlug,
extra
);
}
async sendTriggerMessage(target, trigger) {
await this.loadMessagesFromAllProviders();
@ -1972,14 +2017,32 @@ class _ASRouter {
const telemetryObject = { port: target.portID };
TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
const message = await this.handleMessageRequest({
triggerId: trigger.id,
triggerParam: trigger.param,
triggerContext: trigger.context,
});
// Return all the messages so that it can record the Reach event
const messages =
(await this.handleMessageRequest({
triggerId: trigger.id,
triggerParam: trigger.param,
triggerContext: trigger.context,
returnAll: true,
})) || [];
TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
await this._sendMessageToTarget(message, target, trigger);
// Record the Reach event for all the messages with `forReachEvent`,
// only send the first message without forReachEvent to the target
const nonReachMessages = [];
for (const message of messages) {
if (message.forReachEvent) {
this._recordReachEvent(message);
} else {
nonReachMessages.push(message);
}
}
await this._sendMessageToTarget(
nonReachMessages[0] || null,
target,
trigger
);
}
renderWNMessages(browserWindow, messageIds) {

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

@ -19,98 +19,96 @@ const EXPERIMENT_PAYLOAD = {
{
slug: "control",
ratio: 1,
value: [
{
id: "xman_test_message",
content: {
text: "This is a test CFR",
addon: {
id: "954390",
icon:
"resource://activity-stream/data/content/assets/cfr_fb_container.png",
title: "Facebook Container",
users: 1455872,
author: "Mozilla",
rating: 4.5,
amo_url:
"https://addons.mozilla.org/firefox/addon/facebook-container/",
value: {
id: "xman_test_message",
content: {
text: "This is a test CFR",
addon: {
id: "954390",
icon:
"resource://activity-stream/data/content/assets/cfr_fb_container.png",
title: "Facebook Container",
users: 1455872,
author: "Mozilla",
rating: 4.5,
amo_url:
"https://addons.mozilla.org/firefox/addon/facebook-container/",
},
buttons: {
primary: {
label: {
string_id: "cfr-doorhanger-extension-ok-button",
},
action: {
data: {
url: null,
},
type: "INSTALL_ADDON_FROM_URL",
},
},
buttons: {
primary: {
secondary: [
{
label: {
string_id: "cfr-doorhanger-extension-ok-button",
string_id: "cfr-doorhanger-extension-cancel-button",
},
action: {
type: "CANCEL",
},
},
{
label: {
string_id:
"cfr-doorhanger-extension-never-show-recommendation",
},
},
{
label: {
string_id:
"cfr-doorhanger-extension-manage-settings-button",
},
action: {
data: {
url: null,
origin: "CFR",
category: "general-cfraddons",
},
type: "INSTALL_ADDON_FROM_URL",
type: "OPEN_PREFERENCES_PAGE",
},
},
secondary: [
{
label: {
string_id: "cfr-doorhanger-extension-cancel-button",
},
action: {
type: "CANCEL",
},
},
{
label: {
string_id:
"cfr-doorhanger-extension-never-show-recommendation",
},
},
{
label: {
string_id:
"cfr-doorhanger-extension-manage-settings-button",
},
action: {
data: {
origin: "CFR",
category: "general-cfraddons",
},
type: "OPEN_PREFERENCES_PAGE",
},
},
],
},
category: "cfrAddons",
bucket_id: "CFR_M1",
info_icon: {
label: {
string_id: "cfr-doorhanger-extension-sumo-link",
},
sumo_path: "extensionrecommendations",
},
heading_text: "Welcome to the experiment",
notification_text: {
string_id: "cfr-doorhanger-extension-notification2",
},
},
trigger: {
id: "openURL",
params: [
"www.facebook.com",
"facebook.com",
"www.instagram.com",
"instagram.com",
"www.whatsapp.com",
"whatsapp.com",
"web.whatsapp.com",
"www.messenger.com",
"messenger.com",
],
},
template: "cfr_doorhanger",
frequency: {
lifetime: 3,
category: "cfrAddons",
bucket_id: "CFR_M1",
info_icon: {
label: {
string_id: "cfr-doorhanger-extension-sumo-link",
},
sumo_path: "extensionrecommendations",
},
heading_text: "Welcome to the experiment",
notification_text: {
string_id: "cfr-doorhanger-extension-notification2",
},
targeting: "true",
},
],
trigger: {
id: "openURL",
params: [
"www.facebook.com",
"facebook.com",
"www.instagram.com",
"instagram.com",
"www.whatsapp.com",
"whatsapp.com",
"web.whatsapp.com",
"www.messenger.com",
"messenger.com",
],
},
template: "cfr_doorhanger",
frequency: {
lifetime: 3,
},
targeting: "true",
},
groups: ["cfr"],
},
],

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

@ -208,6 +208,7 @@ describe("ASRouter", () => {
},
ExperimentAPI: {
getExperiment: sandbox.stub().returns({ branch: { value: [] } }),
getAllBranches: sandbox.stub().returns([{ branch: { value: [] } }]),
ready: sandbox.stub().resolves(),
},
SpecialMessageActions: {
@ -2349,6 +2350,48 @@ describe("ASRouter", () => {
100
);
});
it("should record the Reach event if found any", async () => {
let messages = [
{
id: "foo1",
forReachEvent: true,
experimentSlug: "exp01",
branchSlug: "branch01",
group: "cfr",
template: "simple_template",
trigger: { id: "foo" },
content: { title: "Foo1", body: "Foo123-1" },
},
{
id: "foo2",
group: "cfr",
template: "simple_template",
trigger: { id: "bar" },
content: { title: "Foo2", body: "Foo123-2" },
provider: "onboarding",
},
{
id: "foo3",
forReachEvent: true,
experimentSlug: "exp02",
branchSlug: "branch02",
group: "cfr",
template: "simple_template",
trigger: { id: "foo" },
content: { title: "Foo1", body: "Foo123-1" },
},
];
sandbox.stub(Router, "handleMessageRequest").resolves(messages);
sandbox.spy(Services.telemetry, "recordEvent");
const msg = fakeAsyncMessage({
type: "TRIGGER",
data: { trigger: { id: "foo" } },
});
await Router.onMessage(msg);
assert.calledTwice(Services.telemetry.recordEvent);
});
});
describe(".includeBundle", () => {
@ -4002,12 +4045,15 @@ describe("ASRouter", () => {
};
global.ExperimentAPI.getExperiment.returns({
branch: { value: ["foo", "bar"] },
branch: {
slug: "branch01",
value: { id: "id01", trigger: { id: "openURL" } },
},
});
const result = await MessageLoaderUtils.loadMessagesForProvider(args);
assert.lengthOf(result.messages, 2);
assert.lengthOf(result.messages, 1);
});
it("should fetch messages from the ExperimentAPI", async () => {
global.ExperimentAPI.ready.throws();
@ -4022,6 +4068,44 @@ describe("ASRouter", () => {
assert.notCalled(global.ExperimentAPI.getExperiment);
assert.calledOnce(stub);
});
it("should fetch branches with trigger", async () => {
const args = {
type: "remote-experiments",
messageGroups: ["cfr"],
};
global.ExperimentAPI.getExperiment.returns({
slug: "exp01",
branch: {
slug: "branch01",
value: { id: "id01", trigger: { id: "openURL" } },
},
});
global.ExperimentAPI.getAllBranches.returns([
{
slug: "branch01",
value: { id: "id01", trigger: { id: "openURL" } },
},
{
slug: "branch02",
value: { id: "id02", trigger: { id: "openURL" } },
},
{
// This branch should not be loaded as it doesn't have the trigger
slug: "branch03",
value: { id: "id03" },
},
]);
const result = await MessageLoaderUtils.loadMessagesForProvider(args);
assert.equal(result.messages.length, 2);
assert.equal(result.messages[0].id, "id01");
assert.equal(result.messages[1].id, "id02");
assert.equal(result.messages[1].group, "cfr");
assert.equal(result.messages[1].experimentSlug, "exp01");
assert.equal(result.messages[1].branchSlug, "branch02");
assert.ok(result.messages[1].forReachEvent);
});
it("should fetch json from url", async () => {
let result = await MessageLoaderUtils.loadMessagesForProvider({
location: "http://fake.com/endpoint",

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

@ -12,8 +12,6 @@
const EXPORTED_SYMBOLS = ["ExperimentManager", "_ExperimentManager"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
@ -26,9 +24,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
Sampling: "resource://gre/modules/components-utils/Sampling.jsm",
TelemetryEvents: "resource://normandy/lib/TelemetryEvents.jsm",
TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
RemoteSettings: "resource://services-settings/remote-settings.js",
requestIdleCallback: "resource://gre/modules/Timer.jsm",
ASRouterTargeting: "resource://activity-stream/lib/ASRouterTargeting.jsm",
});
XPCOMUtils.defineLazyGetter(this, "log", () => {
@ -46,20 +41,6 @@ const TELEMETRY_EXPERIMENT_TYPE_PREFIX = "normandy-";
// Also included in telemetry
const DEFAULT_EXPERIMENT_TYPE = "messaging_experiment";
// Experiment groups that are currently collecting the Reach events.
//
// 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"
//
// Note: although Events telemetry only takes "_", you can still have "-"(s) in
// the group names here. They will be automatically swapped whenever needed.
const REACH_EVENT_GROUPS = ["cfr"];
const REACH_EVENT_CATEGORY = "messaging_experiments";
const REACH_EVENT_METHOD = "reach";
// Experiment recipe collection ID on RemoteSettings
const COLLECTION_ID = "messaging-experiments";
/**
* A module for processes Experiment recipes, choosing and storing enrollment state,
* and sending experiment-related Telemetry.
@ -142,10 +123,6 @@ class _ExperimentManager {
}
}
if (activeExperiments.length) {
requestIdleCallback(() => this.sendReachEvents());
}
this.sessions.delete(sourceToCheck);
}
@ -322,88 +299,6 @@ class _ExperimentManager {
const index = await Sampling.ratioSample(input, ratios);
return branches[index];
}
/**
* Sends Reach events for active experiments
*
* Note:
*
* * To avoid interrupting the enrollment process, this is done in a browser
* idle callback other than in `this.onRecipe()` or `this.enroll()`
*
* @param {RemoteSettings} remoteSettingsClient for test only
*
*/
async sendReachEvents(remoteSettingsClient) {
let recipes;
for (const group of REACH_EVENT_GROUPS) {
const experiment = this.store.getExperimentForGroup(group);
if (!experiment) {
log.debug("Skipping sending Reach events for no active experiment");
continue;
}
// Need recipes for the branches information. Note that it defers the
// RemoteSettings call until seeing the first active experiment that
// might record a reach event.
if (!recipes) {
try {
const client = remoteSettingsClient || RemoteSettings(COLLECTION_ID);
// Do not sync if it's empty, let RemoteSettingsExperimentLoader do that
recipes = await client.get({ syncIfEmpty: false });
} catch (e) {
log.debug(
"Reach events not recorded, error getting recipes from remote settings"
);
return;
}
}
const recipe = recipes.find(
recipe => recipe.arguments.slug === experiment.slug
);
if (!recipe) {
log.debug(
"Can't find experiment recipe, skipping sending Reach events"
);
continue;
}
// Check targeting for every branch, and only record those qualified ones
let qualifiedBranches = [];
for (const branch of recipe.arguments.branches) {
if (
branch.value?.content?.targeting &&
Boolean(
await ASRouterTargeting.isMatch(
branch.value.content.targeting,
this.filterContext,
err => {
log.debug("Targeting failed because of an error");
Cu.reportError(err);
}
)
)
) {
qualifiedBranches.push(branch.slug);
}
}
if (qualifiedBranches.length) {
// Events telemetry only takes underscore
const underscored = group.split("-").join("_");
const extra = { branches: qualifiedBranches.join(";") };
Services.telemetry.recordEvent(
REACH_EVENT_CATEGORY,
REACH_EVENT_METHOD,
underscored,
experiment.slug,
extra
);
}
}
}
}
const ExperimentManager = new _ExperimentManager();

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

@ -1,234 +0,0 @@
"use strict";
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/MSTestUtils.jsm"
);
const { TelemetryTestUtils } = ChromeUtils.import(
"resource://testing-common/TelemetryTestUtils.jsm"
);
const EVENT_CATEGORY = "messaging_experiments";
const EVENT_METHOD = "reach";
const EVENT_OBJECT = "cfr";
const COLLECTION_ID = "messaging-experiments";
const fakeRemoteSettingsClient = {
get() {},
};
add_task(async function test_sendReachEvents() {
const sandbox = sinon.createSandbox();
const manager = ExperimentFakes.manager();
const experiment = ExperimentFakes.experiment("cfr_exp_01");
const RECIPE_CFR = {
arguments: ExperimentFakes.recipe("cfr_exp_01", {
branches: [
{
slug: "control",
value: {
content: {
targeting: "true",
},
},
},
{
slug: "variant_1",
value: {
content: {
targeting: "false",
},
},
},
{
slug: "variant_2",
value: {
content: {
targeting: "true",
},
},
},
],
}),
};
const RECIPE_FOO = {
arguments: ExperimentFakes.recipe("foo", {
branches: [
{
slug: "control",
value: {
content: {
targeting: "true",
},
},
},
{
slug: "variant_1",
value: {
content: {
targeting: "true",
},
},
},
],
}),
};
sandbox.stub(manager.store, "getExperimentForGroup").returns(experiment);
sandbox
.stub(fakeRemoteSettingsClient, "get")
.returns([RECIPE_CFR, RECIPE_FOO]);
const extra = { branches: "control;variant_2" };
const expectedEvents = [
[EVENT_CATEGORY, EVENT_METHOD, EVENT_OBJECT, "cfr_exp_01", extra],
];
Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, true);
Services.telemetry.clearEvents();
await manager.sendReachEvents(fakeRemoteSettingsClient);
TelemetryTestUtils.assertEvents(expectedEvents);
Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, false);
sandbox.restore();
});
add_task(async function test_sendReachEvents_Failuure_RemoteSettings() {
const sandbox = sinon.createSandbox();
const manager = ExperimentFakes.manager();
const experiment = ExperimentFakes.experiment("cfr_exp_01");
sandbox.stub(manager.store, "getExperimentForGroup").returns(experiment);
sandbox.stub(fakeRemoteSettingsClient, "get").throws();
Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, true);
Services.telemetry.clearEvents();
await manager.sendReachEvents(fakeRemoteSettingsClient);
TelemetryTestUtils.assertNumberOfEvents(0);
Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, false);
sandbox.restore();
});
add_task(async function test_sendReachEvents_Failure_No_Active() {
const sandbox = sinon.createSandbox();
const manager = ExperimentFakes.manager();
const RECIPE_CFR = {
arguments: ExperimentFakes.recipe("cfr_exp_01", {
branches: [
{
slug: "control",
value: {
content: {
targeting: "true",
},
},
},
{
slug: "variant_1",
value: {
content: {
targeting: "true",
},
},
},
],
}),
};
sandbox.stub(manager.store, "getExperimentForGroup").returns(undefined);
sandbox.stub(fakeRemoteSettingsClient, "get").resolves([RECIPE_CFR]);
Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, true);
Services.telemetry.clearEvents();
await manager.sendReachEvents(fakeRemoteSettingsClient);
TelemetryTestUtils.assertNumberOfEvents(0);
Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, false);
sandbox.restore();
});
add_task(async function test_sendReachEvents_Failure_No_Recipe() {
const sandbox = sinon.createSandbox();
const manager = ExperimentFakes.manager();
const experiment = ExperimentFakes.experiment("cfr_exp_01", {
active: false,
});
const RECIPE_CFR = {
arguments: ExperimentFakes.recipe("cfr_exp_02", {
branches: [
{
slug: "control",
value: {
content: {
targeting: "true",
},
},
},
{
slug: "variant_1",
value: {
content: {
targeting: "true",
},
},
},
],
}),
};
sandbox.stub(manager.store, "getExperimentForGroup").returns(experiment);
sandbox.stub(fakeRemoteSettingsClient, "get").resolves([RECIPE_CFR]);
Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, true);
Services.telemetry.clearEvents();
await manager.sendReachEvents(fakeRemoteSettingsClient);
TelemetryTestUtils.assertNumberOfEvents(0);
Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, false);
sandbox.restore();
});
add_task(async function test_sendReachEvents_Failure_No_Qualified() {
const sandbox = sinon.createSandbox();
const manager = ExperimentFakes.manager();
const experiment = ExperimentFakes.experiment("cfr_exp_01");
const RECIPE_CFR = {
arguments: ExperimentFakes.recipe("cfr_exp_01", {
branches: [
{
slug: "control",
value: {
content: {
targeting: "false",
},
},
},
{
slug: "variant_1",
value: {
content: {
targeting: "false",
},
},
},
],
}),
};
sandbox.stub(manager.store, "getExperimentForGroup").returns(experiment);
sandbox.stub(fakeRemoteSettingsClient, "get").resolves([RECIPE_CFR]);
Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, true);
Services.telemetry.clearEvents();
await manager.sendReachEvents(fakeRemoteSettingsClient);
TelemetryTestUtils.assertNumberOfEvents(0);
Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, false);
sandbox.restore();
});

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

@ -5,7 +5,6 @@ firefox-appdir = browser
[test_ExperimentManager_enroll.js]
[test_ExperimentManager_lifecycle.js]
[test_ExperimentManager_reach.js]
[test_ExperimentManager_unenroll.js]
[test_ExperimentStore.js]
[test_SharedDataMap.js]