Bug 1525185 - Telemetry for HTML about:addons r=mixedpuppy,rpl, a=jcristau
authorMark Striemer <mstriemer@mozilla.com>
Fri, 07 Jun 2019 23:16:57 +0000
changeset 536883 d5aba1791f114e11166dec81517a7261a11b4859
parent 536882 9870fe7e0c59c18edad254db9fa9da2428318800
child 536884 c39a1f9bcf7c48c91fc1bf6dddf84f9b23be2638
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, rpl, jcristau
bugs1525185
milestone68.0
Bug 1525185 - Telemetry for HTML about:addons r=mixedpuppy,rpl, a=jcristau Differential Revision: https://phabricator.services.mozilla.com/D31726
toolkit/mozapps/extensions/content/aboutaddons.css
toolkit/mozapps/extensions/content/aboutaddons.html
toolkit/mozapps/extensions/content/aboutaddons.js
toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js
toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js
toolkit/mozapps/extensions/test/browser/browser_html_list_view.js
toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js
toolkit/mozapps/extensions/test/browser/browser_html_plugins.js
toolkit/mozapps/extensions/test/browser/browser_html_updates.js
toolkit/mozapps/extensions/test/browser/browser_interaction_telemetry.js
toolkit/mozapps/extensions/test/browser/head.js
--- a/toolkit/mozapps/extensions/content/aboutaddons.css
+++ b/toolkit/mozapps/extensions/content/aboutaddons.css
@@ -55,16 +55,20 @@ addon-list message-bar-stack.pending-uni
   box-shadow: var(--card-shadow);
 }
 
 addon-card:not([expanded]) > .addon.card:hover {
   box-shadow: var(--card-shadow-hover);
   cursor: pointer;
 }
 
+addon-card[expanded] .addon.card {
+  padding-bottom: 0;
+}
+
 .addon-card-collapsed {
   display: flex;
 }
 
 addon-list addon-card > .addon.card {
   -moz-user-select: none;
 }
 
