From 5cfefdf0ed90a686e088b420f8243872149b4180 Mon Sep 17 00:00:00 2001 From: Andrei Oprea Date: Fri, 24 Apr 2020 15:30:28 +0000 Subject: [PATCH] Bug 1631456 - Create a CFR message loader for ExperimentAPI r=k88hudson Differential Revision: https://phabricator.services.mozilla.com/D71766 --- browser/app/profile/firefox.js | 1 + browser/components/newtab/lib/ASRouter.jsm | 28 +++ .../newtab/test/browser/browser.ini | 1 + .../browser_asrouter_experimentsAPILoader.js | 165 ++++++++++++++++++ .../test/unit/asrouter/ASRouter.test.js | 69 ++++++++ 5 files changed, 264 insertions(+) create mode 100644 browser/components/newtab/test/browser/browser_asrouter_experimentsAPILoader.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 3eb60d0c618e..669e158ec4b5 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1287,6 +1287,7 @@ pref("browser.newtabpage.activity-stream.asrouter.providers.message-groups", "{\ // this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream // repackager of this code using an alternate snippet url, please keep your users safe pref("browser.newtabpage.activity-stream.asrouter.providers.snippets", "{\"id\":\"snippets\",\"enabled\":true,\"type\":\"remote\",\"url\":\"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/\",\"updateCycleInMs\":14400000}"); +pref("browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", "{\"id\":\"messaging-experiments\",\"enabled\":true,\"type\":\"remote-experiments\",\"messageGroups\":[\"cfr\",\"whats-new-panel\",\"moments-page\",\"snippets\",\"cfr-fxa\"],\"updateCycleInMs\":3600000}"); // The pref that controls if ASRouter uses the remote fluent files. // It's enabled by default, but could be disabled to force ASRouter to use the local files. diff --git a/browser/components/newtab/lib/ASRouter.jsm b/browser/components/newtab/lib/ASRouter.jsm index 4e27e46261f5..7acce9945c5e 100644 --- a/browser/components/newtab/lib/ASRouter.jsm +++ b/browser/components/newtab/lib/ASRouter.jsm @@ -35,6 +35,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { Downloader: "resource://services-settings/Attachments.jsm", RemoteL10n: "resource://activity-stream/lib/RemoteL10n.jsm", MigrationUtils: "resource:///modules/MigrationUtils.jsm", + ExperimentAPI: "resource://messaging-system/experiments/ExperimentAPI.jsm", }); XPCOMUtils.defineLazyServiceGetters(this, { BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"], @@ -336,6 +337,31 @@ const MessageLoaderUtils = { return RemoteSettings(bucket).get(); }, + async _experimentsAPILoader(provider, options) { + try { + await ExperimentAPI.ready(); + } catch (e) { + 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(); + }, + _handleRemoteSettingsUndesiredEvent(event, providerId, dispatchToAS) { if (dispatchToAS) { dispatchToAS( @@ -363,6 +389,8 @@ const MessageLoaderUtils = { return this._remoteSettingsLoader; case "json": return this._localJsonLoader; + case "remote-experiments": + return this._experimentsAPILoader; case "local": default: return this._localLoader; diff --git a/browser/components/newtab/test/browser/browser.ini b/browser/components/newtab/test/browser/browser.ini index 498fd3aa7483..94b94034c1d7 100644 --- a/browser/components/newtab/test/browser/browser.ini +++ b/browser/components/newtab/test/browser/browser.ini @@ -39,3 +39,4 @@ skip-if = (os == "linux") # Test setup only implemented for OSX and Windows tags = remote-settings [browser_asrouter_momentspagehub.js] tags = remote-settings +[browser_asrouter_experimentsAPILoader.js] diff --git a/browser/components/newtab/test/browser/browser_asrouter_experimentsAPILoader.js b/browser/components/newtab/test/browser/browser_asrouter_experimentsAPILoader.js new file mode 100644 index 000000000000..0655918afdc5 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_asrouter_experimentsAPILoader.js @@ -0,0 +1,165 @@ +const { RemoteSettings } = ChromeUtils.import( + "resource://services-settings/remote-settings.js" +); +const { ASRouter } = ChromeUtils.import( + "resource://activity-stream/lib/ASRouter.jsm" +); +const { RemoteSettingsExperimentLoader } = ChromeUtils.import( + "resource://messaging-system/lib/RemoteSettingsExperimentLoader.jsm" +); +const { ExperimentAPI } = ChromeUtils.import( + "resource://messaging-system/experiments/ExperimentAPI.jsm" +); + +const EXPERIMENT_PAYLOAD = { + enabled: true, + arguments: { + slug: "test_xman_cfr", + branches: [ + { + 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/", + }, + buttons: { + primary: { + label: { + string_id: "cfr-doorhanger-extension-ok-button", + }, + action: { + data: { + url: null, + }, + type: "INSTALL_ADDON_FROM_URL", + }, + }, + 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, + }, + targeting: "true", + }, + ], + groups: ["cfr"], + }, + ], + isHighVolume: "false,", + userFacingName: "About:Welcome Pull Factor Reinforcement", + isEnrollmentPaused: false, + experimentDocumentUrl: + "https://experimenter.services.mozilla.com/experiments/aboutwelcome-pull-factor-reinforcement/", + userFacingDescription: + "This study uses 4 different variants of about:welcome with a goal of testing new experiment framework and get insights on whether reinforcing pull-factors improves retention.", + }, + filter_expression: "true", + id: "test_xman_cfr", +}; + +add_task(async function test_loading_experimentsAPI() { + // Force the WNPanel provider cache to 0 by modifying updateCycleInMs + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", + `{"id":"messaging-experiments","enabled":true,"type":"remote-experiments","messageGroups":["cfr","whats-new-panel","moments-page","snippets","cfr-fxa"],"frequency":{"custom":[{"period":"daily","cap":1}]},"updateCycleInMs":0}`, + ], + ], + }); + const client = RemoteSettings("messaging-experiments"); + await client.db.clear(); + await client.db.create( + // Modify targeting to ensure the messages always show up + { ...EXPERIMENT_PAYLOAD } + ); + await client.db.saveLastModified(42); // Prevent from loading JSON dump. + + // Fetch the new recipe from RS + await RemoteSettingsExperimentLoader.updateRecipes(); + await BrowserTestUtils.waitForCondition( + () => ExperimentAPI.getExperiment({ group: "cfr" }), + "ExperimentAPI should return an experiment" + ); + + // Reload the provider + await ASRouter._updateMessageProviders(); + // Wait to load the messages from the messaging-experiments provider + await ASRouter.loadMessagesFromAllProviders(); + + Assert.ok(ASRouter.state.messages.find(m => m.id === "xman_test_message")); + + await client.db.clear(); + await SpecialPowers.popPrefEnv(); + // Reload the provider + await ASRouter._updateMessageProviders(); +}); diff --git a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js index 2ec2f8ee7dda..ca00981c3e2a 100644 --- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js +++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js @@ -206,6 +206,10 @@ describe("ASRouter", () => { return Promise.resolve("/path/to/download"); } }, + ExperimentAPI: { + getExperiment: sandbox.stub().returns({ branch: { value: [] } }), + ready: sandbox.stub().resolves(), + }, }); await createRouterAndInit(); }); @@ -4161,6 +4165,71 @@ describe("ASRouter", () => { }); }); describe("#loadMessagesForProvider", () => { + it("should fetch messages from the ExperimentAPI", async () => { + const args = { + type: "remote-experiments", + messageGroups: ["asrouter"], + }; + + await MessageLoaderUtils.loadMessagesForProvider(args); + + assert.calledOnce(global.ExperimentAPI.getExperiment); + assert.calledWithExactly(global.ExperimentAPI.getExperiment, { + group: "asrouter", + }); + }); + it("should handle the case of no experiments in the ExperimentAPI", async () => { + const args = { + type: "remote-experiments", + messageGroups: ["asrouter"], + }; + + global.ExperimentAPI.getExperiment.throws(); + const stub = sandbox.stub(MessageLoaderUtils, "reportError"); + + await MessageLoaderUtils.loadMessagesForProvider(args); + + assert.calledOnce(stub); + }); + it("should handle the case of no experiments in the ExperimentAPI", async () => { + const args = { + type: "remote-experiments", + messageGroups: ["asrouter"], + }; + + global.ExperimentAPI.getExperiment.returns(null); + + const result = await MessageLoaderUtils.loadMessagesForProvider(args); + + assert.lengthOf(result.messages, 0); + }); + it("should normally load ExperimentAPI messages", async () => { + const args = { + type: "remote-experiments", + messageGroups: ["asrouter"], + }; + + global.ExperimentAPI.getExperiment.returns({ + branch: { value: ["foo", "bar"] }, + }); + + const result = await MessageLoaderUtils.loadMessagesForProvider(args); + + assert.lengthOf(result.messages, 2); + }); + it("should fetch messages from the ExperimentAPI", async () => { + global.ExperimentAPI.ready.throws(); + const args = { + type: "remote-experiments", + messageGroups: ["asrouter"], + }; + const stub = sandbox.stub(MessageLoaderUtils, "reportError"); + + await MessageLoaderUtils.loadMessagesForProvider(args); + + assert.notCalled(global.ExperimentAPI.getExperiment); + assert.calledOnce(stub); + }); it("should fetch json from url", async () => { let result = await MessageLoaderUtils.loadMessagesForProvider({ location: "http://fake.com/endpoint",