Bug 1432986 - Sync shield-recipe-client v83 from Github (commit 43f0ce2) r=Gijs

MozReview-Commit-ID: 9TGdnKZ5zaf

--HG--
extra : rebase_source : ba22c24bd1d8a83592ec63cf40f789f51680b9a9
This commit is contained in:
Mike Cooper 2018-01-24 15:40:22 -08:00
Родитель 1d38c4309b
Коммит 976e9ae554
14 изменённых файлов: 302 добавлений и 107 удалений

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

@ -202,6 +202,7 @@ this.Bootstrap = {
"lib/ShieldPreferences.jsm", "lib/ShieldPreferences.jsm",
"lib/ShieldRecipeClient.jsm", "lib/ShieldRecipeClient.jsm",
"lib/Storage.jsm", "lib/Storage.jsm",
"lib/TelemetryEvents.jsm",
"lib/Uptake.jsm", "lib/Uptake.jsm",
"lib/Utils.jsm", "lib/Utils.jsm",
].map(m => `resource://shield-recipe-client/${m}`); ].map(m => `resource://shield-recipe-client/${m}`);

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

@ -165,7 +165,7 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
this.sendStudyList(message.target); this.sendStudyList(message.target);
break; break;
case "Shield:RemoveStudy": case "Shield:RemoveStudy":
this.removeStudy(message.data); this.removeStudy(message.data.recipeId, message.data.reason);
break; break;
case "Shield:OpenDataPreferences": case "Shield:OpenDataPreferences":
this.openDataPreferences(); this.openDataPreferences();
@ -195,8 +195,8 @@ XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => {
* Disable an active study and remove its add-on. * Disable an active study and remove its add-on.
* @param {String} studyName * @param {String} studyName
*/ */
async removeStudy(recipeId) { async removeStudy(recipeId, reason) {
await AddonStudies.stop(recipeId); await AddonStudies.stop(recipeId, reason);
// Update any open tabs with the new study list now that it has changed. // Update any open tabs with the new study list now that it has changed.
Services.mm.broadcastAsyncMessage("Shield:ReceiveStudyList", { Services.mm.broadcastAsyncMessage("Shield:ReceiveStudyList", {

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

@ -109,7 +109,7 @@ class StudyListItem extends React.Component {
} }
handleClickRemove() { handleClickRemove() {
sendPageEvent("RemoveStudy", this.props.study.recipeId); sendPageEvent("RemoveStudy", {recipeId: this.props.study.recipeId, reason: "individual-opt-out"});
} }
render() { render() {

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

@ -8,7 +8,7 @@
<em:type>2</em:type> <em:type>2</em:type>
<em:bootstrap>true</em:bootstrap> <em:bootstrap>true</em:bootstrap>
<em:unpack>false</em:unpack> <em:unpack>false</em:unpack>
<em:version>80</em:version> <em:version>83</em:version>
<em:name>Shield Recipe Client</em:name> <em:name>Shield Recipe Client</em:name>
<em:description>Client to download and run recipes for SHIELD, Heartbeat, etc.</em:description> <em:description>Client to download and run recipes for SHIELD, Heartbeat, etc.</em:description>
<em:multiprocessCompatible>true</em:multiprocessCompatible> <em:multiprocessCompatible>true</em:multiprocessCompatible>

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

@ -35,10 +35,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/Fil
XPCOMUtils.defineLazyModuleGetter(this, "IndexedDB", "resource://gre/modules/IndexedDB.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "IndexedDB", "resource://gre/modules/IndexedDB.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Addons", "resource://shield-recipe-client/lib/Addons.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Addons", "resource://shield-recipe-client/lib/Addons.jsm");
XPCOMUtils.defineLazyModuleGetter( XPCOMUtils.defineLazyModuleGetter(this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm");
this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm"
);
XPCOMUtils.defineLazyModuleGetter(this, "LogManager", "resource://shield-recipe-client/lib/LogManager.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 */ Cu.importGlobalProperties(["fetch"]); /* globals fetch */
@ -92,12 +91,23 @@ function getStore(db) {
* Mark a study object as having ended. Modifies the study in-place. * Mark a study object as having ended. Modifies the study in-place.
* @param {IDBDatabase} db * @param {IDBDatabase} db
* @param {Study} study * @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.active = false;
study.studyEndDate = new Date(); study.studyEndDate = new Date();
await getStore(db).put(study); await getStore(db).put(study);
Services.obs.notifyObservers(study, STUDY_ENDED_TOPIC, `${study.recipeId}`); 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 = { this.AddonStudies = {
@ -145,7 +155,7 @@ this.AddonStudies = {
for (const study of activeStudies) { for (const study of activeStudies) {
const addon = await AddonManager.getAddonByID(study.addonId); const addon = await AddonManager.getAddonByID(study.addonId);
if (!addon) { if (!addon) {
await markAsEnded(db, study); await markAsEnded(db, study, "uninstalled-sideload");
} }
} }
await this.close(); await this.close();
@ -168,7 +178,7 @@ this.AddonStudies = {
// Use a dedicated DB connection instead of the shared one so that we can // 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. // close it without fear of affecting other users of the shared connection.
const db = await openDatabase(); const db = await openDatabase();
await markAsEnded(db, matchingStudy); await markAsEnded(db, matchingStudy, "uninstalled");
await db.close(); await db.close();
} }
}, },
@ -258,12 +268,24 @@ this.AddonStudies = {
studyStartDate: new Date(), studyStartDate: new Date(),
}; };
TelemetryEvents.sendEvent("enroll", "addon_study", name, {
addonId: install.addon.id,
addonVersion: install.addon.version,
});
try { try {
await getStore(db).add(study); await getStore(db).add(study);
await Addons.applyInstall(install, false); await Addons.applyInstall(install, false);
return study; return study;
} catch (err) { } catch (err) {
await getStore(db).delete(recipeId); await getStore(db).delete(recipeId);
TelemetryEvents.sendEvent("unenroll", "addon_study", name, {
reason: "install-failure",
addonId: install.addon.id,
addonVersion: install.addon.version,
});
throw err; throw err;
} finally { } finally {
Services.obs.notifyObservers(addonFile, "flush-cache-entry"); Services.obs.notifyObservers(addonFile, "flush-cache-entry");
@ -300,21 +322,22 @@ this.AddonStudies = {
/** /**
* Stop an active study, uninstalling the associated add-on. * Stop an active study, uninstalling the associated add-on.
* @param {Number} recipeId * @param {Number} recipeId
* @param {String} reason Why the study is ending. Optional, defaults to "unknown".
* @throws * @throws
* If no study is found with the given recipeId. * If no study is found with the given recipeId.
* If the study is already inactive. * If the study is already inactive.
*/ */
async stop(recipeId) { async stop(recipeId, reason = "unknown") {
const db = await getDatabase(); const db = await getDatabase();
const study = await getStore(db).get(recipeId); const study = await getStore(db).get(recipeId);
if (!study) { if (!study) {
throw new Error(`No study found for recipe ${recipeId}`); throw new Error(`No study found for recipe ${recipeId}.`);
} }
if (!study.active) { if (!study.active) {
throw new Error(`Cannot stop study for recipe ${recipeId}; it is already inactive.`); throw new Error(`Cannot stop study for recipe ${recipeId}; it is already inactive.`);
} }
await markAsEnded(db, study); await markAsEnded(db, study, reason);
try { try {
await Addons.uninstall(study.addonId); await Addons.uninstall(study.addonId);

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

@ -61,6 +61,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "JSONFile", "resource://gre/modules/JSON
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LogManager", "resource://shield-recipe-client/lib/LogManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LogManager", "resource://shield-recipe-client/lib/LogManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment", "resource://gre/modules/TelemetryEnvironment.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"]; this.EXPORTED_SYMBOLS = ["PreferenceExperiments"];
@ -186,7 +187,10 @@ this.PreferenceExperiments = {
if (getPref(UserPreferences, experiment.preferenceName, experiment.preferenceType) !== experiment.preferenceValue) { if (getPref(UserPreferences, experiment.preferenceName, experiment.preferenceType) !== experiment.preferenceValue) {
// if not, stop the experiment, and skip the remaining steps // if not, stop the experiment, and skip the remaining steps
log.info(`Stopping experiment "${experiment.name}" because its value changed`); 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; continue;
} }
@ -351,6 +355,7 @@ this.PreferenceExperiments = {
store.saveSoon(); store.saveSoon();
TelemetryEnvironment.setExperimentActive(name, branch, {type: EXPERIMENT_TYPE_PREFIX + experimentType}); TelemetryEnvironment.setExperimentActive(name, branch, {type: EXPERIMENT_TYPE_PREFIX + experimentType});
TelemetryEvents.sendEvent("enroll", "preference_study", name, {experimentType, branch});
await this.saveStartupPrefs(); await this.saveStartupPrefs();
}, },
@ -377,8 +382,10 @@ this.PreferenceExperiments = {
observer() { observer() {
const newValue = getPref(UserPreferences, preferenceName, preferenceType); const newValue = getPref(UserPreferences, preferenceName, preferenceType);
if (newValue !== preferenceValue) { if (newValue !== preferenceValue) {
PreferenceExperiments.stop(experimentName, false) PreferenceExperiments.stop(experimentName, {
.catch(Cu.reportError); didResetValue: false,
reason: "user-preference-changed",
}).catch(Cu.reportError);
} }
}, },
}; };
@ -448,14 +455,22 @@ this.PreferenceExperiments = {
* Stop an active experiment, deactivate preference watchers, and optionally * Stop an active experiment, deactivate preference watchers, and optionally
* reset the associated preference to its previous value. * reset the associated preference to its previous value.
* @param {string} experimentName * @param {string} experimentName
* @param {boolean} [resetValue=true] * @param {Object} options
* If true, reset the preference to its original value. * @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} * @rejects {Error}
* If there is no stored experiment with the given name, or if the * If there is no stored experiment with the given name, or if the
* experiment has already expired. * experiment has already expired.
*/ */
async stop(experimentName, resetValue = true) { async stop(experimentName, {resetValue = true, reason = "unknown"} = {}) {
log.debug(`PreferenceExperiments.stop(${experimentName})`); log.debug(`PreferenceExperiments.stop(${experimentName})`);
if (reason === "unknown") {
log.warn(`experiment ${experimentName} ending for unknown reason`);
}
const store = await ensureStorage(); const store = await ensureStorage();
if (!(experimentName in store.data)) { if (!(experimentName in store.data)) {
@ -496,6 +511,10 @@ this.PreferenceExperiments = {
store.saveSoon(); store.saveSoon();
TelemetryEnvironment.setExperimentInactive(experimentName, experiment.branch); TelemetryEnvironment.setExperimentInactive(experimentName, experiment.branch);
TelemetryEvents.sendEvent("unenroll", "preference_study", experimentName, {
didResetValue: resetValue ? "true" : "false",
reason,
});
await this.saveStartupPrefs(); await this.saveStartupPrefs();
}, },

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

@ -73,22 +73,24 @@ this.ShieldPreferences = {
let prefValue; let prefValue;
switch (prefName) { switch (prefName) {
// If the FHR pref changes, set the opt-out-study pref to the value it is changing to. // 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); prefValue = Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED_PREF);
Services.prefs.setBoolPref(OPT_OUT_STUDIES_ENABLED_PREF, prefValue); Services.prefs.setBoolPref(OPT_OUT_STUDIES_ENABLED_PREF, prefValue);
break; break;
}
// If the opt-out pref changes to be false, disable all current studies. // 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); prefValue = Services.prefs.getBoolPref(OPT_OUT_STUDIES_ENABLED_PREF);
if (!prefValue) { if (!prefValue) {
for (const study of await AddonStudies.getAll()) { for (const study of await AddonStudies.getAll()) {
if (study.active) { if (study.active) {
await AddonStudies.stop(study.recipeId); await AddonStudies.stop(study.recipeId, "general-opt-out");
} }
} }
} }
break; break;
}
} }
}, },

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

@ -22,22 +22,11 @@ XPCOMUtils.defineLazyModuleGetter(this, "ShieldPreferences",
"resource://shield-recipe-client/lib/ShieldPreferences.jsm"); "resource://shield-recipe-client/lib/ShieldPreferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonStudies", XPCOMUtils.defineLazyModuleGetter(this, "AddonStudies",
"resource://shield-recipe-client/lib/AddonStudies.jsm"); "resource://shield-recipe-client/lib/AddonStudies.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEvents",
"resource://shield-recipe-client/lib/TelemetryEvents.jsm");
this.EXPORTED_SYMBOLS = ["ShieldRecipeClient"]; 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 PREF_LOGGING_LEVEL = "extensions.shield-recipe-client.logging.level";
const SHIELD_INIT_NOTIFICATION = "shield-init-complete"; const SHIELD_INIT_NOTIFICATION = "shield-init-complete";
@ -82,6 +71,12 @@ this.ShieldRecipeClient = {
log.error("Failed to initialize preferences UI:", err); log.error("Failed to initialize preferences UI:", err);
} }
try {
TelemetryEvents.init();
} catch (err) {
log.error("Failed to initialize telemetry events:", err);
}
await RecipeRunner.init(); await RecipeRunner.init();
Services.obs.notifyObservers(null, SHIELD_INIT_NOTIFICATION); Services.obs.notifyObservers(null, SHIELD_INIT_NOTIFICATION);
}, },

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

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

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

@ -5,6 +5,7 @@ Cu.import("resource://testing-common/TestUtils.jsm", this);
Cu.import("resource://testing-common/AddonTestUtils.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/Addons.jsm", this);
Cu.import("resource://shield-recipe-client/lib/AddonStudies.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 // Initialize test utils
AddonTestUtils.initMochitest(this); AddonTestUtils.initMochitest(this);
@ -175,8 +176,9 @@ decorate_task(
decorate_task( decorate_task(
withWebExtension({version: "2.0"}), withWebExtension({version: "2.0"}),
withStub(TelemetryEvents, "sendEvent"),
AddonStudies.withStudies(), AddonStudies.withStudies(),
async function testStart([addonId, addonFile]) { async function testStart([addonId, addonFile], sendEventStub) {
const startupPromise = AddonTestUtils.promiseWebExtensionStartup(addonId); const startupPromise = AddonTestUtils.promiseWebExtensionStartup(addonId);
const addonUrl = Services.io.newFileURI(addonFile).spec; 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."); 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); await AddonStudies.stop(args.recipeId);
} }
); );
@ -245,14 +253,25 @@ decorate_task(
studyFactory({active: true, addonId: testStopId, studyEndDate: null}), studyFactory({active: true, addonId: testStopId, studyEndDate: null}),
]), ]),
withInstalledWebExtension({id: testStopId}), withInstalledWebExtension({id: testStopId}),
async function testStop([study], [addonId, addonFile]) { withStub(TelemetryEvents, "sendEvent"),
await AddonStudies.stop(study.recipeId); async function testStop([study], [addonId, addonFile], sendEventStub) {
await AddonStudies.stop(study.recipeId, "test-reason");
const newStudy = await AddonStudies.get(study.recipeId); const newStudy = await AddonStudies.get(study.recipeId);
ok(!newStudy.active, "stop marks the study as inactive."); ok(!newStudy.active, "stop marks the study as inactive.");
ok(newStudy.studyEndDate, "stop saves the study end date."); ok(newStudy.studyEndDate, "stop saves the study end date.");
const addon = await Addons.get(addonId); const addon = await Addons.get(addonId);
is(addon, null, "stop uninstalls the study add-on."); 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: true, addonId: "installed@example.com"}),
studyFactory({active: false, addonId: "already.gone@example.com", studyEndDate: new Date(2012, 1)}), studyFactory({active: false, addonId: "already.gone@example.com", studyEndDate: new Date(2012, 1)}),
]), ]),
withStub(TelemetryEvents, "sendEvent"),
withInstalledWebExtension({id: "installed@example.com"}), withInstalledWebExtension({id: "installed@example.com"}),
async function testInit([activeStudy, activeInstalledStudy, inactiveStudy]) { async function testInit([activeUninstalledStudy, activeInstalledStudy, inactiveStudy], sendEventStub) {
await AddonStudies.init(); 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.active, "init marks studies as inactive if their add-on is not installed.");
ok( ok(
newActiveStudy.studyEndDate, newActiveStudy.studyEndDate,
"init sets the study end date if a study's add-on is not installed." "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); const newInactiveStudy = await AddonStudies.get(inactiveStudy.recipeId);
is( is(
@ -304,6 +333,9 @@ decorate_task(
newActiveInstalledStudy, newActiveInstalledStudy,
"init does not modify studies whose add-on is still installed." "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",
);
}
);

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

@ -4,6 +4,7 @@ Cu.import("resource://gre/modules/Preferences.jsm", this);
Cu.import("resource://gre/modules/TelemetryEnvironment.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/PreferenceExperiments.jsm", this);
Cu.import("resource://shield-recipe-client/lib/CleanupManager.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 // Save ourselves some typing
const {withMockExperiments} = PreferenceExperiments; const {withMockExperiments} = PreferenceExperiments;
@ -101,7 +102,8 @@ decorate_task(
withMockExperiments, withMockExperiments,
withMockPreferences, withMockPreferences,
withStub(PreferenceExperiments, "startObserver"), 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", "oldvalue", "default");
mockPreferences.set("fake.preference", "uservalue", "user"); mockPreferences.set("fake.preference", "uservalue", "user");
@ -407,8 +409,12 @@ decorate_task(
withMockExperiments, withMockExperiments,
withMockPreferences, withMockPreferences,
withSpy(PreferenceExperiments, "stopObserver"), 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"); is(Preferences.get("fake.preference"), null, "preference should start unset");
mockPreferences.set(`${startupPrefs}.fake.preference`, "experimentvalue", "user"); mockPreferences.set(`${startupPrefs}.fake.preference`, "experimentvalue", "user");
mockPreferences.set("fake.preference", "experimentvalue", "default"); mockPreferences.set("fake.preference", "experimentvalue", "default");
experiments.test = experimentFactory({ experiments.test = experimentFactory({
@ -422,7 +428,7 @@ decorate_task(
}); });
PreferenceExperiments.startObserver("test", "fake.preference", "string", "experimentvalue"); 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"); ok(stopObserverSpy.calledWith("test"), "stop removed an observer");
is(experiments.test.expired, true, "stop marked the experiment as expired"); is(experiments.test.expired, true, "stop marked the experiment as expired");
is( is(
@ -435,6 +441,15 @@ decorate_task(
"stop cleared the startup preference for fake.preference.", "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(); PreferenceExperiments.stopAllObservers();
}, },
); );
@ -505,8 +520,8 @@ decorate_task(
withMockExperiments, withMockExperiments,
withMockPreferences, withMockPreferences,
withStub(PreferenceExperiments, "stopObserver"), withStub(PreferenceExperiments, "stopObserver"),
withStub(TelemetryEvents, "sendEvent"),
async function(experiments, mockPreferences, stopObserver) { async function testStopReset(experiments, mockPreferences, stopObserverStub, sendEventStub) {
mockPreferences.set("fake.preference", "customvalue", "default"); mockPreferences.set("fake.preference", "customvalue", "default");
experiments.test = experimentFactory({ experiments.test = experimentFactory({
name: "test", name: "test",
@ -518,12 +533,20 @@ decorate_task(
peferenceBranchType: "default", peferenceBranchType: "default",
}); });
await PreferenceExperiments.stop("test", false); await PreferenceExperiments.stop("test", {reason: "test-reason", resetValue: false});
is( is(
DefaultPreferences.get("fake.preference"), DefaultPreferences.get("fake.preference"),
"customvalue", "customvalue",
"stop did not modify the preference", "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, withMockExperiments,
withStub(TelemetryEnvironment, "setExperimentActive"), withStub(TelemetryEnvironment, "setExperimentActive"),
withStub(TelemetryEnvironment, "setExperimentInactive"), withStub(TelemetryEnvironment, "setExperimentInactive"),
async function testInitTelemetry(experiments, setActiveStub, setInactiveStub) { withStub(TelemetryEvents, "sendEvent"),
async function testStartAndStopTelemetry(experiments, setActiveStub, setInactiveStub, sendEventStub) {
await PreferenceExperiments.start({ await PreferenceExperiments.start({
name: "test", name: "test",
branch: "branch", branch: "branch",
@ -691,10 +715,28 @@ decorate_task(
Assert.deepEqual( Assert.deepEqual(
setActiveStub.getCall(0).args, setActiveStub.getCall(0).args,
["test", "branch", {type: "normandy-exp"}], ["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()"); 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, withMockExperiments,
withStub(TelemetryEnvironment, "setExperimentActive"), withStub(TelemetryEnvironment, "setExperimentActive"),
withStub(TelemetryEnvironment, "setExperimentInactive"), withStub(TelemetryEnvironment, "setExperimentInactive"),
async function testInitTelemetry(experiments, setActiveStub, setInactiveStub) { withStub(TelemetryEvents, "sendEvent"),
async function testInitTelemetryExperimentType(experiments, setActiveStub, setInactiveStub, sendEventStub) {
await PreferenceExperiments.start({ await PreferenceExperiments.start({
name: "test", name: "test",
branch: "branch", branch: "branch",
@ -720,6 +763,15 @@ decorate_task(
"start() should register the experiment with the provided type", "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. // start sets the passed preference in a way that is hard to mock.
// Reset the preference so it doesn't interfere with other tests. // Reset the preference so it doesn't interfere with other tests.
Services.prefs.getDefaultBranch("fake.preference").deleteBranch(""); Services.prefs.getDefaultBranch("fake.preference").deleteBranch("");
@ -742,7 +794,8 @@ decorate_task(
withMockExperiments, withMockExperiments,
withMockPreferences, withMockPreferences,
withStub(PreferenceExperiments, "stop"), 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"); mockPreferences.set("fake.preference", "experiment value", "default");
experiments.test = experimentFactory({ experiments.test = experimentFactory({
name: "test", name: "test",
@ -753,7 +806,7 @@ decorate_task(
await PreferenceExperiments.init(); await PreferenceExperiments.init();
ok(stopStub.calledWith("test"), "Experiment is stopped because value changed"); ok(stopStub.calledWith("test"), "Experiment is stopped because value changed");
is(Preferences.get("fake.preference"), "changed value", "Preference value was not changed"); is(Preferences.get("fake.preference"), "changed value", "Preference value was not changed");
} },
); );
// init should register an observer for experiments // 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",
);
}
);

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

@ -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/lib/PreferenceExperiments.jsm", this);
Cu.import("resource://shield-recipe-client-content/AboutPages.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/AddonStudies.jsm", this);
Cu.import("resource://shield-recipe-client/lib/TelemetryEvents.jsm", this);
function withStubInits(testFunction) { function withStubInits(testFunction) {
return decorate( return decorate(
@ -12,6 +13,7 @@ function withStubInits(testFunction) {
withStub(AddonStudies, "init"), withStub(AddonStudies, "init"),
withStub(PreferenceExperiments, "init"), withStub(PreferenceExperiments, "init"),
withStub(RecipeRunner, "init"), withStub(RecipeRunner, "init"),
withStub(TelemetryEvents, "init"),
testFunction testFunction
); );
} }
@ -39,6 +41,7 @@ decorate_task(
ok(AddonStudies.init.called, "startup calls AddonStudies.init"); ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.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(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.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(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.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");
} }
); );

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

@ -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/SandboxManager.jsm", this);
Cu.import("resource://shield-recipe-client/lib/NormandyDriver.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/NormandyApi.jsm", this);
Cu.import("resource://shield-recipe-client/lib/TelemetryEvents.jsm", this);
Cu.import("resource://shield-recipe-client/lib/Utils.jsm", this); Cu.import("resource://shield-recipe-client/lib/Utils.jsm", this);
// Load mocking/stubbing library, sinon // Load mocking/stubbing library, sinon
@ -25,6 +26,8 @@ registerCleanupFunction(async function() {
delete window.sinon; 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}/; this.UUID_REGEX = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/;

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

@ -1,35 +1,24 @@
fbjs@0.8.14 BSD-3-Clause fbjs@0.8.16 MIT
BSD License MIT License
For fbjs software
Copyright (c) 2013-present, Facebook, Inc. Copyright (c) 2013-present, Facebook, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Permission is hereby granted, free of charge, to any person obtaining a copy of
are permitted provided that the following conditions are met: 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 The above copyright notice and this permission notice shall be included in all
list of conditions and the following disclaimer. copies or substantial portions of the Software.
* Redistributions in binary form must reproduce the above copyright notice, THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
this list of conditions and the following disclaimer in the documentation IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
and/or other materials provided with the distribution. 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
* Neither the name Facebook nor the names of its contributors may be used to IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
endorse or promote products derived from this software without specific CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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.
react-dom@15.6.1 BSD-3-Clause 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 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
create-react-class@15.6.0 BSD-3-Clause create-react-class@15.6.2 MIT
BSD License MIT License
For React software
Copyright (c) 2013-present, Facebook, Inc. Copyright (c) 2013-present, Facebook, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Permission is hereby granted, free of charge, to any person obtaining a copy
are permitted provided that the following conditions are met: 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 The above copyright notice and this permission notice shall be included in all
list of conditions and the following disclaimer. copies or substantial portions of the Software.
* Redistributions in binary form must reproduce the above copyright notice, THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
this list of conditions and the following disclaimer in the documentation IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
and/or other materials provided with the distribution. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* Neither the name Facebook nor the names of its contributors may be used to LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
endorse or promote products derived from this software without specific OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
prior written permission. SOFTWARE.
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.
mozjexl@1.1.5 MIT mozjexl@1.1.5 MIT