@@ -232,21 +236,21 @@ addon-details {
   color: var(--in-content-deemphasized-text);
 }
 
 .addon-detail-description {
   margin: 16px 0;
 }
 
 .addon-detail-contribute {
+  display: flex;
   padding: var(--card-padding);
   border: 1px solid var(--in-content-box-border-color);
   border-radius: var(--panel-border-radius);
   margin-bottom: var(--card-padding);
-  display: flex;
   flex-direction: column;
 }
 
 .addon-detail-contribute > label {
   font-style: italic;
 }
 
 .addon-detail-contribute-button {
@@ -274,18 +278,17 @@ addon-details {
 .addon-detail-row.addon-detail-help-row {
   display: block;
   color: var(--in-content-deemphasized-text);
   padding-top: 4px;
   padding-bottom: var(--card-padding);
   border: none;
 }
 
-.addon-detail-row-has-help,
-.addon-detail-row:last-of-type {
+.addon-detail-row-has-help {
   padding-bottom: 0;
 }
 
 .addon-detail-row input[type="checkbox"] {
   margin: 0;
 }
 
 .addon-detail-rating {
--- a/toolkit/mozapps/extensions/content/aboutaddons.html
+++ b/toolkit/mozapps/extensions/content/aboutaddons.html
@@ -112,63 +112,64 @@
               data-l10n-attrs="accesskey">
             </button>
           </div>
           <div class="addon-detail-row addon-detail-row-updates">
             <label data-l10n-id="addon-detail-updates-label"></label>
             <div>
               <button class="button-link" data-l10n-id="addon-detail-update-check-label" action="update-check" hidden></button>
               <label>
-                <input type="radio" name="autoupdate" value="1"/>
+                <input type="radio" name="autoupdate" value="1" data-telemetry-value="default">
                 <span data-l10n-id="addon-detail-updates-radio-default"></span>
               </label>
               <label>
-                <input type="radio" name="autoupdate" value="2"/>
+                <input type="radio" name="autoupdate" value="2" data-telemetry-value="enabled">
                 <span data-l10n-id="addon-detail-updates-radio-on"></span>
               </label>
               <label>
-                <input type="radio" name="autoupdate" value="0"/>
+                <input type="radio" name="autoupdate" value="0" data-telemetry-value="">
                 <span data-l10n-id="addon-detail-updates-radio-off"></span>
               </label>
             </div>
           </div>
           <div class="addon-detail-row addon-detail-row-has-help addon-detail-row-private-browsing">
             <label data-l10n-id="detail-private-browsing-label"></label>
             <div>
               <label>
-                <input type="radio" name="private-browsing" value="1"/>
+                <input type="radio" name="private-browsing" value="1" data-telemetry-value="on">
                 <span data-l10n-id="addon-detail-private-browsing-allow"></span>
               </label>
               <label>
-                <input type="radio" name="private-browsing" value="0"/>
+                <input type="radio" name="private-browsing" value="0" data-telemetry-value="off">
                 <span data-l10n-id="addon-detail-private-browsing-disallow"></span>
               </label>
             </div>
           </div>
           <div class="addon-detail-row addon-detail-help-row" data-l10n-id="addon-detail-private-browsing-help">
             <a target="_blank" data-l10n-name="learn-more"></a>
           </div>
           <div class="addon-detail-row addon-detail-row-author">
             <label data-l10n-id="addon-detail-author-label"></label>
+            <a target="_blank" data-telemetry-name="author"></a>
           </div>
           <div class="addon-detail-row addon-detail-row-version">
             <label data-l10n-id="addon-detail-version-label"></label>
           </div>
           <div class="addon-detail-row addon-detail-row-lastUpdated">
             <label data-l10n-id="addon-detail-last-updated-label"></label>
           </div>
           <div class="addon-detail-row addon-detail-row-homepage">
             <label data-l10n-id="addon-detail-homepage-label"></label>
-            <a target="_blank"></a>
+            <a target="_blank" data-telemetry-name="homepage"></a>
           </div>
           <div class="addon-detail-row addon-detail-row-rating">
             <label data-l10n-id="addon-detail-rating-label"></label>
             <div class="addon-detail-rating">
               <five-star-rating></five-star-rating>
-              <a target="_blank"></a>
+              <a target="_blank" data-telemetry-name="rating"></a>
             </div>
           </div>
         </section>
         <inline-options-browser name="preferences"></inline-options-browser>
         <addon-permissions-list name="permissions"></addon-permissions-list>
         <update-release-notes name="release-notes"></update-release-notes>
       </named-deck>
     </template>
--- a/toolkit/mozapps/extensions/content/aboutaddons.js
+++ b/toolkit/mozapps/extensions/content/aboutaddons.js
@@ -1057,16 +1057,22 @@ class AddonDetails extends HTMLElement {
     this.inlineOptions.destroyBrowser();
     this.deck.removeEventListener("view-changed", this);
   }
 
   handleEvent(e) {
     if (e.type == "view-changed" && e.target == this.deck) {
       switch (this.deck.selectedViewName) {
         case "release-notes":
+          AMTelemetry.recordActionEvent({
+            object: "aboutAddons",
+            view: getTelemetryViewName(this),
+            action: "releaseNotes",
+            addon: this.addon,
+          });
           let releaseNotes = this.querySelector("update-release-notes");
           let uri = this.releaseNotesUri;
           if (uri) {
             releaseNotes.loadForUri(uri);
           }
           break;
         case "preferences":
           if (getOptionsType(this.addon) == "inline") {
@@ -1142,100 +1148,94 @@ class AddonDetails extends HTMLElement {
     // Full description.
     let description = this.querySelector(".addon-detail-description");
     if (addon.getFullDescription) {
       description.appendChild(addon.getFullDescription(document));
     } else if (addon.fullDescription) {
       description.appendChild(nl2br(addon.fullDescription));
     }
 
-    // Contribute.
-    if (!addon.contributionURL) {
-      this.querySelector(".addon-detail-contribute").remove();
-    }
-
-    // Auto updates setting.
-    if (!hasPermission(addon, "upgrade")) {
-      this.querySelector(".addon-detail-row-updates").remove();
-    }
+    this.querySelector(".addon-detail-contribute").hidden =
+      !addon.contributionURL;
+    this.querySelector(".addon-detail-row-updates").hidden =
+      !hasPermission(addon, "upgrade");
 
     let pbRow = this.querySelector(".addon-detail-row-private-browsing");
     if (!allowPrivateBrowsingByDefault && addon.type == "extension" &&
         addon.incognito != "not_allowed") {
       let isAllowed = await isAllowedInPrivateBrowsing(addon);
       pbRow.querySelector(`[value="${isAllowed ? 1 : 0}"]`).checked = true;
       let learnMore = pbRow.nextElementSibling
         .querySelector('a[data-l10n-name="learn-more"]');
       learnMore.href = SUPPORT_URL + "extensions-pb";
     } else {
       // Remove the help row, which is right after the settings.
-      pbRow.nextElementSibling.remove();
+      pbRow.nextElementSibling.hidden = true;
       // Then remove the actual settings.
-      pbRow.remove();
+      pbRow.hidden = true;
     }
 
     // Author.
     let creatorRow = this.querySelector(".addon-detail-row-author");
     if (addon.creator) {
-      let creator;
-      if (addon.creator.url) {
-        creator = document.createElement("a");
-        creator.href = addon.creator.url;
-        creator.target = "_blank";
-        creator.textContent = addon.creator.name;
+      let link = creatorRow.querySelector("a");
+      link.hidden = !addon.creator.url;
+      if (link.hidden) {
+        creatorRow.appendChild(new Text(addon.creator.name));
       } else {
-        creator = new Text(addon.creator.name);
+        link.href = addon.creator.url;
+        link.target = "_blank";
+        link.textContent = addon.creator.name;
       }
-      creatorRow.appendChild(creator);
     } else {
-      creatorRow.remove();
+      creatorRow.hidden = true;
     }
 
     // Version. Don't show a version for LWTs.
     let version = this.querySelector(".addon-detail-row-version");
     if (addon.version && !/@personas\.mozilla\.org/.test(addon.id)) {
       version.appendChild(new Text(addon.version));
     } else {
-      version.remove();
+      version.hidden = true;
     }
 
     // Last updated.
     let updateDate = this.querySelector(".addon-detail-row-lastUpdated");
     if (addon.updateDate) {
       let lastUpdated = addon.updateDate.toLocaleDateString(undefined, {
         year: "numeric",
         month: "long",
         day: "numeric",
       });
       updateDate.appendChild(new Text(lastUpdated));
     } else {
-      updateDate.remove();
+      updateDate.hidden = true;
     }
 
     // Homepage.
     let homepageRow = this.querySelector(".addon-detail-row-homepage");
     if (addon.homepageURL) {
       let homepageURL = homepageRow.querySelector("a");
       homepageURL.href = addon.homepageURL;
       homepageURL.textContent = addon.homepageURL;
     } else {
-      homepageRow.remove();
+      homepageRow.hidden = true;
     }
 
     // Rating.
     let ratingRow = this.querySelector(".addon-detail-row-rating");
     if (addon.averageRating) {
       ratingRow.querySelector("five-star-rating").rating = addon.averageRating;
       let reviews = ratingRow.querySelector("a");
       reviews.href = addon.reviewURL;
       document.l10n.setAttributes(reviews, "addon-detail-reviews-link", {
         numberOfReviews: addon.reviewCount,
       });
     } else {
-      ratingRow.remove();
+      ratingRow.hidden = true;
     }
 
     this.update();
   }
 
   showPrefs() {
     if (getOptionsType(this.addon) == "inline") {
       this.deck.selectedViewName = "preferences";
@@ -1322,16 +1322,17 @@ class AddonCard extends HTMLElement {
 
   async handleEvent(e) {
     let {addon} = this;
     let action = e.target.getAttribute("action");
 
     if (e.type == "click") {
       switch (action) {
         case "toggle-disabled":
+          this.recordActionEvent(addon.userDisabled ? "enable" : "disable");
           if (addon.userDisabled) {
             if (shouldShowPermissionsPrompt(addon)) {
               await showPermissionsPrompt(addon);
             } else {
               await addon.enable();
             }
           } else {
             await addon.disable();
@@ -1342,22 +1343,25 @@ class AddonCard extends HTMLElement {
           }
           break;
         case "ask-to-activate":
           if (hasPermission(addon, "ask-to-activate")) {
             addon.userDisabled = AddonManager.STATE_ASK_TO_ACTIVATE;
           }
           break;
         case "always-activate":
+          this.recordActionEvent("enable");
           addon.userDisabled = false;
           break;
         case "never-activate":
+          this.recordActionEvent("disable");
           addon.userDisabled = true;
           break;
         case "update-check":
+          this.recordActionEvent("checkForUpdate");
           let listener = {
             onUpdateAvailable(addon, install) {
               attachUpdateHandler(install);
             },
             onNoUpdateAvailable: () => {
               this.sendEvent("no-update");
             },
           };
@@ -1372,34 +1376,39 @@ class AddonCard extends HTMLElement {
             this.update();
             this.sendEvent("update-cancelled");
           });
           // Clear the install since it will be removed from the global list of
           // available updates (whether it succeeds or fails).
           this.updateInstall = null;
           break;
         case "contribute":
+          this.recordActionEvent("contribute");
           windowRoot.ownerGlobal.openUILinkIn(addon.contributionURL, "tab", {
             triggeringPrincipal:
               Services.scriptSecurityManager.createNullPrincipal({}),
           });
           break;
         case "preferences":
           if (getOptionsType(addon) == "tab") {
+            this.recordActionEvent("preferences", "external");
             openOptionsInTab(addon.optionsURL);
           } else if (getOptionsType(addon) == "inline") {
+            this.recordActionEvent("preferences", "inline");
             loadViewFn("detail", this.addon.id, "preferences");
           }
           break;
         case "remove":
           {
             this.panel.hide();
             let {
               remove, report,
             } = windowRoot.ownerGlobal.promptRemoveExtension(addon);
+            let value = remove ? "accepted" : "cancelled";
+            this.recordActionEvent("uninstall", value);
             if (remove) {
               await addon.uninstall(true);
               this.sendEvent("remove");
               if (report) {
                 openAbuseReport({
                   addonId: addon.id, reportEntryPoint: "uninstall",
                 });
               }
@@ -1420,24 +1429,38 @@ class AddonCard extends HTMLElement {
         case "report":
           this.panel.hide();
           openAbuseReport({addonId: addon.id, reportEntryPoint: "menu"});
           break;
         default:
           // Handle a click on the card itself.
           if (!this.expanded) {
             loadViewFn("detail", this.addon.id);
+          } else if (e.target.localName == "a" &&
+                     e.target.getAttribute("data-telemetry-name")) {
+            let value = e.target.getAttribute("data-telemetry-name");
+            AMTelemetry.recordLinkEvent({
+              object: "aboutAddons",
+              addon,
+              value,
+              extra: {
+                view: getTelemetryViewName(this),
+              },
+            });
           }
           break;
       }
     } else if (e.type == "change") {
       let {name} = e.target;
+      let telemetryValue = e.target.getAttribute("data-telemetry-value");
       if (name == "autoupdate") {
+        this.recordActionEvent("setAddonUpdate", telemetryValue);
         addon.applyBackgroundUpdates = e.target.value;
       } else if (name == "private-browsing") {
+        this.recordActionEvent("privateBrowsingAllowed", telemetryValue);
         let policy = WebExtensionPolicy.getByID(addon.id);
         let extension = policy && policy.extension;
 
         if (e.target.value == "1") {
           await ExtensionPermissions.add(
             addon.id, PRIVATE_BROWSING_PERMS, extension);
         } else {
           await ExtensionPermissions.remove(
@@ -1631,16 +1654,26 @@ class AddonCard extends HTMLElement {
 
     // Return the promise of details rendering to wait on in DetailView.
     return doneRenderPromise;
   }
 
   sendEvent(name, detail) {
     this.dispatchEvent(new CustomEvent(name, {detail}));
   }
+
+  recordActionEvent(action, value) {
+    AMTelemetry.recordActionEvent({
+      object: "aboutAddons",
+      view: getTelemetryViewName(this),
+      action,
+      addon: this.addon,
+      value,
+    });
+  }
 }
 customElements.define("addon-card", AddonCard);
 
 /**
  * A child element of `<recommended-addon-list>`. It should be initialized
  * by calling `setDiscoAddon()` first. Call `setAddon(addon)` if it has been
  * installed, and call `setAddon(null)` upon uninstall.
  *
@@ -1972,16 +2005,22 @@ class AddonList extends HTMLElement {
 
     const addonName = document.createElement("span");
     addonName.setAttribute("data-l10n-name", "addon-name");
     const message = document.createElement("span");
     message.append(addonName);
     const undo = document.createElement("button");
     undo.setAttribute("action", "undo");
     undo.addEventListener("click", () => {
+      AMTelemetry.recordActionEvent({
+        object: "aboutAddons",
+        view: getTelemetryViewName(this),
+        action: "undo",
+        addon,
+      });
       addon.cancelUninstall();
     });
 
     document.l10n.setAttributes(message, "pending-uninstall-description", {
       "addon": addon.name,
     });
     document.l10n.setAttributes(undo, "pending-uninstall-undo-button");
 
@@ -2598,42 +2637,42 @@ class DiscoveryView {
   render() {
     let discopane = document.createElement("discovery-pane");
     discopane.render();
     return discopane;
   }
 }
 
 // Generic view management.
-let root = null;
+let mainEl = null;
 
 /**
  * The name of the view for an element, used for telemetry.
  *
  * @param {Element} el The element to find the view from. A parent of the
  *                     element must define a current-view property.
  * @returns {string} The current view name.
  */
 function getTelemetryViewName(el) {
   return el.closest("[current-view]").getAttribute("current-view");
 }
 
 /**
  * Called from extensions.js once, when about:addons is loading.
  */
 function initialize(opts) {
-  root = document.getElementById("main");
+  mainEl = document.getElementById("main");
   loadViewFn = opts.loadViewFn;
   replaceWithDefaultViewFn = opts.replaceWithDefaultViewFn;
   setCategoryFn = opts.setCategoryFn;
   AddonCardListenerHandler.startup();
   window.addEventListener("unload", () => {
-    // Clear out the root node so the disconnectedCallback will trigger
+    // Clear out the main node so the disconnectedCallback will trigger
     // properly and all of the custom elements can cleanup.
-    root.textContent = "";
+    mainEl.textContent = "";
     AddonCardListenerHandler.shutdown();
   }, {once: true});
 }
 
 /**
  * Called from extensions.js to load a view. The view's render method should
  * resolve once the view has been updated to conform with other about:addons
  * views.
@@ -2650,15 +2689,15 @@ async function show(type, param) {
     let elem = discoverView.render();
     await document.l10n.translateFragment(elem);
     container.append(elem);
   } else if (type == "updates") {
     await new UpdatesView({param, root: container}).render();
   } else {
     throw new Error(`Unknown view type: ${type}`);
   }
-  root.textContent = "";
-  root.appendChild(container);
+  mainEl.textContent = "";
+  mainEl.appendChild(container);
 }
 
 function hide() {
-  root.textContent = "";
+  mainEl.textContent = "";
 }
--- a/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
@@ -10,16 +10,21 @@ let gProvider;
 let promptService;
 
 AddonTestUtils.initMochitest(this);
 
 function getAddonCard(doc, addonId) {
   return doc.querySelector(`addon-card[addon-id="${addonId}"]`);
 }
 
+function getDetailRows(card) {
+  return Array.from(card.querySelectorAll(
+    '[name="details"] .addon-detail-row:not([hidden])'));
+}
+
 function checkLabel(row, name) {
   let id;
   if (name == "private-browsing") {
     // This id is carried over from the old about:addons.
     id = "detail-private-browsing-label";
   } else {
     id = `addon-detail-${name}-label`;
   }
@@ -96,17 +101,17 @@ add_task(async function enableHtmlViews(
   gProvider.createAddons([{
     id: "addon1@mochi.test",
     name: "Test add-on 1",
     creator: {name: "The creator", url: "http://example.com/me"},
     version: "3.1",
     description: "Short description",
     fullDescription: "Longer description\nWith brs!",
     type: "extension",
-    contributionURL: "http://foo.com",
+    contributionURL: "http://localhost/contribute",
     averageRating: 4.279,
     userPermissions: {
       origins: ["<all_urls>", "file://*/*"],
       permissions: ["alarms", "contextMenus", "tabs", "webNavigation"],
     },
     reviewCount: 5,
     reviewURL: "http://example.com/reviews",
     homepageURL: "http://example.com/addon1",
@@ -138,77 +143,114 @@ add_task(async function enableHtmlViews(
       height: 92,
     }],
   }]);
 
   promptService = mockPromptService();
 });
 
 add_task(async function testOpenDetailView() {
+  Services.telemetry.clearEvents();
+  let id = "test@mochi.test";
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       name: "Test",
-      applications: {gecko: {id: "test@mochi.test"}},
+      applications: {gecko: {id}},
+    },
+    useAddonManager: "temporary",
+  });
+  let id2 = "test2@mochi.test";
+  let extension2 = ExtensionTestUtils.loadExtension({
+    manifest: {
+      name: "Test",
+      applications: {gecko: {id: id2}},
+    },
+    useAddonManager: "temporary",
+  });
+
+  await extension.startup();
+  await extension2.startup();
+
+  const goBack = async win => {
+    let loaded = waitForViewLoad(win);
+    win.managerWindow.document.getElementById("go-back").click();
+    await loaded;
+  };
+
+  let win = await loadInitialView("extension");
+  let doc = win.document;
+
+  // Test click on card to open details.
+  let card = getAddonCard(doc, id);
+  ok(!card.querySelector("addon-details"), "The card doesn't have details");
+  let loaded = waitForViewLoad(win);
+  EventUtils.synthesizeMouseAtCenter(card, {clickCount: 1}, win);
+  await loaded;
+
+  card = getAddonCard(doc, id);
+  ok(card.querySelector("addon-details"), "The card now has details");
+
+  await goBack(win);
+
+  // Test using more options menu.
+  card = getAddonCard(doc, id);
+  loaded = waitForViewLoad(win);
+  card.querySelector('[action="expand"]').click();
+  await loaded;
+
+  card = getAddonCard(doc, id);
+  ok(card.querySelector("addon-details"), "The card now has details");
+
+  await goBack(win);
+
+  card = getAddonCard(doc, id2);
+  loaded = waitForViewLoad(win);
+  card.querySelector('[action="expand"]').click();
+  await loaded;
+
+  await closeView(win);
+  await extension.unload();
+  await extension2.unload();
+
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "view", "aboutAddons", "list", {type: "extension"}],
+    ["addonsManager", "view", "aboutAddons", "detail",
+     {type: "extension", addonId: id}],
+    ["addonsManager", "view", "aboutAddons", "list", {type: "extension"}],
+    ["addonsManager", "view", "aboutAddons", "detail",
+     {type: "extension", addonId: id}],
+    ["addonsManager", "view", "aboutAddons", "list", {type: "extension"}],
+    ["addonsManager", "view", "aboutAddons", "detail",
+     {type: "extension", addonId: id2}],
+  ]);
+});
+
+add_task(async function testDetailOperations() {
+  Services.telemetry.clearEvents();
+  let id = "test@mochi.test";
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      name: "Test",
+      applications: {gecko: {id}},
     },
     useAddonManager: "temporary",
   });
 
   await extension.startup();
 
   let win = await loadInitialView("extension");
   let doc = win.document;
 
-  // Test click on card to open details.
-  let card = getAddonCard(doc, "test@mochi.test");
+  let card = getAddonCard(doc, id);
   ok(!card.querySelector("addon-details"), "The card doesn't have details");
   let loaded = waitForViewLoad(win);
   EventUtils.synthesizeMouseAtCenter(card, {clickCount: 1}, win);
   await loaded;
 
-  card = getAddonCard(doc, "test@mochi.test");
-  ok(card.querySelector("addon-details"), "The card now has details");
-
-  loaded = waitForViewLoad(win);
-  win.managerWindow.document.getElementById("go-back").click();
-  await loaded;
-
-  // Test using more options menu.
-  card = getAddonCard(doc, "test@mochi.test");
-  loaded = waitForViewLoad(win);
-  card.querySelector('[action="expand"]').click();
-  await loaded;
-
-  card = getAddonCard(doc, "test@mochi.test");
-  ok(card.querySelector("addon-details"), "The card now has details");
-
-  await closeView(win);
-  await extension.unload();
-});
-
-add_task(async function testDetailOperations() {
-  let extension = ExtensionTestUtils.loadExtension({
-    manifest: {
-      name: "Test",
-      applications: {gecko: {id: "test@mochi.test"}},
-    },
-    useAddonManager: "temporary",
-  });
-
-  await extension.startup();
-
-  let win = await loadInitialView("extension");
-  let doc = win.document;
-
-  let card = getAddonCard(doc, "test@mochi.test");
-  ok(!card.querySelector("addon-details"), "The card doesn't have details");
-  let loaded = waitForViewLoad(win);
-  EventUtils.synthesizeMouseAtCenter(card, {clickCount: 1}, win);
-  await loaded;
-
-  card = getAddonCard(doc, "test@mochi.test");
+  card = getAddonCard(doc, id);
   let panel = card.querySelector("panel-list");
 
   // Check button visibility.
   let disableButton = panel.querySelector('[action="toggle-disabled"]');
   ok(!disableButton.hidden, "The disable button is visible");
 
   let removeButton = panel.querySelector('[action="remove"]');
   ok(!removeButton.hidden, "The remove button is visible");
@@ -231,18 +273,17 @@ add_task(async function testDetailOperat
 
   // The (disabled) text should be shown now.
   Assert.deepEqual(
     doc.l10n.getAttributes(name),
     {id: "addon-name-disabled", args: {name: "Test"}},
     "The name is updated to the disabled text");
 
   // Enable the add-on.
-  let extensionStarted = AddonTestUtils.promiseWebExtensionStartup(
-    "test@mochi.test");
+  let extensionStarted = AddonTestUtils.promiseWebExtensionStartup(id);
   disableToggled = BrowserTestUtils.waitForEvent(card, "update");
   disableButton.click();
   await Promise.all([disableToggled, extensionStarted]);
 
   // Name is just the add-on name again.
   is(name.textContent, "Test", "The name is reset when enabled");
   is(doc.l10n.getAttributes(name).id, "", "There is no l10n name");
 
@@ -256,19 +297,19 @@ add_task(async function testDetailOperat
   // Tell the mock prompt service that the prompt was accepted.
   promptService._response = 0;
   removeButton.click();
   await viewChanged;
 
   // We're on the list view now and there's no card for this extension.
   const addonList = doc.querySelector("addon-list");
   ok(addonList, "There's an addon-list now");
-  ok(!getAddonCard(doc, "test@mochi.test"),
+  ok(!getAddonCard(doc, id),
      "The extension no longer has a card");
-  let addon = await AddonManager.getAddonByID("test@mochi.test");
+  let addon = await AddonManager.getAddonByID(id);
   ok(addon && !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL),
      "The addon is pending uninstall");
 
   // Ensure that a pending uninstall bar has been created for the
   // pending uninstall extension, and pressing the undo button will
   // refresh the list and render a card to the re-enabled extension.
   assertHasPendingUninstalls(addonList, 1);
   assertHasPendingUninstallAddon(addonList, addon);
@@ -278,37 +319,56 @@ add_task(async function testDetailOperat
   info("Wait for the pending uninstall addon complete restart");
   await extensionStarted;
 
   card = getAddonCard(doc, addon.id);
   ok(card, "Addon card rendered after clicking pending uninstall undo button");
 
   await closeView(win);
   await extension.unload();
+
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "view", "aboutAddons", "list", {type: "extension"}],
+    ["addonsManager", "view", "aboutAddons", "detail",
+     {type: "extension", addonId: id}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "extension", addonId: id, action: "disable", view: "detail"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "extension", addonId: id, action: "enable"}],
+    ["addonsManager", "action", "aboutAddons", "cancelled",
+     {type: "extension", addonId: id, action: "uninstall", view: "detail"}],
+    ["addonsManager", "action", "aboutAddons", "accepted",
+     {type: "extension", addonId: id, action: "uninstall", view: "detail"}],
+    ["addonsManager", "view", "aboutAddons", "list", {type: "extension"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "extension", addonId: id, action: "undo", view: "list"}],
+  ]);
 });
 
 add_task(async function testFullDetails() {
+  Services.telemetry.clearEvents();
+  let id = "addon1@mochi.test";
   let win = await loadInitialView("extension");
   let doc = win.document;
 
   // The list card.
-  let card = getAddonCard(doc, "addon1@mochi.test");
+  let card = getAddonCard(doc, id);
   ok(!card.hasAttribute("expanded"), "The list card is not expanded");
 
   // Make sure the preview is hidden.
   let preview = card.querySelector(".card-heading-image");
   ok(preview, "There is a preview");
   is(preview.hidden, true, "The preview is hidden");
 
   let loaded = waitForViewLoad(win);
   card.querySelector('[action="expand"]').click();
   await loaded;
 
   // This is now the detail card.
-  card = getAddonCard(doc, "addon1@mochi.test");
+  card = getAddonCard(doc, id);
   ok(card.hasAttribute("expanded"), "The detail card is expanded");
 
   // Make sure the preview is hidden.
   preview = card.querySelector(".card-heading-image");
   ok(preview, "There is a preview");
   is(preview.hidden, true, "The preview is hidden");
 
   let details = card.querySelector("addon-details");
@@ -318,18 +378,22 @@ add_task(async function testFullDetails(
 
   let desc = details.querySelector(".addon-detail-description");
   is(desc.innerHTML, "Longer description<br>With brs!",
      "The full description replaces newlines with <br>");
 
   let contrib = details.querySelector(".addon-detail-contribute");
   ok(contrib, "The contribution section is visible");
 
-  let rows = Array.from(
-    card.querySelectorAll('[name="details"] .addon-detail-row'));
+  let waitForTab = BrowserTestUtils.waitForNewTab(
+    gBrowser, "http://localhost/contribute");
+  contrib.querySelector("button").click();
+  BrowserTestUtils.removeTab(await waitForTab);
+
+  let rows = getDetailRows(card);
 
   // Auto updates.
   let row = rows.shift();
   checkLabel(row, "updates");
   let expectedOptions = [
     {value: "1", label: "addon-detail-updates-radio-default", checked: false},
     {value: "2", label: "addon-detail-updates-radio-on", checked: true},
     {value: "0", label: "addon-detail-updates-radio-off", checked: false},
@@ -409,16 +473,24 @@ add_task(async function testFullDetails(
   await testRating(4.250, "4.3", "full,full,full,full,half");
   await testRating(4.749, "4.7", "full,full,full,full,half");
   await testRating(5.000, "5", "full,full,full,full,full");
 
   // That should've been all the rows.
   is(rows.length, 0, "There are no more rows left");
 
   await closeView(win);
+
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "view", "aboutAddons", "list", {type: "extension"}],
+    ["addonsManager", "view", "aboutAddons", "detail",
+     {type: "extension", addonId: id}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "extension", addonId: id, action: "contribute", view: "detail"}],
+  ]);
 });
 
 add_task(async function testMinimalExtension() {
   let win = await loadInitialView("extension");
   let doc = win.document;
 
   let card = getAddonCard(doc, "addon2@mochi.test");
   ok(!card.hasAttribute("expanded"), "The list card is not expanded");
@@ -431,20 +503,19 @@ add_task(async function testMinimalExten
 
   // Check all the deck buttons are hidden.
   assertDeckHeadingButtons(details.tabGroup, ["details", "permissions"]);
 
   let desc = details.querySelector(".addon-detail-description");
   is(desc.textContent, "", "There is no full description");
 
   let contrib = details.querySelector(".addon-detail-contribute");
-  ok(!contrib, "There is no contribution element");
+  ok(contrib.hidden, "The contribution element is hidden");
 
-  let rows = Array.from(
-    card.querySelectorAll('[name="details"] .addon-detail-row'));
+  let rows = getDetailRows(card);
 
   // Automatic updates.
   let row = rows.shift();
   checkLabel(row, "updates");
 
   // Private browsing settings.
   row = rows.shift();
   checkLabel(row, "private-browsing");
@@ -489,18 +560,17 @@ add_task(async function testDefaultTheme
   // Make sure the preview is hidden.
   preview = card.querySelector(".card-heading-image");
   ok(preview, "There is a preview");
   is(preview.hidden, true, "The preview is hidden");
 
   // Check all the deck buttons are hidden.
   assertDeckHeadingHidden(card.details.tabGroup);
 
-  let rows = Array.from(
-    card.querySelectorAll('[name="details"] .addon-detail-row'));
+  let rows = getDetailRows(card);
 
   // Author.
   let author = rows.shift();
   checkLabel(author, "author");
   let text = author.lastChild;
   is(text.textContent, "Mozilla", "The author is set");
 
   // Version.
@@ -547,102 +617,120 @@ add_task(async function testStaticTheme(
   is(preview.src, "http://example.com/preview.png", "The preview URL is set");
   is(preview.width, "664", "The width is set");
   is(preview.height, "90", "The height is set");
   is(preview.hidden, false, "The preview is visible");
 
   // Check all the deck buttons are hidden.
   assertDeckHeadingHidden(card.details.tabGroup);
 
-  let rows = Array.from(
-    card.querySelectorAll('[name="details"] .addon-detail-row'));
+  let rows = getDetailRows(card);
 
   // Automatic updates.
   let row = rows.shift();
   checkLabel(row, "updates");
 
   // Author.
   let author = rows.shift();
   checkLabel(author, "author");
-  let text = author.lastChild;
+  let text = author.lastElementChild;
   is(text.textContent, "Artist", "The author is set");
 
   is(rows.length, 0, "There was only 1 row");
 
   await closeView(win);
 });
 
 add_task(async function testPrivateBrowsingExtension() {
+  Services.telemetry.clearEvents();
+  let id = "pb@mochi.test";
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       name: "My PB extension",
-      applications: {gecko: {id: "pb@mochi.test"}},
+      applications: {gecko: {id}},
     },
     useAddonManager: "permanent",
   });
 
   await extension.startup();
 
   let win = await loadInitialView("extension");
   let doc = win.document;
 
   // The add-on shouldn't show that it's allowed yet.
-  let card = getAddonCard(doc, "pb@mochi.test");
+  let card = getAddonCard(doc, id);
   let badge = card.querySelector(".addon-badge-private-browsing-allowed");
   ok(badge.hidden, "The PB badge is hidden initially");
-  ok(!await hasPrivateAllowed("pb@mochi.test"), "PB is not allowed");
+  ok(!await hasPrivateAllowed(id), "PB is not allowed");
 
   // Load the detail view.
   let loaded = waitForViewLoad(win);
   card.querySelector('[action="expand"]').click();
   await loaded;
 
   // The badge is still hidden on the detail view.
-  card = getAddonCard(doc, "pb@mochi.test");
+  card = getAddonCard(doc, id);
   badge = card.querySelector(".addon-badge-private-browsing-allowed");
   ok(badge.hidden, "The PB badge is hidden on the detail view");
-  ok(!await hasPrivateAllowed("pb@mochi.test"), "PB is not allowed");
+  ok(!await hasPrivateAllowed(id), "PB is not allowed");
 
   let pbRow = card.querySelector(".addon-detail-row-private-browsing");
 
   // Allow private browsing.
   let [allow, disallow] = pbRow.querySelectorAll("input");
   let updated = BrowserTestUtils.waitForEvent(card, "update");
   allow.click();
   await updated;
   ok(!badge.hidden, "The PB badge is now shown");
-  ok(await hasPrivateAllowed("pb@mochi.test"), "PB is allowed");
+  ok(await hasPrivateAllowed(id), "PB is allowed");
 
   // Disable the add-on and change the value.
   updated = BrowserTestUtils.waitForEvent(card, "update");
   card.querySelector('[action="toggle-disabled"]').click();
   await updated;
 
   // It's still allowed in PB.
   ok(!badge.hidden, "The PB badge is shown");
-  ok(await hasPrivateAllowed("pb@mochi.test"), "PB is allowed");
+  ok(await hasPrivateAllowed(id), "PB is allowed");
 
   // Disallow PB.
   updated = BrowserTestUtils.waitForEvent(card, "update");
   disallow.click();
   await updated;
 
   ok(badge.hidden, "The PB badge is hidden");
-  ok(!await hasPrivateAllowed("pb@mochi.test"), "PB is disallowed");
+  ok(!await hasPrivateAllowed(id), "PB is disallowed");
 
   // Allow PB.
   updated = BrowserTestUtils.waitForEvent(card, "update");
   allow.click();
   await updated;
 
   ok(!badge.hidden, "The PB badge is hidden");
-  ok(await hasPrivateAllowed("pb@mochi.test"), "PB is disallowed");
+  ok(await hasPrivateAllowed(id), "PB is disallowed");
+
+  await closeView(win);
+  await extension.unload();
 
-  await extension.unload();
-  await closeView(win);
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "view", "aboutAddons", "list", {type: "extension"}],
+    ["addonsManager", "view", "aboutAddons", "detail",
+     {type: "extension", addonId: id}],
+    ["addonsManager", "action", "aboutAddons", "on",
+     {type: "extension", addonId: id, action: "privateBrowsingAllowed",
+      view: "detail"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "extension", addonId: id, action: "disable"}],
+    ["addonsManager", "action", "aboutAddons", "off",
+     {type: "extension", addonId: id, action: "privateBrowsingAllowed",
+      view: "detail"}],
+    ["addonsManager", "action", "aboutAddons", "on",
+     {type: "extension", addonId: id, action: "privateBrowsingAllowed",
+      view: "detail"}],
+  ]);
 });
 
 add_task(async function testInvalidExtension() {
   let win = await open_manager("addons://detail/foo");
   let categoryUtils = new CategoryUtilities(win);
   is(categoryUtils.selectedCategory, "discover",
      "Should fall back to the discovery pane");
 
--- a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js
@@ -7,20 +7,16 @@ const {
 
 const {
   ExtensionUtils: {
     promiseEvent,
     promiseObserved,
   },
 }  = ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
-const {
-  TelemetryTestUtils,
-} = ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm");
-
 // The response to the discovery API, as documented at:
 // https://addons-server.readthedocs.io/en/latest/topics/api/discovery.html
 //
 // The test is designed to easily verify whether the discopane works with the
 // latest AMO API, by replacing API_RESPONSE_FILE's content with latest AMO API
 // response, e.g. from https://addons.allizom.org/api/v4/discovery/?lang=en-US
 // The response must contain at least one theme, and one extension.
 const API_RESPONSE_FILE = RELATIVE_DIR + "discovery/api_response.json";
@@ -259,26 +255,16 @@ async function testAddonUninstall(card) 
   await updatePromise;
 
   Assert.deepEqual(
     getVisibleActions(card).map(getActionName),
     ["install-addon"],
     "Should have an Install button after uninstall");
 }
 
-function checkTelemetryEvents(expectations) {
-  TelemetryTestUtils.assertEvents(expectations, {
-    category: "addonsManager",
-    method(actual) {
-      return actual === "action" || actual === "link";
-    },
-    object: "aboutAddons",
-  });
-}
-
 add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({
     set: [
       ["extensions.getAddons.discovery.api_url",
        `http://${AMO_TEST_HOST}/discoapi`],
       // Enable HTML for all because some tests load non-discopane views.
       ["extensions.htmlaboutaddons.enabled", true],
       ["extensions.htmlaboutaddons.discover.enabled", true],
@@ -445,37 +431,24 @@ add_task(async function install_from_dis
     [
       "manage-addon",
       "manage-addon",
       ...new Array(apiResultArray.length - 2).fill("install-addon"),
       "open-amo",
     ],
     "The Install buttons should be replaced with Manage buttons");
 
-  checkTelemetryEvents([{
-    category: "addonsManager",
-    method: "action",
-    object: "aboutAddons",
-    extra: {
-      action: "installFromRecommendation",
-      view: "discover",
-      addonId: FIRST_EXTENSION_ID,
-      type: "extension",
-    },
-  }, {
-    category: "addonsManager",
-    method: "action",
-    object: "aboutAddons",
-    extra: {
-      action: "installFromRecommendation",
-      view: "discover",
-      addonId: FIRST_THEME_ID,
-      type: "theme",
-    },
-  }]);
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "action", "aboutAddons", null,
+     {action: "installFromRecommendation", view: "discover",
+      addonId: FIRST_EXTENSION_ID, type: "extension"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {action: "installFromRecommendation", view: "discover",
+      addonId: FIRST_THEME_ID, type: "theme"}],
+  ]);
 
   // End of the testing installation from a card.
 
   // Click on the Manage button to verify that it does something useful,
   // and in order to be able to force the discovery pane to be rendered again.
   let loaded = waitForViewLoad(win);
   getCardByAddonId(win, FIRST_EXTENSION_ID)
     .querySelector("[action='manage-addon']").click();
@@ -484,27 +457,21 @@ add_task(async function install_from_dis
     let addonCard =
       win.document.querySelector(
         `addon-card[addon-id="${FIRST_EXTENSION_ID}"]`);
     ok(addonCard, "Add-on details should be shown");
     ok(addonCard.expanded, "The card should have been expanded");
     // TODO bug 1540253: Check that the "recommended" badge is visible.
   }
 
-  checkTelemetryEvents([{
-    category: "addonsManager",
-    method: "action",
-    object: "aboutAddons",
-    extra: {
-      action: "manage",
-      view: "discover",
-      addonId: FIRST_EXTENSION_ID,
-      type: "extension",
-    },
-  }]);
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "action", "aboutAddons", null,
+     {action: "manage", view: "discover", addonId: FIRST_EXTENSION_ID,
+      type: "extension"}],
+  ], {methods: ["action"]});
 
   // Now we are going to force an updated rendering and check that the cards are
   // in the expected order, and then test uninstallation of the above add-ons.
   await switchToDiscoView(win);
   await waitForAllImagesLoaded(win);
 
   Assert.deepEqual(
     getVisibleActions(win.document).map(getActionName),
@@ -684,31 +651,18 @@ add_task(async function discopane_intera
   Services.telemetry.clearEvents();
 
   // "Find more add-ons" button.
   await testClickInDiscoCard("[action='open-amo']", "find-more-link-bottom");
 
   // Link to AMO listing
   await testClickInDiscoCard(".disco-addon-author a", "discopane-entry-link");
 
-  checkTelemetryEvents([{
-    category: "addonsManager",
-    method: "link",
-    object: "aboutAddons",
-    value: "discomore",
-    extra: {
-      view: "discover",
-    },
-  }, {
-    category: "addonsManager",
-    method: "link",
-    object: "aboutAddons",
-    value: "discohome",
-    extra: {
-      view: "discover",
-    },
-  }]);
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "link", "aboutAddons", "discomore", {view: "discover"}],
+    ["addonsManager", "link", "aboutAddons", "discohome", {view: "discover"}],
+  ]);
 
   is(apiHandler.requestCount, 1, "Discovery API should be fetched once");
 
   await closeView(win);
   await SpecialPowers.popPrefEnv();
 });
