Bug 1500147 - Part 1: Record telemetry for about:addons pages r=rpl,janerik
authorMark Striemer <mstriemer@mozilla.com>
Thu, 07 Feb 2019 16:47:45 +0000
changeset 458272 dec5ea7cb93f0cbd9db782d5b70db44651e88056
parent 458271 5a7505f349c91e9693c60bf196824341933534e6
child 458273 0a5e45e5783401a625ea582968567be21b1b978c
push id35522
push usernbeleuzu@mozilla.com
push dateSat, 09 Feb 2019 03:34:29 +0000
treeherdermozilla-central@4e56ef85817a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrpl, janerik
bugs1500147
milestone67.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 1500147 - Part 1: Record telemetry for about:addons pages r=rpl,janerik Differential Revision: https://phabricator.services.mozilla.com/D13154
browser/base/content/test/webextensions/browser_permissions_local_file.js
browser/components/preferences/in-content/preferences.js
toolkit/components/telemetry/Events.yaml
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/content/extensions.xml
toolkit/mozapps/extensions/test/browser/browser.ini
toolkit/mozapps/extensions/test/browser/browser_interaction_telemetry.js
toolkit/mozapps/extensions/test/browser/browser_manualupdates.js
--- a/browser/base/content/test/webextensions/browser_permissions_local_file.js
+++ b/browser/base/content/test/webextensions/browser_permissions_local_file.js
@@ -15,9 +15,47 @@ async function installFile(filename) {
 
   await BrowserOpenAddonsMgr("addons://list/extension");
   let contentWin = gBrowser.selectedTab.linkedBrowser.contentWindow;
 
   // Do the install...
   contentWin.gViewController.doCommand("cmd_installFromFile");
 }
 
-add_task(() => testInstallMethod(installFile, "installLocal"));
+add_task(async function test_install_extension_from_local_file() {
+  // Clear any telemetry data that might be from a separate test.
+  Services.telemetry.clearEvents();
+
+  // Listen for the first installId so we can check it later.
+  let firstInstallId = null;
+  AddonManager.addInstallListener({
+    onNewInstall(install) {
+      firstInstallId = install.installId;
+      AddonManager.removeInstallListener(this);
+    },
+  });
+
+  // Install the add-ons.
+  await testInstallMethod(installFile, "installLocal");
+
+  // Check we got an installId.
+  ok(firstInstallId != null && !isNaN(firstInstallId), "There was an installId found");
+
+  // Check the telemetry.
+  let snapshot = Services.telemetry.snapshotEvents(
+    Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+
+  // Make sure we got some data.
+  ok(snapshot.parent && snapshot.parent.length > 0, "Got parent telemetry events in the snapshot");
+
+  // Only look at the related events after stripping the timestamp and category.
+  let relatedEvents = snapshot.parent
+    .filter(([timestamp, category, method, object]) =>
+      category == "addonsManager" && method == "action" && object == "aboutAddons")
+    .map(relatedEvent => relatedEvent.slice(4, 6));
+
+  // testInstallMethod installs the extension three times.
+  Assert.deepEqual(relatedEvents, [
+    [firstInstallId.toString(), {action: "installFromFile", view: "list"}],
+    [(++firstInstallId).toString(), {action: "installFromFile", view: "list"}],
+    [(++firstInstallId).toString(), {action: "installFromFile", view: "list"}],
+  ], "The telemetry is recorded correctly");
+});
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -14,16 +14,18 @@
 /* import-globals-from ../../../base/content/utilityOverlay.js */
 /* import-globals-from ../../../../toolkit/content/preferencesBindings.js */
 /* global MozXULElement */
 
 "use strict";
 
 var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
+ChromeUtils.defineModuleGetter(this, "AMTelemetry",
+                               "resource://gre/modules/AddonManager.jsm");
 ChromeUtils.defineModuleGetter(this, "formAutofillParent",
                                "resource://formautofill/FormAutofillParent.jsm");
 
 var gLastHash = "";
 const gXULDOMParser = new DOMParser();
 
 var gCategoryInits = new Map();
 function init_category_if_required(category) {
@@ -125,16 +127,20 @@ function init_all() {
   let helpButton = document.getElementById("helpButton");
   let helpUrl = Services.urlFormatter.formatURLPref("app.support.baseURL") + "preferences";
   helpButton.setAttribute("href", helpUrl);
 
   document.getElementById("addonsButton")
     .addEventListener("click", () => {
       let mainWindow = window.docShell.rootTreeItem.domWindow;
       mainWindow.BrowserOpenAddonsMgr();
+      AMTelemetry.recordLinkEvent({
+        object: "aboutPreferences",
+        value: "about:addons",
+      });
     });
 
   document.dispatchEvent(new CustomEvent("Initialized", {
     "bubbles": true,
     "cancelable": true,
   }));
 }
 
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -116,16 +116,73 @@ addonsManager:
         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).
       num_strings: The number of permission description strings in the extension permission doorhanger
     notification_emails: ["addons-dev-internal@mozilla.com"]
     expiry_version: "73"
     record_in_processes: ["main"]
     bug_numbers: [1433335, 1515697, 1523641]
     release_channel_collection: opt-out
+  link:
+    description: >
+      A link method event triggered when a user follows a link. The object is the page that the link is on:
+      aboutAddons or aboutPreferences. The value is where the link goes: about:addons, about:preferences,
+      about:debugging, support (on SUMO) or rating, search, author or homepage (on AMO).
+    objects:
+      - aboutAddons
+      - aboutPreferences
+      - customize
+    extra_keys:
+      view: The view the user was on (list, detail or updates).
+      type: "For search: the type of page for this view (especially extension or theme list)."
+    notification_emails: ["addons-dev-internal@mozilla.com"]
+    expiry_version: "73"
+    record_in_processes: ["main"]
+    bug_numbers: [1500147]
+    release_channel_collection: opt-out
+  view:
+    description: >
+      A view method event is triggered when a user views a page in about:addons. The object is always
+      aboutAddons. The value is the view name: discover, list, updates or detail.
+    objects:
+      - aboutAddons
+    extra_keys:
+      type: >
+        The type of the view, for about:addons views shared between the supported add-on types
+        it is set to an extension type, while for views related to updates it is set to
+        "recent" or "available".
+      source: The source of the installation for an add-on.
+      addonId: The id of the add-on being acted upon.
+    notification_emails: ["addons-dev-internal@mozilla.com"]
+    expiry_version: "73"
+    record_in_processes: ["main"]
+    bug_numbers: [1500147]
+    release_channel_collection: opt-out
+  action:
+    description: >
+      An action method event is triggered when a user performs an action through the UI on an add-on. The object is
+      where in the product the action was performed.
+    objects:
+      - aboutAddons
+      - browserAction
+      - customize
+      - pageAction
+    extra_keys:
+      action: >
+        The action that was performed. Options include disable, enable, uninstall, undo, contribute, preferences,
+        installFromFile, manage, checkForUpdates, checkForUpdate, setUpdatePolicy, setAddonUpdate, resetUpdatePolicy
+        and releaseNotes.
+      type: "For enable, disable, uninstall and undo: the add-on type that is being acted upon."
+      view: The view for the event, when object is aboutAddons.
+      addonId: The id of the add-on being acted upon.
+    notification_emails: ["addons-dev-internal@mozilla.com"]
+    expiry_version: "73"
+    record_in_processes: ["main"]
+    bug_numbers: [1500147]
+    release_channel_collection: opt-out
 
 extensions.data:
   migrateResult:
     objects: ["storageLocal"]
     bug_numbers: [1470213]
     notification_emails: ["addons-dev-internal@mozilla.com"]
     expiry_version: "70"
     record_in_processes: ["main"]
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -3706,28 +3706,56 @@ var AMTelemetry = {
     case "dictionary":
       return addonType;
     default:
       // Currently this should only include plugins and gmp-plugins
       return "other";
     }
   },
 
