From 976e9ae5542979996c507e65928ac3c81fe2cbe0 Mon Sep 17 00:00:00 2001 From: Mike Cooper Date: Wed, 24 Jan 2018 15:40:22 -0800 Subject: [PATCH] Bug 1432986 - Sync shield-recipe-client v83 from Github (commit 43f0ce2) r=Gijs MozReview-Commit-ID: 9TGdnKZ5zaf --HG-- extra : rebase_source : ba22c24bd1d8a83592ec63cf40f789f51680b9a9 --- .../shield-recipe-client/bootstrap.js | 1 + .../content/AboutPages.jsm | 6 +- .../content/about-studies/shield-studies.js | 2 +- .../shield-recipe-client/install.rdf.in | 2 +- .../shield-recipe-client/lib/AddonStudies.jsm | 41 ++++++-- .../lib/PreferenceExperiments.jsm | 31 ++++-- .../lib/ShieldPreferences.jsm | 8 +- .../lib/ShieldRecipeClient.jsm | 21 ++-- .../lib/TelemetryEvents.jsm | 36 +++++++ .../test/browser/browser_AddonStudies.js | 57 ++++++++++- .../browser/browser_PreferenceExperiments.js | 95 ++++++++++++++++--- .../browser/browser_ShieldRecipeClient.js | 19 ++++ .../shield-recipe-client/test/browser/head.js | 3 + .../vendor/LICENSE_THIRDPARTY | 87 +++++++---------- 14 files changed, 302 insertions(+), 107 deletions(-) create mode 100644 browser/extensions/shield-recipe-client/lib/TelemetryEvents.jsm diff --git a/browser/extensions/shield-recipe-client/bootstrap.js b/browser/extensions/shield-recipe-client/bootstrap.js index 4105e4b9d030..62001f3fa00c 100644 --- a/browser/extensions/shield-recipe-client/bootstrap.js +++ b/browser/extensions/shield-recipe-client/bootstrap.js @@ -202,6 +202,7 @@ this.Bootstrap = { "lib/ShieldPreferences.jsm", "lib/ShieldRecipeClient.jsm", "lib/Storage.jsm", + "lib/TelemetryEvents.jsm", "lib/Uptake.jsm", "lib/Utils.jsm", ].map(m => `resource://shield-recipe-client/${m}`); diff --git a/browser/extensions/shield-recipe-client/content/AboutPages.jsm b/browser/extensions/shield-recipe-client/content/AboutPages.jsm index 009db4a3c08d..eac6334054da 100644 --- a/browser/extensions/shield-recipe-client/content/AboutPages.jsm +++ b/browser/extensions/shield-recipe-client/content/AboutPages.jsm @@ -165,7 +165,7 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => { this.sendStudyList(message.target); break; case "Shield:RemoveStudy": - this.removeStudy(message.data); + this.removeStudy(message.data.recipeId, message.data.reason); break; case "Shield:OpenDataPreferences": this.openDataPreferences(); @@ -195,8 +195,8 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => { * Disable an active study and remove its add-on. * @param {String} studyName */ - async removeStudy(recipeId) { - await AddonStudies.stop(recipeId); + async removeStudy(recipeId, reason) { + await AddonStudies.stop(recipeId, reason); // Update any open tabs with the new study list now that it has changed. Services.mm.broadcastAsyncMessage("Shield:ReceiveStudyList", { diff --git a/browser/extensions/shield-recipe-client/content/about-studies/shield-studies.js b/browser/extensions/shield-recipe-client/content/about-studies/shield-studies.js index 41ad2f43c5a1..f2e03a9ab640 100644 --- a/browser/extensions/shield-recipe-client/content/about-studies/shield-studies.js +++ b/browser/extensions/shield-recipe-client/content/about-studies/shield-studies.js @@ -109,7 +109,7 @@ class StudyListItem extends React.Component { } handleClickRemove() { - sendPageEvent("RemoveStudy", this.props.study.recipeId); + sendPageEvent("RemoveStudy", {recipeId: this.props.study.recipeId, reason: "individual-opt-out"}); } render() { diff --git a/browser/extensions/shield-recipe-client/install.rdf.in b/browser/extensions/shield-recipe-client/install.rdf.in index afbd1e03e286..c737212f0519 100644 --- a/browser/extensions/shield-recipe-client/install.rdf.in +++ b/browser/extensions/shield-recipe-client/install.rdf.in @@ -8,7 +8,7 @@ 2 true false - 80 + 83 Shield Recipe Client Client to download and run recipes for SHIELD, Heartbeat, etc. true diff --git a/browser/extensions/shield-recipe-client/lib/AddonStudies.jsm b/browser/extensions/shield-recipe-client/lib/AddonStudies.jsm index f383de9c62e6..b4971bb07ec5 100644 --- a/browser/extensions/shield-recipe-client/lib/AddonStudies.jsm +++ b/browser/extensions/shield-recipe-client/lib/AddonStudies.jsm @@ -35,10 +35,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/Fil XPCOMUtils.defineLazyModuleGetter(this, "IndexedDB", "resource://gre/modules/IndexedDB.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Addons", "resource://shield-recipe-client/lib/Addons.jsm"); -XPCOMUtils.defineLazyModuleGetter( - this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm" -); +XPCOMUtils.defineLazyModuleGetter(this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LogManager", "resource://shield-recipe-client/lib/LogManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEvents", "resource://shield-recipe-client/lib/TelemetryEvents.jsm"); Cu.importGlobalProperties(["fetch"]); /* globals fetch */ @@ -92,12 +91,23 @@ function getStore(db) { * Mark a study object as having ended. Modifies the study in-place. * @param {IDBDatabase} db * @param {Study} study + * @param {String} reason Why the study is ending. */ -async function markAsEnded(db, study) { +async function markAsEnded(db, study, reason) { + if (reason === "unknown") { + log.warn(`Study ${study.name} ending for unknown reason.`); + } + study.active = false; study.studyEndDate = new Date(); await getStore(db).put(study); + Services.obs.notifyObservers(study, STUDY_ENDED_TOPIC, `${study.recipeId}`); + TelemetryEvents.sendEvent("unenroll", "addon_study", study.name, { + addonId: study.addonId, + addonVersion: study.addonVersion, + reason, + }); } this.AddonStudies = { @@ -145,7 +155,7 @@ this.AddonStudies = { for (const study of activeStudies) { const addon = await AddonManager.getAddonByID(study.addonId); if (!addon) { - await markAsEnded(db, study); + await markAsEnded(db, study, "uninstalled-sideload"); } } await this.close(); @@ -168,7 +178,7 @@ this.AddonStudies = { // Use a dedicated DB connection instead of the shared one so that we can // close it without fear of affecting other users of the shared connection. const db = await openDatabase(); - await markAsEnded(db, matchingStudy); + await markAsEnded(db, matchingStudy, "uninstalled"); await db.close(); } }, @@ -258,12 +268,24 @@ this.AddonStudies = { studyStartDate: new Date(), }; + TelemetryEvents.sendEvent("enroll", "addon_study", name, { + addonId: install.addon.id, + addonVersion: install.addon.version, + }); + try { await getStore(db).add(study); await Addons.applyInstall(install, false); return study; } catch (err) { await getStore(db).delete(recipeId); + + TelemetryEvents.sendEvent("unenroll", "addon_study", name, { + reason: "install-failure", + addonId: install.addon.id, + addonVersion: install.addon.version, + }); + throw err; } finally { Services.obs.notifyObservers(addonFile, "flush-cache-entry"); @@ -300,21 +322,22 @@ this.AddonStudies = { /** * Stop an active study, uninstalling the associated add-on. * @param {Number} recipeId + * @param {String} reason Why the study is ending. Optional, defaults to "unknown". * @throws * If no study is found with the given recipeId. * If the study is already inactive. */ - async stop(recipeId) { + async stop(recipeId, reason = "unknown") { const db = await getDatabase(); const study = await getStore(db).get(recipeId); if (!study) { - throw new Error(`No study found for recipe ${recipeId}`); + throw new Error(`No study found for recipe ${recipeId}.`); } if (!study.active) { throw new Error(`Cannot stop study for recipe ${recipeId}; it is already inactive.`); } - await markAsEnded(db, study); + await markAsEnded(db, study, reason); try { await Addons.uninstall(study.addonId); diff --git a/browser/extensions/shield-recipe-client/lib/PreferenceExperiments.jsm b/browser/extensions/shield-recipe-client/lib/PreferenceExperiments.jsm index 2811da4025ba..8f75f0618f97 100644 --- a/browser/extensions/shield-recipe-client/lib/PreferenceExperiments.jsm +++ b/browser/extensions/shield-recipe-client/lib/PreferenceExperiments.jsm @@ -61,6 +61,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "JSONFile", "resource://gre/modules/JSON XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LogManager", "resource://shield-recipe-client/lib/LogManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment", "resource://gre/modules/TelemetryEnvironment.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEvents", "resource://shield-recipe-client/lib/TelemetryEvents.jsm"); this.EXPORTED_SYMBOLS = ["PreferenceExperiments"]; @@ -186,7 +187,10 @@ this.PreferenceExperiments = { if (getPref(UserPreferences, experiment.preferenceName, experiment.preferenceType) !== experiment.preferenceValue) { // if not, stop the experiment, and skip the remaining steps log.info(`Stopping experiment "${experiment.name}" because its value changed`); - await this.stop(experiment.name, false); + await this.stop(experiment.name, { + didResetValue: false, + reason: "user-preference-changed-sideload", + }); continue; } @@ -351,6 +355,7 @@ this.PreferenceExperiments = { store.saveSoon(); TelemetryEnvironment.setExperimentActive(name, branch, {type: EXPERIMENT_TYPE_PREFIX + experimentType}); + TelemetryEvents.sendEvent("enroll", "preference_study", name, {experimentType, branch}); await this.saveStartupPrefs(); }, @@ -377,8 +382,10 @@ this.PreferenceExperiments = { observer() { const newValue = getPref(UserPreferences, preferenceName, preferenceType); if (newValue !== preferenceValue) { - PreferenceExperiments.stop(experimentName, false) - .catch(Cu.reportError); + PreferenceExperiments.stop(experimentName, { + didResetValue: false, + reason: "user-preference-changed", + }).catch(Cu.reportError); } }, }; @@ -448,14 +455,22 @@ this.PreferenceExperiments = { * Stop an active experiment, deactivate preference watchers, and optionally * reset the associated preference to its previous value. * @param {string} experimentName - * @param {boolean} [resetValue=true] - * If true, reset the preference to its original value. + * @param {Object} options + * @param {boolean} [options.resetValue = true] + * If true, reset the preference to its original value prior to + * the experiment. Optional, defauls to true. + * @param {String} [options.reason = "unknown"] + * Reason that the experiment is ending. Optional, defaults to + * "unknown". * @rejects {Error} * If there is no stored experiment with the given name, or if the * experiment has already expired. */ - async stop(experimentName, resetValue = true) { + async stop(experimentName, {resetValue = true, reason = "unknown"} = {}) { log.debug(`PreferenceExperiments.stop(${experimentName})`); + if (reason === "unknown") { + log.warn(`experiment ${experimentName} ending for unknown reason`); + } const store = await ensureStorage(); if (!(experimentName in store.data)) { @@ -496,6 +511,10 @@ this.PreferenceExperiments = { store.saveSoon(); TelemetryEnvironment.setExperimentInactive(experimentName, experiment.branch); + TelemetryEvents.sendEvent("unenroll", "preference_study", experimentName, { + didResetValue: resetValue ? "true" : "false", + reason, + }); await this.saveStartupPrefs(); }, diff --git a/browser/extensions/shield-recipe-client/lib/ShieldPreferences.jsm b/browser/extensions/shield-recipe-client/lib/ShieldPreferences.jsm index c2a1e9e61ffe..5530a8d53dd6 100644 --- a/browser/extensions/shield-recipe-client/lib/ShieldPreferences.jsm +++ b/browser/extensions/shield-recipe-client/lib/ShieldPreferences.jsm @@ -73,22 +73,24 @@ this.ShieldPreferences = { let prefValue; switch (prefName) { // If the FHR pref changes, set the opt-out-study pref to the value it is changing to. - case FHR_UPLOAD_ENABLED_PREF: + case FHR_UPLOAD_ENABLED_PREF: { prefValue = Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED_PREF); Services.prefs.setBoolPref(OPT_OUT_STUDIES_ENABLED_PREF, prefValue); break; + } // If the opt-out pref changes to be false, disable all current studies. - case OPT_OUT_STUDIES_ENABLED_PREF: + case OPT_OUT_STUDIES_ENABLED_PREF: { prefValue = Services.prefs.getBoolPref(OPT_OUT_STUDIES_ENABLED_PREF); if (!prefValue) { for (const study of await AddonStudies.getAll()) { if (study.active) { - await AddonStudies.stop(study.recipeId); + await AddonStudies.stop(study.recipeId, "general-opt-out"); } } } break; + } } }, diff --git a/browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm b/browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm index d2e4d97b258e..099c10d2ae2f 100644 --- a/browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm +++ b/browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm @@ -22,22 +22,11 @@ XPCOMUtils.defineLazyModuleGetter(this, "ShieldPreferences", "resource://shield-recipe-client/lib/ShieldPreferences.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonStudies", "resource://shield-recipe-client/lib/AddonStudies.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEvents", + "resource://shield-recipe-client/lib/TelemetryEvents.jsm"); this.EXPORTED_SYMBOLS = ["ShieldRecipeClient"]; -const {PREF_STRING, PREF_BOOL, PREF_INT} = Ci.nsIPrefBranch; - -const REASONS = { - APP_STARTUP: 1, // The application is starting up. - APP_SHUTDOWN: 2, // The application is shutting down. - ADDON_ENABLE: 3, // The add-on is being enabled. - ADDON_DISABLE: 4, // The add-on is being disabled. (Also sent during uninstallation) - ADDON_INSTALL: 5, // The add-on is being installed. - ADDON_UNINSTALL: 6, // The add-on is being uninstalled. - ADDON_UPGRADE: 7, // The add-on is being upgraded. - ADDON_DOWNGRADE: 8, // The add-on is being downgraded. -}; -const PREF_DEV_MODE = "extensions.shield-recipe-client.dev_mode"; const PREF_LOGGING_LEVEL = "extensions.shield-recipe-client.logging.level"; const SHIELD_INIT_NOTIFICATION = "shield-init-complete"; @@ -82,6 +71,12 @@ this.ShieldRecipeClient = { log.error("Failed to initialize preferences UI:", err); } + try { + TelemetryEvents.init(); + } catch (err) { + log.error("Failed to initialize telemetry events:", err); + } + await RecipeRunner.init(); Services.obs.notifyObservers(null, SHIELD_INIT_NOTIFICATION); }, diff --git a/browser/extensions/shield-recipe-client/lib/TelemetryEvents.jsm b/browser/extensions/shield-recipe-client/lib/TelemetryEvents.jsm new file mode 100644 index 000000000000..11be19da1d19 --- /dev/null +++ b/browser/extensions/shield-recipe-client/lib/TelemetryEvents.jsm @@ -0,0 +1,36 @@ +/* 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/. */ +"use strict"; + +const {utils: Cu, interfaces: Ci} = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +this.EXPORTED_SYMBOLS = ["TelemetryEvents"]; + +const TELEMETRY_CATEGORY = "normandy"; + +const TelemetryEvents = { + init() { + Services.telemetry.registerEvents(TELEMETRY_CATEGORY, { + enroll: { + methods: ["enroll"], + objects: ["preference_study", "addon_study"], + extra_keys: ["experimentType", "branch", "addonId", "addonVersion"], + record_on_release: true, + }, + + unenroll: { + methods: ["unenroll"], + objects: ["preference_study", "addon_study"], + extra_keys: ["reason", "didResetValue", "addonId", "addonVersion"], + record_on_release: true, + }, + }); + }, + + sendEvent(method, object, value, extra) { + Services.telemetry.recordEvent(TELEMETRY_CATEGORY, method, object, value, extra); + }, +}; diff --git a/browser/extensions/shield-recipe-client/test/browser/browser_AddonStudies.js b/browser/extensions/shield-recipe-client/test/browser/browser_AddonStudies.js index 3f8b382f3972..99cc33a8910e 100644 --- a/browser/extensions/shield-recipe-client/test/browser/browser_AddonStudies.js +++ b/browser/extensions/shield-recipe-client/test/browser/browser_AddonStudies.js @@ -5,6 +5,7 @@ Cu.import("resource://testing-common/TestUtils.jsm", this); Cu.import("resource://testing-common/AddonTestUtils.jsm", this); Cu.import("resource://shield-recipe-client/lib/Addons.jsm", this); Cu.import("resource://shield-recipe-client/lib/AddonStudies.jsm", this); +Cu.import("resource://shield-recipe-client/lib/TelemetryEvents.jsm", this); // Initialize test utils AddonTestUtils.initMochitest(this); @@ -175,8 +176,9 @@ decorate_task( decorate_task( withWebExtension({version: "2.0"}), + withStub(TelemetryEvents, "sendEvent"), AddonStudies.withStudies(), - async function testStart([addonId, addonFile]) { + async function testStart([addonId, addonFile], sendEventStub) { const startupPromise = AddonTestUtils.promiseWebExtensionStartup(addonId); const addonUrl = Services.io.newFileURI(addonFile).spec; @@ -211,6 +213,12 @@ decorate_task( ); ok(study.studyStartDate, "start assigns a value to the study start date."); + Assert.deepEqual( + sendEventStub.getCall(0).args, + ["enroll", "addon_study", args.name, {addonId, addonVersion: "2.0"}], + "AddonStudies.start() should send the correct telemetry event" + ); + await AddonStudies.stop(args.recipeId); } ); @@ -245,14 +253,25 @@ decorate_task( studyFactory({active: true, addonId: testStopId, studyEndDate: null}), ]), withInstalledWebExtension({id: testStopId}), - async function testStop([study], [addonId, addonFile]) { - await AddonStudies.stop(study.recipeId); + withStub(TelemetryEvents, "sendEvent"), + async function testStop([study], [addonId, addonFile], sendEventStub) { + await AddonStudies.stop(study.recipeId, "test-reason"); const newStudy = await AddonStudies.get(study.recipeId); ok(!newStudy.active, "stop marks the study as inactive."); ok(newStudy.studyEndDate, "stop saves the study end date."); const addon = await Addons.get(addonId); is(addon, null, "stop uninstalls the study add-on."); + + Assert.deepEqual( + sendEventStub.getCall(0).args, + ["unenroll", "addon_study", study.name, { + addonId, + addonVersion: study.addonVersion, + reason: "test-reason", + }], + "stop should send the correct telemetry event" + ); } ); @@ -280,16 +299,26 @@ decorate_task( studyFactory({active: true, addonId: "installed@example.com"}), studyFactory({active: false, addonId: "already.gone@example.com", studyEndDate: new Date(2012, 1)}), ]), + withStub(TelemetryEvents, "sendEvent"), withInstalledWebExtension({id: "installed@example.com"}), - async function testInit([activeStudy, activeInstalledStudy, inactiveStudy]) { + async function testInit([activeUninstalledStudy, activeInstalledStudy, inactiveStudy], sendEventStub) { await AddonStudies.init(); - const newActiveStudy = await AddonStudies.get(activeStudy.recipeId); + const newActiveStudy = await AddonStudies.get(activeUninstalledStudy.recipeId); ok(!newActiveStudy.active, "init marks studies as inactive if their add-on is not installed."); ok( newActiveStudy.studyEndDate, "init sets the study end date if a study's add-on is not installed." ); + Assert.deepEqual( + sendEventStub.getCall(0).args, + ["unenroll", "addon_study", activeUninstalledStudy.name, { + addonId: activeUninstalledStudy.addonId, + addonVersion: activeUninstalledStudy.addonVersion, + reason: "uninstalled-sideload", + }], + "AddonStudies.init() should send the correct telemetry event" + ); const newInactiveStudy = await AddonStudies.get(inactiveStudy.recipeId); is( @@ -304,6 +333,9 @@ decorate_task( newActiveInstalledStudy, "init does not modify studies whose add-on is still installed." ); + + // Only activeUninstalledStudy should have generated any events + ok(sendEventStub.calledOnce); } ); @@ -324,3 +356,18 @@ decorate_task( ); } ); + +// stop should pass "unknown" to TelemetryEvents for `reason` if none specified +decorate_task( + AddonStudies.withStudies([studyFactory({ active: true })]), + withStub(TelemetryEvents, "sendEvent"), + async function testStopUnknownReason([study], sendEventStub) { + await AddonStudies.stop(study.recipeId); + is( + sendEventStub.getCall(0).args[3].reason, + "unknown", + "stop should send the correct telemetry event", + "AddonStudies.stop() should use unknown as the default reason", + ); + } +); diff --git a/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js b/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js index 6388a78ce552..aac3f2aef34e 100644 --- a/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js +++ b/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js @@ -4,6 +4,7 @@ Cu.import("resource://gre/modules/Preferences.jsm", this); Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this); Cu.import("resource://shield-recipe-client/lib/PreferenceExperiments.jsm", this); Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm", this); +Cu.import("resource://shield-recipe-client/lib/TelemetryEvents.jsm", this); // Save ourselves some typing const {withMockExperiments} = PreferenceExperiments; @@ -101,7 +102,8 @@ decorate_task( withMockExperiments, withMockPreferences, withStub(PreferenceExperiments, "startObserver"), - async function testStart(experiments, mockPreferences, startObserverStub) { + withStub(TelemetryEvents, "sendEvent"), + async function testStart(experiments, mockPreferences, startObserverStub, sendEventStub) { mockPreferences.set("fake.preference", "oldvalue", "default"); mockPreferences.set("fake.preference", "uservalue", "user"); @@ -407,8 +409,12 @@ decorate_task( withMockExperiments, withMockPreferences, withSpy(PreferenceExperiments, "stopObserver"), - async function testStop(experiments, mockPreferences, stopObserverSpy) { + withStub(TelemetryEvents, "sendEvent"), + async function testStop(experiments, mockPreferences, stopObserverSpy, sendEventStub) { + // this assertion is mostly useful for --verify test runs, to make + // sure that tests clean up correctly. is(Preferences.get("fake.preference"), null, "preference should start unset"); + mockPreferences.set(`${startupPrefs}.fake.preference`, "experimentvalue", "user"); mockPreferences.set("fake.preference", "experimentvalue", "default"); experiments.test = experimentFactory({ @@ -422,7 +428,7 @@ decorate_task( }); PreferenceExperiments.startObserver("test", "fake.preference", "string", "experimentvalue"); - await PreferenceExperiments.stop("test"); + await PreferenceExperiments.stop("test", {reason: "test-reason"}); ok(stopObserverSpy.calledWith("test"), "stop removed an observer"); is(experiments.test.expired, true, "stop marked the experiment as expired"); is( @@ -435,6 +441,15 @@ decorate_task( "stop cleared the startup preference for fake.preference.", ); + Assert.deepEqual( + sendEventStub.getCall(0).args, + ["unenroll", "preference_study", experiments.test.name, { + didResetValue: "true", + reason: "test-reason", + }], + "stop should send the correct telemetry event" + ); + PreferenceExperiments.stopAllObservers(); }, ); @@ -505,8 +520,8 @@ decorate_task( withMockExperiments, withMockPreferences, withStub(PreferenceExperiments, "stopObserver"), - - async function(experiments, mockPreferences, stopObserver) { + withStub(TelemetryEvents, "sendEvent"), + async function testStopReset(experiments, mockPreferences, stopObserverStub, sendEventStub) { mockPreferences.set("fake.preference", "customvalue", "default"); experiments.test = experimentFactory({ name: "test", @@ -518,12 +533,20 @@ decorate_task( peferenceBranchType: "default", }); - await PreferenceExperiments.stop("test", false); + await PreferenceExperiments.stop("test", {reason: "test-reason", resetValue: false}); is( DefaultPreferences.get("fake.preference"), "customvalue", "stop did not modify the preference", ); + Assert.deepEqual( + sendEventStub.getCall(0).args, + ["unenroll", "preference_study", experiments.test.name, { + didResetValue: "false", + reason: "test-reason", + }], + "stop should send the correct telemetry event" + ); } ); @@ -678,7 +701,8 @@ decorate_task( withMockExperiments, withStub(TelemetryEnvironment, "setExperimentActive"), withStub(TelemetryEnvironment, "setExperimentInactive"), - async function testInitTelemetry(experiments, setActiveStub, setInactiveStub) { + withStub(TelemetryEvents, "sendEvent"), + async function testStartAndStopTelemetry(experiments, setActiveStub, setInactiveStub, sendEventStub) { await PreferenceExperiments.start({ name: "test", branch: "branch", @@ -691,10 +715,28 @@ decorate_task( Assert.deepEqual( setActiveStub.getCall(0).args, ["test", "branch", {type: "normandy-exp"}], - "Experiment is registerd by start()", + "Experiment is registered by start()", ); - await PreferenceExperiments.stop("test"); + await PreferenceExperiments.stop("test", {reason: "test-reason"}); ok(setInactiveStub.calledWith("test", "branch"), "Experiment is unregistered by stop()"); + + Assert.deepEqual( + sendEventStub.getCall(0).args, + ["enroll", "preference_study", "test", { + experimentType: "exp", + branch: "branch", + }], + "PreferenceExperiments.start() should send the correct telemetry event" + ); + + Assert.deepEqual( + sendEventStub.getCall(1).args, + ["unenroll", "preference_study", "test", { + reason: "test-reason", + didResetValue: "true", + }], + "PreferenceExperiments.stop() should send the correct telemetry event" + ); }, ); @@ -703,7 +745,8 @@ decorate_task( withMockExperiments, withStub(TelemetryEnvironment, "setExperimentActive"), withStub(TelemetryEnvironment, "setExperimentInactive"), - async function testInitTelemetry(experiments, setActiveStub, setInactiveStub) { + withStub(TelemetryEvents, "sendEvent"), + async function testInitTelemetryExperimentType(experiments, setActiveStub, setInactiveStub, sendEventStub) { await PreferenceExperiments.start({ name: "test", branch: "branch", @@ -720,6 +763,15 @@ decorate_task( "start() should register the experiment with the provided type", ); + Assert.deepEqual( + sendEventStub.getCall(0).args, + ["enroll", "preference_study", "test", { + experimentType: "pref-test", + branch: "branch", + }], + "start should include the passed reason in the telemetry event" + ); + // start sets the passed preference in a way that is hard to mock. // Reset the preference so it doesn't interfere with other tests. Services.prefs.getDefaultBranch("fake.preference").deleteBranch(""); @@ -742,7 +794,8 @@ decorate_task( withMockExperiments, withMockPreferences, withStub(PreferenceExperiments, "stop"), - async function testInitChanges(experiments, mockPreferences, stopStub) { + withStub(TelemetryEvents, "sendEvent"), + async function testInitChanges(experiments, mockPreferences, stopStub, sendEventStub) { mockPreferences.set("fake.preference", "experiment value", "default"); experiments.test = experimentFactory({ name: "test", @@ -753,7 +806,7 @@ decorate_task( await PreferenceExperiments.init(); ok(stopStub.calledWith("test"), "Experiment is stopped because value changed"); is(Preferences.get("fake.preference"), "changed value", "Preference value was not changed"); - } + }, ); // init should register an observer for experiments @@ -935,3 +988,21 @@ decorate_task( ); }, ); + +// stop should pass "unknown" to telemetry event for `reason` if none is specified +decorate_task( + withMockExperiments, + withMockPreferences, + withStub(PreferenceExperiments, "stopObserver"), + withStub(TelemetryEvents, "sendEvent"), + async function testStopUnknownReason(experiments, mockPreferences, stopObserverStub, sendEventStub) { + mockPreferences.set("fake.preference", "default value", "default"); + experiments.test = experimentFactory({ name: "test", preferenceName: "fake.preference" }); + await PreferenceExperiments.stop("test"); + is( + sendEventStub.getCall(0).args[3].reason, + "unknown", + "PreferenceExperiments.stop() should use unknown as the default reason", + ); + } +); diff --git a/browser/extensions/shield-recipe-client/test/browser/browser_ShieldRecipeClient.js b/browser/extensions/shield-recipe-client/test/browser/browser_ShieldRecipeClient.js index 65f42314afdc..5ab592e15264 100644 --- a/browser/extensions/shield-recipe-client/test/browser/browser_ShieldRecipeClient.js +++ b/browser/extensions/shield-recipe-client/test/browser/browser_ShieldRecipeClient.js @@ -5,6 +5,7 @@ Cu.import("resource://shield-recipe-client/lib/RecipeRunner.jsm", this); Cu.import("resource://shield-recipe-client/lib/PreferenceExperiments.jsm", this); Cu.import("resource://shield-recipe-client-content/AboutPages.jsm", this); Cu.import("resource://shield-recipe-client/lib/AddonStudies.jsm", this); +Cu.import("resource://shield-recipe-client/lib/TelemetryEvents.jsm", this); function withStubInits(testFunction) { return decorate( @@ -12,6 +13,7 @@ function withStubInits(testFunction) { withStub(AddonStudies, "init"), withStub(PreferenceExperiments, "init"), withStub(RecipeRunner, "init"), + withStub(TelemetryEvents, "init"), testFunction ); } @@ -39,6 +41,7 @@ decorate_task( ok(AddonStudies.init.called, "startup calls AddonStudies.init"); ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); ok(RecipeRunner.init.called, "startup calls RecipeRunner.init"); + ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init"); } ); @@ -52,6 +55,7 @@ decorate_task( ok(AddonStudies.init.called, "startup calls AddonStudies.init"); ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); ok(RecipeRunner.init.called, "startup calls RecipeRunner.init"); + ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init"); } ); @@ -65,5 +69,20 @@ decorate_task( ok(AddonStudies.init.called, "startup calls AddonStudies.init"); ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); ok(RecipeRunner.init.called, "startup calls RecipeRunner.init"); + ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init"); + } +); + +decorate_task( + withStubInits, + async function testStartupTelemetryEventsInitFail() { + TelemetryEvents.init.throws(); + + await ShieldRecipeClient.startup(); + ok(AboutPages.init.called, "startup calls AboutPages.init"); + ok(AddonStudies.init.called, "startup calls AddonStudies.init"); + ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); + ok(RecipeRunner.init.called, "startup calls RecipeRunner.init"); + ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init"); } ); diff --git a/browser/extensions/shield-recipe-client/test/browser/head.js b/browser/extensions/shield-recipe-client/test/browser/head.js index 77c57bff2624..8c65119e8478 100644 --- a/browser/extensions/shield-recipe-client/test/browser/head.js +++ b/browser/extensions/shield-recipe-client/test/browser/head.js @@ -8,6 +8,7 @@ Cu.import("resource://shield-recipe-client/lib/Addons.jsm", this); Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm", this); Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this); Cu.import("resource://shield-recipe-client/lib/NormandyApi.jsm", this); +Cu.import("resource://shield-recipe-client/lib/TelemetryEvents.jsm", this); Cu.import("resource://shield-recipe-client/lib/Utils.jsm", this); // Load mocking/stubbing library, sinon @@ -25,6 +26,8 @@ registerCleanupFunction(async function() { delete window.sinon; }); +// Prep Telemetry to receive events from tests +TelemetryEvents.init(); this.UUID_REGEX = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/; diff --git a/browser/extensions/shield-recipe-client/vendor/LICENSE_THIRDPARTY b/browser/extensions/shield-recipe-client/vendor/LICENSE_THIRDPARTY index de7be412c04e..13a9d68f5095 100644 --- a/browser/extensions/shield-recipe-client/vendor/LICENSE_THIRDPARTY +++ b/browser/extensions/shield-recipe-client/vendor/LICENSE_THIRDPARTY @@ -1,35 +1,24 @@ -fbjs@0.8.14 BSD-3-Clause -BSD License - -For fbjs software +fbjs@0.8.16 MIT +MIT License Copyright (c) 2013-present, Facebook, Inc. -All rights reserved. -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * Neither the name Facebook nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. react-dom@15.6.1 BSD-3-Clause @@ -157,38 +146,28 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -create-react-class@15.6.0 BSD-3-Clause -BSD License - -For React software +create-react-class@15.6.2 MIT +MIT License Copyright (c) 2013-present, Facebook, Inc. -All rights reserved. -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * Neither the name Facebook nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. mozjexl@1.1.5 MIT