--- a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js
@@ -2,20 +2,16 @@
 "use strict";
 
 const {ClientID} = ChromeUtils.import("resource://gre/modules/ClientID.jsm");
 
 const {
   AddonTestUtils,
 } = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
 
-const {
-  TelemetryTestUtils,
-} = ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm");
-
 AddonTestUtils.initMochitest(this);
 const server = AddonTestUtils.createHttpServer();
 const serverBaseUrl = `http://localhost:${server.identity.primaryPort}/`;
 server.registerPathHandler("/sumo/personalized-addons",
   (request, response) => {
     response.write("This is a SUMO page that explains personalized add-ons.");
   });
 
@@ -96,27 +92,19 @@ add_task(async function clientid_enabled
   getNoticeButton(win).click();
 
   info(`Waiting for new tab with URL: ${expectedUrl}`);
   let tab = await tabPromise;
   BrowserTestUtils.removeTab(tab);
 
   await closeView(win);
 
-  TelemetryTestUtils.assertEvents([{
-    method: "link",
-    value: "disconotice",
-    extra: {
-      view: "discover",
-    },
-  }], {
-    category: "addonsManager",
-    method: "link",
-    object: "aboutAddons",
-  });
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "link", "aboutAddons", "disconotice", {view: "discover"}],
+  ], {methods: ["link"]});
 });
 
 // Test that the clientid is not sent when disabled via prefs.
 add_task(async function clientid_disabled() {
   // Temporarily override the prefs that we had set in setup.
   await SpecialPowers.pushPrefEnv({
     set: [["browser.discovery.enabled", false]],
   });
--- a/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js
@@ -34,16 +34,17 @@ function waitForThemeChange(list) {
   return BrowserTestUtils.waitForEvent(list, "move", () => ++moveCount == 2);
 }
 
 add_task(async function enableHtmlViews() {
   await SpecialPowers.pushPrefEnv({
     set: [["extensions.htmlaboutaddons.enabled", true]],
   });
   promptService = mockPromptService();
+  Services.telemetry.clearEvents();
 });
 
 let extensionsCreated = 0;
 
 function createExtensions(manifestExtras) {
   return manifestExtras.map(extra => ExtensionTestUtils.loadExtension({
     manifest: {
       name: "Test extension",
@@ -53,45 +54,46 @@ function createExtensions(manifestExtras
       },
       ...extra,
     },
     useAddonManager: "temporary",
   }));
 }
 
 add_task(async function testExtensionList() {
+  let id = "test@mochi.test";
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       name: "Test extension",
-      applications: {gecko: {id: "test@mochi.test"}},
+      applications: {gecko: {id}},
       icons: {
         32: "test-icon.png",
       },
     },
     useAddonManager: "temporary",
   });
   await extension.startup();
 
-  let addon = await AddonManager.getAddonByID("test@mochi.test");
+  let addon = await AddonManager.getAddonByID(id);
   ok(addon, "The add-on can be found");
 
   let win = await loadInitialView("extension");
   let doc = win.document;
 
   // Find the addon-list to listen for events.
   let list = doc.querySelector("addon-list");
 
   // There shouldn't be any disabled extensions.
   let disabledSection = getSection(doc, "disabled");
   ok(isEmpty(disabledSection), "The disabled section is empty");
 
   // The loaded extension should be in the enabled list.
   let enabledSection = getSection(doc, "enabled");
   ok(!isEmpty(enabledSection), "The enabled section isn't empty");
-  let card = getCardByAddonId(enabledSection, "test@mochi.test");
+  let card = getCardByAddonId(enabledSection, id);
   ok(card, "The card is in the enabled section");
 
   // Check the properties of the card.
   is(card.querySelector(".addon-name").textContent, "Test extension",
      "The name is set");
   let icon = card.querySelector(".addon-icon");
   ok(icon.src.endsWith("/test-icon.png"), "The icon is set");
 
@@ -121,17 +123,17 @@ add_task(async function testExtensionLis
   await cancelled;
 
   let removed = BrowserTestUtils.waitForEvent(list, "remove");
   // Tell the mock prompt service that the prompt was accepted.
   promptService._response = 0;
   removeButton.click();
   await removed;
 
-  addon = await AddonManager.getAddonByID("test@mochi.test");
+  addon = await AddonManager.getAddonByID(id);
   ok(addon && !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL),
      "The addon is pending uninstall");
 
   // Ensure that a pending uninstall bar has been created for the
   // pending uninstall extension, and pressing the undo button will
   // refresh the list and render a card to the re-enabled extension.
   assertHasPendingUninstalls(list, 1);
   assertHasPendingUninstallAddon(list, addon);
@@ -232,16 +234,31 @@ add_task(async function testExtensionLis
 
   ok(themeAddon.pendingOperations & AddonManager.PENDING_UNINSTALL,
      "The theme addon is pending after the list extension view is closed");
 
   await themeAddon.uninstall();
 
   ok(!await AddonManager.getAddonByID(themeAddon.id),
      "The theme addon is fully uninstalled");
+
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "view", "aboutAddons", "list", {type: "extension"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "extension", addonId: id, view: "list", action: "disable"}],
+    ["addonsManager", "action", "aboutAddons", "cancelled",
+     {type: "extension", addonId: id, view: "list", action: "uninstall"}],
+    ["addonsManager", "action", "aboutAddons", "accepted",
+     {type: "extension", addonId: id, view: "list", action: "uninstall"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "extension", addonId: id, view: "list", action: "undo"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "extension", addonId: "test-2@mochi.test", view: "list",
+      action: "undo"}],
+  ]);
 });
 
 add_task(async function testMouseSupport() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       name: "Test extension",
       applications: {gecko: {id: "test@mochi.test"}},
     },