+  convertToString(value) {
+    if (value == null) {
+      // Convert null and undefined to empty strings.
+      return "";
+    }
+    switch (typeof(value)) {
+      case "string":
+        return value;
+      case "boolean":
+        return value ? "1" : "0";
+    }
+    return String(value);
+  },
+
   /**
    * Convert all the telemetry event's extra_vars into strings, if needed.
    *
    * @param {object} extraVars
+   * @returns {object} The formatted extra vars.
    */
-  formatExtraVars(extraVars) {
+  formatExtraVars({addon, ...extraVars}) {
+    if (addon) {
+      extraVars.addonId = addon.id;
+      extraVars.type = addon.type;
+    }
+
     // All the extra_vars in a telemetry event have to be strings.
-    for (var key of Object.keys(extraVars)) {
-      if (typeof(extraVars[key]) !== "string") {
-        extraVars[key] = String(extraVars[key]);
+    for (var [key, value] of Object.entries(extraVars)) {
+      if (value == undefined) {
+        delete extraVars[key];
+      } else {
+        extraVars[key] = this.convertToString(value);
       }
     }
+
+    if (extraVars.addonId) {
+      extraVars.addonId = this.getTrimmedString(extraVars.addonId);
+    }
+
+    return extraVars;
   },
 
   /**
    * Record an install or update event for the given AddonInstall instance.
    *
    * @param {AddonInstall} install
    *        The AddonInstall instance to record an install or update event for.
    * @param {object} extraVars
@@ -3781,18 +3809,17 @@ var AMTelemetry = {
 
     if (eventMethod === "update") {
       // For "update" telemetry events, also include an extra var which determine
       // if the update has been requested by the user.
       extra.updated_from = install.isUserRequestedUpdate ? "user" : "app";
     }
 
     // All the extra vars in a telemetry event have to be strings.
-    extra = {...extraVars, ...extra};
-    this.formatExtraVars(extra);
+    extra = this.formatExtraVars({...extraVars, ...extra});
 
     this.recordEvent({method: eventMethod, object, value: installId, extra});
   },
 
   /**
    * Record a manage event for the given addon.
    *
    * @param {AddonWrapper} addon
@@ -3830,23 +3857,105 @@ var AMTelemetry = {
     }
 
     let object = this.getEventObjectFromAddonType(addon.type);
     let value = this.getTrimmedString(addon.id);
 
     extra = {...extraVars, ...extra};
 
     let hasExtraVars = Object.keys(extra).length > 0;
-    this.formatExtraVars(extra);
+    extra = this.formatExtraVars(extra);
 
     this.recordEvent({method, object, value, extra: hasExtraVars ? extra : null});
   },
 
+  /**
+   * Record an event for when a link is clicked.
+   *
+   * @param {object} opts
+   * @param {string} opts.object
+   *        The object of the event, should be an identifier for where the link
+   *        is located. The accepted values are listed in the
+   *        addonsManager.link object of the Events.yaml file.
+   * @param {string} opts.value The identifier for the link destination.
+   * @param {object} opts.extra
+   *        The extra data to be sent, all keys must be registered in the
+   *        extra_keys section of addonsManager.link in Events.yaml.
+   */
+  recordLinkEvent({object, value, extra = null}) {
+    this.recordEvent({method: "link", object, value, extra});
+  },
+
+  /**
+   * Record an event for an action that took place.
+   *
+   * @param {object} opts
+   * @param {string} opts.object
+   *        The object of the event, should an identifier for where the action
+   *        took place. The accepted values are listed in the
+   *        addonsManager.action object of the Events.yaml file.
+   * @param {string} opts.action The identifier for the action.
+   * @param {string} opts.value An optional value for the action.
+   * @param {AddonWrapper} opts.addon
+   *        An optional add-on object related to the event. Passing this will
+   *        set extra.addonId and extra.type based on the add-on.
+   * @param {string} opts.view The current view, when object is aboutAddons.
+   * @param {object} opts.extra
+   *        The extra data to be sent, all keys must be registered in the
+   *        extra_keys section of addonsManager.action in Events.yaml. If
+   *        opts.addon is passed then it will overwrite the addonId and type
+   *        properties in this object, if they are set.
+   */
+  recordActionEvent({object, action, value, addon, view, extra}) {
+    extra = {...extra, action, addon, view};
+    this.recordEvent({
+      method: "action",
+      object,
+      // Treat null and undefined as null.
+      value: value == null ? null : this.convertToString(value),
+      extra: this.formatExtraVars(extra),
+    });
+  },
+
+  /**
+   * Record an event for a view load in about:addons.
+   *
+   * @param {object} opts
+   * @param {string} opts.view
+   *        The identifier for the view. The accepted values are listed in the
+   *        object property of addonsManager.view object of the Events.yaml
+   *        file.
+   * @param {AddonWrapper} opts.addon
+   *        An optional add-on object related to the event.
+   * @param {string} opts.type
+   *        An optional type for the view. If opts.addon is set it will
+   *        overwrite this value with the type of the add-on.
+   */
+  recordViewEvent({view, addon, type}) {
+    this.recordEvent({
+      method: "view",
+      object: "aboutAddons",
+      value: view,
+      extra: this.formatExtraVars({type, addon}),
+    });
+  },
+
   recordEvent({method, object, value, extra}) {
-    Services.telemetry.recordEvent("addonsManager", method, object, value, extra);
+    if (typeof value != "string") {
+      // The value must be a string or null, make sure it's valid so sending
+      // the event doesn't fail.
+      value = null;
+    }
+    try {
+      Services.telemetry.recordEvent("addonsManager", method, object, value, extra);
+    } catch (err) {
+      // If the telemetry throws just log the error so it doesn't break any
+      // functionality.
+      Cu.reportError(err);
+    }
   },
 };
 
 this.AddonManager.init();
 
 // Setup the AMTelemetry once the AddonManager has been started.
 this.AddonManager.addManagerListener(AMTelemetry);
 
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -8,16 +8,18 @@
 /* globals ProcessingInstruction */
 /* exported UPDATES_RELEASENOTES_TRANSFORMFILE, XMLURI_PARSE_ERROR, loadView, gBrowser */
 
 const {DeferredTask} = ChromeUtils.import("resource://gre/modules/DeferredTask.jsm");
 const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
 const {AddonRepository} = ChromeUtils.import("resource://gre/modules/addons/AddonRepository.jsm");
 const {AddonSettings} = ChromeUtils.import("resource://gre/modules/addons/AddonSettings.jsm");
 
+ChromeUtils.defineModuleGetter(this, "AMTelemetry",
+                               "resource://gre/modules/AddonManager.jsm");
 ChromeUtils.defineModuleGetter(this, "E10SUtils", "resource://gre/modules/E10SUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "Extension",
                                "resource://gre/modules/Extension.jsm");
 ChromeUtils.defineModuleGetter(this, "ExtensionParent",
                                "resource://gre/modules/ExtensionParent.jsm");
 ChromeUtils.defineModuleGetter(this, "ExtensionPermissions",
                                "resource://gre/modules/ExtensionPermissions.jsm");
 ChromeUtils.defineModuleGetter(this, "PluralForm",
@@ -145,20 +147,22 @@ function initialize(event) {
   });
   if (!isDiscoverEnabled()) {
     gViewDefault = "addons://list/extension";
   }
 
   let helpButton = document.getElementById("helpButton");
   let helpUrl = Services.urlFormatter.formatURLPref("app.support.baseURL") + "addons-help";
   helpButton.setAttribute("href", helpUrl);
+  helpButton.addEventListener("click", () => recordLinkTelemetry("support"));
 
   document.getElementById("preferencesButton")
     .addEventListener("click", () => {
       let mainWindow = getMainWindow();
+      recordLinkTelemetry("about:preferences");
       if ("switchToTabHavingURI" in mainWindow) {
         mainWindow.switchToTabHavingURI("about:preferences", true, {
           triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
         });
       }
     });
 
   let categories = document.getElementById("categories");
@@ -221,16 +225,85 @@ function shutdown() {
   gViewController.shutdown();
   Services.obs.removeObserver(sendEMPong, "EM-ping");
 }
 
 function sendEMPong(aSubject, aTopic, aData) {
   Services.obs.notifyObservers(window, "EM-pong");
 }
 
+function recordLinkTelemetry(target) {
+  let extra = {view: getCurrentViewName()};
+  if (target == "search") {
+    let searchBar = document.getElementById("header-search");
+    extra.type = searchBar.getAttribute("data-addon-type");
+  }
+  AMTelemetry.recordLinkEvent({object: "aboutAddons", value: target, extra});
+}
+
+function recordActionTelemetry({action, value, view, addon}) {
+  view = view || getCurrentViewName();
+  AMTelemetry.recordActionEvent({
+    // The max-length for an object is 20, which it enough to be unique.
+    object: "aboutAddons",
+    value,
+    view,
+    action,
+    addon,
+  });
+}
+
+async function recordViewTelemetry(param) {
+  let type;
+  let addon;
+
+  if (param in AddonManager.addonTypes || ["recent", "available"].includes(param)) {
+    type = param;
+  } else if (param) {
+    let id = param.replace("/preferences", "");
+    addon = await AddonManager.getAddonByID(id);
+  }
+
+  AMTelemetry.recordViewEvent({view: getCurrentViewName(), addon, type});
+}
+
+function recordSetUpdatePolicyTelemetry() {
+  // Record telemetry for changing the update policy.
+  let updatePolicy = [];
+  if (AddonManager.autoUpdateDefault) {
+    updatePolicy.push("default");
+  }
+  if (AddonManager.updateEnabled) {
+    updatePolicy.push("enabled");
+  }
+  recordActionTelemetry({action: "setUpdatePolicy", value: updatePolicy.join(",")});
+}
+
+function recordSetAddonUpdateTelemetry(addon) {
+  let updates = addon.applyBackgroundUpdates;
+  let updatePolicy = "";
+  if (updates == "1") {
+    updatePolicy = "default";
+  } else if (updates == "2") {
+    updatePolicy = "enabled";
+  }
+  recordActionTelemetry({action: "setAddonUpdate", value: updatePolicy, addon});
+}
+
+function getCurrentViewName() {
+  let view = gViewController.currentViewObj;
+  let entries = Object.entries(gViewController.viewObjects);
+  let viewIndex = entries.findIndex(([name, viewObj]) => {
+    return viewObj == view;
+  });
+  if (viewIndex != -1)
+    return entries[viewIndex][0];
+  return "other";
+}
+
 // Used by external callers to load a specific view into the manager
 function loadView(aViewId) {
   if (!gViewController.initialViewSelected) {
     // The caller opened the window and immediately loaded the view so it
     // should be the initial history entry
 
     gViewController.loadInitialView(aViewId);
   } else {
@@ -288,16 +361,18 @@ function isDiscoverEnabled() {
     return false;
   }
 
   return true;
 }
 
 function setSearchLabel(type) {
   let searchLabel = document.getElementById("search-label");
+  document.getElementById("header-search")
+    .setAttribute("data-addon-type", type);
   let keyMap = {
     extension: "extension",
     shortcuts: "extension",
     theme: "theme",
   };
   if (type in keyMap) {
     searchLabel
       .textContent = gStrings.ext.GetStringFromName(`searchLabel.${keyMap[type]}`);
@@ -871,24 +946,28 @@ var gViewController = {
     gCategories.select(aViewId, aPreviousView);
 
     this.currentViewId = aViewId;
     this.currentViewObj = viewObj;
 
     this.displayedView = this.currentViewObj;
     this.currentViewObj.node.setAttribute("loading", "true");
 
+    recordViewTelemetry(view.param);
+
     let headingName = document.getElementById("heading-name");
+    let headingLabel;
     try {
-      headingName.textContent = gStrings.ext.GetStringFromName(`listHeading.${view.param}`);
-      setSearchLabel(view.param);
+      headingLabel = gStrings.ext.GetStringFromName(`listHeading.${view.param}`);
     } catch (e) {
-      // In tests we sometimes render this view with a type we don't support, that's fine.
-      headingName.textContent = "";
+      // Some views don't have a label, like the updates view.
+      headingLabel = "";
     }
+    headingName.textContent = headingLabel;
+    setSearchLabel(view.param);
 
 
     if (aViewId == aPreviousView)
       this.currentViewObj.refresh(view.param, ++this.currentViewRequest, aState);
     else
       this.currentViewObj.show(view.param, ++this.currentViewRequest, aState);
 
     this.backButton.hidden = this.currentViewObj.isRoot || !gHistory.canGoBack;
@@ -968,29 +1047,32 @@ var gViewController = {
           // add-ons, we also need to auto-check for updates.
           AddonManager.updateEnabled = true;
           AddonManager.autoUpdateDefault = true;
         } else {
           // Both prefs are true, i.e. the checkbox is checked.
           // Toggle the auto pref to false, but don't touch the enabled check.
           AddonManager.autoUpdateDefault = false;
         }
+
+        recordSetUpdatePolicyTelemetry();
       },
     },
 
     cmd_resetAddonAutoUpdate: {
       isEnabled() {
         return true;
       },
       async doCommand() {
         let aAddonList = await AddonManager.getAllAddons();
         for (let addon of aAddonList) {
           if ("applyBackgroundUpdates" in addon)
             addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
         }
+        recordActionTelemetry({action: "resetUpdatePolicy"});
       },
     },
 
     cmd_goToDiscoverPane: {
       isEnabled() {
         return gDiscoverView.enabled;
       },
       doCommand() {
@@ -1115,16 +1197,18 @@ var gViewController = {
         for (let addon of aAddonList) {
           if (addon.permissions & AddonManager.PERM_CAN_UPGRADE) {
             pendingChecks++;
             addon.findUpdates(updateCheckListener,
                               AddonManager.UPDATE_WHEN_USER_REQUESTED);
           }
         }
 
+        recordActionTelemetry({action: "checkForUpdates"});
+
         if (pendingChecks == 0)
           updateStatus();
       },
     },
 
     cmd_findItemUpdates: {
       isEnabled(aAddon) {
         if (!aAddon)
@@ -1142,35 +1226,42 @@ var gViewController = {
           },
           onNoUpdateAvailable(aAddon) {
             gEventManager.delegateAddonEvent("onNoUpdateAvailable",
                                              [aAddon]);
           },
         };
         gEventManager.delegateAddonEvent("onCheckingUpdate", [aAddon]);
         aAddon.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+        recordActionTelemetry({action: "checkForUpdate", addon: aAddon});
       },
     },
 
     cmd_showItemPreferences: {
       isEnabled(aAddon) {
         if (!aAddon || (!aAddon.isActive && aAddon.type !== "plugin")) {
           return false;
         }
         if (gViewController.currentViewObj == gDetailView) {
           return aAddon.optionsType && !hasInlineOptions(aAddon);
         }
         return aAddon.type == "plugin" || aAddon.optionsType;
       },
       doCommand(aAddon) {
-        if (hasInlineOptions(aAddon)) {
+        let inline = hasInlineOptions(aAddon);
+        let view = getCurrentViewName();
+
+        if (inline) {
           gViewController.commands.cmd_showItemDetails.doCommand(aAddon, true);
         } else if (aAddon.optionsType == AddonManager.OPTIONS_TYPE_TAB) {
           openOptionsInTab(aAddon.optionsURL);
         }
+
+        let value = inline ? "inline" : "external";
+        recordActionTelemetry({action: "preferences", value, view, addon: aAddon});
       },
     },
 
     cmd_enableItem: {
       isEnabled(aAddon) {
         if (!aAddon)
           return false;
         let addonType = AddonManager.addonTypes[aAddon.type];
@@ -1197,16 +1288,17 @@ var gViewController = {
                 },
               },
             };
             Services.obs.notifyObservers(subject, "webextension-permission-prompt");
             return;
           }
         }
         aAddon.enable();
+        recordActionTelemetry({action: "enable", addon: aAddon});
       },
       getTooltip(aAddon) {
         if (!aAddon)
           return "";
         return gStrings.ext.GetStringFromName("enableAddonTooltip");
       },
     },
 
@@ -1215,16 +1307,17 @@ var gViewController = {
         if (!aAddon)
           return false;
         let addonType = AddonManager.addonTypes[aAddon.type];
         return (!(addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) &&
                 hasPermission(aAddon, "disable"));
       },
       doCommand(aAddon) {
         aAddon.disable();
+        recordActionTelemetry({action: "disable", addon: aAddon});
       },
       getTooltip(aAddon) {
         if (!aAddon)
           return "";
         return gStrings.ext.GetStringFromName("disableAddonTooltip");
       },
     },
 
