Bug 1483002 - Added WEBEXT probes keyed by addon id. r=janerik,mixedpuppy
authorLuca Greco <lgreco@mozilla.com>
Wed, 05 Sep 2018 20:16:35 +0000
changeset 435021 9b7d4a94002a49f12b5d25030569e3b4c381718c
parent 435020 6b63ab504b030089ff795eacbac4f1b2246e3e13
child 435022 c32643cc46b693e3e3e8f6f1f4ab8f376dfb1641
push id107530
push userapavel@mozilla.com
push dateThu, 06 Sep 2018 04:44:27 +0000
treeherdermozilla-inbound@5f5d7a3ce332 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanerik, mixedpuppy
bugs1483002
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1483002 - Added WEBEXT probes keyed by addon id. r=janerik,mixedpuppy This patch contains a set of changes needed to add WEBEXT telemetry probes keyed by addon id. The telemetry probes keyed by addon id has been added as separate telemetry histograms named after the related generic WEBEXT probe with the additional "_BY_ADDONID" suffix. A set of small helper methods have been defined in a new ExtensionTelemetry object, exported by the ExtensionUtils.jsm. Differential Revision: https://phabricator.services.mozilla.com/D4437
browser/components/extensions/parent/ext-browserAction.js
browser/components/extensions/parent/ext-pageAction.js
browser/components/extensions/test/browser/browser_ext_browserAction_telemetry.js
browser/components/extensions/test/browser/browser_ext_pageAction_telemetry.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionStorageIDB.jsm
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/child/ext-storage.js
toolkit/components/extensions/parent/ext-backgroundPage.js
toolkit/components/extensions/test/xpcshell/head_telemetry.js
toolkit/components/extensions/test/xpcshell/test_ext_background_telemetry.js
toolkit/components/extensions/test/xpcshell/test_ext_extension_content_telemetry.js
toolkit/components/extensions/test/xpcshell/test_ext_extension_startup_telemetry.js
toolkit/components/extensions/test/xpcshell/test_ext_storage_idb_data_migration.js
toolkit/components/extensions/test/xpcshell/test_ext_storage_telemetry.js
toolkit/components/telemetry/Histograms.json
--- a/browser/components/extensions/parent/ext-browserAction.js
+++ b/browser/components/extensions/parent/ext-browserAction.js
@@ -3,38 +3,35 @@
 "use strict";
 
 ChromeUtils.defineModuleGetter(this, "CustomizableUI",
                                "resource:///modules/CustomizableUI.jsm");
 ChromeUtils.defineModuleGetter(this, "clearTimeout",
                                "resource://gre/modules/Timer.jsm");
 ChromeUtils.defineModuleGetter(this, "setTimeout",
                                "resource://gre/modules/Timer.jsm");
-ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
-                               "resource://gre/modules/TelemetryStopwatch.jsm");
 ChromeUtils.defineModuleGetter(this, "ViewPopup",
                                "resource:///modules/ExtensionPopups.jsm");
 
 var {
   DefaultWeakMap,
   ExtensionError,
+  ExtensionTelemetry,
 } = ExtensionUtils;
 
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
   IconDetails,
   StartupCache,
 } = ExtensionParent;
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["InspectorUtils"]);
 
 const POPUP_PRELOAD_TIMEOUT_MS = 200;
-const POPUP_OPEN_MS_HISTOGRAM = "WEBEXT_BROWSERACTION_POPUP_OPEN_MS";
-const POPUP_RESULT_HISTOGRAM = "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT";
 
 // WeakMap[Extension -> BrowserAction]
 const browserActionMap = new WeakMap();
 
 XPCOMUtils.defineLazyGetter(this, "browserAreas", () => {
   return {
     "navbar": CustomizableUI.AREA_NAVBAR,
     "menupanel": CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
@@ -174,46 +171,50 @@ this.browserAction = class extends Exten
         node.onmousedown = event => this.handleEvent(event);
         node.onmouseover = event => this.handleEvent(event);
         node.onmouseout = event => this.handleEvent(event);
 
         this.updateButton(node, this.globals, true);
       },
 
       onViewShowing: async event => {
-        TelemetryStopwatch.start(POPUP_OPEN_MS_HISTOGRAM, this);
+        const {extension} = this;
+
+        ExtensionTelemetry.browserActionPopupOpen.stopwatchStart(extension, this);
         let document = event.target.ownerDocument;
         let tabbrowser = document.defaultView.gBrowser;
 
         let tab = tabbrowser.selectedTab;
         let popupURL = this.getProperty(tab, "popup");
         this.tabManager.addActiveTabPermission(tab);
 
         // Popups are shown only if a popup URL is defined; otherwise
         // a "click" event is dispatched. This is done for compatibility with the
         // Google Chrome onClicked extension API.
         if (popupURL) {
           try {
             let popup = this.getPopup(document.defaultView, popupURL);
             let attachPromise = popup.attach(event.target);
             event.detail.addBlocker(attachPromise);
             await attachPromise;
-            TelemetryStopwatch.finish(POPUP_OPEN_MS_HISTOGRAM, this);
+            ExtensionTelemetry.browserActionPopupOpen.stopwatchFinish(extension, this);
             if (this.eventQueue.length) {
-              let histogram = Services.telemetry.getHistogramById(POPUP_RESULT_HISTOGRAM);
-              histogram.add("popupShown");
+              ExtensionTelemetry.browserActionPreloadResult.histogramAdd({
+                category: "popupShown",
+                extension: this.extension,
+              });
               this.eventQueue = [];
             }
           } catch (e) {
-            TelemetryStopwatch.cancel(POPUP_OPEN_MS_HISTOGRAM, this);
+            ExtensionTelemetry.browserActionPopupOpen.stopwatchCancel(extension, this);
             Cu.reportError(e);
             event.preventDefault();
           }
         } else {
-          TelemetryStopwatch.cancel(POPUP_OPEN_MS_HISTOGRAM, this);
+          ExtensionTelemetry.browserActionPopupOpen.stopwatchCancel(extension, this);
           // This isn't not a hack, but it seems to provide the correct behavior
           // with the fewest complications.
           event.preventDefault();
           this.emit("click", tabbrowser.selectedBrowser);
           // Ensure we close any popups this node was in:
           CustomizableUI.hidePanelForNode(event.target);
         }
       },
@@ -323,18 +324,20 @@ this.browserAction = class extends Exten
           this.pendingPopup = this.getPopup(window, popupURL, true);
         }
         break;
       }
 
       case "mouseout":
         if (this.pendingPopup) {
           if (this.eventQueue.length) {
-            let histogram = Services.telemetry.getHistogramById(POPUP_RESULT_HISTOGRAM);
-            histogram.add(`clearAfter${this.eventQueue.pop()}`);
+            ExtensionTelemetry.browserActionPreloadResult.histogramAdd({
+              category: `clearAfter${this.eventQueue.pop()}`,
+              extension: this.extension,
+            });
             this.eventQueue = [];
           }
           this.clearPopup();
         }
         break;
 
 
       case "popupshowing":
--- a/browser/components/extensions/parent/ext-pageAction.js
+++ b/browser/components/extensions/parent/ext-pageAction.js
@@ -1,33 +1,29 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 ChromeUtils.defineModuleGetter(this, "PageActions",
                                "resource:///modules/PageActions.jsm");
 ChromeUtils.defineModuleGetter(this, "PanelPopup",
                                "resource:///modules/ExtensionPopups.jsm");
-ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
-                               "resource://gre/modules/TelemetryStopwatch.jsm");
-
 
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
   IconDetails,
   StartupCache,
 } = ExtensionParent;
 
 var {
   DefaultWeakMap,
+  ExtensionTelemetry,
 } = ExtensionUtils;
 