--- a/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js
@@ -1,19 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 /* eslint max-len: ["error", 80] */
 "use strict";
 
 const {
   AddonTestUtils,
 } = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
-const {
-  TelemetryTestUtils,
-} = ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm");
 
 AddonTestUtils.initMochitest(this);
 
 function makeResult({guid, type}) {
   return {
     addon: {
       authors: [{name: "Some author"}],
       current_version: {
@@ -180,31 +177,20 @@ async function testListRecommendations({
   let shown = BrowserTestUtils.waitForEvent(recommendedList, "card-shown");
   await extension.unload();
   await shown;
 
   is_element_visible(hiddenCard, "The card is now shown");
 
   await closeView(win);
 
-  TelemetryTestUtils.assertEvents([{
-    category: "addonsManager",
-    method: "action",
-    object: "aboutAddons",
-    extra: {
-      action: "installFromRecommendation",
-      view: "list",
-      addonId,
-      type,
-    },
-  }], {
-    category: "addonsManager",
-    method: "action",
-    object: "aboutAddons",
-  });
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "action", "aboutAddons", null,
+     {action: "installFromRecommendation", view: "list", addonId, type}],
+  ], {methods: ["action"]});
 }
 
 add_task(async function testExtensionList() {
   await testListRecommendations({type: "extension"});
 });
 
 add_task(async function testThemeList() {
   await testListRecommendations({
--- a/toolkit/mozapps/extensions/test/browser/browser_html_plugins.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_plugins.js
@@ -18,16 +18,18 @@ add_task(async function enableHtmlViews(
     getFullDescription(doc) {
       let a = doc.createElement("a");
       a.textContent = "A link";
       a.href = "http://example.com/no-ask-to-activate";
       return a;
     },
     type: "plugin",
   }]);
+
+  Services.telemetry.clearEvents();
 });
 
 add_task(async function testAskToActivate() {
   function checkItems(items, checked) {
     for (let item of items) {
       let action = item.getAttribute("action");
       ok(!item.disabled, `${action} is enabled`);
       if (action == checked) {
@@ -36,16 +38,17 @@ add_task(async function testAskToActivat
         ok(!item.checked, `${action} isn't checked`);
       }
     }
   }
 
   let plugins = await AddonManager.getAddonsByTypes(["plugin"]);
   let flash = plugins.find(
     plugin => plugin.description == TEST_PLUGIN_DESCRIPTION);
+  let addonId = flash.id;
   let win = await loadInitialView("plugin");
   let doc = win.document;
 
   let card = doc.querySelector(`addon-card[addon-id="${flash.id}"]`);
   let panelItems = card.querySelectorAll("panel-item:not([hidden])");
   let actions = Array.from(panelItems).map(item => item.getAttribute("action"));
   Assert.deepEqual(actions, [
     "ask-to-activate", "always-activate", "never-activate", "preferences",
@@ -79,17 +82,44 @@ add_task(async function testAskToActivat
   updated = BrowserTestUtils.waitForEvent(card, "update");
   panelItems[0].click();
   await updated;
   checkItems(panelItems, "ask-to-activate");
   is(flash.userDisabled, AddonManager.STATE_ASK_TO_ACTIVATE,
      "Flash is ask-to-activate");
   ok(flash.isActive, "Flash is active");
 
+  // Check the detail view, too.
+  let loaded = waitForViewLoad(win);
+  card.querySelector("[action=expand]").click();
+  await loaded;
+
+  // Set the state to always activate.
+  card = doc.querySelector("addon-card");
+  panelItems = card.querySelectorAll("panel-item");
+  checkItems(panelItems, "ask-to-activate");
+  updated = BrowserTestUtils.waitForEvent(card, "update");
+  panelItems[1].click();
+  await updated;
+  checkItems(panelItems, "always-activate");
+
   await closeView(win);
+
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "view", "aboutAddons", "list", {type: "plugin"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "plugin", addonId, view: "list", action: "enable"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "plugin", addonId, view: "list", action: "disable"}],
+    // Ask-to-activate doesn't trigger a telemetry event.
+    ["addonsManager", "view", "aboutAddons", "detail",
+     {type: "plugin", addonId}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "plugin", addonId, view: "detail", action: "enable"}],
+  ]);
 });
 
 add_task(async function testNoAskToActivate() {
   function checkItems(menuItems) {
     for (let item of menuItems) {
       let action = item.getAttribute("action");
       if (action === "ask-to-activate") {
         ok(item.disabled, "ask-to-activate is disabled");
--- a/toolkit/mozapps/extensions/test/browser/browser_html_updates.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js
@@ -4,52 +4,54 @@ const {AddonTestUtils} =
   ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
 
 AddonTestUtils.initMochitest(this);
 
 add_task(async function enableHtmlViews() {
   await SpecialPowers.pushPrefEnv({
     set: [["extensions.htmlaboutaddons.enabled", true]],
   });
+  Services.telemetry.clearEvents();
 });
 
 function loadDetailView(win, id) {
   let doc = win.document;
   let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
   let loaded = waitForViewLoad(win);
   EventUtils.synthesizeMouseAtCenter(card, {clickCount: 1}, win);
   return loaded;
 }
 
 add_task(async function testChangeAutoUpdates() {
+  let id = "test@mochi.test";
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       name: "Test",
-      applications: {gecko: {id: "test@mochi.test"}},
+      applications: {gecko: {id}},
     },
     // Use permanent so the add-on can be updated.
     useAddonManager: "permanent",
   });
 
   await extension.startup();
-  let addon = await AddonManager.getAddonByID("test@mochi.test");
+  let addon = await AddonManager.getAddonByID(id);
 
   let win = await loadInitialView("extension");
   let doc = win.document;
 
   let getInputs = updateRow => ({
     default: updatesRow.querySelector('input[value="1"]'),
     on: updatesRow.querySelector('input[value="2"]'),
     off: updatesRow.querySelector('input[value="0"]'),
     checkForUpdate: updatesRow.querySelector('[action="update-check"]'),
   });
 
-  await loadDetailView(win, "test@mochi.test");
+  await loadDetailView(win, id);
 
-  let card = doc.querySelector('addon-card[addon-id="test@mochi.test"]');
+  let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
   ok(card.querySelector("addon-details"), "The card now has details");
 
   let updatesRow = card.querySelector(".addon-detail-row-updates");
   let inputs = getInputs(updatesRow);
   is(addon.applyBackgroundUpdates, 1, "Default is set");
   ok(inputs.default.checked, "The default option is selected");
   ok(inputs.checkForUpdate.hidden, "Update check is hidden");
 
@@ -64,19 +66,19 @@ add_task(async function testChangeAutoUp
   ok(!inputs.checkForUpdate.hidden, "Update check is visible");
 
   // Go back to the list view and check the details view again.
   let loaded = waitForViewLoad(win);
   win.managerWindow.document.getElementById("go-back").click();
   await loaded;
 
   // Load the detail view again.
-  await loadDetailView(win, "test@mochi.test");
+  await loadDetailView(win, id);
 
-  card = doc.querySelector('addon-card[addon-id="test@mochi.test"]');
+  card = doc.querySelector(`addon-card[addon-id="${id}"]`);
   updatesRow = card.querySelector(".addon-detail-row-updates");
   inputs = getInputs(updatesRow);
 
   ok(inputs.off.checked, "Off is still selected");
 
   // Disable global updates.
   let updated = BrowserTestUtils.waitForEvent(card, "update");
   AddonManager.autoUpdateDefault = false;
@@ -100,16 +102,33 @@ add_task(async function testChangeAutoUp
 
   // Enable updates again.
   updated = BrowserTestUtils.waitForEvent(card, "update");
   AddonManager.autoUpdateDefault = true;
   await updated;
 
   await closeView(win);
   await extension.unload();
+
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "view", "aboutAddons", "list", {type: "extension"}],
+    ["addonsManager", "view", "aboutAddons", "detail",
+     {type: "extension", addonId: id}],
+    ["addonsManager", "action", "aboutAddons", "enabled",
+     {type: "extension", addonId: id, action: "setAddonUpdate"}],
+    ["addonsManager", "action", "aboutAddons", "",
+     {type: "extension", addonId: id, action: "setAddonUpdate"}],
+    ["addonsManager", "view", "aboutAddons", "list", {type: "extension"}],
+    ["addonsManager", "view", "aboutAddons", "detail",
+     {type: "extension", addonId: id}],
+    ["addonsManager", "action", "aboutAddons", "default",
+     {type: "extension", addonId: id, action: "setAddonUpdate"}],
+    ["addonsManager", "action", "aboutAddons", "enabled",
+     {type: "extension", addonId: id, action: "setAddonUpdate"}],
+  ]);
 });
 
 async function setupExtensionWithUpdate(id, {releaseNotes} = {}) {
   await SpecialPowers.pushPrefEnv({
     set: [["extensions.checkUpdateSecurity", false]],
   });
 
   let server = AddonTestUtils.createHttpServer();
@@ -259,16 +278,17 @@ add_task(async function testUpdateAvaila
   // Check for updates again, there shouldn't be an update.
   await checkForUpdate(card, "no-update");
 
   await closeView(win);
   await extension.unload();
 });
 
 add_task(async function testReleaseNotesLoad() {
+  Services.telemetry.clearEvents();
   let id = "update-with-notes@mochi.test";
   let extension = await setupExtensionWithUpdate(id, {
     releaseNotes: `
       <html xmlns="http://www.w3.org/1999/xhtml">
         <head><link rel="stylesheet" href="remove-me.css"/></head>
         <body>
           <script src="no-scripts.js"></script>
           <h1>My release notes</h1>
@@ -351,16 +371,30 @@ add_task(async function testReleaseNotes
   info("Install the update to clean it up");
   await installUpdate(card, "update-installed");
 
   // There's no more update but release notes are still shown.
   assertUpdateState({card, shown: false, releaseNotes: true});
 
   await closeView(win);
   await extension.unload();
+
+  assertAboutAddonsTelemetryEvents([
+    ["addonsManager", "view", "aboutAddons", "list", {type: "extension"}],
+    ["addonsManager", "view", "aboutAddons", "detail",
+     {type: "extension", addonId: id}],
+    ["addonsManager", "action", "aboutAddons", "",
+     {type: "extension", addonId: id, action: "setAddonUpdate"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "extension", addonId: id, action: "checkForUpdate"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "extension", addonId: id, action: "releaseNotes"}],
+    ["addonsManager", "action", "aboutAddons", null,
+     {type: "extension", addonId: id, action: "releaseNotes"}],
+  ]);
 });
 
 add_task(async function testReleaseNotesError() {
   let id = "update-with-notes-error@mochi.test";
   let extension = await setupExtensionWithUpdate(id, {releaseNotes: "ERROR"});
 
   let win = await loadInitialView("extension");
   let doc = win.document;
--- a/toolkit/mozapps/extensions/test/browser/browser_interaction_telemetry.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_interaction_telemetry.js
@@ -1,36 +1,33 @@
+/**
+ * These tests run in both the XUL and HTML version of about:addons. When adding
+ * a new test it should be defined as a function that accepts a boolean isHtmlViews
+ * and added to the testFns array.
+ */
+
 const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", {});
 
 AddonTestUtils.initMochitest(this);
 
 let gManagerWindow;
 let gCategoryUtilities;
 
 const TELEMETRY_METHODS = ["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);
-}
+add_task(function setupPromptService() {
+  let promptService = mockPromptService();
+  promptService._response = 0; // Accept dialogs.
+});
 
 async function installTheme() {
   let id = "theme@mochi.test";
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       applications: {gecko: {id}},
       manifest_version: 2,
       name: "atheme",
@@ -66,187 +63,283 @@ async function installExtension(manifest
       "action.html": "<h1>do something</h1>",
     },
     useAddonManager: "permanent",
   });
   await extension.startup();
   return extension;
 }
 
-add_task(function clearInitialTelemetry() {
+function getAddonCard(doc, id) {
+  if (doc.ownerGlobal != gManagerWindow) {
+    return doc.querySelector(`addon-card[addon-id="${id}"]`);
+  }
+  return doc.querySelector(`.addon[value="${id}"]`);
+}
+
+function openDetailView(doc, id) {
+  if (doc.ownerGlobal != gManagerWindow) {
+    let card = getAddonCard(doc, id);
+    card.querySelector('[action="expand"]').click();
+  } else {
+    getAddonCard(doc, id).click();
+  }
+}
+
+async function enableAndDisable(doc, row) {
+  if (doc.ownerGlobal != gManagerWindow) {
+    let toggleButton = row.querySelector('[action="toggle-disabled"]');
+    let disabled = BrowserTestUtils.waitForEvent(row, "update");
+    toggleButton.click();
+    await disabled;
+    let enabled = BrowserTestUtils.waitForEvent(row, "update");
+    toggleButton.click();
+    await enabled;
+  } else {
+    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");
+  }
+}
+
+async function removeAddonAndUndo(doc, row) {
+  let isHtml = doc.ownerGlobal != gManagerWindow;
+  let id = isHtml ? row.addon.id : row.mAddon.id;
+  let started = AddonTestUtils.promiseWebExtensionStartup(id);
+  if (isHtml) {
+    let removed = BrowserTestUtils.waitForEvent(row, "remove");
+    row.querySelector('[action="remove"]').click();
+    await removed;
+
+    let undoBanner = doc.querySelector(`message-bar[addon-id="${row.addon.id}"]`);
+    undoBanner.querySelector('[action="undo"]').click();
+    await TestUtils.waitForCondition(() => getAddonCard(doc, row.addon.id));
+  } else {
+    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");
+
+    doc.getAnonymousElementByAttribute(row, "anonid", "undo-btn").click();
+    await TestUtils.waitForCondition(() => !row.hasAttribute("pending"), "Wait for undo");
+  }
+  await started;
+}
+
+async function openPrefs(doc, row) {
+  if (doc.ownerGlobal != gManagerWindow) {
+    row.querySelector('[action="preferences"]').click();
+  } else {
+    let prefsButton;
+    await TestUtils.waitForCondition(() => {
+      prefsButton = doc.getAnonymousElementByAttribute(row, "anonid", "preferences-btn");
+      return prefsButton;
+    });
+    prefsButton.click();
+  }
+}
+
+function changeAutoUpdates(doc) {
+  if (doc.ownerGlobal != gManagerWindow) {
+    let row = doc.querySelector(".addon-detail-row-updates");
+    let checked = row.querySelector(":checked");
+    is(checked.value, "1", "Use default is selected");
+    row.querySelector('[value="0"]').click();
+    row.querySelector('[action="update-check"]').click();
+    row.querySelector('[value="2"]').click();
+    row.querySelector('[value="1"]').click();
+  } else {
+    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();
+  }
+}
+
+function clickLinks(doc) {
+  if (doc.ownerGlobal != gManagerWindow) {
+    let links = ["author", "homepage", "rating"];
+    for (let linkType of links) {
+      doc.querySelector(`.addon-detail-row-${linkType} a`).click();
+    }
+  } else {
+    // 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([]);
+    link.click();
+    doc.getElementById("detail-homepage").click();
+    doc.getElementById("detail-reviews").click();
+  }
+}
+
+async function init(startPage, isHtmlViews) {
+  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();
+  }
+
+  await gCategoryUtilities.openType(startPage);
+
+  if (isHtmlViews) {
+    return gManagerWindow.document.getElementById("html-view-browser").contentDocument;
+  }
+  return gManagerWindow.document;
+}
+
+/* Test functions start here. */
+
+async function setup(isHtmlViews) {
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.htmlaboutaddons.enabled", isHtmlViews]],
+  });
   // Clear out any telemetry data that existed before this file is run.
   Services.telemetry.clearEvents();
-});
+}
 
-add_task(async function testBasicViewTelemetry() {
+async function testBasicViewTelemetry(isHtmlViews) {
   let addons = await Promise.all([
     installTheme(),
     installExtension(),
   ]);
-  await init("discover");
-
-  let doc = gManagerWindow.document;
+  let doc = await init("discover", isHtmlViews);
 
   await gCategoryUtilities.openType("theme");
-  doc.querySelector('.addon[value="theme@mochi.test"]').click();
+  openDetailView(doc, "theme@mochi.test");
   await wait_for_view_load(gManagerWindow);
 
   await gCategoryUtilities.openType("extension");
-  doc.querySelector('.addon[value="extension@mochi.test"]').click();
+  openDetailView(doc, "extension@mochi.test");
   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"}],
   ], {filterMethods: ["view"]});
 
   await close_manager(gManagerWindow);
   await Promise.all(addons.map(addon => addon.unload()));
-});
+}
 
-add_task(async function testExtensionEvents() {
+async function testExtensionEvents(isHtmlViews) {
   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}"]`);
+  let doc = await init("extension", isHtmlViews);
 
   // Check/clear the current telemetry.
   assertTelemetryMatches([["view", "aboutAddons", "list", {type: "extension"}]],
-                         {filterMethods: ["view"]});
+                          {filterMethods: ["view"]});
+
+  let row = getAddonCard(doc, addonId);
 
   // 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");
+  await enableAndDisable(doc, row);
   assertTelemetryMatches([
     ["action", "aboutAddons", null, {action: "disable", addonId, type, view: "list"}],
     ["action", "aboutAddons", null, {action: "enable", addonId, type, view: "list"}],
   ], {filterMethods: ["action"]});
 
   // 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");
+  await removeAddonAndUndo(doc, row);
+  let uninstallValue = isHtmlViews ? "accepted" : null;
   assertTelemetryMatches([
-    ["action", "aboutAddons", null, {action: "uninstall", addonId, type, view: "list"}],
+    ["action", "aboutAddons", uninstallValue, {action: "uninstall", addonId, type, view: "list"}],
     ["action", "aboutAddons", null, {action: "undo", addonId, type, view: "list"}],
   ], {filterMethods: ["action"]});
 
   // 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();
+  // Find the row again since it was modified on uninstall.
+  row = getAddonCard(doc, addonId);
+  await openPrefs(doc, row);
   BrowserTestUtils.removeTab(await waitForNewTab);
   assertTelemetryMatches([
     ["action", "aboutAddons", "external", {action: "preferences", type, addonId, view: "list"}],
   ], {filterMethods: ["action"]});
 
   // Go to the detail view.
-  row.click();
+  openDetailView(doc, addonId);
   await wait_for_view_load(gManagerWindow);
   assertTelemetryMatches([
     ["view", "aboutAddons", "detail", {type, addonId}],
   ], {filterMethods: ["view"]});
 
   // 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();
+  changeAutoUpdates(doc);
   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"}],
   ], {filterMethods: ["action"]});
 
-  // 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();
+  clickLinks(doc);
 
   // The support button will open a new tab.
   waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
-  doc.getElementById("helpButton").click();
+  gManagerWindow.document.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();
+  row = getAddonCard(doc, addonId);
+  await openPrefs(doc, row);
   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"}],
   ], {filterMethods: ["action", "link"]});
 
   // 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();
+  row = getAddonCard(doc, addonId);
+  await openPrefs(doc, row);
   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"}],
   ], {filterMethods: ["action", "view"]});
 
   await close_manager(gManagerWindow);
   await addon.unload();
   await upgraded.unload();