@@ -1248,23 +1341,26 @@ var gViewController = {
 
     cmd_uninstallItem: {
       isEnabled(aAddon) {
         if (!aAddon)
           return false;
         return hasPermission(aAddon, "uninstall");
       },
       async doCommand(aAddon) {
+        let view = getCurrentViewName();
+
         // Make sure we're on the list view, which supports undo.
         if (gViewController.currentViewObj != gListView) {
           await new Promise(resolve => {
             document.addEventListener("ViewChanged", resolve, {once: true});
             gViewController.loadView(`addons://list/${aAddon.type}`);
           });
         }
+        recordActionTelemetry({action: "uninstall", view, addon: aAddon});
         gViewController.currentViewObj.getListItemForID(aAddon.id).uninstall();
       },
       getTooltip(aAddon) {
         if (!aAddon)
           return "";
         return gStrings.ext.GetStringFromName("uninstallAddonTooltip");
       },
     },
@@ -1305,27 +1401,29 @@ var gViewController = {
             source: "about:addons",
             method: "install-from-file",
           };
 
           let browser = getBrowserElement();
           for (let file of fp.files) {
             let install = await AddonManager.getInstallForFile(file, null, installTelemetryInfo);
             AddonManager.installAddonFromAOM(browser, document.documentURIObject, install);
+            recordActionTelemetry({action: "installFromFile", value: install.installId});
           }
         });
       },
     },
 
     cmd_debugAddons: {
       isEnabled() {
         return true;
       },
       doCommand() {
         let mainWindow = getMainWindow();
+        recordLinkTelemetry("about:debugging");
         if ("switchToTabHavingURI" in mainWindow) {
           mainWindow.switchToTabHavingURI("about:debugging#addons", true, {
             triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
           });
         }
       },
     },
 
