--- a/browser/base/content/test/webextensions/browser_extension_sideloading.js
+++ b/browser/base/content/test/webextensions/browser_extension_sideloading.js
@@ -44,16 +44,55 @@ async function createXULExtension(detail
}
function promiseEvent(eventEmitter, event) {
return new Promise(resolve => {
eventEmitter.once(event, resolve);
});
}
+function testTelemetryEvents() {
+ const expectedExtra = {source: "app-profile", method: "sideload"};
+ const noPermissionsExtra = {...expectedExtra, perms_length: "0", origins_length: "0"};
+ const baseEvent = {object: "extension", extra: expectedExtra};
+ const createBaseEventAddon = (n) => ({...baseEvent, value: `addon${n}@tests.mozilla.org`});
+ const getEventsForAddonId = (events, addonId) => events.filter(ev => ev.value === addonId);
+
+ const amEvents = getTelemetryEvents();
+
+ // Test telemetry events for addon1 (1 permission and 1 origin).
+ const baseEventAddon1 = createBaseEventAddon(1);
+ Assert.deepEqual(getEventsForAddonId(amEvents, baseEventAddon1.value), [
+ {
+ ...baseEventAddon1, method: "sideload_prompt",
+ extra: {...expectedExtra, perms_length: "1", origins_length: "1"},
+ },
+ {...baseEventAddon1, method: "uninstall"},
+ ], "Got the expected telemetry events for addon1");
+
+ // Test telemetry events for addon2 (no permissions).
+ const baseEventAddon2 = createBaseEventAddon(2);
+ Assert.deepEqual(getEventsForAddonId(amEvents, baseEventAddon2.value), [
+ {...baseEventAddon2, method: "sideload_prompt", extra: {...noPermissionsExtra}},
+ {...baseEventAddon2, method: "enable"},
+ {...baseEventAddon2, method: "uninstall"},
+ ], "Got the expected telemetry events for addon2");
+
+ // Test telemetry events for addon3 (no permissions and 1 origin).
+ const baseEventAddon3 = createBaseEventAddon(3);
+ Assert.deepEqual(getEventsForAddonId(amEvents, baseEventAddon3.value), [
+ {
+ ...baseEventAddon3, method: "sideload_prompt",
+ extra: {...expectedExtra, perms_length: "0", origins_length: "1"},
+ },
+ {...baseEventAddon3, method: "enable"},
+ {...baseEventAddon3, method: "uninstall"},
+ ], "Got the expected telemetry events for addon3");
+}
+
let cleanup;
add_task(async function() {
const DEFAULT_ICON_URL = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
await SpecialPowers.pushPrefEnv({
set: [
["xpinstall.signatures.required", false],
@@ -105,16 +144,17 @@ add_task(async function() {
registerCleanupFunction(async function() {
// Return to about:blank when we're done
gBrowser.selectedBrowser.loadURI("about:blank");
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
});
hookExtensionsTelemetry();
+ hookAMTelemetryEvents();
let changePromise = new Promise(resolve => {
ExtensionsUI.on("change", function listener() {
ExtensionsUI.off("change", listener);
resolve();
});
});
ExtensionsUI._checkForSideloaded();
@@ -265,9 +305,11 @@ add_task(async function() {
await new Promise(resolve => setTimeout(resolve, 100));
for (let addon of [addon1, addon2, addon3, addon4]) {
await addon.uninstall();
}
BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ testTelemetryEvents();
});
--- a/browser/base/content/test/webextensions/browser_extension_update_background.js
+++ b/browser/base/content/test/webextensions/browser_extension_update_background.js
@@ -1,14 +1,15 @@
const {AddonManagerPrivate} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm", {});
const ID = "update2@tests.mozilla.org";
const ID_ICON = "update_icon2@tests.mozilla.org";
const ID_PERMS = "update_perms@tests.mozilla.org";
const ID_LEGACY = "legacy_update@tests.mozilla.org";
+const FAKE_INSTALL_TELEMETRY_SOURCE = "fake-install-source";
requestLongerTimeout(2);
function promiseViewLoaded(tab, viewid) {
let win = tab.linkedBrowser.contentWindow;
if (win.gViewController && !win.gViewController.isLoading &&
win.gViewController.currentViewId == viewid) {
return Promise.resolve();
@@ -47,29 +48,62 @@ add_task(async function setup() {
registerCleanupFunction(async function() {
// Return to about:blank when we're done
gBrowser.selectedBrowser.loadURI("about:blank");
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
});
});
hookExtensionsTelemetry();
+hookAMTelemetryEvents();
+
+function testTelemetryEvents(addonId) {
+ // Test that the expected telemetry events have been recorded (and that they include the
+ // permission_prompt event).
+ const amEvents = getTelemetryEvents();
+ const updateEvents = amEvents.filter(evt => evt.method === "update").map(evt => {
+ delete evt.value;
+ return evt;
+ });
+
+ Assert.deepEqual(updateEvents.map(evt => evt.extra && evt.extra.step), [
+ // First update (cancelled).
+ "started", "download_started", "download_completed", "permissions_prompt", "cancelled",
+ // Second update (completed).
+ "started", "download_started", "download_completed", "permissions_prompt", "completed",
+ ], "Got the steps from the collected telemetry events");
+
+ const method = "update";
+ const object = "extension";
+ const baseExtra = {
+ addon_id: addonId,
+ source: FAKE_INSTALL_TELEMETRY_SOURCE,
+ step: "permissions_prompt",
+ updated_from: "app",
+ };
+
+ Assert.deepEqual(updateEvents.filter(evt => evt.extra && evt.extra.step === "permissions_prompt"), [
+ {method, object, extra: {...baseExtra, perms_length: "1", origins_length: "1"}},
+ {method, object, extra: {...baseExtra, perms_length: "1", origins_length: "1"}}
+ ], "Got the expected permission_prompts events");
+}
// Helper function to test background updates.
async function backgroundUpdateTest(url, id, checkIconFn) {
await SpecialPowers.pushPrefEnv({set: [
// Turn on background updates
["extensions.update.enabled", true],
// Point updates to the local mochitest server
["extensions.update.background.url", `${BASE}/browser_webext_update.json`],
]});
// Install version 1.0 of the test extension
- let addon = await promiseInstallAddon(url);
+ let addon = await promiseInstallAddon(url, {source: FAKE_INSTALL_TELEMETRY_SOURCE});
+ let addonId = addon.id;
ok(addon, "Addon was installed");
is(getBadgeStatus(), "", "Should not start out with an addon alert badge");
// Trigger an update check and wait for the update for this addon
// to be downloaded.
let updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
@@ -165,18 +199,20 @@ async function backgroundUpdateTest(url,
BrowserTestUtils.removeTab(tab);
is(getBadgeStatus(), "", "Addon alert badge should be gone");
// Should have recorded 1 canceled followed by 1 accepted update.
expectTelemetry(["updateRejected", "updateAccepted"]);
- addon.uninstall();
+ await addon.uninstall();
await SpecialPowers.popPrefEnv();
+
+ testTelemetryEvents(addonId);
}
function checkDefaultIcon(icon) {
is(icon, "chrome://mozapps/skin/extensions/extensionGeneric.svg",
"Popup has the default extension icon");
}
add_task(() => backgroundUpdateTest(`${BASE}/browser_webext_update1.xpi`,
--- a/browser/base/content/test/webextensions/head.js
+++ b/browser/base/content/test/webextensions/head.js
@@ -7,16 +7,18 @@ XPCOMUtils.defineLazyGetter(this, "Manag
// eslint-disable-next-line no-shadow
const {Management} = ChromeUtils.import("resource://gre/modules/Extension.jsm", {});
return Management;
});
ChromeUtils.import("resource://testing-common/CustomizableUITestUtils.jsm", this);
let gCUITestUtils = new CustomizableUITestUtils(window);
+const {AMTelemetry} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm", {});
+
/**
* Wait for the given PopupNotification to display
*
* @param {string} name
* The name of the notification to wait for.
*
* @returns {Promise}
* Resolves with the notification window.
@@ -63,23 +65,27 @@ function promiseInstallEvent(addon, even
});
}
/**
* Install an (xpi packaged) extension
*
* @param {string} url
* URL of the .xpi file to install
+ * @param {Object?} installTelemetryInfo
+ * an optional object that contains additional details used by the telemetry events.
*
* @returns {Promise}
* Resolves when the extension has been installed with the Addon
* object as the resolution value.
*/
-async function promiseInstallAddon(url) {
- let install = await AddonManager.getInstallForURL(url, "application/x-xpinstall");
+async function promiseInstallAddon(url, installTelemetryInfo) {
+ let install = await AddonManager.getInstallForURL(url, "application/x-xpinstall",
+ null, null, null, null, null,
+ installTelemetryInfo);
install.install();
let addon = await new Promise(resolve => {
install.addListener({
onInstallEnded(_install, _addon) {
resolve(_addon);
},
});
@@ -487,8 +493,26 @@ function hookExtensionsTelemetry() {
});
}
function expectTelemetry(values) {
Assert.deepEqual(values, collectedTelemetry);
collectedTelemetry = [];
}
+let collectedTelemetryEvents = [];
+function hookAMTelemetryEvents() {
+ let originalRecordEvent = AMTelemetry.recordEvent;
+ AMTelemetry.recordEvent = (event) => {
+ collectedTelemetryEvents.push(event);
+ };
+ registerCleanupFunction(() => {
+ Assert.deepEqual([], collectedTelemetryEvents, "No unexamined telemetry events after test is finished");
+ AMTelemetry.recordEvent = originalRecordEvent;
+ });
+}
+
+function getTelemetryEvents() {
+ let events = collectedTelemetryEvents;
+ collectedTelemetryEvents = [];
+
+ return events;
+}
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -116,16 +116,29 @@ var ExtensionsUI = {
this.sideloaded.delete(addon);
this._updateNotifications();
let strings = this._buildStrings({
addon,
permissions: addon.userPermissions,
type: "sideload",
});
+
+ // Notify the "webextension-sideload-prompt" message to allow AMTelemetry to record an event
+ // for this permision prompt.
+ Services.obs.notifyObservers({
+ wrappedJSObject: {
+ info: {
+ type: "sideload",
+ addon,
+ permissions: addon.userPermissions,
+ }
+ }
+ }, "webextension-sideload-prompt");
+
this.showAddonsManager(browser, strings, addon.iconURL, "sideload")
.then(async answer => {
if (answer) {
await addon.enable();
}
this.emit("sideload-response");
});
},
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -98,22 +98,24 @@ addonsManager:
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"]
+ methods: ["disable", "enable", "sideload_prompt", "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).
+ perms_length: The number of permissions shown to the user for a sideload extension permissions doorhanger
+ origins_length: The number of origins shown to the user for a sideload extension permissions doorhanger
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:
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -1236,17 +1236,20 @@ var AddonManagerInternal = {
if (difference.origins.length == 0 && difference.permissions.length == 0) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
let subject = {wrappedJSObject: {
addon: info.addon,
permissions: difference,
- resolve, reject
+ resolve, reject,
+ // Reference to the related AddonInstall object (used in AMTelemetry to
+ // link the recorded event to the other events from the same install flow).
+ install: info.install,
}};
Services.obs.notifyObservers(subject, "webextension-update-permissions");
});
},
// Returns true if System Addons should be updated
systemUpdateEnabled() {
if (!Services.prefs.getBoolPref(PREF_SYS_ADDON_UPDATE_ENABLED)) {
@@ -3558,16 +3561,20 @@ var AMTelemetry = {
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");
+ Services.obs.addObserver(this, "webextension-permission-prompt");
+ Services.obs.addObserver(this, "webextension-update-permissions");
+ Services.obs.addObserver(this, "webextension-sideload-prompt");
+
AddonManager.addInstallListener(this);
AddonManager.addAddonListener(this);
},
// Observer Service notification callback.
observe(subject, topic, data) {
switch (topic) {
@@ -3581,16 +3588,44 @@ var AMTelemetry = {
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;
}
+ case "webextension-sideload-prompt":
+ case "webextension-permission-prompt": {
+ const {info} = subject.wrappedJSObject;
+ const {permissions, origins} = info.permissions || {permissions: [], origins: []};
+ if (info.type === "sideload") {
+ this.recordManageEvent(info.addon, "sideload_prompt", {
+ perms_length: permissions.length,
+ origins_length: origins.length,
+ });
+ } else {
+ this.recordInstallEvent(info.install, {
+ step: "permissions_prompt",
+ perms_length: permissions.length,
+ origins_length: origins.length,
+ });
+ }
+ break;
+ }
+ case "webextension-update-permissions": {
+ const update = subject.wrappedJSObject;
+ const {permissions, origins} = update.permissions || {permissions: [], origins: []};
+ this.recordInstallEvent(update.install, {
+ step: "permissions_prompt",
+ perms_length: permissions.length,
+ origins_length: origins.length,
+ });
+ break;
+ }
}
},
// AddonManager install listener callbacks.
onNewInstall(install) {
this.recordInstallEvent(install, {step: "started"});
},