-});
+}
 
-add_task(async function testGeneralActions() {
-  await init("extension");
+async function testGeneralActions(isHtmlViews) {
+  await init("extension", isHtmlViews);
 
   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");
@@ -293,22 +386,22 @@ add_task(async function testGeneralActio
     ["action", "aboutAddons", null, {action: "checkForUpdates", view: "shortcuts"}],
     ["link", "aboutAddons", "about:debugging", {view: "shortcuts"}],
     ["link", "aboutAddons", "search", {view: "shortcuts", type: "shortcuts"}],
   ], {filterMethods: TELEMETRY_METHODS});
 
   await close_manager(gManagerWindow);
 
   assertTelemetryMatches([]);
-});
+}
 
-add_task(async function testPreferencesLink() {
+async function testPreferencesLink(isHtmlViews) {
   assertTelemetryMatches([]);
 
-  await init("theme");
+  await init("theme", isHtmlViews);
 
   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");
@@ -324,9 +417,36 @@ add_task(async function testPreferencesL
 
   assertTelemetryMatches([
     ["view", "aboutAddons", "list", {type: "theme"}],
     ["link", "aboutAddons", "about:preferences", {view: "list"}],
     ["link", "aboutPreferences", "about:addons"],
   ], {filterMethods: ["link", "view"]});
 
   await close_manager(gManagerWindow);
-});
+}
+
+const testFns = [
+  testBasicViewTelemetry,
+  testExtensionEvents,
+  testGeneralActions,
+  testPreferencesLink,
+];
+
+/**
+ * Setup the tasks. This will add tasks for each of testFns to run with the
+ * XUL and HTML version of about:addons.
+ *
+ * To add a test, add it to the testFns array.
+ */
+function addTestTasks(isHtmlViews) {
+  add_task(() => setup(isHtmlViews));
+
+  for (let fn of testFns) {
+    let localTestFnName = fn.name + (isHtmlViews ? "HTML" : "XUL");
+    // Get an informative name for the function in stack traces.
+    let obj = {[localTestFnName]: () => fn(isHtmlViews)};
+    add_task(obj[localTestFnName]);
+  }
+}
+
+addTestTasks(false);
+addTestTasks(true);
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 /* globals end_test */
 
 /* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */
 
 var {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+const {TelemetryTestUtils} = ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm");
 
 var tmp = {};
 ChromeUtils.import("resource://gre/modules/AddonManager.jsm", tmp);
 ChromeUtils.import("resource://gre/modules/Log.jsm", tmp);
 var AddonManager = tmp.AddonManager;
 var AddonManagerPrivate = tmp.AddonManagerPrivate;
 var Log = tmp.Log;
 
@@ -1448,16 +1449,26 @@ function waitAppMenuNotificationShown(id
     PanelUI.notificationPanel.addEventListener("popupshown", popupshown);
   });
 }
 
 function acceptAppMenuNotificationWhenShown(id, addonId) {
   return waitAppMenuNotificationShown(id, addonId, true);
 }
 
