Bug 1484255 - Add Telemetry Events for the certificate error pages. r=nhnt11,keeler
☠☠ backed out by 111455f16f4b ☠ ☠
authorJohann Hofmann <jhofmann@mozilla.com>
Mon, 15 Oct 2018 23:00:08 +0000
changeset 489704 1119f9458b5d76cef62577b526c508f7908f4710
parent 489703 5622861eb245cc20b3a121ffb73f1b58c9745934
child 489705 2fd7d3ae070e853281fa49408996ecc9308990a9
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersnhnt11, keeler
bugs1484255
milestone64.0a1
Bug 1484255 - Add Telemetry Events for the certificate error pages. r=nhnt11,keeler Differential Revision: https://phabricator.services.mozilla.com/D8281
browser/actors/NetErrorChild.jsm
browser/app/profile/firefox.js
browser/base/content/aboutNetError-new.xhtml
browser/base/content/aboutNetError.xhtml
browser/base/content/browser.js
browser/base/content/test/about/browser.ini
browser/base/content/test/about/browser_aboutCertError.js
browser/base/content/test/about/browser_aboutCertError_telemetry.js
browser/base/content/test/about/head.js
browser/components/nsBrowserGlue.js
docshell/base/nsDocShell.cpp
security/manager/ssl/nsISecurityUITelemetry.idl
toolkit/components/telemetry/Events.yaml
--- a/browser/actors/NetErrorChild.jsm
+++ b/browser/actors/NetErrorChild.jsm
@@ -9,16 +9,18 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
 
 ChromeUtils.defineModuleGetter(this, "BrowserUtils",
                                "resource://gre/modules/BrowserUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "WebNavigationFrames",
                                "resource://gre/modules/WebNavigationFrames.jsm");
 
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
+
 XPCOMUtils.defineLazyGetter(this, "gPipNSSBundle", function() {
   return Services.strings.createBundle("chrome://pipnss/locale/pipnss.properties");
 });
 XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
   return Services.strings.createBundle("chrome://branding/locale/brand.properties");
 });
 XPCOMUtils.defineLazyPreferenceGetter(this, "newErrorPagesEnabled",
   "browser.security.newcerterrorpage.enabled");
