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/ShieldRecipeClient.jsm",
"lib/Storage.jsm",
"lib/TelemetryEvents.jsm",
"lib/Uptake.jsm",
"lib/Utils.jsm",
].map(m => `resource://shield-recipe-client/${m}`);

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

@ -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", {

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

@ -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() {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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}/;

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

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