+const ABOUT_ADDONS_METHODS = new Set(["action", "view", "link"]);
+function assertAboutAddonsTelemetryEvents(events, filters = {}) {
+  TelemetryTestUtils.assertEvents(events, {
+    category: "addonsManager",
+    method: (actual) => filters.methods ?
+      filters.methods.includes(actual) : ABOUT_ADDONS_METHODS.has(actual),
+    object: "aboutAddons",
+  });
+}
+
 function assertTelemetryMatches(events, {filterMethods} = {}) {
   let snapshot = Services.telemetry.snapshotEvents(
     Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, true);
 
   if (events.length == 0) {
     ok(!snapshot.parent || snapshot.parent.length == 0, "There are no telemetry events");
     return;
   }
@@ -1472,19 +1483,21 @@ function assertTelemetryMatches(events, 
   }).map(relatedEvent => relatedEvent.slice(2, 6));
 
   // Events are now [method, object, value, extra] as expected.
   Assert.deepEqual(relatedEvents, events, "The events are recorded correctly");
 }
 
 /* HTML view helpers */
 async function loadInitialView(type) {
+  // Force the first page load to be the view we want.
+  let viewId = type == "discover" ? "discover/" : `list/${type}`;
+  Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, `addons://${viewId}`);
+
   let managerWindow = await open_manager(null);
-  let categoryUtilities = new CategoryUtilities(managerWindow);
-  await categoryUtilities.openType(type);
 
   let browser = managerWindow.document.getElementById("html-view-browser");
   let win = browser.contentWindow;
   win.managerWindow = managerWindow;
   return win;
 }
 
 function waitForViewLoad(win) {