@@ -1345,16 +1443,17 @@ var gViewController = {
     cmd_contribute: {
       isEnabled(aAddon) {
         if (!aAddon)
           return false;
         return ("contributionURL" in aAddon && aAddon.contributionURL);
       },
       doCommand(aAddon) {
         openURL(aAddon.contributionURL);
+        recordActionTelemetry({action: "contribute", addon: aAddon});
       },
     },
 
     cmd_askToActivateItem: {
       isEnabled(aAddon) {
         if (!aAddon)
           return false;
         let addonType = AddonManager.addonTypes[aAddon.type];
@@ -1960,16 +2059,18 @@ var gHeader = {
       let url = AddonRepository.getSearchURL(query);
 
       let browser = getBrowserElement();
       let chromewin = browser.ownerGlobal;
       chromewin.openLinkIn(url, "tab", {
         fromChrome: true,
         triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
       });
+
+      recordLinkTelemetry("search");
     });
   },
 
   focusSearchBox() {
     this._search.focus();
   },
 
   onKeyPress(aEvent) {
@@ -2418,16 +2519,20 @@ var gListView = {
 
     this._listBox.addEventListener("keydown", (aEvent) => {
       if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
         var item = this._listBox.selectedItem;
         if (item)
           item.showInDetailView();
       }
     });
+    this._listBox.addEventListener("Uninstall", (event) =>
+      recordActionTelemetry({action: "uninstall", view: "list", addon: event.target.mAddon}));
+    this._listBox.addEventListener("Undo", (event) =>
+      recordActionTelemetry({action: "undo", addon: event.target.mAddon}));
 
     document.getElementById("signing-learn-more").setAttribute("href", SUPPORT_URL + "unsigned-addons");
     document.getElementById("legacy-extensions-learnmore-link").addEventListener("click", evt => {
       gViewController.loadView("addons://legacy/");
     });
 
     try {
       document.getElementById("private-browsing-learnmore-link")
@@ -2664,22 +2769,23 @@ var gDetailView = {
   _addon: null,
   _loadingTimer: null,
   _autoUpdate: null,
   isRoot: false,
   restartingAddon: false,
 
   initialize() {
     this.node = document.getElementById("detail-view");
+    this.node.addEventListener("click", this.recordClickTelemetry);
     this.headingImage = this.node.querySelector(".card-heading-image");
 
     this._autoUpdate = document.getElementById("detail-autoUpdate");
-
     this._autoUpdate.addEventListener("command", () => {
       this._addon.applyBackgroundUpdates = this._autoUpdate.value;
+      recordSetAddonUpdateTelemetry(this._addon);
     }, true);
 
     document.getElementById("detail-private-browsing-learnmore-link")
             .setAttribute("href", SUPPORT_URL + "extensions-pb");
 
     this._privateBrowsing = document.getElementById("detail-privateBrowsing");
     this._privateBrowsing.addEventListener("command", async () => {
       let addon = this._addon;
@@ -2711,16 +2817,26 @@ var gDetailView = {
   shutdown() {
     AddonManager.removeManagerListener(this);
   },
 
   onUpdateModeChanged() {
     this.onPropertyChanged(["applyBackgroundUpdates"]);
   },
 
+  recordClickTelemetry(event) {
+    if (event.target.id == "detail-reviews") {
+      recordLinkTelemetry("rating");
+    } else if (event.target.id == "detail-homepage") {
+      recordLinkTelemetry("homepage");
+    } else if (event.originalTarget.getAttribute("anonid") == "creator-link") {
+      recordLinkTelemetry("author");
+    }
+  },
+
   async _updateView(aAddon, aIsRemote, aScrollToPreferences) {
     // Skip updates to avoid flickering while restarting the addon.
     if (this.restartingAddon) {
       return;
     }
 
     setSearchLabel(aAddon.type);
 
@@ -3405,16 +3521,19 @@ var gUpdatesView = {
     this._emptyNotice = document.getElementById("updates-list-empty");
 
     this._categoryItem = gCategories.get("addons://updates/available");
 
     this._updateSelected = document.getElementById("update-selected-btn");
     this._updateSelected.addEventListener("command", function() {
       gUpdatesView.installSelected();
     });
+    this.node.addEventListener("RelNotesShow", (event) => {
+      recordActionTelemetry({action: "releaseNotes", addon: event.target.mAddon});
+    });
 
     this.updateAvailableCount(true);
 
     AddonManager.addAddonListener(this);
     AddonManager.addInstallListener(this);
   },
 
   shutdown() {
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -1156,33 +1156,33 @@
                               this.mAddon.install.state != AddonManager.STATE_INSTALLED);
           this._showStatus(showProgress ? "progress" : "none");
         ]]></body>
       </method>
 
       <method name="_fetchReleaseNotes">
         <parameter name="aURI"/>
         <body><![CDATA[
+          let sendToggleEvent = () => {
+            var event = document.createEvent("Events");
+            event.initEvent("RelNotesToggle", true, true);
+            this.dispatchEvent(event);
+          };
+
           if (!aURI || this._relNotesLoaded) {
             sendToggleEvent();
             return;
           }
 
           var relNotesData = null, transformData = null;
 
           this._relNotesLoaded = true;
           this._relNotesLoading.hidden = false;
           this._relNotesError.hidden = true;
 
-          let sendToggleEvent = () => {
-            var event = document.createEvent("Events");
-            event.initEvent("RelNotesToggle", true, true);
-            this.dispatchEvent(event);
-          };
-
           let showRelNotes = () => {
             if (!relNotesData || !transformData)
               return;
 
             this._relNotesLoading.hidden = true;
 
             var processor = new XSLTProcessor();
             processor.flags |= XSLTProcessor.DISABLE_ALL_LOADS;
@@ -1246,17 +1246,17 @@
             this._relNotesToggle.setAttribute(
               "label",
               this._relNotesToggle.getAttribute("showlabel")
             );
             this._relNotesToggle.setAttribute(
               "tooltiptext",
               this._relNotesToggle.getAttribute("showtooltip")
             );
-            var event = document.createEvent("Events");
+            let event = document.createEvent("Events");
             event.initEvent("RelNotesToggle", true, true);
             this.dispatchEvent(event);
           } else {
             this._relNotesContainer.style.height = this._relNotesContainer.scrollHeight +
                                                    "px";
             this.setAttribute("show-relnotes", true);
             this._relNotesToggle.setAttribute(
               "label",
@@ -1265,16 +1265,21 @@
             this._relNotesToggle.setAttribute(
               "tooltiptext",
               this._relNotesToggle.getAttribute("hidetooltip")
             );
             var uri = this.mManualUpdate ?
                       this.mManualUpdate.releaseNotesURI :
                       this.mAddon.releaseNotesURI;
             this._fetchReleaseNotes(uri);
+
+            // Dispatch an event so extensions.js can record telemetry.
+            let event = document.createEvent("Events");
+            event.initEvent("RelNotesShow", true, true);
+            this.dispatchEvent(event);
           }
         ]]></body>
       </method>
 
       <method name="undo">
         <body><![CDATA[
           gViewController.commands.cmd_cancelOperation.doCommand(this.mAddon);
         ]]></body>
@@ -1292,16 +1297,21 @@
 
             // We must set userDisabled to true first, this will call
             // _updateState which will clear any pending attribute set.
             this.mAddon.disable().then(() => {
               // This won't update any other add-on manager views (bug 582002)
               this.setAttribute("pending", "uninstall");
             });
           }
+
+          // Dispatch an event so extensions.js can track telemetry.
+          var event = document.createEvent("Events");
+          event.initEvent("Uninstall", true, true);
+          this.dispatchEvent(event);
         ]]></body>
       </method>
 
       <method name="showPreferences">
         <body><![CDATA[
           gViewController.doCommand("cmd_showItemPreferences", this.mAddon);
         ]]></body>
       </method>
@@ -1541,16 +1551,21 @@
           // uninstalling doesn't. Things will still work if not, the add-on
           // will just still be active until finally getting uninstalled.
 
           if (this.isPending("uninstall"))
             this.mAddon.cancelUninstall();
           else if (this.getAttribute("wasDisabled") != "true")
             this.mAddon.enable();
 
+          // Dispatch an event so extensions.js can record telemetry.
+          var event = document.createEvent("Events");
+          event.initEvent("Undo", true, true);
+          this.dispatchEvent(event);
+
           this.removeAttribute("pending");
         ]]></body>
       </method>
 
       <method name="onExternalInstall">
         <parameter name="aAddon"/>
         <parameter name="aExistingAddon"/>
         <body><![CDATA[
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -73,16 +73,17 @@ skip-if = os == "linux" && !debug # Bug 
 skip-if = true # Bug 1449071 - Frequent failures
 [browser_globalwarnings.js]
 [browser_gmpProvider.js]
 skip-if = os == 'linux' && !debug # Bug 1398766
 [browser_inlinesettings_browser.js]
 skip-if = os == 'mac' || os == 'linux' # Bug 1483347
 [browser_installssl.js]
 skip-if = verify
+[browser_interaction_telemetry.js]
 [browser_langpack_signing.js]
 [browser_legacy.js]
 [browser_legacy_pre57.js]
 [browser_list.js]
 [browser_manage_shortcuts.js]
 [browser_manualupdates.js]
 [browser_pluginprefs.js]
 [browser_pluginprefs_is_not_disabled.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_interaction_telemetry.js
@@ -0,0 +1,353 @@
+const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", {});
+
+AddonTestUtils.initMochitest(this);
+
+let gManagerWindow;
+let gCategoryUtilities;
+const TELEMETRY_CATEGORY = "addonsManager";
+const TELEMETRY_METHODS = new Set(["action", "link", "view"]);
+const addonId = "extension@mochi.test";
+
+registerCleanupFunction(() => {
+  // AddonTestUtils with open_manager cause this reference to be maintained and creates a leak.
+  gManagerWindow = null;
+});
+
+async function init(startPage) {
+  gManagerWindow = await open_manager(null);
+  gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+  // When about:addons initially loads it will load the last view that
+  // was open. If that's different than startPage, then clear the events
+  // so that we can reliably test them.
+  if (gCategoryUtilities.selectedCategory != startPage) {
+    Services.telemetry.clearEvents();
+  }
+
+  return gCategoryUtilities.openType(startPage);
+}
+
+function assertTelemetryMatches(events) {
+  let snapshot = Services.telemetry.snapshotEvents(
+    Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+
+  if (events.length == 0) {
+    ok(!snapshot.parent || snapshot.parent.length == 0, "There are no telemetry events");
+    return;
+  }
+
+  // Make sure we got some data.
+  ok(snapshot.parent && snapshot.parent.length > 0, "Got parent telemetry events in the snapshot");
+
+  // Only look at the related events after stripping the timestamp and category.
+  let relatedEvents = snapshot.parent
+    .filter(([timestamp, category, method]) =>
+      category == TELEMETRY_CATEGORY && TELEMETRY_METHODS.has(method))
+    .map(relatedEvent => relatedEvent.slice(2, 6));
+
+  // Events are now [method, object, value, extra] as expected.
+  Assert.deepEqual(relatedEvents, events, "The events are recorded correctly");
+}
+
+async function installTheme() {
+  let id = "theme@mochi.test";
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      applications: {gecko: {id}},
+      manifest_version: 2,
+      name: "atheme",
+      description: "wow. such theme.",
+      author: "Pixel Pusher",
+      version: "1",
+      theme: {},
+    },
+    useAddonManager: "temporary",
+  });
+  await extension.startup();
+  return extension;
+}
+
+async function installExtension(manifest = {}) {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      applications: {gecko: {id: addonId}},
+      manifest_version: 2,
+      name: "extension",
+      description: "wow. such extension.",
+      author: "Code Pusher",
+      version: "1",
+      chrome_url_overrides: {newtab: "new.html"},
+      options_ui: {page: "options.html", open_in_tab: true},
+      browser_action: {default_popup: "action.html"},
+      page_action: {default_popup: "action.html"},
+      ...manifest,
+    },
+    files: {
+      "new.html": "<h1>yo</h1>",
+      "options.html": "<h1>options</h1>",
+      "action.html": "<h1>do something</h1>",
+    },
+    useAddonManager: "permanent",
+  });
+  await extension.startup();
+  return extension;
+}
+
+add_task(function clearInitialTelemetry() {
+  // Clear out any telemetry data that existed before this file is run.
+  Services.telemetry.clearEvents();
+});
+
+add_task(async function testBasicViewTelemetry() {
+  let addons = await Promise.all([
+    installTheme(),
+    installExtension(),
+  ]);
+  await init("discover");
+
+  let doc = gManagerWindow.document;
+
+  await gCategoryUtilities.openType("theme");
+  doc.querySelector('.addon[value="theme@mochi.test"]').click();
+  await wait_for_view_load(gManagerWindow);
+
+  await gCategoryUtilities.openType("extension");
+  doc.querySelector('.addon[value="extension@mochi.test"]').click();
+  await wait_for_view_load(gManagerWindow);
+
+  assertTelemetryMatches([
+    ["view", "aboutAddons", "discover"],
+    ["view", "aboutAddons", "list", {type: "theme"}],
+    ["view", "aboutAddons", "detail", {type: "theme", addonId: "theme@mochi.test"}],
+    ["view", "aboutAddons", "list", {type: "extension"}],
+    ["view", "aboutAddons", "detail", {type: "extension", addonId: "extension@mochi.test"}],
+  ]);
+
+  await close_manager(gManagerWindow);
+  await Promise.all(addons.map(addon => addon.unload()));
+});
+
+add_task(async function testExtensionEvents() {
+  let addon = await installExtension();
+  let type = "extension";
+  await init("extension");
+
+  let doc = gManagerWindow.document;
+  let list = doc.getElementById("addon-list");
+  let row = list.querySelector(`[value="${addonId}"]`);
+
+  // Check/clear the current telemetry.
+  assertTelemetryMatches([["view", "aboutAddons", "list", {type: "extension"}]]);
+
+  // Check disable/enable.
+  is(row.getAttribute("active"), "true", "The add-on is enabled");
+  doc.getAnonymousElementByAttribute(row, "anonid", "disable-btn").click();
+  await TestUtils.waitForCondition(() => row.getAttribute("active") == "false", "Wait for disable");
+  doc.getAnonymousElementByAttribute(row, "anonid", "enable-btn").click();
+  await TestUtils.waitForCondition(() => row.getAttribute("active") == "true", "Wait for enable");
+  assertTelemetryMatches([
+    ["action", "aboutAddons", null, {action: "disable", addonId, type, view: "list"}],
+    ["action", "aboutAddons", null, {action: "enable", addonId, type, view: "list"}],
+  ]);
+
+  // Check remove/undo.
+  is(row.getAttribute("status"), "installed", "The add-on is installed");
+  ok(!row.hasAttribute("pending"), "The add-on is not pending");
+  doc.getAnonymousElementByAttribute(row, "anonid", "remove-btn").click();
+  await TestUtils.waitForCondition(() => row.getAttribute("pending") == "uninstall", "Wait for uninstall");
+  // Find the row again since the binding changed.
+  doc.getAnonymousElementByAttribute(row, "anonid", "undo-btn").click();
+  await TestUtils.waitForCondition(() => !row.hasAttribute("pending"), "Wait for undo");
+  assertTelemetryMatches([
+    ["action", "aboutAddons", null, {action: "uninstall", addonId, type, view: "list"}],
+    ["action", "aboutAddons", null, {action: "undo", addonId, type, view: "list"}],
+  ]);
+
+  // Open the preferences page.
+  let waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+  let prefsButton;
+  await TestUtils.waitForCondition(() => {
+    prefsButton = doc.getAnonymousElementByAttribute(row, "anonid", "preferences-btn");
+    return prefsButton;
+  });
+  prefsButton.click();
+  BrowserTestUtils.removeTab(await waitForNewTab);
+  assertTelemetryMatches([
+    ["action", "aboutAddons", "external", {action: "preferences", type, addonId, view: "list"}],
+  ]);
+
+  // Go to the detail view.
+  row.click();
+  await wait_for_view_load(gManagerWindow);
+  assertTelemetryMatches([
+    ["view", "aboutAddons", "detail", {type, addonId}],
+  ]);
+
+  // Check updates.
+  let autoUpdate = doc.getElementById("detail-autoUpdate");
+  is(autoUpdate.value, "1", "Use default is selected");
+  // Turn off auto update.
+  autoUpdate.querySelector('[value="0"]').click();
+  // Check for updates.
+  let checkForUpdates = doc.getElementById("detail-findUpdates-btn");
+  is(checkForUpdates.hidden, false, "The check for updates button is visible");
+  checkForUpdates.click();
+  // Turn on auto update.
+  autoUpdate.querySelector('[value="2"]').click();
+  // Set auto update to default again.
+  autoUpdate.querySelector('[value="1"]').click();
+  assertTelemetryMatches([
+    ["action", "aboutAddons", "", {action: "setAddonUpdate", type, addonId, view: "detail"}],
+    ["action", "aboutAddons", null, {action: "checkForUpdate", type, addonId, view: "detail"}],
+    ["action", "aboutAddons", "enabled", {action: "setAddonUpdate", type, addonId, view: "detail"}],
+    ["action", "aboutAddons", "default", {action: "setAddonUpdate", type, addonId, view: "detail"}],
+  ]);
+
+  // Check links.
+  let creator = doc.getElementById("detail-creator");
+  let label = doc.getAnonymousElementByAttribute(creator, "anonid", "label");
+  let link = doc.getAnonymousElementByAttribute(creator, "anonid", "creator-link");
+  // Check that clicking the label doesn't trigger a telemetry event.
+  label.click();
+  assertTelemetryMatches([]);
+
+  // These links don't actually have a URL, so they don't open a tab. They're only
+  // shown when there is a URL though.
+  link.click();
+  doc.getElementById("detail-homepage").click();
+  doc.getElementById("detail-reviews").click();
+
+  // The support button will open a new tab.
+  waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+  doc.getElementById("helpButton").click();
+  BrowserTestUtils.removeTab(await waitForNewTab);
+
+  // Check that the preferences button includes the view.
+  waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+  let prefsBtn;
+  await TestUtils.waitForCondition(() => {
+    prefsBtn = doc.getElementById("detail-prefs-btn");
+    return prefsBtn;
+  });
+  prefsBtn.click();
+  BrowserTestUtils.removeTab(await waitForNewTab);
+
+  assertTelemetryMatches([
+    ["link", "aboutAddons", "author", {view: "detail"}],
+    ["link", "aboutAddons", "homepage", {view: "detail"}],
+    ["link", "aboutAddons", "rating", {view: "detail"}],
+    ["link", "aboutAddons", "support", {view: "detail"}],
+    ["action", "aboutAddons", "external", {action: "preferences", type, addonId, view: "detail"}],
+  ]);
+
+  // Update the preferences and check that inline changes.
+  await gCategoryUtilities.openType("extension");
+  let upgraded = await installExtension({options_ui: {page: "options.html"}, version: "2"});
+  row = list.querySelector(`[value="${addonId}"]`);
+  await TestUtils.waitForCondition(() => {
+    prefsBtn = doc.getAnonymousElementByAttribute(row, "anonid", "preferences-btn");
+    return prefsBtn;
+  });
+  prefsBtn.click();
+  await wait_for_view_load(gManagerWindow);
+  assertTelemetryMatches([
+    ["view", "aboutAddons", "list", {type}],
+    ["action", "aboutAddons", "inline", {action: "preferences", type, addonId, view: "list"}],
+    ["view", "aboutAddons", "detail", {type: "extension", addonId: "extension@mochi.test"}],
+  ]);
+
+  await close_manager(gManagerWindow);
+  await addon.unload();
+  await upgraded.unload();
+});
+
+add_task(async function testGeneralActions() {
+  await init("extension");
+
+  let doc = gManagerWindow.document;
+  let menu = doc.getElementById("utils-menu");
+  let checkForUpdates = doc.getElementById("utils-updateNow");
+  let recentUpdates = doc.getElementById("utils-viewUpdates");
+  let debugAddons = doc.getElementById("utils-debugAddons");
+  let updatePolicy = doc.getElementById("utils-autoUpdateDefault");
+  let resetUpdatePolicy = doc.getElementById("utils-resetAddonUpdatesToAutomatic");
+  let manageShortcuts = doc.getElementById("manage-shortcuts");
+
+  async function clickInGearMenu(item) {
+    let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+    menu.openPopup();
+    await shown;
+    item.click();
+    menu.hidePopup();
+  }
+
+  await clickInGearMenu(checkForUpdates);
+  await clickInGearMenu(recentUpdates);
+  await wait_for_view_load(gManagerWindow);
+  await clickInGearMenu(updatePolicy);
+  await clickInGearMenu(updatePolicy);
+  await clickInGearMenu(resetUpdatePolicy);
+
+  // Check shortcuts view.
+  await clickInGearMenu(manageShortcuts);
+  await wait_for_view_load(gManagerWindow);
+  await clickInGearMenu(checkForUpdates);
+
+  let waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+  await clickInGearMenu(debugAddons);
+  BrowserTestUtils.removeTab(await waitForNewTab);
+
+  waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+  let searchBox = doc.getElementById("header-search");
+  searchBox.value = "something";
+  searchBox.doCommand();
+  BrowserTestUtils.removeTab(await waitForNewTab);
+
+  assertTelemetryMatches([
+    ["view", "aboutAddons", "list", {type: "extension"}],
+    ["action", "aboutAddons", null, {action: "checkForUpdates", view: "list"}],
+    ["view", "aboutAddons", "updates", {type: "recent"}],
+    ["action", "aboutAddons", "default,enabled", {action: "setUpdatePolicy", view: "updates"}],
+    ["action", "aboutAddons", "enabled", {action: "setUpdatePolicy", view: "updates"}],
+    ["action", "aboutAddons", null, {action: "resetUpdatePolicy", view: "updates"}],
+    ["view", "aboutAddons", "shortcuts"],
+    ["action", "aboutAddons", null, {action: "checkForUpdates", view: "shortcuts"}],
+    ["link", "aboutAddons", "about:debugging", {view: "shortcuts"}],
+    ["link", "aboutAddons", "search", {view: "shortcuts", type: "shortcuts"}],
+  ]);
+
+  await close_manager(gManagerWindow);
+
+  assertTelemetryMatches([]);
+});
+
+add_task(async function testPreferencesLink() {
+  assertTelemetryMatches([]);
+
+  await init("theme");
+
+  let doc = gManagerWindow.document;
+
+  // Open the about:preferences page from about:addons.
+  let waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser, "about:preferences");
+  doc.getElementById("preferencesButton").click();
+  let tab = await waitForNewTab;
+  let getAddonsButton = () => tab.linkedBrowser.contentDocument.getElementById("addonsButton");
+
+  // Wait for the page to load.
+  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+  // Open the about:addons page from about:preferences.
+  getAddonsButton().click();
+
+  // Close the about:preferences tab.
+  BrowserTestUtils.removeTab(tab);
+
+  assertTelemetryMatches([
+    ["view", "aboutAddons", "list", {type: "theme"}],
+    ["link", "aboutAddons", "about:preferences", {view: "list"}],
+    ["link", "aboutPreferences", "about:addons"],
+  ]);
+
+  await close_manager(gManagerWindow);
+});
--- a/toolkit/mozapps/extensions/test/browser/browser_manualupdates.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_manualupdates.js
@@ -27,16 +27,21 @@ async function test() {
   run_next_test();
 }
 
 async function end_test() {
   await close_manager(gManagerWindow);
   finish();
 }
 