-const popupOpenTimingHistogram = "WEBEXT_PAGEACTION_POPUP_OPEN_MS";
-
 // WeakMap[Extension -> PageAction]
 let pageActionMap = new WeakMap();
 
 this.pageAction = class extends ExtensionAPI {
   static for(extension) {
     return pageActionMap.get(extension);
   }
 
@@ -259,47 +255,49 @@ this.pageAction = class extends Extensio
   }
 
   // Handles a click event on the page action button for the given
   // window.
   // If the page action has a |popup| property, a panel is opened to
   // that URL. Otherwise, a "click" event is emitted, and dispatched to
   // the any click listeners in the add-on.
   async handleClick(window) {
-    TelemetryStopwatch.start(popupOpenTimingHistogram, this);
+    const {extension} = this;
+
+    ExtensionTelemetry.pageActionPopupOpen.stopwatchStart(extension, this);
     let tab = window.gBrowser.selectedTab;
     let popupURL = this.tabContext.get(tab).popup;
 
     this.tabManager.addActiveTabPermission(tab);
 
     // If the widget has a popup URL defined, we open a popup, but do not
     // dispatch a click event to the extension.
     // If it has no popup URL defined, we dispatch a click event, but do not
     // open a popup.
     if (popupURL) {
       if (this.popupNode && this.popupNode.panel.state !== "closed") {
         // The panel is being toggled closed.
-        TelemetryStopwatch.cancel(popupOpenTimingHistogram, this);
+        ExtensionTelemetry.pageActionPopupOpen.stopwatchCancel(extension, this);
         window.BrowserPageActions.togglePanelForAction(this.browserPageAction,
                                                        this.popupNode.panel);
         return;
       }
 
-      this.popupNode = new PanelPopup(this.extension, window.document, popupURL,
+      this.popupNode = new PanelPopup(extension, window.document, popupURL,
                                       this.browserStyle);
       // Remove popupNode when it is closed.
       this.popupNode.panel.addEventListener("popuphiding", () => {
         this.popupNode = undefined;
       }, {once: true});
       await this.popupNode.contentReady;
       window.BrowserPageActions.togglePanelForAction(this.browserPageAction,
                                                      this.popupNode.panel);
-      TelemetryStopwatch.finish(popupOpenTimingHistogram, this);
+      ExtensionTelemetry.pageActionPopupOpen.stopwatchFinish(extension, this);
     } else {
-      TelemetryStopwatch.cancel(popupOpenTimingHistogram, this);
+      ExtensionTelemetry.pageActionPopupOpen.stopwatchCancel(extension, this);
       this.emit("click", tab);
     }
   }
 
   /**
    * Updates the `tabData` for any location change, however it only updates the button
    * when the selected tab has a location change, or the selected tab has changed.
    *
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_telemetry.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_telemetry.js
@@ -1,14 +1,19 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 const TIMING_HISTOGRAM = "WEBEXT_BROWSERACTION_POPUP_OPEN_MS";
+const TIMING_HISTOGRAM_KEYED = "WEBEXT_BROWSERACTION_POPUP_OPEN_MS_BY_ADDONID";
 const RESULT_HISTOGRAM = "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT";
+const RESULT_HISTOGRAM_KEYED = "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT_BY_ADDONID";
+
+const EXTENSION_ID1 = "@test-extension1";
+const EXTENSION_ID2 = "@test-extension2";
 
 // Keep this in sync with the order in Histograms.json for
 // WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT
 const CATEGORIES = [
   "popupShown",
   "clearAfterHover",
   "clearAfterMousedown",
 ];
@@ -43,103 +48,181 @@ add_task(async function testBrowserActio
         "browser_style": true,
       },
     },
 
     files: {
       "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body><div></div></body></html>`,
     },
   };
-  let extension1 = ExtensionTestUtils.loadExtension(extensionOptions);
-  let extension2 = ExtensionTestUtils.loadExtension(extensionOptions);
+  let extension1 = ExtensionTestUtils.loadExtension({
+    ...extensionOptions,
+    manifest: {
+      ...extensionOptions.manifest,
+      "applications": {
+        "gecko": {"id": EXTENSION_ID1},
+      },
+    },
+  });
+  let extension2 = ExtensionTestUtils.loadExtension({
+    ...extensionOptions,
+    manifest: {
+      ...extensionOptions.manifest,
+      "applications": {
+        "gecko": {"id": EXTENSION_ID2},
+      },
+    },
+  });
 
   let histogram = Services.telemetry.getHistogramById(TIMING_HISTOGRAM);
+  let histogramKeyed = Services.telemetry.getKeyedHistogramById(TIMING_HISTOGRAM_KEYED);
 
   histogram.clear();
+  histogramKeyed.clear();
 
   is(histogram.snapshot().sum, 0,
      `No data recorded for histogram: ${TIMING_HISTOGRAM}.`);
+  is(Object.keys(histogramKeyed).length, 0,
+     `No data recorded for histogram: ${TIMING_HISTOGRAM_KEYED}.`);
 
   await extension1.startup();
   await extension2.startup();
 
   is(histogram.snapshot().sum, 0,
      `No data recorded for histogram after startup: ${TIMING_HISTOGRAM}.`);
+  is(Object.keys(histogramKeyed).length, 0,
+     `No data recorded for histogram after startup: ${TIMING_HISTOGRAM_KEYED}.`);
 
   clickBrowserAction(extension1);
   await awaitExtensionPanel(extension1);
   let sumOld = histogram.snapshot().sum;
   ok(sumOld > 0,
      `Data recorded for first extension for histogram: ${TIMING_HISTOGRAM}.`);
+
+  let oldKeyedSnapshot = histogramKeyed.snapshot();
+  Assert.deepEqual(Object.keys((oldKeyedSnapshot)), [EXTENSION_ID1],
+                   `Data recorded for first extension for histogram: ${TIMING_HISTOGRAM_KEYED}.`);
+  ok(oldKeyedSnapshot[EXTENSION_ID1].sum > 0,
+     `Data recorded for first extension for histogram: ${TIMING_HISTOGRAM_KEYED}.`);
+
   await closeBrowserAction(extension1);
 
   clickBrowserAction(extension2);
   await awaitExtensionPanel(extension2);
   let sumNew = histogram.snapshot().sum;
   ok(sumNew > sumOld,
      `Data recorded for second extension for histogram: ${TIMING_HISTOGRAM}.`);
   sumOld = sumNew;
+
+  let newKeyedSnapshot = histogramKeyed.snapshot();
+  Assert.deepEqual(Object.keys((newKeyedSnapshot)).sort(), [EXTENSION_ID1, EXTENSION_ID2],
+                   `Data recorded for second extension for histogram: ${TIMING_HISTOGRAM_KEYED}.`);
+  ok(newKeyedSnapshot[EXTENSION_ID2].sum > 0,
+     `Data recorded for second extension for histogram: ${TIMING_HISTOGRAM_KEYED}.`);
+  is(newKeyedSnapshot[EXTENSION_ID1].sum, oldKeyedSnapshot[EXTENSION_ID1].sum,
+     `Data recorded for first extension should not change for histogram: ${TIMING_HISTOGRAM_KEYED}.`);
+  oldKeyedSnapshot = newKeyedSnapshot;
+
   await closeBrowserAction(extension2);
 
   clickBrowserAction(extension2);
   await awaitExtensionPanel(extension2);
   sumNew = histogram.snapshot().sum;
   ok(sumNew > sumOld,
      `Data recorded for second opening of popup for histogram: ${TIMING_HISTOGRAM}.`);
   sumOld = sumNew;
+
+  newKeyedSnapshot = histogramKeyed.snapshot();
+  ok(newKeyedSnapshot[EXTENSION_ID2].sum > oldKeyedSnapshot[EXTENSION_ID2].sum,
+     `Data recorded for second opening of popup for histogram: ${TIMING_HISTOGRAM_KEYED}.`);
+  is(newKeyedSnapshot[EXTENSION_ID1].sum, oldKeyedSnapshot[EXTENSION_ID1].sum,
+     `Data recorded for first extension should not change for histogram: ${TIMING_HISTOGRAM_KEYED}.`);
+  oldKeyedSnapshot = newKeyedSnapshot;
+
   await closeBrowserAction(extension2);
 
   clickBrowserAction(extension1);
   await awaitExtensionPanel(extension1);
   sumNew = histogram.snapshot().sum;
   ok(sumNew > sumOld,
      `Data recorded for second opening of popup for histogram: ${TIMING_HISTOGRAM}.`);
 
+  newKeyedSnapshot = histogramKeyed.snapshot();
+  ok(newKeyedSnapshot[EXTENSION_ID1].sum > oldKeyedSnapshot[EXTENSION_ID1].sum,
+     `Data recorded for second opening of popup for histogram: ${TIMING_HISTOGRAM_KEYED}.`);
+  is(newKeyedSnapshot[EXTENSION_ID2].sum, oldKeyedSnapshot[EXTENSION_ID2].sum,
+     `Data recorded for second extension should not change for histogram: ${TIMING_HISTOGRAM_KEYED}.`);
+
+  await closeBrowserAction(extension1);
+
   await extension1.unload();
   await extension2.unload();
 });
 
 add_task(async function testBrowserActionTelemetryResults() {
   let extensionOptions = {
     manifest: {
+      "applications": {
+        "gecko": {"id": EXTENSION_ID1},
+      },
       "browser_action": {
         "default_popup": "popup.html",
         "browser_style": true,
       },
     },
 
     files: {
       "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body><div></div></body></html>`,
     },
   };
   let extension = ExtensionTestUtils.loadExtension(extensionOptions);
 
   let histogram = Services.telemetry.getHistogramById(RESULT_HISTOGRAM);
+  let histogramKeyed = Services.telemetry.getKeyedHistogramById(RESULT_HISTOGRAM_KEYED);
 
   histogram.clear();
+  histogramKeyed.clear();
 
   is(histogram.snapshot().sum, 0,
-     `No data recorded for histogram: ${TIMING_HISTOGRAM}.`);
+     `No data recorded for histogram: ${RESULT_HISTOGRAM}.`);
+  is(Object.keys(histogramKeyed).length, 0,
+     `No data recorded for histogram: ${RESULT_HISTOGRAM_KEYED}.`);
 
   await extension.startup();
 
   // Make sure the mouse isn't hovering over the browserAction widget to start.
   EventUtils.synthesizeMouseAtCenter(gURLBar, {type: "mouseover"}, window);
 
   let widget = getBrowserActionWidget(extension).forWindow(window);
 
   // Hover the mouse over the browserAction widget and then move it away.
   EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mouseover", button: 0}, window);
   EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mouseout", button: 0}, window);
   EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"}, window);
+
   assertOnlyOneTypeSet(histogram.snapshot(), "clearAfterHover");
+
+  let keyedSnapshot = histogramKeyed.snapshot();
+  Assert.deepEqual(Object.keys((keyedSnapshot)), [EXTENSION_ID1],
+                   `Data recorded for histogram: ${RESULT_HISTOGRAM_KEYED}.`);
+  assertOnlyOneTypeSet(keyedSnapshot[EXTENSION_ID1], "clearAfterHover");
+
   histogram.clear();
+  histogramKeyed.clear();
 
   // TODO: Create a test for cancel after mousedown.
   // This is tricky because calling mouseout after mousedown causes a
   // "Hover" event to be added to the queue in ext-browserAction.js.
 
   clickBrowserAction(extension);
   await awaitExtensionPanel(extension);
+
   assertOnlyOneTypeSet(histogram.snapshot(), "popupShown");
 
+  keyedSnapshot = histogramKeyed.snapshot();
+  Assert.deepEqual(Object.keys((keyedSnapshot)), [EXTENSION_ID1],
+                   `Data recorded for histogram: ${RESULT_HISTOGRAM_KEYED}.`);
+  assertOnlyOneTypeSet(keyedSnapshot[EXTENSION_ID1], "popupShown");
+
+  await closeBrowserAction(extension);
+
   await extension.unload();
 });
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_telemetry.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_telemetry.js
@@ -1,16 +1,24 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 const HISTOGRAM = "WEBEXT_PAGEACTION_POPUP_OPEN_MS";
+const HISTOGRAM_KEYED = "WEBEXT_PAGEACTION_POPUP_OPEN_MS_BY_ADDONID";
+
+const EXTENSION_ID1 = "@test-extension1";
+const EXTENSION_ID2 = "@test-extension2";
+
+function snapshotCountsSum(snapshot) {
+  return snapshot.counts.reduce((a, b) => a + b, 0);
+}
 
 function histogramCountsSum(histogram) {
-  return histogram.snapshot().counts.reduce((a, b) => a + b, 0);
+  return snapshotCountsSum(histogram.snapshot());
 }
 
 add_task(async function testPageActionTelemetry() {
   let extensionOptions = {
     manifest: {
       "page_action": {
         "default_popup": "popup.html",
         "browser_style": true,
@@ -25,49 +33,105 @@ add_task(async function testPageActionTe
         });
       });
     },
 
     files: {
       "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body><div></div></body></html>`,
     },
   };
-  let extension1 = ExtensionTestUtils.loadExtension(extensionOptions);
-  let extension2 = ExtensionTestUtils.loadExtension(extensionOptions);
+  let extension1 = ExtensionTestUtils.loadExtension({
+    ...extensionOptions,
+    manifest: {
+      ...extensionOptions.manifest,
+      "applications": {
+        "gecko": {"id": EXTENSION_ID1},
+      },
+    },
+  });
+  let extension2 = ExtensionTestUtils.loadExtension({
+    ...extensionOptions,
+    manifest: {
+      ...extensionOptions.manifest,
+      "applications": {
+        "gecko": {"id": EXTENSION_ID2},
+      },
+    },
+  });
 
   let histogram = Services.telemetry.getHistogramById(HISTOGRAM);
+  let histogramKeyed = Services.telemetry.getKeyedHistogramById(HISTOGRAM_KEYED);
+
   histogram.clear();
+  histogramKeyed.clear();
+
   is(histogramCountsSum(histogram), 0,
      `No data recorded for histogram: ${HISTOGRAM}.`);
+  is(Object.keys(histogramKeyed).length, 0,
+     `No data recorded for histogram: ${HISTOGRAM_KEYED}.`);
 
   await extension1.startup();
   await extension1.awaitMessage("action-shown");
   await extension2.startup();
   await extension2.awaitMessage("action-shown");
+
   is(histogramCountsSum(histogram), 0,
      `No data recorded for histogram after PageAction shown: ${HISTOGRAM}.`);
+  is(Object.keys(histogramKeyed).length, 0,
+     `No data recorded for histogram after PageAction shown: ${HISTOGRAM_KEYED}.`);
 
   clickPageAction(extension1, window);
   await awaitExtensionPanel(extension1);
+
   is(histogramCountsSum(histogram), 1,
      `Data recorded for first extension for histogram: ${HISTOGRAM}.`);
+  let keyedSnapshot = histogramKeyed.snapshot();
+  Assert.deepEqual(Object.keys((keyedSnapshot)), [EXTENSION_ID1],
+                   `Data recorded for first extension histogram: ${HISTOGRAM_KEYED}.`);
+  is(snapshotCountsSum(keyedSnapshot[EXTENSION_ID1]), 1,
+     `Data recorded for first extension for histogram: ${HISTOGRAM_KEYED}.`);
+
   await closePageAction(extension1, window);
 
   clickPageAction(extension2, window);
   await awaitExtensionPanel(extension2);
+
   is(histogramCountsSum(histogram), 2,
      `Data recorded for second extension for histogram: ${HISTOGRAM}.`);
+  keyedSnapshot = histogramKeyed.snapshot();
+  Assert.deepEqual(Object.keys((keyedSnapshot)).sort(), [EXTENSION_ID1, EXTENSION_ID2],
+                   `Data recorded for second extension histogram: ${HISTOGRAM_KEYED}.`);
+  is(snapshotCountsSum(keyedSnapshot[EXTENSION_ID2]), 1,
+     `Data recorded for second extension for histogram: ${HISTOGRAM_KEYED}.`);
+  is(snapshotCountsSum(keyedSnapshot[EXTENSION_ID1]), 1,
+     `Data recorded for first extension should not change for histogram: ${HISTOGRAM_KEYED}.`);
+
   await closePageAction(extension2, window);
 
   clickPageAction(extension2, window);
   await awaitExtensionPanel(extension2);
+
   is(histogramCountsSum(histogram), 3,
      `Data recorded for second opening of popup for histogram: ${HISTOGRAM}.`);
+  keyedSnapshot = histogramKeyed.snapshot();
+  is(snapshotCountsSum(keyedSnapshot[EXTENSION_ID2]), 2,
+     `Data recorded for second opening of popup for histogram: ${HISTOGRAM_KEYED}.`);
+  is(snapshotCountsSum(keyedSnapshot[EXTENSION_ID1]), 1,
+     `Data recorded for first extension should not change for histogram: ${HISTOGRAM_KEYED}.`);
+
   await closePageAction(extension2, window);
 
   clickPageAction(extension1, window);
   await awaitExtensionPanel(extension1);
+
   is(histogramCountsSum(histogram), 4,
      `Data recorded for second opening of popup for histogram: ${HISTOGRAM}.`);
+  keyedSnapshot = histogramKeyed.snapshot();
+  is(snapshotCountsSum(keyedSnapshot[EXTENSION_ID1]), 2,
+     `Data recorded for second opening of popup for histogram: ${HISTOGRAM_KEYED}.`);
+  is(snapshotCountsSum(keyedSnapshot[EXTENSION_ID1]), 2,
+     `Data recorded for second extension should not change for histogram: ${HISTOGRAM_KEYED}.`);
+
+  await closePageAction(extension1, window);
 
   await extension1.unload();
   await extension2.unload();
 });
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -49,17 +49,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
   FileSource: "resource://gre/modules/L10nRegistry.jsm",
   L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
   Log: "resource://gre/modules/Log.jsm",
   MessageChannel: "resource://gre/modules/MessageChannel.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   PluralForm: "resource://gre/modules/PluralForm.jsm",
   Schemas: "resource://gre/modules/Schemas.jsm",
-  TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
   XPIProvider: "resource://gre/modules/addons/XPIProvider.jsm",
 });
 
 XPCOMUtils.defineLazyGetter(
   this, "processScript",
   () => Cc["@mozilla.org/webextensions/extension-process-script;1"]
           .getService().wrappedJSObject);
 
@@ -94,16 +93,17 @@ var {
   ParentAPIManager,
   StartupCache,
   apiManager: Management,
 } = ExtensionParent;
 
 const {
   getUniqueId,
   promiseTimeout,
+  ExtensionTelemetry,
 } = ExtensionUtils;
 
 const {
   EventEmitter,
 } = ExtensionCommon;
 
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
 
@@ -1760,17 +1760,17 @@ class Extension extends ExtensionData {
       // so during upgrades and add-on restarts, startup() gets called
       // before the last shutdown has completed, and this fails when
       // there's another active add-on with the same ID.
       this.policy.active = true;
     }
 
     this.policy.extension = this;
 
-    TelemetryStopwatch.start("WEBEXT_EXTENSION_STARTUP_MS", this);
+    ExtensionTelemetry.extensionStartup.stopwatchStart(this);
     try {
       await this.loadManifest();
 
       if (!this.hasShutdown) {
         await this.initLocale();
       }
 
       if (this.errors.length) {
@@ -1819,17 +1819,17 @@ class Extension extends ExtensionData {
 
       await Promise.all([
         Management.emit("startup", this),
         this.runManifest(this.manifest),
       ]);
 
       Management.emit("ready", this);
       this.emit("ready");
-      TelemetryStopwatch.finish("WEBEXT_EXTENSION_STARTUP_MS", this);
+      ExtensionTelemetry.extensionStartup.stopwatchFinish(this);
     } catch (errors) {
       for (let e of [].concat(errors)) {
         dump(`Extension error: ${e.message || e} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
         Cu.reportError(e);
       }
 
       if (this.policy) {
         this.policy.active = false;
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -11,17 +11,16 @@ var EXPORTED_SYMBOLS = ["ExtensionConten
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   LanguageDetector: "resource:///modules/translation/LanguageDetector.jsm",
   MessageChannel: "resource://gre/modules/MessageChannel.jsm",
   Schemas: "resource://gre/modules/Schemas.jsm",
-  TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
   WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
                                    "@mozilla.org/content/style-sheet-service;1",
                                    "nsIStyleSheetService");
 
 XPCOMUtils.defineLazyServiceGetter(this, "processScript",
@@ -37,16 +36,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["crypto", "TextEncoder"]);
 
 const {
   DefaultMap,
   DefaultWeakMap,
+  ExtensionTelemetry,
   getInnerWindowID,
   getWinUtils,
   promiseDocumentIdle,
   promiseDocumentLoaded,
   promiseDocumentReady,
 } = ExtensionUtils;
 
 const {
@@ -64,17 +64,16 @@ const {
 } = ExtensionChild;
 
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
 
 
 var DocumentManager;
 
 const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
-const CONTENT_SCRIPT_INJECTION_HISTOGRAM = "WEBEXT_CONTENT_SCRIPT_INJECTION_MS";
 
 var apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("content", Schemas);
     this.initialized = false;
   }
 
   lazyInit() {
@@ -488,29 +487,31 @@ class Script {
         document.blockParsing(promise, {blockScriptCreated: false});
       }
 
       scripts = await promise;
     }
 
     let result;
 
+    const {extension} = context;
+
     // The evaluations below may throw, in which case the promise will be
     // automatically rejected.
-    TelemetryStopwatch.start(CONTENT_SCRIPT_INJECTION_HISTOGRAM, context);
+    ExtensionTelemetry.contentScriptInjection.stopwatchStart(extension, context);
     try {
       for (let script of scripts) {
         result = script.executeInGlobal(context.cloneScope);
       }
 
       if (this.matcher.jsCode) {
         result = Cu.evalInSandbox(this.matcher.jsCode, context.cloneScope, "latest");
       }
     } finally {
-      TelemetryStopwatch.finish(CONTENT_SCRIPT_INJECTION_HISTOGRAM, context);
+      ExtensionTelemetry.contentScriptInjection.stopwatchFinish(extension, context);
     }
 
     await cssPromise;
     return result;
   }
 }
 
 var contentScripts = new DefaultWeakMap(matcher => {
--- a/toolkit/components/extensions/ExtensionStorageIDB.jsm
+++ b/toolkit/components/extensions/ExtensionStorageIDB.jsm
@@ -3,31 +3,36 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["ExtensionStorageIDB"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/IndexedDB.jsm");
+ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
   ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
   Services: "resource://gre/modules/Services.jsm",
   OS: "resource://gre/modules/osfile.jsm",
 });
 
 // The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
 // storage used by the browser.storage.local API is not directly accessible from the extension code).
 XPCOMUtils.defineLazyGetter(this, "WEBEXT_STORAGE_USER_CONTEXT_ID", () => {
   return ContextualIdentityService.getDefaultPrivateIdentity(
     "userContextIdInternal.webextStorageLocal").userContextId;
 });
 
+var {
+  getTrimmedString,
+} = ExtensionUtils;
+
 const IDB_NAME = "webExtensions-storage-local";
 const IDB_DATA_STORENAME = "storage-local-data";
 const IDB_VERSION = 1;
 const IDB_MIGRATE_RESULT_HISTOGRAM = "WEBEXT_STORAGE_LOCAL_IDB_MIGRATE_RESULT_COUNT";
 
 // Whether or not the installed extensions should be migrated to the storage.local IndexedDB backend.
 const BACKEND_ENABLED_PREF = "extensions.webextensions.ExtensionStorageIDB.enabled";
 const IDB_MIGRATED_PREF_BRANCH = "extensions.webextensions.ExtensionStorageIDB.migrated";
@@ -43,39 +48,16 @@ var DataMigrationTelemetry = {
 
     // Ensure that these telemetry events category is enabled.
     Services.telemetry.setEventRecordingEnabled("extensions.data", true);
 
     this.resultHistogram = Services.telemetry.getHistogramById(IDB_MIGRATE_RESULT_HISTOGRAM);
   },
 
   /**
-   * 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)}`;
-  },
-
-  /**
    * Get the DOMException error name for a given error object.
    *
    * @param {Error | undefined} error
    *        The Error object to convert into a string, or undefined if there was no error.
    *
    * @returns {string | undefined}
    *          The DOMException error name (sliced to a maximum of 80 chars),
    *          "OtherError" if the error object is not a DOMException instance,
@@ -83,17 +65,17 @@ var DataMigrationTelemetry = {
    */
   getErrorName(error) {
     if (!error) {
       return undefined;
     }
 
     if (error instanceof DOMException) {
       if (error.name.length > 80) {
-        return this.getTrimmedString(error.name);
+        return getTrimmedString(error.name);
       }
 
       return error.name;
     }
 
     return "OtherError";
   },
 
@@ -145,17 +127,17 @@ var DataMigrationTelemetry = {
         extra.has_olddata = hasOldData ? "y" : "n";
       }
 
       if (error) {
         extra.error_name = this.getErrorName(error);
       }
 
       Services.telemetry.recordEvent("extensions.data", "migrateResult", "storageLocal",
-                                     this.getTrimmedString(extensionId), extra);
+                                     getTrimmedString(extensionId), extra);
     } catch (err) {
       // Report any telemetry error on the browser console, but
       // we treat it as a non-fatal error and we don't re-throw
       // it to the caller.
       Cu.reportError(err);
     }
   },
 };
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -7,16 +7,18 @@
 
 var EXPORTED_SYMBOLS = ["ExtensionUtils"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "setTimeout",
                                "resource://gre/modules/Timer.jsm");
+ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
+                               "resource://gre/modules/TelemetryStopwatch.jsm");
 
 // xpcshell doesn't handle idle callbacks well.
 XPCOMUtils.defineLazyGetter(this, "idleTimeout",
                             () => Services.appinfo.name === "XPCShell" ? 500 : undefined);
 
 // It would be nicer to go through `Services.appinfo`, but some tests need to be
 // able to replace that field with a custom implementation before it is first
 // called.
@@ -262,27 +264,182 @@ function flushJarCache(jarPath) {
 const chromeModifierKeyMap = {
   "Alt": "alt",
   "Command": "accel",
   "Ctrl": "accel",
   "MacCtrl": "control",
   "Shift": "shift",
 };
 
+/**
+ * Get a trimmed version of the given string if it is longer than 80 chars (used in telemetry
+ * when a string may be longer than allowed).
+ *
+ * @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.
+ */
+function 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)}`;
+}
+
+/**
+ * This is a internal helper object which contains a collection of helpers used to make it easier
+ * to collect extension telemetry (in both the general histogram and in the one keyed by addon id).
+ *
+ * This helper object is not exported from ExtensionUtils, it is used by the ExtensionTelemetry
+ * Proxy which is exported and used by the callers to record telemetry data for one of the
+ * supported metrics.
+ */
+const ExtensionTelemetryHelpers = {
+  // Allow callers to refer to the existing metrics by accessing it as properties of the
+  // ExtensionTelemetry.metrics (e.g. ExtensionTelemetry.metrics.extensionStartup).
+
+  // Cache of the metrics helper lazily created by the ExtensionTelemetry Proxy.
+  _metricsMap: new Map(),
+
+  // Map of the base histogram ids for the metrics recorded for the extensions.
+  _histograms: {
+    "extensionStartup": "WEBEXT_EXTENSION_STARTUP_MS",
+    "backgroundPageLoad": "WEBEXT_BACKGROUND_PAGE_LOAD_MS",
+    "browserActionPopupOpen": "WEBEXT_BROWSERACTION_POPUP_OPEN_MS",
+    "browserActionPreloadResult": "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT",
+    "contentScriptInjection": "WEBEXT_CONTENT_SCRIPT_INJECTION_MS",
+    "pageActionPopupOpen": "WEBEXT_PAGEACTION_POPUP_OPEN_MS",
+    "storageLocalGetJSON": "WEBEXT_STORAGE_LOCAL_GET_MS",
+    "storageLocalSetJSON": "WEBEXT_STORAGE_LOCAL_SET_MS",
+    "storageLocalGetIDB": "WEBEXT_STORAGE_LOCAL_IDB_GET_MS",
+    "storageLocalSetIDB": "WEBEXT_STORAGE_LOCAL_IDB_SET_MS",
+  },
+  // Wraps a call to a TelemetryStopwatch method.
+  /**
+   * Wraps a call to a TelemetryStopwatch method for a given metric and extension.
+   *
+   * @param {string} method
+   *        The stopwatch method to call ("start", "finish" or "cancel").
+   * @param {string} metric
+   *        The stopwatch metric to record (used to retrieve the base histogram id from the _histogram object).
+   * @param {Extension | BrowserExtensionContent} extension
+   *        The extension to record the telemetry for.
+   * @param {any | undefined} [obj = extension]
+   *        An optional telemetry stopwatch object (which defaults to the extension parameter when missing).
+   */
+  _wrappedStopwatchMethod(method, metric, extension, obj = extension) {
+    if (!extension) {
+      throw new Error(`Mandatory extension parameter is undefined`);
+    }
+
+    const baseId = this._histograms[metric];
+    if (!baseId) {
+      throw new Error(`Unknown metric ${metric}`);
+    }
+
+    // Record metric in the general histogram.
+    TelemetryStopwatch[method](baseId, obj);
+
+    // Record metric in the histogram keyed by addon id.
+    let extensionId = getTrimmedString(extension.id);
+    TelemetryStopwatch[`${method}Keyed`](`${baseId}_BY_ADDONID`, extensionId, obj);
+  },
+  /**
+   * Record a telemetry category and/or value for a given metric.
+   *
+   * @param {string} metric
+   *        The metric to record (used to retrieve the base histogram id from the _histogram object).
+   * @param {Object}                              options
+   * @param {Extension | BrowserExtensionContent} options.extension
+   *        The extension to record the telemetry for.
+   * @param {string | undefined}                  [options.category]
+   *        An optional histogram category.
+   * @param {number | undefined}                  [options.value]
+   *        An optional value to record.
+   */
+  _histogramAdd(metric, {category, extension, value}) {
+    if (!extension) {
+      throw new Error(`Mandatory extension parameter is undefined`);
+    }
+
+    const baseId = this._histograms[metric];
+    if (!baseId) {
+      throw new Error(`Unknown metric ${metric}`);
+    }
+
+    const histogram = Services.telemetry.getHistogramById(baseId);
+    if (typeof category === "string") {
+      histogram.add(category, value);
+    } else {
+      histogram.add(value);
+    }
+
+    const keyedHistogram = Services.telemetry.getKeyedHistogramById(`${baseId}_BY_ADDONID`);
+    const extensionId = getTrimmedString(extension.id);
+
+    if (typeof category === "string") {
+      keyedHistogram.add(extensionId, category, value);
+    } else {
+      keyedHistogram.add(extensionId, value);
+    }
+  },
+};
+
+/**
+ * This proxy object provides the telemetry helpers for the currently supported metrics (the ones listed in
+ * ExtensionTelemetryHelpers._histograms), the telemetry helpers for a particular metric are lazily created
+ * when the related property is being accessed on this object for the first time, e.g.:
+ *
+ *      ExtensionTelemetry.extensionStartup.stopwatchStart(extension);
+ *      ExtensionTelemetry.browserActionPreloadResult.histogramAdd({category: "Shown", extension});
+ */
+const ExtensionTelemetry = new Proxy(ExtensionTelemetryHelpers, {
+  get(target, prop, receiver) {
+    if (!(prop in target._histograms)) {
+      throw new Error(`Unknown metric ${prop}`);
+    }
+
+    // Lazily create and cache the metric result object.
+    if (!target._metricsMap.has(prop)) {
+      target._metricsMap.set(prop, {
+        // Stopwatch histogram helpers.
+        stopwatchStart: target._wrappedStopwatchMethod.bind(target, "start", prop),
+        stopwatchFinish: target._wrappedStopwatchMethod.bind(target, "finish", prop),
+        stopwatchCancel: target._wrappedStopwatchMethod.bind(target, "cancel", prop),
+        // Result histogram helpers.
+        histogramAdd: target._histogramAdd.bind(target, prop),
+      });
+    }
+
+    return target._metricsMap.get(prop);
+  },
+});
+
 var ExtensionUtils = {
   chromeModifierKeyMap,
   flushJarCache,
   getInnerWindowID,
   getMessageManager,
+  getTrimmedString,
   getUniqueId,
   filterStack,
   getWinUtils,
   promiseDocumentIdle,
   promiseDocumentLoaded,
   promiseDocumentReady,
   promiseEvent,
   promiseObserved,
   promiseTimeout,
   DefaultMap,
   DefaultWeakMap,
   ExtensionError,
+  ExtensionTelemetry,
   LimitedSet,
 };
--- a/toolkit/components/extensions/child/ext-storage.js
+++ b/toolkit/components/extensions/child/ext-storage.js
@@ -1,52 +1,47 @@
 "use strict";
 
 ChromeUtils.defineModuleGetter(this, "ExtensionStorage",
                                "resource://gre/modules/ExtensionStorage.jsm");
 ChromeUtils.defineModuleGetter(this, "ExtensionStorageIDB",
                                "resource://gre/modules/ExtensionStorageIDB.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
-ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
-                               "resource://gre/modules/TelemetryStopwatch.jsm");
 
-// Telemetry histogram keys for the JSONFile backend.
-const storageGetHistogram = "WEBEXT_STORAGE_LOCAL_GET_MS";
-const storageSetHistogram = "WEBEXT_STORAGE_LOCAL_SET_MS";
-// Telemetry  histogram keys for the IndexedDB backend.
-const storageGetIDBHistogram = "WEBEXT_STORAGE_LOCAL_IDB_GET_MS";
-const storageSetIDBHistogram = "WEBEXT_STORAGE_LOCAL_IDB_SET_MS";
+var {
+  ExtensionTelemetry,
+} = ExtensionUtils;
 
 // Wrap a storage operation in a TelemetryStopWatch.
-async function measureOp(histogram, fn) {
+async function measureOp(telemetryMetric, extension, fn) {
   const stopwatchKey = {};
-  TelemetryStopwatch.start(histogram, stopwatchKey);
+  telemetryMetric.stopwatchStart(extension, stopwatchKey);
   try {
     let result = await fn();
-    TelemetryStopwatch.finish(histogram, stopwatchKey);
+    telemetryMetric.stopwatchFinish(extension, stopwatchKey);
     return result;
   } catch (err) {
-    TelemetryStopwatch.cancel(histogram, stopwatchKey);
+    telemetryMetric.stopwatchCancel(extension, stopwatchKey);
     throw err;
   }
 }
 
 this.storage = class extends ExtensionAPI {
   getLocalFileBackend(context, {deserialize, serialize}) {
     return {
       get(keys) {
-        return measureOp(storageGetHistogram, () => {
+        return measureOp(ExtensionTelemetry.storageLocalGetJSON, context.extension, () => {
           return context.childManager.callParentAsyncFunction(
             "storage.local.JSONFileBackend.get",
             [serialize(keys)]).then(deserialize);
         });
       },
       set(items) {
-        return measureOp(storageSetHistogram, () => {
+        return measureOp(ExtensionTelemetry.storageLocalSetJSON, context.extension, () => {
           return context.childManager.callParentAsyncFunction(
             "storage.local.JSONFileBackend.set", [serialize(items)]);
         });
       },
       remove(keys) {
         return context.childManager.callParentAsyncFunction(
           "storage.local.JSONFileBackend.remove", [serialize(keys)]);
       },
@@ -71,23 +66,23 @@ this.storage = class extends ExtensionAP
         throw err;
       });
 
       return dbPromise;
     }
 
     return {
       get(keys) {
-        return measureOp(storageGetIDBHistogram, async () => {
+        return measureOp(ExtensionTelemetry.storageLocalGetIDB, context.extension, async () => {
           const db = await getDB();
           return db.get(keys);
         });
       },
       set(items) {
-        return measureOp(storageSetIDBHistogram, async () => {
+        return measureOp(ExtensionTelemetry.storageLocalSetIDB, context.extension, async () => {
           const db = await getDB();
           const changes = await db.set(items, {
             serialize: ExtensionStorage.serialize,
           });
 
           if (changes) {
             fireOnChanged(changes);
           }
--- a/toolkit/components/extensions/parent/ext-backgroundPage.js
+++ b/toolkit/components/extensions/parent/ext-backgroundPage.js
@@ -1,19 +1,21 @@
 "use strict";
 
-ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
-                               "resource://gre/modules/TelemetryStopwatch.jsm");
-
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 var {
   HiddenExtensionPage,
   promiseExtensionViewLoaded,
 } = ExtensionParent;
 
+ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
+var {
+  ExtensionTelemetry,
+} = ExtensionUtils;
+
 XPCOMUtils.defineLazyPreferenceGetter(this, "DELAYED_STARTUP",
                                       "extensions.webextensions.background-delayed-startup");
 
 // Responsible for the background_page section of the manifest.
 class BackgroundPage extends HiddenExtensionPage {
   constructor(extension, options) {
     super(extension, "background");
 
@@ -23,35 +25,39 @@ class BackgroundPage extends HiddenExten
     if (this.page) {
       this.url = this.extension.baseURI.resolve(this.page);
     } else if (this.isGenerated) {
       this.url = this.extension.baseURI.resolve("_generated_background_page.html");
     }
   }
 
   async build() {
-    TelemetryStopwatch.start("WEBEXT_BACKGROUND_PAGE_LOAD_MS", this);
+    const {extension} = this;
+
+    ExtensionTelemetry.backgroundPageLoad.stopwatchStart(extension, this);
+
     await this.createBrowserElement();
-    this.extension._backgroundPageFrameLoader = this.browser.frameLoader;
+    extension._backgroundPageFrameLoader = this.browser.frameLoader;
 
     extensions.emit("extension-browser-inserted", this.browser);
 
-    this.browser.loadURI(this.url, {triggeringPrincipal: this.extension.principal});
+    this.browser.loadURI(this.url, {triggeringPrincipal: extension.principal});
 
     let context = await promiseExtensionViewLoaded(this.browser);
-    TelemetryStopwatch.finish("WEBEXT_BACKGROUND_PAGE_LOAD_MS", this);
+
+    ExtensionTelemetry.backgroundPageLoad.stopwatchFinish(extension, this);
 
     if (context) {
       // Wait until all event listeners registered by the script so far
       // to be handled.
       await Promise.all(context.listenerPromises);
       context.listenerPromises = null;
     }
 
-    this.extension.emit("startup");
+    extension.emit("startup");
   }
 
   shutdown() {
     this.extension._backgroundPageFrameLoader = null;
     super.shutdown();
   }
 }
 
--- a/toolkit/components/extensions/test/xpcshell/head_telemetry.js
+++ b/toolkit/components/extensions/test/xpcshell/head_telemetry.js
@@ -11,26 +11,71 @@ const IS_OOP = Services.prefs.getBoolPre
 
 function arraySum(arr) {
   return arr.reduce((a, b) => a + b, 0);
 }
 
 function clearHistograms() {
   Services.telemetry.snapshotHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN,
                                         true /* clear */);
+  Services.telemetry.snapshotKeyedHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN,
+                                             true /* clear */);
 }
 
 function getSnapshots(process) {
   return Services.telemetry.snapshotHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN,
                                                false /* clear */)[process];
 }
 
+function getKeyedSnapshots(process) {
+  return Services.telemetry.snapshotKeyedHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN,
+                                                    false /* clear */)[process];
+}
+
 // TODO Bug 1357509: There is no good way to make sure that the parent received
 // the histogram entries from the extension and content processes.  Let's stick
 // to the ugly, spinning the event loop until we have a good approach.
 function promiseTelemetryRecorded(id, process, expectedCount) {
   let condition = () => {
     let snapshot = Services.telemetry.snapshotHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN,
                                                          false /* clear */)[process][id];
     return snapshot && arraySum(snapshot.counts) >= expectedCount;
   };
   return ContentTaskUtils.waitForCondition(condition);
 }
+
+function promiseKeyedTelemetryRecorded(id, process, expectedKey, expectedCount) {
+  let condition = () => {
+    let snapshot = Services.telemetry.snapshotKeyedHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN,
+                                                              false /* clear */)[process][id];
+    return snapshot && snapshot[expectedKey] && arraySum(snapshot[expectedKey].counts) >= expectedCount;
+  };
+  return ContentTaskUtils.waitForCondition(condition);
+}
+
+function assertHistogramSnapshot(histogramId, {keyed, processSnapshot, expectedValue}, msg) {
+  let histogram;
+
+  if (keyed) {
+    histogram = Services.telemetry.getKeyedHistogramById(histogramId);
+  } else {
+    histogram = Services.telemetry.getHistogramById(histogramId);
+  }
+
+  let res = processSnapshot(histogram.snapshot());
+  Assert.deepEqual(res, expectedValue, msg);
+  return res;
+}
+
+function assertHistogramEmpty(histogramId) {
+  assertHistogramSnapshot(histogramId, {
+    processSnapshot: (snapshot) => snapshot.sum,
+    expectedValue: 0,
+  }, `No data recorded for histogram: ${histogramId}.`);
+}
+
+function assertKeyedHistogramEmpty(histogramId) {
+  assertHistogramSnapshot(histogramId, {
+    keyed: true,
+    processSnapshot: (snapshot) => Object.keys(snapshot).length,
+    expectedValue: 0,
+  }, `No data recorded for histogram: ${histogramId}.`);
+}
--- a/toolkit/components/extensions/test/xpcshell/test_ext_background_telemetry.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_telemetry.js
@@ -1,40 +1,79 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 const HISTOGRAM = "WEBEXT_BACKGROUND_PAGE_LOAD_MS";
+const HISTOGRAM_KEYED = "WEBEXT_BACKGROUND_PAGE_LOAD_MS_BY_ADDONID";
 
 add_task(async function test_telemetry() {
   let extension1 = ExtensionTestUtils.loadExtension({
     background() {
       browser.test.sendMessage("loaded");
     },
   });
 
   let extension2 = ExtensionTestUtils.loadExtension({
     background() {
       browser.test.sendMessage("loaded");
     },
   });
 
-  let histogram = Services.telemetry.getHistogramById(HISTOGRAM);
-  histogram.clear();
-  equal(histogram.snapshot().sum, 0,
-        `No data recorded for histogram: ${HISTOGRAM}.`);
+  clearHistograms();
+
+  assertHistogramEmpty(HISTOGRAM);
+  assertKeyedHistogramEmpty(HISTOGRAM_KEYED);
 
   await extension1.startup();
   await extension1.awaitMessage("loaded");
 
+  const processSnapshot = (snapshot) => {
+    return snapshot.sum > 0;
+  };
+
+  const processKeyedSnapshot = (snapshot) => {
+    let res = {};
+    for (let key of Object.keys(snapshot)) {
+      res[key] = snapshot[key].sum > 0;
+    }
+    return res;
+  };
+
+  assertHistogramSnapshot(HISTOGRAM, {processSnapshot, expectedValue: true},
+                          `Data recorded for first extension for histogram: ${HISTOGRAM}.`);
+
+  assertHistogramSnapshot(HISTOGRAM_KEYED, {
+    keyed: true,
+    processSnapshot: processKeyedSnapshot,
+    expectedValue: {
+      [extension1.extension.id]: true,
+    },
+  }, `Data recorded for first extension for histogram ${HISTOGRAM_KEYED}`);
+
+  let histogram = Services.telemetry.getHistogramById(HISTOGRAM);
+  let histogramKeyed = Services.telemetry.getKeyedHistogramById(HISTOGRAM_KEYED);
   let histogramSum = histogram.snapshot().sum;
-  ok(histogramSum > 0,
-     `Data recorded for first extension for histogram: ${HISTOGRAM}.`);
+  let histogramSumExt1 = histogramKeyed.snapshot()[extension1.extension.id].sum;
 
   await extension2.startup();
   await extension2.awaitMessage("loaded");
 
-  ok(histogram.snapshot().sum > histogramSum,
-     `Data recorded for second extension for histogram: ${HISTOGRAM}.`);
+  assertHistogramSnapshot(HISTOGRAM, {
+    processSnapshot: (snapshot) => snapshot.sum > histogramSum,
+    expectedValue: true,
+  }, `Data recorded for second extension for histogram: ${HISTOGRAM}.`);
+
+  assertHistogramSnapshot(HISTOGRAM_KEYED, {
+    keyed: true,
+    processSnapshot: processKeyedSnapshot,
+    expectedValue: {
+      [extension1.extension.id]: true,
+      [extension2.extension.id]: true,
+    },
+  }, `Data recorded for second extension for histogram ${HISTOGRAM_KEYED}`);
+
+  equal(histogramKeyed.snapshot()[extension1.extension.id].sum, histogramSumExt1,
+        `Data recorder for first extension is unchanged on the keyed histogram ${HISTOGRAM_KEYED}`);
 
   await extension1.unload();
   await extension2.unload();
 });
--- a/toolkit/components/extensions/test/xpcshell/test_ext_extension_content_telemetry.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extension_content_telemetry.js
@@ -1,13 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 const HISTOGRAM = "WEBEXT_CONTENT_SCRIPT_INJECTION_MS";
+const HISTOGRAM_KEYED = "WEBEXT_CONTENT_SCRIPT_INJECTION_MS_BY_ADDONID";
 
 const server = createHttpServer();
 server.registerDirectory("/data/", do_get_file("data"));
 
 const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
 
 add_task(async function test_telemetry() {
   function contentScript() {
@@ -40,37 +41,61 @@ add_task(async function test_telemetry()
       "content_script.js": contentScript,
     },
   });
 
   clearHistograms();
 
   let process = IS_OOP ? "content" : "parent";
   ok(!(HISTOGRAM in getSnapshots(process)), `No data recorded for histogram: ${HISTOGRAM}.`);
+  ok(!(HISTOGRAM_KEYED in getKeyedSnapshots(process)),
+     `No data recorded for keyed histogram: ${HISTOGRAM_KEYED}.`);
 
   await extension1.startup();
+  let extensionId = extension1.extension.id;
+
+  info(`Started extension with id ${extensionId}`);
+
   ok(!(HISTOGRAM in getSnapshots(process)),
      `No data recorded for histogram after startup: ${HISTOGRAM}.`);
+  ok(!(HISTOGRAM_KEYED in getKeyedSnapshots(process)),
+     `No data recorded for keyed histogram: ${HISTOGRAM_KEYED}.`);
 
   let contentPage = await ExtensionTestUtils.loadContentPage(`${BASE_URL}/file_sample.html`);
   await extension1.awaitMessage("content-script-run");
   await promiseTelemetryRecorded(HISTOGRAM, process, 1);
+  await promiseKeyedTelemetryRecorded(HISTOGRAM_KEYED, process, extensionId, 1);
 
   equal(arraySum(getSnapshots(process)[HISTOGRAM].counts), 1,
         `Data recorded for histogram: ${HISTOGRAM}.`);
+  equal(arraySum(getKeyedSnapshots(process)[HISTOGRAM_KEYED][extensionId].counts), 1,
+        `Data recorded for histogram: ${HISTOGRAM_KEYED} with key ${extensionId}.`);
 
   await contentPage.close();
   await extension1.unload();
 
   await extension2.startup();
+  let extensionId2 = extension2.extension.id;
+
+  info(`Started extension with id ${extensionId2}`);
+
   equal(arraySum(getSnapshots(process)[HISTOGRAM].counts), 1,
-        `No data recorded for histogram after startup: ${HISTOGRAM}.`);
+        `No new data recorded for histogram after extension2 startup: ${HISTOGRAM}.`);
+  equal(arraySum(getKeyedSnapshots(process)[HISTOGRAM_KEYED][extensionId].counts), 1,
+        `No new data recorded for histogram after extension2 startup: ${HISTOGRAM_KEYED} with key ${extensionId}.`);
+  ok(!(extensionId2 in getKeyedSnapshots(process)[HISTOGRAM_KEYED]),
+     `No data recorded for histogram after startup: ${HISTOGRAM_KEYED} with key ${extensionId2}.`);
 
   contentPage = await ExtensionTestUtils.loadContentPage(`${BASE_URL}/file_sample.html`);
   await extension2.awaitMessage("content-script-run");
   await promiseTelemetryRecorded(HISTOGRAM, process, 2);
+  await promiseKeyedTelemetryRecorded(HISTOGRAM_KEYED, process, extensionId2, 1);
 
   equal(arraySum(getSnapshots(process)[HISTOGRAM].counts), 2,
         `Data recorded for histogram: ${HISTOGRAM}.`);
+  equal(arraySum(getKeyedSnapshots(process)[HISTOGRAM_KEYED][extensionId].counts), 1,
+        `No new data recorded for histogram: ${HISTOGRAM_KEYED} with key ${extensionId}.`);
+  equal(arraySum(getKeyedSnapshots(process)[HISTOGRAM_KEYED][extensionId2].counts), 1,
+        `Data recorded for histogram: ${HISTOGRAM_KEYED} with key ${extensionId2}.`);
 
   await contentPage.close();
   await extension2.unload();
 });
--- a/toolkit/components/extensions/test/xpcshell/test_ext_extension_startup_telemetry.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extension_startup_telemetry.js
@@ -1,29 +1,68 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 const HISTOGRAM = "WEBEXT_EXTENSION_STARTUP_MS";
+const HISTOGRAM_KEYED = "WEBEXT_EXTENSION_STARTUP_MS_BY_ADDONID";
+
+function processSnapshot(snapshot) {
+  return snapshot.sum > 0;
+}
+
+function processKeyedSnapshot(snapshot) {
+  let res = {};
+  for (let key of Object.keys(snapshot)) {
+    res[key] = snapshot[key].sum > 0;
+  }
+  return res;
+}
 
 add_task(async function test_telemetry() {
   let extension1 = ExtensionTestUtils.loadExtension({});
   let extension2 = ExtensionTestUtils.loadExtension({});
 
-  let histogram = Services.telemetry.getHistogramById(HISTOGRAM);
-  histogram.clear();
-  equal(histogram.snapshot().sum, 0,
-        `No data recorded for histogram: ${HISTOGRAM}.`);
+  clearHistograms();
+
+  assertHistogramEmpty(HISTOGRAM);
+  assertKeyedHistogramEmpty(HISTOGRAM_KEYED);
 
   await extension1.startup();
 
+  assertHistogramSnapshot(HISTOGRAM, {processSnapshot, expectedValue: true},
+                          `Data recorded for first extension for histogram: ${HISTOGRAM}.`);
+
+  assertHistogramSnapshot(HISTOGRAM_KEYED, {
+    keyed: true,
+    processSnapshot: processKeyedSnapshot,
+    expectedValue: {
+      [extension1.extension.id]: true,
+    },
+  }, `Data recorded for first extension for histogram ${HISTOGRAM_KEYED}`);
+
+  let histogram = Services.telemetry.getHistogramById(HISTOGRAM);
+  let histogramKeyed = Services.telemetry.getKeyedHistogramById(HISTOGRAM_KEYED);
   let histogramSum = histogram.snapshot().sum;
-  ok(histogramSum > 0,
-     `Data recorded for first extension for histogram: ${HISTOGRAM}.`);
+  let histogramSumExt1 = histogramKeyed.snapshot()[extension1.extension.id].sum;
 
   await extension2.startup();
 
-  ok(histogram.snapshot().sum > histogramSum,
-     `Data recorded for second extension for histogram: ${HISTOGRAM}.`);
+  assertHistogramSnapshot(HISTOGRAM, {
+    processSnapshot: (snapshot) => snapshot.sum > histogramSum,
+    expectedValue: true,
+  }, `Data recorded for second extension for histogram: ${HISTOGRAM}.`);
+
+  assertHistogramSnapshot(HISTOGRAM_KEYED, {
+    keyed: true,
+    processSnapshot: processKeyedSnapshot,
+    expectedValue: {
+      [extension1.extension.id]: true,
+      [extension2.extension.id]: true,
+    },
+  }, `Data recorded for second extension for histogram ${HISTOGRAM_KEYED}`);
+
+  equal(histogramKeyed.snapshot()[extension1.extension.id].sum, histogramSumExt1,
+        `Data recorder for first extension is unchanged on the keyed histogram ${HISTOGRAM_KEYED}`);
 
   await extension1.unload();
   await extension2.unload();
 });
--- 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
@@ -5,21 +5,21 @@
 // This test file verifies various scenarios related to the data migration
 // from the JSONFile backend to the IDB backend.
 
 AddonTestUtils.init(this);
 AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionStorage.jsm");
+ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 ChromeUtils.import("resource://gre/modules/TelemetryController.jsm");
 
 const {
   ExtensionStorageIDB,
-  DataMigrationTelemetry,
 } = ChromeUtils.import("resource://gre/modules/ExtensionStorageIDB.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   OS: "resource://gre/modules/osfile.jsm",
 });
 
 const {
   promiseShutdownManager,
@@ -247,17 +247,17 @@ add_task(async function test_extensionId
     },
     background,
   });
 
   await extension.startup();
 
   await extension.awaitMessage("storage-local-data-migrated");
 
-  const expectedTrimmedExtensionId = DataMigrationTelemetry.getTrimmedString(EXTENSION_ID);
+  const expectedTrimmedExtensionId = ExtensionUtils.getTrimmedString(EXTENSION_ID);
 
   equal(expectedTrimmedExtensionId.length, 80, "The trimmed version of the extensionId should be 80 chars long");
 
   assertTelemetryEvents(expectedTrimmedExtensionId, [
     {
       method: "migrateResult",
       extra: {
         backend: "IndexedDB",
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_telemetry.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_telemetry.js
@@ -2,32 +2,43 @@
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/ExtensionStorageIDB.jsm");
 
 const HISTOGRAM_JSON_IDS = [
   "WEBEXT_STORAGE_LOCAL_SET_MS", "WEBEXT_STORAGE_LOCAL_GET_MS",
 ];
+const KEYED_HISTOGRAM_JSON_IDS = [
+  "WEBEXT_STORAGE_LOCAL_SET_MS_BY_ADDONID", "WEBEXT_STORAGE_LOCAL_GET_MS_BY_ADDONID",
+];
 
 const HISTOGRAM_IDB_IDS = [
   "WEBEXT_STORAGE_LOCAL_IDB_SET_MS", "WEBEXT_STORAGE_LOCAL_IDB_GET_MS",
 ];
+const KEYED_HISTOGRAM_IDB_IDS = [
+  "WEBEXT_STORAGE_LOCAL_IDB_SET_MS_BY_ADDONID", "WEBEXT_STORAGE_LOCAL_IDB_GET_MS_BY_ADDONID",
+];
 
 const HISTOGRAM_IDS = [].concat(HISTOGRAM_JSON_IDS, HISTOGRAM_IDB_IDS);
+const KEYED_HISTOGRAM_IDS = [].concat(KEYED_HISTOGRAM_JSON_IDS, KEYED_HISTOGRAM_IDB_IDS);
 
 const EXTENSION_ID1 = "@test-extension1";
 const EXTENSION_ID2 = "@test-extension2";
 
 async function test_telemetry_background() {
   const expectedEmptyHistograms = ExtensionStorageIDB.isBackendEnabled ?
           HISTOGRAM_JSON_IDS : HISTOGRAM_IDB_IDS;
+  const expectedEmptyKeyedHistograms = ExtensionStorageIDB.isBackendEnabled ?
+          KEYED_HISTOGRAM_JSON_IDS : KEYED_HISTOGRAM_IDB_IDS;
 
   const expectedNonEmptyHistograms = ExtensionStorageIDB.isBackendEnabled ?
           HISTOGRAM_IDB_IDS : HISTOGRAM_JSON_IDS;
+  const expectedNonEmptyKeyedHistograms = ExtensionStorageIDB.isBackendEnabled ?
+          KEYED_HISTOGRAM_IDB_IDS : KEYED_HISTOGRAM_JSON_IDS;
 
   const server = createHttpServer();
   server.registerDirectory("/data/", do_get_file("data"));
 
   const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
 
   async function contentScript() {
     await browser.storage.local.set({a: "b"});
@@ -51,100 +62,145 @@ async function test_telemetry_background
       await browser.storage.local.get("a");
       browser.test.sendMessage("backgroundDone");
     },
     files: {
       "content_script.js": contentScript,
     },
   };
 
-  let extInfo1 = {
+  let extension1 = ExtensionTestUtils.loadExtension({
     ...baseExtInfo,
     manifest: {
       ...baseManifest,
       applications: {
         gecko: {id: EXTENSION_ID1},
       },
     },
-  };
-  let extInfo2 = {
+  });
+  let extension2 = ExtensionTestUtils.loadExtension({
     ...baseExtInfo,
     manifest: {
       ...baseManifest,
       applications: {
         gecko: {id: EXTENSION_ID2},
       },
     },
-  };
-
-  let extension1 = ExtensionTestUtils.loadExtension(extInfo1);
-  let extension2 = ExtensionTestUtils.loadExtension(extInfo2);
+  });
 
   clearHistograms();
 
   let process = IS_OOP ? "extension" : "parent";
   let snapshots = getSnapshots(process);
+  let keyedSnapshots = getKeyedSnapshots(process);
 
   for (let id of HISTOGRAM_IDS) {
     ok(!(id in snapshots), `No data recorded for histogram: ${id}.`);
   }
 
+  for (let id of KEYED_HISTOGRAM_IDS) {
+    Assert.deepEqual(Object.keys(keyedSnapshots[id] || {}), [], `No data recorded for histogram: ${id}.`);
+  }
+
   await extension1.startup();
   await extension1.awaitMessage("backgroundDone");
   for (let id of expectedNonEmptyHistograms) {
     await promiseTelemetryRecorded(id, process, 1);
   }
+  for (let id of expectedNonEmptyKeyedHistograms) {
+    await promiseKeyedTelemetryRecorded(id, process, EXTENSION_ID1, 1);
+  }
 
   // Telemetry from extension1's background page should be recorded.
   snapshots = getSnapshots(process);
+  keyedSnapshots = getKeyedSnapshots(process);
+
   for (let id of expectedNonEmptyHistograms) {
     equal(arraySum(snapshots[id].counts), 1,
           `Data recorded for histogram: ${id}.`);
   }
 
+  for (let id of expectedNonEmptyKeyedHistograms) {
+    Assert.deepEqual(Object.keys(keyedSnapshots[id]), [EXTENSION_ID1],
+                     `Data recorded for histogram: ${id}.`);
+    equal(arraySum(keyedSnapshots[id][EXTENSION_ID1].counts), 1,
+          `Data recorded for histogram: ${id}.`);
+  }
+
   await extension2.startup();
   await extension2.awaitMessage("backgroundDone");
+
   for (let id of expectedNonEmptyHistograms) {
     await promiseTelemetryRecorded(id, process, 2);
   }
+  for (let id of expectedNonEmptyKeyedHistograms) {
+    await promiseKeyedTelemetryRecorded(id, process, EXTENSION_ID2, 1);
+  }
 
   // Telemetry from extension2's background page should be recorded.
   snapshots = getSnapshots(process);
+  keyedSnapshots = getKeyedSnapshots(process);
+
   for (let id of expectedNonEmptyHistograms) {
     equal(arraySum(snapshots[id].counts), 2,
           `Additional data recorded for histogram: ${id}.`);
   }
 
+  for (let id of expectedNonEmptyKeyedHistograms) {
+    Assert.deepEqual(Object.keys(keyedSnapshots[id]).sort(), [EXTENSION_ID1, EXTENSION_ID2],
+                     `Additional data recorded for histogram: ${id}.`);
+    equal(arraySum(keyedSnapshots[id][EXTENSION_ID2].counts), 1,
+          `Additional data recorded for histogram: ${id}.`);
+  }
+
   await extension2.unload();
 
   // Run a content script.
   process = IS_OOP ? "content" : "parent";
   let expectedCount = IS_OOP ? 1 : 3;
+  let expectedKeyedCount = IS_OOP ? 1 : 2;
 
   let contentPage = await ExtensionTestUtils.loadContentPage(`${BASE_URL}/file_sample.html`);
   await extension1.awaitMessage("contentDone");
 
   for (let id of expectedNonEmptyHistograms) {
     await promiseTelemetryRecorded(id, process, expectedCount);
   }
+  for (let id of expectedNonEmptyKeyedHistograms) {
+    await promiseKeyedTelemetryRecorded(id, process, EXTENSION_ID1, expectedKeyedCount);
+  }
 
   // Telemetry from extension1's content script should be recorded.
   snapshots = getSnapshots(process);
+  keyedSnapshots = getKeyedSnapshots(process);
+
   for (let id of expectedNonEmptyHistograms) {
     equal(arraySum(snapshots[id].counts), expectedCount,
           `Data recorded in content script for histogram: ${id}.`);
   }
 
+  for (let id of expectedNonEmptyKeyedHistograms) {
+    Assert.deepEqual(Object.keys(keyedSnapshots[id]).sort(),
+                     IS_OOP ? [EXTENSION_ID1] : [EXTENSION_ID1, EXTENSION_ID2],
+                     `Additional data recorded for histogram: ${id}.`);
+    equal(arraySum(keyedSnapshots[id][EXTENSION_ID1].counts), expectedKeyedCount,
+          `Additional data recorded for histogram: ${id}.`);
+  }
+
   await extension1.unload();
 
   // Telemetry for histograms that we expect to be empty.
   for (let id of expectedEmptyHistograms) {
     ok(!(id in snapshots), `No data recorded for histogram: ${id}.`);
   }
 
+  for (let id of expectedEmptyKeyedHistograms) {
+    Assert.deepEqual(Object.keys(keyedSnapshots[id] || {}), [], `No data recorded for histogram: ${id}.`);
+  }
+
   await contentPage.close();
 }
 
 add_task(function test_telemetry_background_file_backend() {
   return runWithPrefs([[ExtensionStorageIDB.BACKEND_ENABLED_PREF, false]],
                       test_telemetry_background);
 });
 
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -13516,114 +13516,233 @@
     "bug_numbers": [1353172],
     "expires_in_version": "67",
     "kind": "exponential",
     "releaseChannelCollection": "opt-out",
     "high": 60000,
     "n_buckets": 100,
     "description": "The amount of time it takes to load a WebExtensions background page, from when the build function is called to when the page has finished processing the onload event."
   },
+  "WEBEXT_BACKGROUND_PAGE_LOAD_MS_BY_ADDONID": {
+    "record_in_processes": ["main"],
+    "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+    "bug_numbers": [1483002],
+    "expires_in_version": "67",
+    "kind": "exponential",
+    "releaseChannelCollection": "opt-out",
+    "high": 60000,
+    "n_buckets": 100,
+    "description": "The amount of time it takes to load a WebExtensions background page, from when the build function is called to when the page has finished processing the onload event, keyed by addon id.",
+    "keyed": true
+  },
   "WEBEXT_BROWSERACTION_POPUP_OPEN_MS": {
     "record_in_processes": ["main"],
     "alert_emails": ["addons-dev-internal@mozilla.com"],
     "bug_numbers": [1297167],
     "expires_in_version": "67",
     "kind": "exponential",
     "releaseChannelCollection": "opt-out",
     "high": 50000,
     "n_buckets": 100,
     "description": "The amount of time it takes for a BrowserAction popup to open."
   },
+  "WEBEXT_BROWSERACTION_POPUP_OPEN_MS_BY_ADDONID": {
+    "record_in_processes": ["main"],
+    "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+    "bug_numbers": [1483002],
+    "expires_in_version": "67",
+    "kind": "exponential",
+    "releaseChannelCollection": "opt-out",
+    "high": 50000,
+    "n_buckets": 100,
+    "description": "The amount of time it takes for a BrowserAction popup to open, keyed by addon id.",
+    "keyed": true
+  },
   "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT": {
     "record_in_processes": ["main"],
     "alert_emails": ["addons-dev-internal@mozilla.com"],
     "bug_numbers": [1297167],
     "expires_in_version": "67",
     "kind": "categorical",
     "labels": ["popupShown", "clearAfterHover", "clearAfterMousedown"],
     "releaseChannelCollection": "opt-out",
     "description": "The number of times a browserAction popup is preloaded and results in one of the categories."
   },
+  "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT_BY_ADDONID": {
+    "record_in_processes": ["main"],
+    "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+    "bug_numbers": [1483002],
+    "expires_in_version": "67",
+    "kind": "categorical",
+    "labels": ["popupShown", "clearAfterHover", "clearAfterMousedown"],
+    "releaseChannelCollection": "opt-out",
+    "description": "The number of times a browserAction popup is preloaded and results in one of the categories, keyed by addon id.",
+    "keyed": true
+  },
   "WEBEXT_CONTENT_SCRIPT_INJECTION_MS": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["addons-dev-internal@mozilla.com"],
     "bug_numbers": [1356323],
     "expires_in_version": "67",
     "kind": "exponential",
     "releaseChannelCollection": "opt-out",
     "high": 50000,
     "n_buckets": 100,
     "description": "The amount of time it takes for content scripts from a WebExtension to be injected into a window."
   },
+  "WEBEXT_CONTENT_SCRIPT_INJECTION_MS_BY_ADDONID": {
+    "record_in_processes": ["main", "content"],
+    "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+    "bug_numbers": [1483002],
+    "expires_in_version": "67",
+    "kind": "exponential",
+    "releaseChannelCollection": "opt-out",
+    "high": 50000,
+    "n_buckets": 100,
+    "description": "The amount of time it takes for content scripts from a WebExtension to be injected into a window, keyed by addon id.",
+    "keyed": true
+  },
   "WEBEXT_EXTENSION_STARTUP_MS": {
     "record_in_processes": ["main"],
     "alert_emails": ["addons-dev-internal@mozilla.com"],
     "bug_numbers": [1353171],
     "expires_in_version": "67",
     "kind": "exponential",
     "releaseChannelCollection": "opt-out",
     "high": 50000,
     "n_buckets": 100,
     "description": "The amount of time it takes for a WebExtension to start up, from when the startup function is called to when the startup promise resolves."
   },
+   "WEBEXT_EXTENSION_STARTUP_MS_BY_ADDONID": {
+    "record_in_processes": ["main"],
+     "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+    "bug_numbers": [1483002],
+    "expires_in_version": "67",
+    "kind": "exponential",
+    "releaseChannelCollection": "opt-out",
+    "high": 50000,
+    "n_buckets": 100,
+    "description": "The amount of time it takes for a WebExtension to start up, from when the startup function is called to when the startup promise resolves, keyed by addon id.",
+    "keyed": true
+  },
   "WEBEXT_PAGEACTION_POPUP_OPEN_MS": {
     "record_in_processes": ["main"],
     "alert_emails": ["addons-dev-internal@mozilla.com"],
     "bug_numbers": [1297167],
     "expires_in_version": "67",
     "kind": "exponential",
     "releaseChannelCollection": "opt-out",
     "high": 50000,
     "n_buckets": 100,
     "description": "The amount of time it takes for a PageAction popup to open."
   },
+  "WEBEXT_PAGEACTION_POPUP_OPEN_MS_BY_ADDONID": {
+    "record_in_processes": ["main"],
+    "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+    "bug_numbers": [1483002],
+    "expires_in_version": "67",
+    "kind": "exponential",
+    "releaseChannelCollection": "opt-out",
+    "high": 50000,
+    "n_buckets": 100,
+    "description": "The amount of time it takes for a PageAction popup to open, keyed by addon id.",
+    "keyed": true
+  },
   "WEBEXT_STORAGE_LOCAL_GET_MS": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["addons-dev-internal@mozilla.com"],
     "bug_numbers": [1371398],
     "expires_in_version": "67",
     "kind": "exponential",
     "releaseChannelCollection": "opt-out",
     "high": 50000,
     "n_buckets": 100,
     "description": "The amount of time it takes to perform a get via storage.local using the JSONFile backend."
   },
+  "WEBEXT_STORAGE_LOCAL_GET_MS_BY_ADDONID": {
+    "record_in_processes": ["main", "content"],
+    "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+    "bug_numbers": [1483002],
+    "expires_in_version": "67",
+    "kind": "exponential",
+    "releaseChannelCollection": "opt-out",
+    "high": 50000,
+    "n_buckets": 100,
+    "description": "The amount of time it takes to perform a get via storage.local using the JSONFile backend, keyed by addon id.",
+    "keyed": true
+  },
   "WEBEXT_STORAGE_LOCAL_SET_MS": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["addons-dev-internal@mozilla.com"],
     "bug_numbers": [1371398],
     "expires_in_version": "67",
     "kind": "exponential",
     "releaseChannelCollection": "opt-out",
     "high": 50000,
     "n_buckets": 100,
     "description": "The amount of time it takes to perform a set via storage.local using the JSONFile backend."
   },
+  "WEBEXT_STORAGE_LOCAL_SET_MS_BY_ADDONID": {
+    "record_in_processes": ["main", "content"],
+    "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+    "bug_numbers": [1483002],
+    "expires_in_version": "67",
+    "kind": "exponential",
+    "releaseChannelCollection": "opt-out",
+    "high": 50000,
+    "n_buckets": 100,
+    "description": "The amount of time it takes to perform a set via storage.local using the JSONFile backend, keyed by addon id.",
+    "keyed": true
+  },
   "WEBEXT_STORAGE_LOCAL_IDB_GET_MS": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["addons-dev-internal@mozilla.com"],
     "bug_numbers": [1465120],
     "expires_in_version": "67",
     "kind": "exponential",
     "releaseChannelCollection": "opt-out",
     "high": 50000,
     "n_buckets": 100,
     "description": "The amount of time it takes to perform a get via storage.local using the IndexedDB backend."
   },
+  "WEBEXT_STORAGE_LOCAL_IDB_GET_MS_BY_ADDONID": {
+    "record_in_processes": ["main", "content"],
+    "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+    "bug_numbers": [1483002],
+    "expires_in_version": "67",
+    "kind": "exponential",
+    "releaseChannelCollection": "opt-out",
+    "high": 50000,
+    "n_buckets": 100,
+    "description": "The amount of time it takes to perform a get via storage.local using the IndexedDB backend, keyed by addon id.",
+    "keyed": true
+  },
   "WEBEXT_STORAGE_LOCAL_IDB_SET_MS": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["addons-dev-internal@mozilla.com"],
     "bug_numbers": [1465120],
     "expires_in_version": "67",
     "kind": "exponential",
     "releaseChannelCollection": "opt-out",
     "high": 50000,
     "n_buckets": 100,
     "description": "The amount of time it takes to perform a set via storage.local using the IndexedDB backend."
   },
+  "WEBEXT_STORAGE_LOCAL_IDB_SET_MS_BY_ADDONID": {
+    "record_in_processes": ["main", "content"],
+    "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+    "bug_numbers": [1483002],
+    "expires_in_version": "67",
+    "kind": "exponential",
+    "releaseChannelCollection": "opt-out",
+    "high": 50000,
+    "n_buckets": 100,
+    "description": "The amount of time it takes to perform a set via storage.local using the IndexedDB backend, keyed by addon id.",
+    "keyed": true
+  },
   "WEBEXT_STORAGE_LOCAL_IDB_MIGRATE_RESULT_COUNT": {
     "record_in_processes": ["main"],
     "bug_numbers": [1465129],
     "alert_emails": ["addons-dev-internal@mozilla.com"],
     "expires_in_version": "67",
     "kind": "categorical",
     "labels": ["success", "failure"],
     "releaseChannelCollection": "opt-out",