зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
1d38c4309b
Коммит
976e9ae554
|
@ -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,23 +73,25 @@ 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
|
||||
|
|
Загрузка…
Ссылка в новой задаче