+add_test(function clearOldTelemetry() {
+  Services.telemetry.clearEvents();
+  run_next_test();
+});
+
 
 add_test(function() {
   gAvailableCategory = gManagerWindow.gCategories.get("addons://updates/available");
   is(gCategoryUtilities.isVisible(gAvailableCategory), false, "Available Updates category should initially be hidden");
 
   gProvider.createAddons([{
     id: "addon2@tests.mozilla.org",
     name: "manually updating addon",
@@ -102,16 +107,42 @@ add_test(async function() {
   // The item in the list will be checking for update information asynchronously
   // so we have to wait for it to complete. Doing the same async request should
   // make our callback be called later.
   await AddonManager.getAllInstalls();
   run_next_test();
 });
 
 add_test(function() {
+  function checkReleaseNotesTelemetry() {
+    let snapshot = Services.telemetry.snapshotEvents(
+      Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+    ok(snapshot.parent && snapshot.parent.length > 0, "Got parent telemetry events in the snapshot");
+
+    let releaseNotesEvents = snapshot.parent
+      .filter(([ts, category, method]) =>
+        category == "addonsManager" && method == "action")
+      .map(([ts, category, ...rest]) => rest);
+
+    Assert.deepEqual(releaseNotesEvents, [
+      ["action", "aboutAddons", null, {
+        action: "releaseNotes",
+        type: "extension",
+        addonId: "addon2@tests.mozilla.org",
+        view: "updates",
+      }],
+      ["action", "aboutAddons", null, {
+        action: "releaseNotes",
+        type: "extension",
+        addonId: "addon2@tests.mozilla.org",
+        view: "updates",
+      }],
+    ], "The releaseNotes events are tracked");
+  }
+
   var list = gManagerWindow.document.getElementById("updates-list");
   var item = list.firstChild;
   get_tooltip_info(item).then(({ version }) => {
     is(version, "1.1", "Update item should have version number of the update");
     var postfix = gManagerWindow.document.getAnonymousElementByAttribute(item, "class", "update-postfix");
     is_element_visible(postfix, "'Update' postfix should be visible");
     is_element_visible(item._updateAvailable, "");
     is_element_visible(item._relNotesToggle, "Release notes toggle should be visible");
@@ -134,16 +165,19 @@ add_test(function() {
 
         info("Re-opening release notes");
         item.addEventListener("RelNotesToggle", function() {
           info("Release notes now open");
 
           is_element_hidden(item._relNotesLoading, "Release notes loading message should be hidden");
           is_element_hidden(item._relNotesError, "Release notes error message should be hidden");
           isnot(item._relNotes.childElementCount, 0, "Release notes should have been inserted into container");
+
+          checkReleaseNotesTelemetry();
+
           run_next_test();
         }, {once: true});
         EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
         is_element_visible(item._relNotesLoading, "Release notes loading message should be visible");
       }, {once: true});
       EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
     }, {once: true});
     EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);