--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -292,17 +292,23 @@ var gXPInstallObserver = {
callback() {
secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH);
installInfo.install();
}
};
let secondaryAction = {
label: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow"),
accessKey: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow.accesskey"),
- callback: () => {},
+ callback: () => {
+ for (let install of installInfo.installs) {
+ if (install.state != AddonManager.STATE_CANCELLED) {
+ install.cancel();
+ }
+ }
+ },
};
secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED);
let popup = PopupNotifications.show(browser, notificationID,
messageString, anchorID,
action, [secondaryAction], options);
removeNotificationOnEnd(popup, installInfo.installs);
break; }
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -418,23 +418,24 @@ class ExtensionWrapper {
onMessage(msg, callback) {
this.checkDuplicateListeners(msg);
this.messageHandler.set(msg, callback);
}
}
class AOMExtensionWrapper extends ExtensionWrapper {
- constructor(testScope, xpiFile, installType) {
+ constructor(testScope, xpiFile, installType, installTelemetryInfo) {
super(testScope);
this.onEvent = this.onEvent.bind(this);
this.file = xpiFile;
this.installType = installType;
+ this.installTelemetryInfo = installTelemetryInfo;
this.cleanupFiles = [xpiFile];
Management.on("ready", this.onEvent);
Management.on("shutdown", this.onEvent);
Management.on("startup", this.onEvent);
AddonTestUtils.on("addon-manager-shutdown", this.onEvent);
@@ -561,17 +562,17 @@ class AOMExtensionWrapper extends Extens
this.addon = addon;
return this.startupPromise;
}).catch(e => {
this.state = "unloaded";
return Promise.reject(e);
});
} else if (this.installType === "permanent") {
- return AddonManager.getInstallForFile(xpiFile).then(install => {
+ return AddonManager.getInstallForFile(xpiFile, null, this.installTelemetryInfo).then(install => {
let listener = {
onInstallFailed: () => {
this.state = "unloaded";
this.resolveStartup(Promise.reject(new Error("Install failed")));
},
onInstallEnded: (install, newAddon) => {
this.id = newAddon.id;
this.addon = newAddon;
@@ -733,26 +734,26 @@ var ExtensionTestUtils = {
.QueryInterface(Ci.nsITimerCallback);
manager.observe(null, "addons-startup", null);
},
loadExtension(data) {
if (data.useAddonManager) {
let xpiFile = Extension.generateXPI(data);
- return this.loadExtensionXPI(xpiFile, data.useAddonManager);
+ return this.loadExtensionXPI(xpiFile, data.useAddonManager, data.amInstallTelemetryInfo);
}
let extension = Extension.generate(data);
return new ExtensionWrapper(this.currentScope, extension);
},
- loadExtensionXPI(xpiFile, useAddonManager = "temporary") {
- return new AOMExtensionWrapper(this.currentScope, xpiFile, useAddonManager);
+ loadExtensionXPI(xpiFile, useAddonManager = "temporary", installTelemetryInfo) {
+ return new AOMExtensionWrapper(this.currentScope, xpiFile, useAddonManager, installTelemetryInfo);
},
get remoteContentScripts() {
return REMOTE_CONTENT_SCRIPTS;
},
set remoteContentScripts(val) {
REMOTE_CONTENT_SCRIPTS = !!val;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_idb_data_migration.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_idb_data_migration.js
@@ -191,17 +191,19 @@ add_task(async function test_storage_loc
await extension.awaitMessage("storage-local-data-migrated");
// The histogram values are unmodified.
assertMigrationHistogramCount("success", 1);
assertMigrationHistogramCount("failure", 0);
// No new telemetry events recorded for the extension.
const snapshot = Services.telemetry.snapshotEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
- ok(!snapshot.parent || snapshot.parent.length === 0,
+ const filterByCategory = ([timestamp, category]) => category === EVENT_CATEGORY;
+
+ ok(!snapshot.parent || snapshot.parent.filter(filterByCategory).length === 0,
"No telemetry events should be recorded for an already migrated extension");
Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, false);
Services.prefs.setBoolPref(LEAVE_UUID_PREF, false);
await extension.unload();
equal(Services.prefs.getPrefType(`${IDB_MIGRATED_PREF_BRANCH}.${EXTENSION_ID}`),
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -64,16 +64,62 @@ activity_stream:
- "msamuel@mozilla.com"
expiry_version: never
extra_keys:
addon_version: The Activity Stream addon version.
session_id: The ID of the Activity Stream session in which the event occurred
page: about:home or about_newtab - the page where the event occurred
user_prefs: An integer representaing a user's A-S settings.
+addonsManager:
+ install:
+ description: >
+ This events are recorded during the install and update flow for extensions and themes,
+ the value of the event is an install_id shared by the events related to the same install
+ or update flow.
+ objects: ["extension", "theme", "locale", "dictionary", "other", "unknown"]
+ methods: ["install", "update"]
+ extra_keys:
+ addon_id: A string which identify the extension (when available)
+ error: The AddonManager error related to an install or update failure.
+ perms_length: The number of permissions shown to the user in the extension permission doorhanger
+ origins_length: The number of origins shown to the user in the extension permission doorhanger
+ source: The source that originally triggered the add-on installation
+ method: >
+ The method used by the source to install the add-on (included when the source can use more than one,
+ e.g. install events with source "about:addons" may have "install-from-file" or "url" as method).
+ updated_from: Determine if an update has been requested by the user or the application ("app" / "user")
+ step: >
+ The current step in the install or update flow:
+ - started, postponed, cancelled, failed, permissions_prompt, completed
+ - site_warning, site_blocked, install_disabled_warning
+ - download_started, download_completed, download_failed
+ notification_emails: ["addons-dev-internal@mozilla.com"]
+ expiry_version: "68"
+ record_in_processes: ["main"]
+ bug_numbers: [1433335]
+ release_channel_collection: opt-out
+ manage:
+ description: >
+ This events are recorded when an installed add-ons is being disable/enabled/uninstalled,
+ the value of the event is the addon_id (which also allow to correlate multiple events
+ related to each other).
+ objects: ["extension", "theme", "locale", "dictionary", "other"]
+ methods: ["disable", "enable", "uninstall"]
+ extra_keys:
+ source: The source from which the addon has been installed
+ method: >
+ The method used by the source to install the add-on (included when the source can use more than one,
+ e.g. install events with source "about:addons" may have "install-from-file" or "url" as method).
+ notification_emails: ["addons-dev-internal@mozilla.com"]
+ expiry_version: "68"
+ record_in_processes: ["main"]
+ bug_numbers: [1433335]
+ release_channel_collection: opt-out
+
extensions.data:
migrateResult:
objects: ["storageLocal"]
bug_numbers: [1470213]
notification_emails: ["addons-dev-internal@mozilla.com"]
expiry_version: "70"
record_in_processes: ["main"]
release_channel_collection: opt-out
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -3523,15 +3523,408 @@ var AddonManager = {
return AddonManagerInternal.webAPI;
},
get shutdown() {
return gShutdownBarrier.client;
},
};
+/**
+ * Listens to the AddonManager install and addon events and send telemetry events.
+ */
+var AMTelemetry = {
+ initialized: false,
+
+ // Numeric id included in the install events to correlate multiple events related
+ // to the same install or update flow.
+ nextInstallId: 1,
+
+ // Keep track of extra telemetry information related to the AddonInstall objects:
+ // WeakMap<AddonInstall -> object>
+ trackedInstallMap: new WeakMap(),
+
+ init() {
+ AddonManager.addManagerListener(this);
+ },
+
+ onStartup() {
+ this.setup();
+ },
+
+ async setup() {
+ if (this.initialized) {
+ return;
+ }
+
+ this.initialized = true;
+
+ Services.telemetry.setEventRecordingEnabled("addonsManager", true);
+
+ Services.obs.addObserver(this, "addon-install-origin-blocked");
+ Services.obs.addObserver(this, "addon-install-disabled");
+ Services.obs.addObserver(this, "addon-install-blocked");
+
+ AddonManager.addInstallListener(this);
+ AddonManager.addAddonListener(this);
+ },
+
+ // Observer Service notification callback.
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "addon-install-blocked": {
+ const {installs} = subject.wrappedJSObject;
+ this.recordInstallEvent(installs[0], {step: "site_warning"});
+ break;
+ }
+ case "addon-install-origin-blocked": {
+ const {installs} = subject.wrappedJSObject;
+ this.recordInstallEvent(installs[0], {step: "site_blocked"});
+ break;
+ }
+ case "addon-install-disabled": {
+ const {installs} = subject.wrappedJSObject;
+ this.recordInstallEvent(installs[0], {step: "install_disabled_warning"});
+ break;
+ }
+ }
+ },
+
+ // AddonManager install listener callbacks.
+
+ onNewInstall(install) {
+ this.recordInstallEvent(install, {step: "started"});
+ },
+
+ onInstallCancelled(install) {
+ this.recordInstallEvent(install, {step: "cancelled"});
+ },
+
+ onInstallPostponed(install) {
+ this.recordInstallEvent(install, {step: "postponed"});
+ },
+
+ onInstallFailed(install) {
+ this.recordInstallEvent(install, {step: "failed"});
+ },
+
+ onInstallEnded(install) {
+ this.recordInstallEvent(install, {step: "completed"});
+ },
+
+ onDownloadStarted(install) {
+ this.recordInstallEvent(install, {step: "download_started"});
+ },
+
+ onDownloadCancelled(install) {
+ this.recordInstallEvent(install, {step: "cancelled"});
+ },
+
+ onDownloadEnded(install) {
+ this.recordInstallEvent(install, {step: "download_completed"});
+ },
+
+ onDownloadFailed(install) {
+ this.recordInstallEvent(install, {step: "download_failed"});
+ },
+
+ // Addon listeners callbacks.
+
+ onUninstalled(addon) {
+ this.recordManageEvent(addon, "uninstall");
+ },
+
+ onEnabled(addon) {
+ this.recordManageEvent(addon, "enable");
+ },
+
+ onDisabled(addon) {
+ this.recordManageEvent(addon, "disable");
+ },
+
+ // Internal helpers methods.
+
+ /**
+ * Get a trimmed version of the given string if it is longer than 80 chars.
+ *
+ * @param {string} str
+ * The original string content.
+ *
+ * @returns {string}
+ * The trimmed version of the string when longer than 80 chars, or the given string
+ * unmodified otherwise.
+ */
+ getTrimmedString(str) {
+ if (str.length <= 80) {
+ return str;
+ }
+
+ const length = str.length;
+
+ // Trim the string to prevent a flood of warnings messages logged internally by recordEvent,
+ // the trimmed version is going to be composed by the first 40 chars and the last 37 and 3 dots
+ // that joins the two parts, to visually indicate that the string has been trimmed.
+ return `${str.slice(0, 40)}...${str.slice(length - 37, length)}`;
+ },
+
+ /**
+ * Retrieve the addonId for the given AddonInstall instance.
+ *
+ * @param {AddonInstall} install
+ * The AddonInstall instance to retrieve the addonId from.
+ *
+ * @returns {string | null}
+ * The addonId for the given AddonInstall instance (if any).
+ */
+ getAddonIdFromInstall(install) {
+ // Returns the id of the extension that is being installed, as soon as the
+ // addon is available in the AddonInstall instance (after being downloaded
+ // and validated successfully).
+ if (install.addon) {
+ return install.addon.id;
+ }
+
+ // While updating an addon, the existing addon can be
+ // used to retrieve the addon id since the first update event.
+ if (install.existingAddon) {
+ return install.existingAddon.id;
+ }
+
+ return null;
+ },
+
+ /**
+ * Retrieve the telemetry event object for the given AddonInstall instance.
+ *
+ * @param {AddonInstall} install
+ * The AddonInstall instance to retrieve the event object from.
+ *
+ * @returns {string}
+ * The object for the given AddonInstall instance.
+ */
+ getEventObjectFromInstall(install) {
+ let addonType;
+
+ if (install.type) {
+ addonType = install.type;
+ } else if (install.addon) {
+ addonType = install.addon.type;
+ } else if (install.existingAddon) {
+ addonType = install.existingAddon.type;
+ }
+
+ return this.getEventObjectFromAddonType(addonType);
+ },
+
+ /**
+ * Retrieve the telemetry event source for the given AddonInstall instance.
+ *
+ * @param {AddonInstall} install
+ * The AddonInstall instance to retrieve the source from.
+ *
+ * @returns {string | null}
+ * The source for the given AddonInstall instance.
+ */
+ getSourceFromInstall(install) {
+ if (install.installTelemetryInfo) {
+ return install.installTelemetryInfo.source;
+ } else if (install.existingAddon && install.existingAddon.installTelemetryInfo) {
+ // Get the install source from the existing addon (e.g. for an extension update).
+ return install.existingAddon.installTelemetryInfo.source;
+ }
+
+ return null;
+ },
+
+ /**
+ * Retrieve the telemetry event source method for the given AddonInstall instance.
+ *
+ * @param {AddonInstall} install
+ * The AddonInstall instance to retrieve the source from.
+ *
+ * @returns {string | null}
+ * The method used by the source for the given AddonInstall instance.
+ */
+ getMethodFromInstall(install) {
+ if (install.installTelemetryInfo) {
+ return install.installTelemetryInfo.method;
+ } else if (install.existingAddon && install.existingAddon.installTelemetryInfo) {
+ // Get the install method from the existing addon (e.g. for an extension update).
+ return install.existingAddon.installTelemetryInfo.method;
+ }
+
+ return null;
+ },
+
+ /**
+ * Get the telemetry event object for the given addon type
+ *
+ * @param {string} addonType
+ * The addon type to convert into the related telemetry event object.
+ *
+ * @returns {string}
+ * The object for the given addon type.
+ */
+ getEventObjectFromAddonType(addonType) {
+ switch (addonType) {
+ case undefined:
+ return "unknown";
+ case "extension":
+ case "theme":
+ case "locale":
+ case "dictionary":
+ return addonType;
+ default:
+ // Currently this should only include plugins and gmp-plugins
+ return "other";
+ }
+ },
+
+ /**
+ * Get the additional telemetry information related to a AddonInstall instance,
+ * if the instance is not yet tracked, it also assign its install id (used to correlate
+ * multiple telemetry events related to the same install or update flow) and the
+ * actual method ("install" for new addon installations and "update" for addon updates).
+ *
+ * @param {AddonInstall} install
+ * The AddonInstall instance.
+ *
+ * @returns {object}
+ * The object which contains the additional telemetry information for the given
+ * AddonInstall instance:
+ * - installId
+ * - method, which can be "install" or "update"
+ */
+ getTrackedInstallData(install) {
+ let installData = this.trackedInstallMap.get(install);
+
+ if (!installData) {
+ // Keep track of some additional info related to install flow.
+ installData = {
+ // An numeric id used to correlate multiple telemetry events
+ // related to the same install or update flow.
+ installId: `${this.nextInstallId++}`,
+
+ // Detect the telemetry event method for this AddonInstall instance.
+ method: install.existingAddon ? "update" : "install",
+ };
+
+ this.trackedInstallMap.set(install, installData);
+ }
+
+ return installData;
+ },
+
+ /**
+ * Convert all the telemetry event's extra_vars into strings, if needed.
+ *
+ * @param {object} extraVars
+ */
+ formatExtraVars(extraVars) {
+ // All the extra_vars in a telemetry event have to be strings.
+ for (var key of Object.keys(extraVars)) {
+ if (typeof(extraVars[key]) !== "string") {
+ extraVars[key] = `${extraVars[key]}`;
+ }
+ }
+ },
+
+ /**
+ * Record an install or update event for the given AddonInstall instance.
+ *
+ * @param {AddonInstall} install
+ * The AddonInstall instance to record an install or update event for.
+ * @param {object} extraVars
+ * The additional extra_vars to include in the recorded event.
+ * @param {string} extraVars.step
+ * The current step in the install or update flow.
+ * @param {string} extraVars.permission_length
+ * The number of permissions and origins for the extensions.
+ */
+ recordInstallEvent(install, extraVars) {
+ let addonId = this.getAddonIdFromInstall(install);
+ let object = this.getEventObjectFromInstall(install);
+ let {installId, method} = this.getTrackedInstallData(install);
+
+ let extra = {};
+
+ let source = this.getSourceFromInstall(install);
+ if (typeof source === "string") {
+ extra.source = source;
+ }
+
+ // Also include the install source's method when applicable (e.g. install events with
+ // source "about:addons" may have "install-from-file" or "url" as their source method).
+ let sourceMethod = this.getMethodFromInstall(install);
+ if (typeof sourceMethod === "string") {
+ extra.method = sourceMethod;
+ }
+
+ if (addonId) {
+ extra.addon_id = this.getTrimmedString(addonId);
+ }
+
+ if (install.error) {
+ extra.error = AddonManager.errorToString(install.error);
+ }
+
+ if (method === "update") {
+ // For "update" telemetry events, also include an extra var which determine
+ // if the update has been requested by the user.
+ extra.updated_from = install.isUserRequestedUpdate ? "user" : "app";
+ }
+
+ // All the extra vars in a telemetry event have to be strings.
+ extra = {...extraVars, ...extra};
+ this.formatExtraVars(extra);
+
+ this.recordEvent({method, object, value: installId, extra});
+ },
+
+ /**
+ * Record a manage event for the given addon.
+ *
+ * @param {AddonWrapper} addon
+ * The AddonWrapper instance.
+ * @param {object} extra
+ * The additional extra_vars to include in the recorded event.
+ */
+ recordManageEvent(addon, method, extraVars) {
+ let object = this.getEventObjectFromAddonType(addon.type);
+ let value = this.getTrimmedString(addon.id);
+
+ let extra = {};
+
+ if (addon.installTelemetryInfo) {
+ if ("source" in addon.installTelemetryInfo) {
+ extra.source = addon.installTelemetryInfo.source;
+ }
+
+ // Also include the install source's method when applicable (e.g. install events with
+ // source "about:addons" may have "install-from-file" or "url" as their source method).
+ if ("method" in addon.installTelemetryInfo) {
+ extra.method = addon.installTelemetryInfo.method;
+ }
+ }
+
+ extra = {...extraVars, ...extra};
+
+ let hasExtraVars = Object.keys(extra).length > 0;
+ this.formatExtraVars(extra);
+
+ this.recordEvent({method, object, value, extra: hasExtraVars ? extra : null});
+ },
+
+ recordEvent({method, object, value, extra}) {
+ Services.telemetry.recordEvent("addonsManager", method, object, value, extra);
+ }
+};
+
this.AddonManager.init();
+AMTelemetry.init();
+
// load the timestamps module into AddonManagerInternal
ChromeUtils.import("resource://gre/modules/TelemetryTimestamps.jsm", AddonManagerInternal);
Object.freeze(AddonManagerInternal);
Object.freeze(AddonManagerPrivate);
Object.freeze(AddonManager);
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -1363,21 +1363,24 @@ var AddonTestUtils = {
/**
* A helper method to install a file.
*
* @param {nsIFile} file
* The file to install
* @param {boolean} [ignoreIncompatible = false]
* Optional parameter to ignore add-ons that are incompatible
* with the application
+ * @param {Object} [installTelemetryInfo = undefined]
+ * Optional parameter to set the install telemetry info for the
+ * installed addon
* @returns {Promise}
* Resolves when the install has completed.
*/
- async promiseInstallFile(file, ignoreIncompatible = false) {
- let install = await AddonManager.getInstallForFile(file);
+ async promiseInstallFile(file, ignoreIncompatible = false, installTelemetryInfo) {
+ let install = await AddonManager.getInstallForFile(file, null, installTelemetryInfo);
if (!install)
throw new Error(`No AddonInstall created for ${file.path}`);
if (install.state != AddonManager.STATE_DOWNLOADED)
throw new Error(`Expected file to be downloaded for install of ${file.path}`);
if (ignoreIncompatible && install.addon.appDisabled)
return null;
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -1641,16 +1641,18 @@ class AddonInstall {
*/
checkPrompt() {
(async () => {
if (this.promptHandler) {
let info = {
existingAddon: this.existingAddon ? this.existingAddon.wrapper : null,
addon: this.addon.wrapper,
icon: this.getIcon(),
+ // Used in AMTelemetry to detect the install flow related to this prompt.
+ install: this.wrapper,
};
try {
await this.promptHandler(info);
} catch (err) {
logger.info(`Install of ${this.addon.id} cancelled by user`);
this.state = AddonManager.STATE_CANCELLED;
XPIInstall.installs.delete(this);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_addon_manager_telemetry_events.js
@@ -0,0 +1,343 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+ChromeUtils.import("resource://gre/modules/TelemetryController.jsm");
+
+// We don't have an easy way to serve update manifests from a secure URL.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+const EVENT_CATEGORY = "addonsManager";
+const EVENT_METHODS_INSTALL = ["install", "update"];
+const EVENT_METHODS_MANAGE = [
+ "disable", "enable", "uninstall",
+];
+const EVENT_METHODS = [...EVENT_METHODS_INSTALL, ...EVENT_METHODS_MANAGE];
+
+const FAKE_INSTALL_TELEMETRY_INFO = {
+ source: "fake-install-source",
+ method: "fake-install-method",
+};
+
+function getTelemetryEvents(includeMethods = EVENT_METHODS) {
+ const snapshot = Services.telemetry.snapshotEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+
+ ok(snapshot.parent && snapshot.parent.length > 0, "Got parent telemetry events in the snapshot");
+
+ return snapshot.parent.filter(([timestamp, category, method]) => {
+ const includeMethod = includeMethods ?
+ includeMethods.includes(method) : true;
+
+ return category === EVENT_CATEGORY && includeMethod;
+ }).map(event => {
+ return {method: event[2], object: event[3], value: event[4], extra: event[5]};
+ });
+}
+
+add_task(async function setup() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ // Telemetry test setup needed to ensure that the builtin events are defined
+ // and they can be collected and verified.
+ await TelemetryController.testSetup();
+
+ await promiseStartupManager();
+});
+
+// Test the basic install and management flows.
+add_task(async function test_basic_telemetry_events() {
+ const EXTENSION_ID = "basic@test.extension";
+
+ const manifest = {
+ applications: {gecko: {id: EXTENSION_ID}},
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ useAddonManager: "permanent",
+ amInstallTelemetryInfo: FAKE_INSTALL_TELEMETRY_INFO,
+ });
+
+ await extension.startup();
+
+ const addon = await promiseAddonByID(EXTENSION_ID);
+
+ info("Disabling the extension");
+ const onceAddonDisabled = promiseAddonEvent("onDisabled");
+ addon.disable();
+ await onceAddonDisabled;
+
+ info("Set pending uninstall on the extension");
+ const onceAddonUninstalling = promiseAddonEvent("onUninstalling");
+ addon.uninstall(true);
+ await onceAddonUninstalling;
+
+ info("Cancel pending uninstall");
+ const oncePendingUninstallCancelled = promiseAddonEvent("onOperationCancelled");
+ addon.cancelUninstall();
+ await oncePendingUninstallCancelled;
+
+ info("Re-enabling the extension");
+ const onceAddonStarted = promiseWebExtensionStartup(EXTENSION_ID);
+ const onceAddonEnabled = promiseAddonEvent("onEnabled");
+ addon.enable();
+ await Promise.all([onceAddonEnabled, onceAddonStarted]);
+
+ await extension.unload();
+
+ const amEvents = getTelemetryEvents();
+
+ const amMethods = amEvents.map(evt => evt.method);
+ const expectedMethods = [
+ // These two install methods are related to the steps "started" and "completed".
+ "install", "install",
+ // Sequence of disable and enable (pending uninstall and undo uninstall are not going to
+ // record any telemetry events).
+ "disable", "enable",
+ // The final "uninstall" when the test extension is unloaded.
+ "uninstall",
+ ];
+ Assert.deepEqual(amMethods, expectedMethods, "Got the addonsManager telemetry events in the expected order");
+
+ const installEvents = amEvents.filter(evt => evt.method === "install");
+ const expectedInstallEvents = [
+ {
+ method: "install", object: "extension", value: "1",
+ extra: {
+ addon_id: "basic@test.extension",
+ step: "started",
+ ...FAKE_INSTALL_TELEMETRY_INFO,
+ },
+ },
+ {
+ method: "install", object: "extension", value: "1",
+ extra: {
+ addon_id: "basic@test.extension",
+ step: "completed",
+ ...FAKE_INSTALL_TELEMETRY_INFO,
+ },
+ },
+ ];
+ Assert.deepEqual(installEvents, expectedInstallEvents, "Got the expected addonsManager.install events");
+
+ const manageEvents = amEvents.filter(evt => EVENT_METHODS_MANAGE.includes(evt.method));
+ const expectedExtra = FAKE_INSTALL_TELEMETRY_INFO;
+ const expectedManageEvents = [
+ {
+ method: "disable", object: "extension", value: "basic@test.extension", extra: expectedExtra,
+ },
+ {
+ method: "enable", object: "extension", value: "basic@test.extension", extra: expectedExtra,
+ },
+ {
+ method: "uninstall", object: "extension", value: "basic@test.extension", extra: expectedExtra,
+ },
+ ];
+ Assert.deepEqual(manageEvents, expectedManageEvents, "Got the expected addonsManager.manage events");
+
+ // Verify that on every install flow, the value of the addonsManager.install Telemetry events
+ // is being incremented.
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ useAddonManager: "permanent",
+ amInstallTelemetryInfo: FAKE_INSTALL_TELEMETRY_INFO,
+ });
+
+ await extension.startup();
+ await extension.unload();
+
+ const eventsFromNewInstall = getTelemetryEvents();
+ equal(eventsFromNewInstall.length, 3, "Got the expected number of addonsManager install events");
+
+ const eventValues = eventsFromNewInstall.filter(evt => evt.method === "install").map(evt => evt.value);
+ const expectedValues = ["2", "2"];
+ Assert.deepEqual(eventValues, expectedValues, "Got the expected install id");
+});
+
+add_task(async function test_update_telemetry_events() {
+ const EXTENSION_ID = "basic@test.extension";
+
+ const testserver = AddonTestUtils.createHttpServer({hosts: ["example.com"]});
+ testserver.registerDirectory("/data/", do_get_file("data"));
+
+ const updateUrl = `http://example.com/updates.json`;
+
+ const testAddon = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "1.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ update_url: updateUrl,
+ },
+ },
+ },
+ });
+
+ const testUserRequestedUpdate = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "2.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ update_url: updateUrl,
+ },
+ },
+ },
+ });
+ const testAppRequestedUpdate = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "2.1",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ update_url: updateUrl,
+ },
+ },
+ },
+ });
+
+ testserver.registerFile(`/addons/${EXTENSION_ID}-2.0.xpi`, testUserRequestedUpdate);
+ testserver.registerFile(`/addons/${EXTENSION_ID}-2.1.xpi`, testAppRequestedUpdate);
+
+ let updates = [
+ {
+ "version": "2.0",
+ "update_link": `http://example.com/addons/${EXTENSION_ID}-2.0.xpi`,
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ },
+ },
+ },
+ ];
+
+ testserver.registerPathHandler("/updates.json", (request, response) => {
+ response.write(`{
+ "addons": {
+ "${EXTENSION_ID}": {
+ "updates": ${JSON.stringify(updates)}
+ }
+ }
+ }`);
+ });
+
+ await promiseInstallFile(testAddon, false, FAKE_INSTALL_TELEMETRY_INFO);
+
+ let addon = await AddonManager.getAddonByID(EXTENSION_ID);
+
+ // User requested update.
+ await promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ let installs = await AddonManager.getAllInstalls();
+ await promiseCompleteAllInstalls(installs);
+
+ updates = [
+ {
+ "version": "2.1",
+ "update_link": `http://example.com/addons/${EXTENSION_ID}-2.1.xpi`,
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ },
+ },
+ },
+ ];
+
+ // App requested update.
+ await promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ let installs2 = await AddonManager.getAllInstalls();
+ await promiseCompleteAllInstalls(installs2);
+
+ updates = [
+ {
+ "version": "2.1.1",
+ "update_link": `http://example.com/addons/${EXTENSION_ID}-2.1.1-network-failure.xpi`,
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ },
+ },
+ },
+ ];
+
+ // Update which fails to download.
+ await promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ let installs3 = await AddonManager.getAllInstalls();
+ await promiseCompleteAllInstalls(installs3);
+
+ let amEvents = getTelemetryEvents();
+
+ const installEvents = amEvents.filter(evt => evt.method === "install").map(evt => {
+ delete evt.value;
+ return evt;
+ });
+
+ const addon_id = "basic@test.extension";
+ const object = "extension";
+
+ Assert.deepEqual(installEvents, [
+ {method: "install", object, extra: {addon_id, step: "started", ...FAKE_INSTALL_TELEMETRY_INFO}},
+ {method: "install", object, extra: {addon_id, step: "completed", ...FAKE_INSTALL_TELEMETRY_INFO}},
+ ], "Got the expected addonsManager.install events");
+
+ const updateEvents = amEvents.filter(evt => evt.method === "update").map(evt => {
+ delete evt.value;
+ return evt;
+ });
+
+ const method = "update";
+ const baseExtra = FAKE_INSTALL_TELEMETRY_INFO;
+
+ const expectedUpdateEvents = [
+ // User-requested update to the 2.1 version.
+ {
+ method, object, extra: {...baseExtra, addon_id, step: "started", updated_from: "user"},
+ },
+ {
+ method, object, extra: {...baseExtra, addon_id, step: "download_started", updated_from: "user"},
+ },
+ {
+ method, object, extra: {...baseExtra, addon_id, step: "download_completed", updated_from: "user"},
+ },
+ {
+ method, object, extra: {...baseExtra, addon_id, step: "completed", updated_from: "user"},
+ },
+ // App-requested update to the 2.1 version.
+ {
+ method, object, extra: {...baseExtra, addon_id, step: "started", updated_from: "app"},
+ },
+ {
+ method, object, extra: {...baseExtra, addon_id, step: "download_started", updated_from: "app"},
+ },
+ {
+ method, object, extra: {...baseExtra, addon_id, step: "download_completed", updated_from: "app"},
+ },
+ {
+ method, object, extra: {...baseExtra, addon_id, step: "completed", updated_from: "app"},
+ },
+ // Broken update to the 2.1.1 version (on ERROR_NETWORK_FAILURE).
+ {
+ method, object, extra: {...baseExtra, addon_id, step: "started", updated_from: "app"},
+ },
+ {
+ method, object, extra: {...baseExtra, addon_id, step: "download_started", updated_from: "app"},
+ },
+ {
+ method, object, extra: {
+ ...baseExtra,
+ addon_id,
+ error: "ERROR_NETWORK_FAILURE",
+ step: "download_failed",
+ updated_from: "app",
+ },
+ },
+ ];
+ Assert.deepEqual(updateEvents, expectedUpdateEvents, "Got the expected addonsManager.update events");
+
+ await addon.uninstall();
+});
+
+add_task(async function teardown() {
+ await TelemetryController.testShutdown();
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -2,16 +2,17 @@
skip-if = toolkit == 'android'
tags = addons
head = head_addons.js
firefox-appdir = browser
dupe-manifest =
support-files =
data/**
+[test_addon_manager_telemetry_events.js]
[test_AddonRepository.js]
[test_AddonRepository_cache.js]
# Bug 676992: test consistently hangs on Android
skip-if = os == "android"
[test_AddonRepository_paging.js]
[test_LightweightThemeManager.js]
[test_ProductAddonChecker.js]
[test_XPIStates.js]