@@ -317,16 +319,17 @@ class NetErrorChild extends ActorChild {
     technicalInfo.append("\n");
 
     // Add link to certificate and error message.
     let linkPrefix = gPipNSSBundle.GetStringFromName("certErrorCodePrefix3");
     let detailLink = doc.createElement("a");
     detailLink.append(input.data.codeString);
     detailLink.title = input.data.codeString;
     detailLink.id = "errorCode";
+    detailLink.dataset.telemetryId = "error_code_link";
     let fragment = BrowserUtils.getLocalizedFragment(doc, linkPrefix, detailLink);
     technicalInfo.appendChild(fragment);
     var errorCode = doc.getElementById("errorCode");
     if (errorCode) {
       errorCode.href = "javascript:void(0)";
       errorCode.addEventListener("click", () => {
         let debugInfo = doc.getElementById("certificateErrorDebugInformation");
         debugInfo.style.display = "block";
@@ -348,16 +351,22 @@ class NetErrorChild extends ActorChild {
     this._setTechDetails(msg, doc);
     let learnMoreLink = doc.getElementById("learnMoreLink");
     let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
     let errWhatToDo = doc.getElementById("es_nssBadCert_" + msg.data.codeString);
     let es = doc.getElementById("errorWhatToDoText");
     let errWhatToDoTitle = doc.getElementById("edd_nssBadCert");
     let est = doc.getElementById("errorWhatToDoTitleText");
 
+    doc.body.setAttribute("code", msg.data.codeString);
+
+    // Need to do this here (which is not exactly at load but a few ticks later),
+    // because this is the first time we have access to the error code.
+    this.recordLoadEvent(doc);
+
     switch (msg.data.code) {
       case SSL_ERROR_BAD_CERT_DOMAIN:
       case SEC_ERROR_OCSP_INVALID_SIGNING_CERT:
       case SEC_ERROR_UNKNOWN_ISSUER:
         if (!newErrorPagesEnabled) {
           break;
         }
         if (es) {
@@ -525,21 +534,23 @@ class NetErrorChild extends ActorChild {
       this.onSetAutomatic(aEvent);
       break;
     case "AboutNetErrorResetPreferences":
       this.onResetPreferences(aEvent);
       break;
     case "click":
       if (aEvent.button == 0) {
         if (this.isAboutCertError(doc)) {
+          this.recordClick(aEvent.originalTarget);
           this.onCertError(aEvent.originalTarget, doc.defaultView);
         } else {
           this.onClick(aEvent);
         }
       }
+      break;
     }
   }
 
   receiveMessage(msg) {
     if (msg.name == "CertErrorDetails") {
       let frameDocShell = WebNavigationFrames.findDocShell(msg.data.frameId, this.docShell);
       // We need nsIWebNavigation to access docShell.document.
       frameDocShell && frameDocShell.QueryInterface(Ci.nsIWebNavigation);
@@ -691,16 +702,48 @@ class NetErrorChild extends ActorChild {
       frameId: WebNavigationFrames.getFrameId(win),
       location: win.document.location.href,
       elementId: target.getAttribute("id"),
       isTopFrame: (win.parent === win),
       securityInfoAsString: getSerializedSecurityInfo(win.docShell),
     });
   }
 
+  getCSSClass(doc) {
+    let searchParams = new URL(doc.documentURI).searchParams;
+    return searchParams.get("s");
+  }
+
+  recordLoadEvent(doc) {
+    let cssClass = this.getCSSClass(doc);
+    // Telemetry values for events are max. 80 bytes.
+    let errorCode = doc.body.getAttribute("code").substring(0, 40);
+    Services.telemetry.recordEvent("security.ui.certerror", "load", "aboutcerterror", errorCode, {
+      "has_sts": (cssClass == "badStsCert").toString(),
+      "is_frame": (doc.ownerGlobal.parent != doc.ownerGlobal).toString(),
+    });
+  }
+
+  recordClick(element) {
+    let telemetryId = element.dataset.telemetryId;
+    if (!telemetryId) {
+      return;
+    }
+    let doc = element.ownerDocument;
+    let cssClass = this.getCSSClass(doc);
+    // Telemetry values for events are max. 80 bytes.
+    let errorCode = doc.body.getAttribute("code").substring(0, 40);
+    let panel = doc.getElementById("badCertAdvancedPanel");
+    Services.telemetry.recordEvent("security.ui.certerror", "click", telemetryId, errorCode, {
+      "panel_open": (panel.style.display == "none").toString(),
+      "has_sts": (cssClass == "badStsCert").toString(),
+      "is_frame": (doc.ownerGlobal.parent != doc.ownerGlobal).toString(),
+    });
+  }
+
   onClick(event) {
     let {documentURI} = event.target.ownerDocument;
 
     let elmId = event.originalTarget.getAttribute("id");
     if (elmId == "returnButton") {
       this.mm.sendAsyncMessage("Browser:SSLErrorGoBack", {});
       return;
     }
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -952,16 +952,18 @@ pref("security.alternate_certificate_err
 
 // Enable the new certificate error page only for Nightly
 #if defined(NIGHTLY_BUILD)
 pref("browser.security.newcerterrorpage.enabled", true);
 #else
 pref("browser.security.newcerterrorpage.enabled", false);
 #endif
 
+pref("security.certerrors.recordEventTelemetry", true);
+
 // Whether to start the private browsing mode at application startup
 pref("browser.privatebrowsing.autostart", false);
 
 // Whether the bookmark panel should be shown when bookmarking a page.
 pref("browser.bookmarks.editDialog.showForNewBookmarks", true);
 
 // Don't try to alter this pref, it'll be reset the next time you use the
 // bookmarking dialog
--- a/browser/base/content/aboutNetError-new.xhtml
+++ b/browser/base/content/aboutNetError-new.xhtml
@@ -159,61 +159,61 @@
           <div id="wrongSystemTimeWithoutReferencePanel">
             &certerror.wrongSystemTimeWithoutReference;
           </div>
 
           <!-- Long Description (Note: See netError.dtd for used XHTML tags) -->
           <div id="errorLongDesc" />
 
           <div id="learnMoreContainer">
-            <p><a href="https://support.mozilla.org/kb/what-does-your-connection-is-not-secure-mean" id="learnMoreLink" target="new">&errorReporting.learnMore;</a></p>
+            <p><a href="https://support.mozilla.org/kb/what-does-your-connection-is-not-secure-mean" id="learnMoreLink" target="new" data-telemetry-id="learn_more_link">&errorReporting.learnMore;</a></p>
           </div>
         </div>
 
         <!-- UI for option to report certificate errors to Mozilla. Removed on
              init for other error types .-->
         <div id="prefChangeContainer" class="button-container">
           <p>&prefReset.longDesc;</p>
           <button id="prefResetButton" class="primary" autocomplete="off">&prefReset.label;</button>
         </div>
 
         <div id="certErrorAndCaptivePortalButtonContainer" class="button-container">
-          <button id="returnButton" class="primary" autocomplete="off">&returnToPreviousPage1.label;</button>
+          <button id="returnButton" class="primary" autocomplete="off" data-telemetry-id="return_button_top">&returnToPreviousPage1.label;</button>
           <button id="openPortalLoginPageButton" class="primary" autocomplete="off">&openPortalLoginPage.label2;</button>
           <button id="errorTryAgain" class="primary" autocomplete="off">&retry.label;</button>
-          <button id="advancedButton" autocomplete="off">&continue2.label;</button>
+          <button id="advancedButton" data-telemetry-id="advanced_button" autocomplete="off">&continue2.label;</button>
           <button id="moreInformationButton" autocomplete="off">&moreInformation.label;</button>
         </div>
       </div>
 
       <div id="netErrorButtonContainer" class="button-container">
         <button id="errorTryAgain" class="primary" autocomplete="off">&retry.label;</button>
       </div>
 
       <div id="advancedPanelContainer">
         <div id="badCertAdvancedPanel" class="advanced-panel">
           <p id="badCertTechnicalInfo"/>
           <div id="advancedPanelButtonContainer" class="button-container">
-            <button id="advancedPanelReturnButton" class="primary" autocomplete="off">&returnToPreviousPage1.label;</button>
+            <button id="advancedPanelReturnButton" class="primary" autocomplete="off" data-telemetry-id="return_button_adv">&returnToPreviousPage1.label;</button>
             <button id="advancedPanelErrorTryAgain" class="primary" autocomplete="off">&retry.label;</button>
             <div class="exceptionDialogButtonContainer">
-              <button id="exceptionDialogButton">&securityOverride.exceptionButton1Label;</button>
+              <button id="exceptionDialogButton" data-telemetry-id="exception_button">&securityOverride.exceptionButton1Label;</button>
             </div>
           </div>
         </div>
 
         <div id="certificateErrorReporting">
             <p class="toggle-container-with-text">
-                <input type="checkbox" id="automaticallyReportInFuture" role="checkbox" />
+                <input type="checkbox" id="automaticallyReportInFuture" role="checkbox" data-telemetry-id="auto_report_cb"/>
                 <label for="automaticallyReportInFuture" id="automaticallyReportInFuture">&errorReporting.automatic2;</label>
             </p>
         </div>
 
         <div id="certificateErrorDebugInformation">
-          <button id="copyToClipboard">&certerror.copyToClipboard.label;</button>
+          <button id="copyToClipboard" data-telemetry-id="clipboard_button_top">&certerror.copyToClipboard.label;</button>
           <div id="certificateErrorText"/>
-          <button id="copyToClipboard">&certerror.copyToClipboard.label;</button>
+          <button id="copyToClipboard" data-telemetry-id="clipboard_button_bot">&certerror.copyToClipboard.label;</button>
         </div>
       </div>
     </div>
   </body>
   <script type="application/javascript" src="chrome://browser/content/aboutNetError.js"/>
 </html>
--- a/browser/base/content/aboutNetError.xhtml
+++ b/browser/base/content/aboutNetError.xhtml
@@ -128,59 +128,59 @@
           <div id="wrongSystemTimeWithoutReferencePanel">
             &certerror.wrongSystemTimeWithoutReference;
           </div>
 
           <!-- Long Description (Note: See netError.dtd for used XHTML tags) -->
           <div id="errorLongDesc" />
 
           <div id="learnMoreContainer">
-            <p><a href="https://support.mozilla.org/kb/what-does-your-connection-is-not-secure-mean" id="learnMoreLink" target="new">&errorReporting.learnMore;</a></p>
+            <p><a href="https://support.mozilla.org/kb/what-does-your-connection-is-not-secure-mean" id="learnMoreLink" target="new" data-telemetry-id="learn_more_link">&errorReporting.learnMore;</a></p>
           </div>
         </div>
 
         <!-- UI for option to report certificate errors to Mozilla. Removed on
              init for other error types .-->
         <div id="certificateErrorReporting">
           <p class="toggle-container-with-text">
-            <input type="checkbox" id="automaticallyReportInFuture" role="checkbox" />
+            <input type="checkbox" id="automaticallyReportInFuture" role="checkbox"  data-telemetry-id="auto_report_cb"/>
             <label for="automaticallyReportInFuture" id="automaticallyReportInFuture">&errorReporting.automatic2;</label>
           </p>
         </div>
 
         <div id="prefChangeContainer" class="button-container">
           <p>&prefReset.longDesc;</p>
           <button id="prefResetButton" class="primary" autocomplete="off">&prefReset.label;</button>
         </div>
 
         <div id="certErrorAndCaptivePortalButtonContainer" class="button-container">
-          <button id="returnButton" class="primary" autocomplete="off">&returnToPreviousPage.label;</button>
+          <button id="returnButton" class="primary" autocomplete="off" data-telemetry-id="return_button_top">&returnToPreviousPage.label;</button>
           <button id="openPortalLoginPageButton" class="primary" autocomplete="off">&openPortalLoginPage.label2;</button>
-          <button id="advancedButton" autocomplete="off">&advanced.label;</button>
+          <button id="advancedButton" data-telemetry-id="advanced_button" autocomplete="off">&advanced.label;</button>
           <button id="moreInformationButton" autocomplete="off">&moreInformation.label;</button>
         </div>
       </div>
 
       <div id="netErrorButtonContainer" class="button-container">
         <button id="errorTryAgain" class="primary" autocomplete="off">&retry.label;</button>
       </div>
 
       <div id="advancedPanelContainer">
         <div id="badCertAdvancedPanel" class="advanced-panel">
           <p id="badCertTechnicalInfo"/>
           <div id="advancedPanelButtonContainer" class="button-container">
             <button id="advancedPanelErrorTryAgain" class="primary" autocomplete="off">&retry.label;</button>
           </div>
           <div class="exceptionDialogButtonContainer">
-            <button id="exceptionDialogButton">&securityOverride.exceptionButtonLabel;</button>
+            <button id="exceptionDialogButton" data-telemetry-id="exception_button">&securityOverride.exceptionButtonLabel;</button>
           </div>
         </div>
 
         <div id="certificateErrorDebugInformation">
-          <button id="copyToClipboard">&certerror.copyToClipboard.label;</button>
+          <button id="copyToClipboard" data-telemetry-id="clipboard_button_top">&certerror.copyToClipboard.label;</button>
           <div id="certificateErrorText"/>
-          <button id="copyToClipboard">&certerror.copyToClipboard.label;</button>
+          <button id="copyToClipboard" data-telemetry-id="clipboard_button_bot">&certerror.copyToClipboard.label;</button>
         </div>
       </div>
     </div>
   </body>
   <script type="application/javascript" src="chrome://browser/content/aboutNetError.js"/>
 </html>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3036,25 +3036,20 @@ var BrowserOnClick = {
       break;
       case "Browser:SSLErrorGoBack":
         goBackFromErrorPage();
       break;
     }
   },
 
   onCertError(browser, elementId, isTopFrame, location, securityInfoAsString, frameId) {
-    let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
     let securityInfo;
 
     switch (elementId) {
       case "exceptionDialogButton":
-        if (isTopFrame) {
-          secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_CLICK_ADD_EXCEPTION);
-        }
-
         securityInfo = getSecurityInfo(securityInfoAsString);
         let params = { exceptionAdded: false,
                        securityInfo };
         if (Services.prefs.getBoolPref("browser.security.newcerterrorpage.enabled", false)) {
           let overrideService = Cc["@mozilla.org/security/certoverride;1"]
                                   .getService(Ci.nsICertOverrideService);
           let flags = 0;
           if (securityInfo.isUntrusted) {
@@ -3093,32 +3088,25 @@ var BrowserOnClick = {
 
         // If the user added the exception cert, attempt to reload the page
         if (params.exceptionAdded) {
           browser.reload();
         }
         break;
 
       case "returnButton":
-        if (isTopFrame) {
-          secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_GET_ME_OUT_OF_HERE);
-        }
         goBackFromErrorPage();
         break;
 
       case "advancedPanelReturnButton":
         goBackFromErrorPage();
         break;
 
       case "advancedButton":
       case "moreInformationButton":
-        if (isTopFrame) {
-          secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_UNDERSTAND_RISKS);
-        }
-
         securityInfo = getSecurityInfo(securityInfoAsString);
         let errorInfo = getDetailedCertErrorInfo(location,
                                                  securityInfo);
         let validityInfo = {
           notAfter: securityInfo.serverCert.validity.notAfter,
           notBefore: securityInfo.serverCert.validity.notBefore,
           notAfterLocalTime: securityInfo.serverCert.validity.notAfterLocalTime,
           notBeforeLocalTime: securityInfo.serverCert.validity.notBeforeLocalTime,
--- a/browser/base/content/test/about/browser.ini
+++ b/browser/base/content/test/about/browser.ini
@@ -1,19 +1,19 @@
 [DEFAULT]
 support-files =
   head.js
   print_postdata.sjs
   searchSuggestionEngine.sjs
   searchSuggestionEngine.xml
   POSTSearchEngine.xml
+  dummy_page.html
 
 [browser_aboutCertError.js]
-support-files =
-  dummy_page.html
+[browser_aboutCertError_telemetry.js]
 [browser_aboutHome_search_POST.js]
 [browser_aboutHome_search_composing.js]
 [browser_aboutHome_search_searchbar.js]
 [browser_aboutHome_search_suggestion.js]
 skip-if = os == "mac" || (os == "linux" && (!debug || bits == 64)) # Bug 1399648, bug 1402502
 [browser_aboutHome_search_telemetry.js]
 [browser_aboutNetError.js]
 [browser_aboutStopReload.js]
--- a/browser/base/content/test/about/browser_aboutCertError.js
+++ b/browser/base/content/test/about/browser_aboutCertError.js
@@ -2,57 +2,21 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // This is testing the aboutCertError page (Bug 1207107).
 
 const GOOD_PAGE = "https://example.com/";
 const GOOD_PAGE_2 = "https://example.org/";
-const DUMMY_PAGE = getRootDirectory(gTestPath).replace("chrome://mochitests/content", GOOD_PAGE) + "dummy_page.html";
 const BAD_CERT = "https://expired.example.com/";
 const UNKNOWN_ISSUER = "https://self-signed.example.com ";
 const BAD_STS_CERT = "https://badchain.include-subdomains.pinning.example.com:443";
 const {TabStateFlusher} = ChromeUtils.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
 
-function injectErrorPageFrame(tab, src) {
-  return ContentTask.spawn(tab.linkedBrowser, {frameSrc: src}, async function({frameSrc}) {
-    let loaded = ContentTaskUtils.waitForEvent(content.wrappedJSObject, "DOMFrameContentLoaded");
-    let iframe = content.document.createElement("iframe");
-    iframe.src = frameSrc;
-    content.document.body.appendChild(iframe);
-    await loaded;
-    // We will have race conditions when accessing the frame content after setting a src,
-    // so we can't wait for AboutNetErrorLoad. Let's wait for the certerror class to
-    // appear instead (which should happen at the same time as AboutNetErrorLoad).
-    await ContentTaskUtils.waitForCondition(() =>
-      iframe.contentDocument.body.classList.contains("certerror"));
-  });
-}
-
-async function openErrorPage(src, useFrame) {
-  let tab;
-  if (useFrame) {
-    info("Loading cert error page in an iframe");
-    tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, DUMMY_PAGE);
-    await injectErrorPageFrame(tab, src);
-  } else {
-    let certErrorLoaded;
-    tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
-      gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, src);
-      let browser = gBrowser.selectedBrowser;
-      certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
-    }, false);
-    info("Loading and waiting for the cert error");
-    await certErrorLoaded;
-  }
-
-  return tab;
-}
-
 add_task(async function checkReturnToAboutHome() {
   info("Loading a bad cert page directly and making sure 'return to previous page' goes to about:home");
   for (let useFrame of [false, true]) {
     let tab = await openErrorPage(BAD_CERT, useFrame);
     let browser = tab.linkedBrowser;
 
     is(browser.webNavigation.canGoBack, false, "!webNavigation.canGoBack");
     is(browser.webNavigation.canGoForward, false, "!webNavigation.canGoForward");
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutCertError_telemetry.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const BAD_CERT = "https://expired.example.com/";
+const BAD_STS_CERT = "https://badchain.include-subdomains.pinning.example.com:443";
+
+add_task(async function checkTelemetryClickEvents() {
+  info("Loading a bad cert page and verifying telemetry click events arrive.");
+
+  let oldCanRecord = Services.telemetry.canRecordExtended;
+  Services.telemetry.canRecordExtended = true;
+
+  registerCleanupFunction(() => {
+    Services.telemetry.canRecordExtended = oldCanRecord;
+  });
+
+  // For obvious reasons event telemetry seems to sync with the content
+  // processes asynchronously, so we need to wait for the main process
+  // to catch up through the entire test.
+  Services.telemetry.clearEvents();
+  await TestUtils.waitForCondition(() => {
+    let events = Services.telemetry.snapshotEvents(
+      Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true).content;
+    return !events || !events.length;
+  });
+
+  for (let useFrame of [false, true]) {
+    let recordedObjects = [
+      "advanced_button",
+      "learn_more_link",
+      "auto_report_cb",
+      "error_code_link",
+      "clipboard_button_top",
+      "clipboard_button_bot",
+      "return_button_top",
+      "return_button_adv",
+    ];
+
+    if (!useFrame) {
+      recordedObjects.push("exception_button");
+    }
+
+    for (let object of recordedObjects) {
+      let tab = await openErrorPage(BAD_CERT, useFrame);
+      let browser = tab.linkedBrowser;
+
+      let loadEvents = await TestUtils.waitForCondition(() => {
+        let events = Services.telemetry.snapshotEvents(
+          Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true).content;
+        if (events && events.length) {
+          events = events.filter(e => e[1] == "security.ui.certerror" && e[2] == "load");
+          if (events.length == 1 && events[0][5].is_frame == useFrame.toString()) {
+            return events;
+          }
+        }
+        return null;
+      });
+
+      is(loadEvents.length, 1, `recorded telemetry for the load testing ${object}, useFrame: ${useFrame}`);
+
+      await ContentTask.spawn(browser, {frame: useFrame, objectId: object}, async function({frame, objectId}) {
+        let doc = frame ? content.document.querySelector("iframe").contentDocument : content.document;
+
+        await ContentTaskUtils.waitForCondition(() => doc.body.classList.contains("certerror"), "Wait for certerror to be loaded");
+
+        let domElement = doc.querySelector(`[data-telemetry-id='${objectId}']`);
+        domElement.click();
+      });
+
+      let clickEvents = await TestUtils.waitForCondition(() => {
+        let events = Services.telemetry.snapshotEvents(
+          Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true).content;
+        if (events && events.length) {
+          events = events.filter(e => e[1] == "security.ui.certerror" && e[2] == "click" && e[3] == object);
+          if (events.length == 1 && events[0][5].is_frame == useFrame.toString()) {
+            return events;
+          }
+        }
+        return null;
+      }, "Has captured telemetry events.");
+
+      is(clickEvents.length, 1, `recorded telemetry for the click on ${object}, useFrame: ${useFrame}`);
+
+      // We opened an extra tab for the SUMO page, need to close it.
+      if (object == "learn_more_link") {
+        BrowserTestUtils.removeTab(gBrowser.selectedTab);
+      }
+
+      if (object == "exception_button") {
+        let certOverrideService = Cc["@mozilla.org/security/certoverride;1"]
+                                    .getService(Ci.nsICertOverrideService);
+        certOverrideService.clearValidityOverride("expired.example.com", -1);
+      }
+
+      BrowserTestUtils.removeTab(gBrowser.selectedTab);
+    }
+  }
+});
--- a/browser/base/content/test/about/head.js
+++ b/browser/base/content/test/about/head.js
@@ -1,10 +1,47 @@
 /* eslint-env mozilla/frame-script */
 
+function injectErrorPageFrame(tab, src) {
+  return ContentTask.spawn(tab.linkedBrowser, {frameSrc: src}, async function({frameSrc}) {
+    let loaded = ContentTaskUtils.waitForEvent(content.wrappedJSObject, "DOMFrameContentLoaded");
+    let iframe = content.document.createElement("iframe");
+    iframe.src = frameSrc;
+    content.document.body.appendChild(iframe);
+    await loaded;
+    // We will have race conditions when accessing the frame content after setting a src,
+    // so we can't wait for AboutNetErrorLoad. Let's wait for the certerror class to
+    // appear instead (which should happen at the same time as AboutNetErrorLoad).
+    await ContentTaskUtils.waitForCondition(() =>
+      iframe.contentDocument.body.classList.contains("certerror"));
+  });
+}
+
+async function openErrorPage(src, useFrame) {
+  let dummyPage = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "https://example.com") + "dummy_page.html";
+
+  let tab;
+  if (useFrame) {
+    info("Loading cert error page in an iframe");
+    tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, dummyPage);
+    await injectErrorPageFrame(tab, src);
+  } else {
+    let certErrorLoaded;
+    tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+      gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, src);
+      let browser = gBrowser.selectedBrowser;
+      certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser);
+    }, false);
+    info("Loading and waiting for the cert error");
+    await certErrorLoaded;
+  }
+
+  return tab;
+}
+
 function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
   retryTimes = typeof retryTimes !== "undefined" ? retryTimes : 30;
   var tries = 0;
   var interval = setInterval(function() {
     if (tries >= retryTimes) {
       ok(false, errorMsg);
       moveOn();
     }
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1548,16 +1548,23 @@ BrowserGlue.prototype = {
    * but it will not make it happen later (and out of order) compared
    * to the other ones scheduled together.
    */
   _scheduleStartupIdleTasks() {
     Services.tm.idleDispatchToMainThread(() => {
       ContextualIdentityService.load();
     });
 
+    Services.tm.idleDispatchToMainThread(() => {
+      let enableCertErrorUITelemetry =
+        Services.prefs.getBoolPref("security.certerrors.recordEventTelemetry", false);
+      Services.telemetry.setEventRecordingEnabled("security.ui.certerror",
+        enableCertErrorUITelemetry);
+    });
+
     // Load the Login Manager data from disk off the main thread, some time
     // after startup.  If the data is required before this runs, for example
     // because a restored page contains a password field, it will be loaded on
     // the main thread, and this initialization request will be ignored.
     Services.tm.idleDispatchToMainThread(() => {
       try {
         Services.logins;
       } catch (ex) {
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -4540,37 +4540,24 @@ nsDocShell::DisplayLoadError(nsresult aE
       // HSTS/pinning takes precedence over the expert bad cert pref. We
       // never want to show the "Add Exception" button for these sites.
       // In the future we should differentiate between an HSTS host and a
       // pinned host and display a more informative message to the user.
       if (isStsHost || isPinnedHost) {
         cssClass.AssignLiteral("badStsCert");
       }
 
-      uint32_t bucketId;
-      if (isStsHost) {
-        // measuring STS separately allows us to measure click through
-        // rates easily
-        bucketId = nsISecurityUITelemetry::WARNING_BAD_CERT_TOP_STS;
-      } else {
-        bucketId = nsISecurityUITelemetry::WARNING_BAD_CERT_TOP;
-      }
-
       // See if an alternate cert error page is registered
       nsAutoCString alternateErrorPage;
       nsresult rv =
         Preferences::GetCString("security.alternate_certificate_error_page",
                                 alternateErrorPage);
       if (NS_SUCCEEDED(rv)) {
         errorPage.Assign(alternateErrorPage);
       }
-
-      if (!IsFrame() && errorPage.EqualsIgnoreCase("certerror")) {
-        Telemetry::Accumulate(mozilla::Telemetry::SECURITY_UI, bucketId);
-      }
     } else {
       error = "nssFailure2";
     }
   } else if (NS_ERROR_PHISHING_URI == aError ||
              NS_ERROR_MALWARE_URI == aError ||
              NS_ERROR_UNWANTED_URI == aError ||
              NS_ERROR_HARMFUL_URI == aError) {
     nsAutoCString host;
--- a/security/manager/ssl/nsISecurityUITelemetry.idl
+++ b/security/manager/ssl/nsISecurityUITelemetry.idl
@@ -111,23 +111,23 @@ const uint32_t WARNING_GEOLOCATION_REQUE
 // const uint32_t WARNING_MALWARE_PAGE_FRAME_GET_ME_OUT_OF_HERE = 62;
 // const uint32_t WARNING_MALWARE_PAGE_FRAME_IGNORE_WARNING = 63;
 
 // const uint32_t WARNING_PHISHING_PAGE_FRAME = 64;
 // const uint32_t WARNING_PHISHING_PAGE_FRAME_WHY_BLOCKED = 65;
 // const uint32_t WARNING_PHISHING_PAGE_FRAME_GET_ME_OUT_OF_HERE = 66;
 // const uint32_t WARNING_PHISHING_PAGE_FRAME_IGNORE_WARNING = 67;
 
-const uint32_t WARNING_BAD_CERT_TOP = 68;
-const uint32_t WARNING_BAD_CERT_TOP_STS = 69;
-const uint32_t WARNING_BAD_CERT_TOP_CLICK_ADD_EXCEPTION = 70;
+//const uint32_t WARNING_BAD_CERT_TOP = 68;
+//const uint32_t WARNING_BAD_CERT_TOP_STS = 69;
+//const uint32_t WARNING_BAD_CERT_TOP_CLICK_ADD_EXCEPTION = 70;
 const uint32_t WARNING_BAD_CERT_TOP_CLICK_VIEW_CERT = 71;
 const uint32_t WARNING_BAD_CERT_TOP_DONT_REMEMBER_EXCEPTION = 72;
-const uint32_t WARNING_BAD_CERT_TOP_GET_ME_OUT_OF_HERE = 73;
-const uint32_t WARNING_BAD_CERT_TOP_UNDERSTAND_RISKS = 74;
+//const uint32_t WARNING_BAD_CERT_TOP_GET_ME_OUT_OF_HERE = 73;
+//const uint32_t WARNING_BAD_CERT_TOP_UNDERSTAND_RISKS = 74;
 //     removed WARNING_BAD_CERT_TOP_TECHNICAL_DETAILS = 75;
 
 const uint32_t WARNING_BAD_CERT_TOP_ADD_EXCEPTION_BASE = 76;
 const uint32_t WARNING_BAD_CERT_TOP_ADD_EXCEPTION_FLAG_UNTRUSTED = 1;
 const uint32_t WARNING_BAD_CERT_TOP_ADD_EXCEPTION_FLAG_DOMAIN = 2;
 const uint32_t WARNING_BAD_CERT_TOP_ADD_EXCEPTION_FLAG_TIME = 4;
 
 const uint32_t WARNING_BAD_CERT_TOP_CONFIRM_ADD_EXCEPTION_BASE = 84;
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -547,16 +547,63 @@ devtools.main:
     description: The amount of time a tool was opened for.
     release_channel_collection: opt-out
     expiry_version: never
     extra_keys:
       time_open: Time open.
       os: The OS name and version e.g. "Linux 4.4.0-1014-aws", "Darwin 14.5.0", "Windows_NT 6.1.7601" or "Windows_NT 10.0.15063." This can be used to make sense of data when a feature is only available from a particular operating system build number.
       session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
 
+security.ui.certerror:
+  load:
+    objects: ["aboutcerterror"]
+    bug_numbers:
+      - 1484255
+    description: >
+      The about:certerror page is loaded, keyed by error code, see https://searchfox.org/mozilla-central/source/security/nss/lib/mozpkix/include/pkix/Result.h
+    expiry_version: "70"
+    notification_emails:
+      - jhofmann@mozilla.com
+      - seceng-telemetry@mozilla.com
+    release_channel_collection: opt-in
+    record_in_processes: ["content"]
+    products:
+      - firefox
+    extra_keys:
+      is_frame: If the error page is loaded in an iframe.
+      has_sts: If the error page is for a site with HSTS headers or with a pinned key.
+  click:
+    objects: [
+      "advanced_button",
+      "exception_button",
+      "return_button_top",
+      "return_button_adv",
+      "learn_more_link",
+      "auto_report_cb",
+      "error_code_link",
+      "clipboard_button_top",
+      "clipboard_button_bot",
+    ]
+    bug_numbers:
+      - 1484255
+    description: >
+      User interaction by click events on the cert error page. Keyed by error code, see https://searchfox.org/mozilla-central/source/security/nss/lib/mozpkix/include/pkix/Result.h
+    expiry_version: "70"
+    notification_emails:
+      - jhofmann@mozilla.com
+      - seceng-telemetry@mozilla.com
+    release_channel_collection: opt-in
+    record_in_processes: ["content"]
+    products:
+      - firefox
+    extra_keys:
+      is_frame: If the error page is loaded in an iframe.
+      has_sts: If the error page is for a site with HSTS headers or with a pinned key.
+      panel_open: If the advanced panel was open at the time of the interaction.
+
 security.ui.identitypopup:
   open:
     objects: ["identity_popup"]
     bug_numbers:
       - 1484251
     description: >
       How many times the control center was opened.
       Keyed by the state of the content blocking shield, where the shield-showing key indicates