Bug 1474238 - Add a "report breakage" dialog for Tracking Protection. r=Paolo draft
authorJohann Hofmann <jhofmann@mozilla.com>
Wed, 11 Jul 2018 17:14:35 +0200
changeset 829308 34c534968c82784539aecb25c2ee77f4fb0bbdde
parent 829275 8546719c58dc2310bdafb81fd64a6295c59cd10e
push id118764
push userjhofmann@mozilla.com
push dateWed, 15 Aug 2018 10:58:11 +0000
reviewersPaolo
bugs1474238
milestone63.0a1
Bug 1474238 - Add a "report breakage" dialog for Tracking Protection. r=Paolo MozReview-Commit-ID: 26q5PYLUZGS
browser/app/profile/firefox.js
browser/base/content/browser-contentblocking.js
browser/base/content/test/trackingUI/browser.ini
browser/base/content/test/trackingUI/browser_trackingUI_report_breakage.js
browser/components/controlcenter/content/panel.inc.xul
browser/locales/en-US/chrome/browser/browser.dtd
browser/themes/shared/controlcenter/panel.inc.css
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1488,16 +1488,22 @@ pref("browser.ping-centre.production.end
 pref("media.gmp-provider.enabled", true);
 
 pref("browser.contentblocking.enabled", true);
 #ifdef NIGHTLY_BUILD
 pref("browser.contentblocking.ui.enabled", true);
 #else
 pref("browser.contentblocking.ui.enabled", false);
 #endif
+#ifdef NIGHTLY_BUILD
+pref("browser.contentblocking.reportBreakage.enabled", true);
+#else
+pref("browser.contentblocking.reportBreakage.enabled", false);
+#endif
+pref("browser.contentblocking.reportBreakage.url", "https://tracking-protection-issues.herokuapp.com/new");
 
 pref("privacy.trackingprotection.introCount", 0);
 pref("privacy.trackingprotection.introURL", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/tracking-protection/start/");
 
 // Always enable newtab segregation using containers
 pref("privacy.usercontext.about_newtab_segregation.enabled", true);
 // Enable Contextual Identity Containers
 #ifdef NIGHTLY_BUILD
--- a/browser/base/content/browser-contentblocking.js
+++ b/browser/base/content/browser-contentblocking.js
@@ -127,16 +127,18 @@ var TrackingProtection = {
 
 
 var ContentBlocking = {
   // If the user ignores the doorhanger, we stop showing it after some time.
   MAX_INTROS: 20,
   PREF_ENABLED: "browser.contentblocking.enabled",
   PREF_UI_ENABLED: "browser.contentblocking.ui.enabled",
   PREF_ANIMATIONS_ENABLED: "toolkit.cosmeticAnimations.enabled",
+  PREF_REPORT_BREAKAGE_ENABLED: "browser.contentblocking.reportBreakage.enabled",
+  PREF_REPORT_BREAKAGE_URL: "browser.contentblocking.reportBreakage.url",
   content: null,
   icon: null,
   activeTooltipText: null,
   disabledTooltipText: null,
 
   get appMenuLabel() {
     delete this.appMenuLabel;
     return this.appMenuLabel = document.getElementById("appMenu-tp-label");
@@ -199,16 +201,33 @@ var ContentBlocking = {
   init() {
     let $ = selector => document.querySelector(selector);
     this.content = $("#identity-popup-content-blocking-content");
     this.icon = $("#tracking-protection-icon");
     this.iconBox = $("#tracking-protection-icon-box");
     this.animatedIcon = $("#tracking-protection-icon-animatable-image");
     this.animatedIcon.addEventListener("animationend", () => this.iconBox.removeAttribute("animate"));
 
+    this.identityPopupMultiView = $("#identity-popup-multiView");
+    this.reportBreakageButton = $("#identity-popup-content-blocking-report-breakage");
+    this.reportBreakageURL = $("#identity-popup-breakageReportView-collection-url");
+    this.reportBreakageUA = $("#identity-popup-breakageReportView-collection-userAgent");
+    this.reportBreakageLearnMore = $("#identity-popup-breakageReportView-learn-more");
+
+    let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+    this.reportBreakageLearnMore.href = baseURL + "tracking-protection-pbm";
+
+    this.updateReportBreakageUI = () => {
+      this.reportBreakageButton.hidden = !Services.prefs.getBoolPref(this.PREF_REPORT_BREAKAGE_ENABLED);
+    };
+
+    this.updateReportBreakageUI();
+
+    Services.prefs.addObserver(this.PREF_REPORT_BREAKAGE_ENABLED, this.updateReportBreakageUI);
+
     this.updateAnimationsEnabled = () => {
       this.iconBox.toggleAttribute("animationsenabled",
         Services.prefs.getBoolPref(this.PREF_ANIMATIONS_ENABLED, false));
     };
 
     for (let blocker of this.blockers) {
       if (blocker.init) {
         blocker.init();
@@ -236,16 +255,17 @@ var ContentBlocking = {
   uninit() {
     for (let blocker of this.blockers) {
       if (blocker.uninit) {
         blocker.uninit();
       }
     }
 
     Services.prefs.removeObserver(this.PREF_ANIMATIONS_ENABLED, this.updateAnimationsEnabled);
+    Services.prefs.removeObserver(this.PREF_REPORT_BREAKAGE_ENABLED, this.updateReportBreakageUI);
   },
 
   get enabled() {
     return this.contentBlockingUIEnabled ? this.contentBlockingEnabled : TrackingProtection.enabled;
   },
 
   updateEnabled() {
     this.content.toggleAttribute("enabled", this.enabled);
@@ -281,16 +301,74 @@ var ContentBlocking = {
     document.getElementById("identity-popup").hidePopup();
     BrowserReload();
   },
 
   openPreferences(origin) {
     openPreferences("privacy-trackingprotection", { origin });
   },
 
+  backToMainView() {
+    this.identityPopupMultiView.goBack();
+  },
+
+  submitBreakageReport() {
+    document.getElementById("identity-popup").hidePopup();
+
+    let reportEndpoint = Services.prefs.getStringPref(this.PREF_REPORT_BREAKAGE_URL);
+    if (!reportEndpoint) {
+      return;
+    }
+
+    let formData = new FormData();
+    formData.set("title", this.reportURI.host);
+
+    // Leave the ? at the end of the URL to signify that this URL had its query stripped.
+    let urlWithoutQuery = this.reportURI.asciiSpec.replace(this.reportURI.query, "");
+    let body = `Full URL: ${urlWithoutQuery}\n`;
+    body += `userAgent: ${this.reportBreakageUA.textContent}\n`;
+
+    body += "\n**Preferences**\n";
+    body += `${TrackingProtection.PREF_ENABLED_GLOBALLY}: ${Services.prefs.getBoolPref(TrackingProtection.PREF_ENABLED_GLOBALLY)}\n`;
+    body += `${TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS}: ${Services.prefs.getBoolPref(TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS)}\n`;
+    body += `urlclassifier.trackingTable: ${Services.prefs.getStringPref("urlclassifier.trackingTable")}\n`;
+    body += `network.http.referer.defaultPolicy: ${Services.prefs.getIntPref("network.http.referer.defaultPolicy")}\n`;
+    body += `network.http.referer.defaultPolicy.pbmode: ${Services.prefs.getIntPref("network.http.referer.defaultPolicy.pbmode")}\n`;
+    body += `network.cookie.cookieBehavior: ${Services.prefs.getIntPref("network.cookie.cookieBehavior")}\n`;
+    body += `network.cookie.lifetimePolicy: ${Services.prefs.getIntPref("network.cookie.lifetimePolicy")}\n`;
+    body += `privacy.restrict3rdpartystorage.expiration: ${Services.prefs.getIntPref("privacy.restrict3rdpartystorage.expiration")}\n`;
+    body += `${FastBlock.PREF_ENABLED}: ${Services.prefs.getBoolPref(FastBlock.PREF_ENABLED)}\n`;
+    body += `browser.fastblock.timeout: ${Services.prefs.getIntPref("browser.fastblock.timeout")}\n`;
+
+    let comments = document.getElementById("identity-popup-breakageReportView-collection-comments");
+    body += "\n**Comments**\n" + comments.value;
+
+    formData.set("body", body);
+
+    fetch(reportEndpoint, {
+      method: "POST",
+      credentials: "omit",
+      body: formData,
+    }).then(function(response) {
+      if (!response.ok) {
+        Cu.reportError(`Content Blocking report to ${reportEndpoint} failed with status ${response.status}`);
+      }
+    }).catch(Cu.reportError);
+  },
+
+  showReportBreakageSubview() {
+    // Save this URI to make sure that the user really only submits the location
+    // they see in the report breakage dialog.
+    this.reportURI = gBrowser.currentURI;
+    let urlWithoutQuery = this.reportURI.asciiSpec.replace("?" + this.reportURI.query, "");
+    this.reportBreakageURL.textContent = urlWithoutQuery;
+    this.reportBreakageUA.textContent = navigator.userAgent;
+    this.identityPopupMultiView.showSubView("identity-popup-breakageReportView");
+  },
+
   eventsHistogramAdd(value) {
     if (PrivateBrowsingUtils.isWindowPrivate(window)) {
       return;
     }
     Services.telemetry.getHistogramById("TRACKING_PROTECTION_EVENTS").add(value);
   },
 
   shieldHistogramAdd(value) {
--- a/browser/base/content/test/trackingUI/browser.ini
+++ b/browser/base/content/test/trackingUI/browser.ini
@@ -12,10 +12,11 @@ support-files =
 [browser_trackingUI_appMenu_toggle.js]
 [browser_trackingUI_fetch.js]
 support-files =
   file_trackingUI_fetch.html
   file_trackingUI_fetch.js
   file_trackingUI_fetch.js^headers^
 [browser_trackingUI_open_preferences.js]
 [browser_trackingUI_pbmode_exceptions.js]
+[browser_trackingUI_report_breakage.js]
 [browser_trackingUI_state.js]
 [browser_trackingUI_telemetry.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_report_breakage.js
@@ -0,0 +1,239 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html";
+const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/benignPage.html";
+
+const CB_PREF = "browser.contentblocking.enabled";
+const TP_PREF = "privacy.trackingprotection.enabled";
+const PREF_REPORT_BREAKAGE_ENABLED = "browser.contentblocking.reportBreakage.enabled";
+const PREF_REPORT_BREAKAGE_URL = "browser.contentblocking.reportBreakage.url";
+
+let {HttpServer} = ChromeUtils.import("resource://testing-common/httpd.js", {});
+let {CommonUtils} = ChromeUtils.import("resource://services-common/utils.js", {});
+let {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm", {});
+
+add_task(async function setup() {
+  await UrlClassifierTestUtils.addTestTrackers();
+});
+
+function openIdentityPopup() {
+  let mainView = document.getElementById("identity-popup-mainView");
+  let viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+  gIdentityHandler._identityBox.click();
+  return viewShown;
+}
+
+add_task(async function testReportBreakageVisibility() {
+  let scenarios = [
+    {
+      url: TRACKING_PAGE,
+      prefs: {
+        "browser.contentblocking.enabled": true,
+        "privacy.trackingprotection.enabled": true,
+        "browser.contentblocking.reportBreakage.enabled": true,
+      },
+      buttonVisible: true,
+    },
+    {
+      url: TRACKING_PAGE,
+      hasException: true,
+      prefs: {
+        "browser.contentblocking.enabled": true,
+        "privacy.trackingprotection.enabled": true,
+        "browser.contentblocking.reportBreakage.enabled": true,
+      },
+      buttonVisible: true,
+    },
+    {
+      url: TRACKING_PAGE,
+      prefs: {
+        "browser.contentblocking.enabled": false,
+        "privacy.trackingprotection.enabled": true,
+        "browser.contentblocking.reportBreakage.enabled": true,
+      },
+      buttonVisible: false,
+    },
+    {
+      url: TRACKING_PAGE,
+      prefs: {
+        "browser.contentblocking.enabled": true,
+        "privacy.trackingprotection.enabled": true,
+        "browser.contentblocking.reportBreakage.enabled": false,
+      },
+      buttonVisible: false,
+    },
+    {
+      url: BENIGN_PAGE,
+      prefs: {
+        "browser.contentblocking.enabled": true,
+        "privacy.trackingprotection.enabled": true,
+        "browser.contentblocking.reportBreakage.enabled": true,
+      },
+      buttonVisible: false,
+    },
+  ];
+
+  for (let scenario of scenarios) {
+    for (let pref in scenario.prefs) {
+      Services.prefs.setBoolPref(pref, scenario.prefs[pref]);
+    }
+
+    let uri = Services.io.newURI(scenario.url);
+    if (scenario.hasException) {
+      Services.perms.add(uri, "trackingprotection", Services.perms.ALLOW_ACTION);
+    }
+
+    await BrowserTestUtils.withNewTab(scenario.url, async function() {
+      await openIdentityPopup();
+
+      let reportBreakageButton = document.getElementById("identity-popup-content-blocking-report-breakage");
+      is(BrowserTestUtils.is_visible(reportBreakageButton), scenario.buttonVisible,
+        "report breakage button has the correct visibility");
+    });
+
+    Services.perms.remove(uri, "trackingprotection");
+    for (let pref in scenario.prefs) {
+      Services.prefs.clearUserPref(pref);
+    }
+  }
+});
+
+add_task(async function testReportBreakageCancel() {
+  Services.prefs.setBoolPref(TP_PREF, true);
+  Services.prefs.setBoolPref(CB_PREF, true);
+  Services.prefs.setBoolPref(PREF_REPORT_BREAKAGE_ENABLED, true);
+
+  await BrowserTestUtils.withNewTab(TRACKING_PAGE, async function() {
+    await openIdentityPopup();
+
+    let reportBreakageButton = document.getElementById("identity-popup-content-blocking-report-breakage");
+    ok(BrowserTestUtils.is_visible(reportBreakageButton), "report breakage button is visible");
+    let reportBreakageView = document.getElementById("identity-popup-breakageReportView");
+    let viewShown = BrowserTestUtils.waitForEvent(reportBreakageView, "ViewShown");
+    reportBreakageButton.click();
+    await viewShown;
+
+    ok(true, "Report breakage view was shown");
+
+    let mainView = document.getElementById("identity-popup-mainView");
+    viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
+    let cancelButton = document.getElementById("identity-popup-breakageReportView-cancel");
+    cancelButton.click();
+    await viewShown;
+
+    ok(true, "Main view was shown");
+  });
+
+  Services.prefs.clearUserPref(CB_PREF);
+  Services.prefs.clearUserPref(TP_PREF);
+  Services.prefs.clearUserPref(PREF_REPORT_BREAKAGE_ENABLED);
+});
+
+add_task(async function testReportBreakage() {
+  // Setup a mock server for receiving breakage reports.
+  let server = new HttpServer();
+  server.start(-1);
+  let i = server.identity;
+  let path = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort + "/";
+
+  Services.prefs.setBoolPref(TP_PREF, true);
+  Services.prefs.setBoolPref(CB_PREF, true);
+  Services.prefs.setBoolPref(PREF_REPORT_BREAKAGE_ENABLED, true);
+  Services.prefs.setStringPref(PREF_REPORT_BREAKAGE_URL, path);
+
+  // Make sure that we correctly strip the query.
+  let url = TRACKING_PAGE + "?a=b&1=abc&unicode=🦊";
+  await BrowserTestUtils.withNewTab(url, async function() {
+    await openIdentityPopup();
+
+    let reportBreakageButton = document.getElementById("identity-popup-content-blocking-report-breakage");
+    ok(BrowserTestUtils.is_visible(reportBreakageButton), "report breakage button is visible");
+    let reportBreakageView = document.getElementById("identity-popup-breakageReportView");
+    let viewShown = BrowserTestUtils.waitForEvent(reportBreakageView, "ViewShown");
+    reportBreakageButton.click();
+    await viewShown;
+
+    let submitButton = document.getElementById("identity-popup-breakageReportView-submit");
+    let reportURL = document.getElementById("identity-popup-breakageReportView-collection-url").textContent;
+    let reportUA = document.getElementById("identity-popup-breakageReportView-collection-userAgent").textContent;
+
+    is(reportURL, TRACKING_PAGE, "Shows the correct URL in the report UI.");
+    is(reportUA, navigator.userAgent, "Shows the correct user agent in the report UI.");
+
+    // Make sure that sending the report closes the identity popup.
+    let popuphidden = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popuphidden");
+
+    // Check that we're receiving a good report.
+    await new Promise(resolve => {
+      server.registerPathHandler("/", async (request, response) => {
+        is(request.method, "POST", "request was a post");
+
+        // Extract and "parse" the form data in the request body.
+        let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+        let boundary = request.getHeader("Content-Type").match(/boundary=-+([^-]*)/i)[1];
+        let regex = new RegExp("-+" + boundary + "-*\\s+");
+        let sections = body.split(regex);
+
+        let prefs = [
+          "privacy.trackingprotection.enabled",
+          "privacy.trackingprotection.pbmode.enabled",
+          "urlclassifier.trackingTable",
+          "network.http.referer.defaultPolicy",
+          "network.http.referer.defaultPolicy.pbmode",
+          "network.cookie.cookieBehavior",
+          "network.cookie.lifetimePolicy",
+          "privacy.restrict3rdpartystorage.expiration",
+          "browser.fastblock.enabled",
+          "browser.fastblock.timeout",
+        ];
+        let prefsBody = "";
+
+        for (let pref of prefs) {
+          prefsBody += `${pref}: ${Preferences.get(pref)}\r\n`;
+        }
+
+        Assert.deepEqual(sections, [
+          "",
+          "Content-Disposition: form-data; name=\"title\"\r\n\r\ntracking.example.org\r\n",
+          "Content-Disposition: form-data; name=\"body\"\r\n\r\n" +
+          `Full URL: ${reportURL + "?"}\r\n` +
+          `userAgent: ${reportUA}\r\n\r\n` +
+          "**Preferences**\r\n" +
+          `${prefsBody}\r\n` +
+          "**Comments**\r\n" +
+          "This is a comment\r\n",
+          ""
+        ], "Should send the correct form data");
+
+        resolve();
+      });
+
+      let comments = document.getElementById("identity-popup-breakageReportView-collection-comments");
+      comments.value = "This is a comment";
+      submitButton.click();
+    });
+
+    await popuphidden;
+  });
+
+  // Stop the server.
+  await new Promise(r => server.stop(r));
+
+  Services.prefs.clearUserPref(CB_PREF);
+  Services.prefs.clearUserPref(TP_PREF);
+  Services.prefs.clearUserPref(PREF_REPORT_BREAKAGE_ENABLED);
+  Services.prefs.clearUserPref(PREF_REPORT_BREAKAGE_URL);
+});
+
+add_task(async function cleanup() {
+  // Clear prefs that are touched in this test again for sanity.
+  Services.prefs.clearUserPref(CB_PREF);
+  Services.prefs.clearUserPref(TP_PREF);
+  Services.prefs.clearUserPref(PREF_REPORT_BREAKAGE_ENABLED);
+  Services.prefs.clearUserPref(PREF_REPORT_BREAKAGE_URL);
+
+  UrlClassifierTestUtils.cleanupTestTrackers();
+});
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -106,16 +106,20 @@
                   label="&trackingProtection.unblockPrivate4.label;"
                   accesskey="&trackingProtection.unblockPrivate4.accesskey;"
                   oncommand="ContentBlocking.disableForCurrentPage();" />
           <button id="tracking-action-block"
                   class="tracking-protection-button"
                   label="&trackingProtection.block5.label;"
                   accesskey="&trackingProtection.block5.accesskey;"
                   oncommand="ContentBlocking.enableForCurrentPage();" />
+          <label id="identity-popup-content-blocking-report-breakage"
+                 onclick="ContentBlocking.showReportBreakageSubview();"
+                 class="text-link subviewkeynav"
+                 flex="1">&contentBlocking.openBreakageReportView.label;</label>
         </vbox>
       </hbox>
 
       <!-- Permissions Section -->
       <hbox class="identity-popup-section"
             when-connection="not-secure secure secure-ev secure-cert-user-overridden file extension">
         <vbox id="identity-popup-permissions-content" flex="1" role="group"
               aria-labelledby="identity-popup-permissions-headline">
@@ -228,10 +232,44 @@
       <vbox id="identity-popup-more-info-footer" class="identity-popup-footer">
         <!-- More Security Information -->
         <button id="identity-popup-more-info"
                 label="&identity.moreInfoLinkText2;"
                 oncommand="gIdentityHandler.handleMoreInfoClick(event);"/>
       </vbox>
 
     </panelview>
+
+    <!-- Report Breakage SubView -->
+    <panelview id="identity-popup-breakageReportView"
+               title="&contentBlocking.breakageReportView.label;"
+               descriptionheightworkaround="true">
+        <vbox id="identity-popup-breakageReportView-heading">
+          <description>&contentBlocking.breakageReportView.description;</description>
+          <label id="identity-popup-breakageReportView-learn-more"
+                 class="text-link">&contentBlocking.breakageReportView.learnMore;</label>
+        </vbox>
+        <vbox id="identity-popup-breakageReportView-body" class="panel-view-body-unscrollable">
+          <vbox class="identity-popup-breakageReportView-collection-section">
+            <label class="identity-popup-breakageReportView-collection-label">&contentBlocking.breakageReportView.collection.url.label;</label>
+            <label id="identity-popup-breakageReportView-collection-url"/>
+          </vbox>
+          <vbox class="identity-popup-breakageReportView-collection-section">
+            <label class="identity-popup-breakageReportView-collection-label">&contentBlocking.breakageReportView.collection.userAgent.label;</label>
+            <label id="identity-popup-breakageReportView-collection-userAgent"/>
+          </vbox>
+          <vbox class="identity-popup-breakageReportView-collection-section">
+            <label class="identity-popup-breakageReportView-collection-label">&contentBlocking.breakageReportView.collection.comments.label;</label>
+            <textbox multiline="true" id="identity-popup-breakageReportView-collection-comments"/>
+          </vbox>
+        </vbox>
+        <vbox id="identity-popup-breakageReportView-footer" class="identity-popup-footer">
+          <button id="identity-popup-breakageReportView-cancel"
+                  label="&contentBlocking.breakageReportView.cancel.label;"
+                  oncommand="ContentBlocking.backToMainView();"/>
+          <button id="identity-popup-breakageReportView-submit"
+                  default="true"
+                  label="&contentBlocking.breakageReportView.sendReport.label;"
+                  oncommand="ContentBlocking.submitBreakageReport();"/>
+        </vbox>
+    </panelview>
   </panelmultiview>
 </panel>
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -946,16 +946,26 @@ you can use these alternative items. Oth
      "Trackers [are] Blocked"-->
 <!ENTITY contentBlocking.trackingProtection.blocked.label "Blocked">
 <!-- LOCALIZATION NOTE (contentBlocking.trackingProtection.add.label):
      This is displayed as a link to preferences, where the user can add
      this specific type of content blocking. When this text is shown
      the type of content blocking is currently not enabled. -->
 <!ENTITY contentBlocking.trackingProtection.add.label "Add Blocking…">
 
+<!ENTITY contentBlocking.openBreakageReportView.label "Report Problems">
+<!ENTITY contentBlocking.breakageReportView.label "Report Problems">
+<!ENTITY contentBlocking.breakageReportView.description "Content blocking can cause problems with some websites. When you report problems, you’ll help make &brandShortName; better for everyone. (This will send a URL as well as information about your privacy and content blocking settings to Mozilla.)">
+<!ENTITY contentBlocking.breakageReportView.learnMore "Learn More">
+<!ENTITY contentBlocking.breakageReportView.collection.url.label "URL">
+<!ENTITY contentBlocking.breakageReportView.collection.userAgent.label "&brandShortName; Version Number">
+<!ENTITY contentBlocking.breakageReportView.collection.comments.label "What problems did you have? (Optional)">
+<!ENTITY contentBlocking.breakageReportView.sendReport.label "Send Report">
+<!ENTITY contentBlocking.breakageReportView.cancel.label "Cancel">
+
 <!ENTITY trackingProtection.title "Tracking Protection">
 <!ENTITY trackingProtection.tooltip "Open Tracking Protection Preferences">
 
 <!-- LOCALIZATION NOTE (trackingProtection.unblock3.label, trackingProtection.unblock3.accesskey):
      The associated button with this label and accesskey is only shown when opening the control
      center while looking at a site with trackers in NON-private browsing mode. -->
 <!ENTITY trackingProtection.unblock4.label "Disable Blocking For This Site">
 <!ENTITY trackingProtection.unblock4.accesskey "D">
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -142,16 +142,17 @@
 }
 
 .identity-popup-preferences-button > .toolbarbutton-text {
   display: none;
 }
 
 /* CONTENT */
 
+#identity-popup-content-blocking-report-breakage,
 #identity-popup-content-blocking-disabled-label,
 .identity-popup-content-blocking-category-label,
 .identity-popup-content-blocking-category-state-label,
 .identity-popup-content-blocking-category-add-blocking,
 .identity-popup-permission-label,
 .identity-popup-permission-state-label,
 .identity-popup-security-content > description,
 #identity-popup-security-descriptions > description,
@@ -307,16 +308,63 @@ description#identity-popup-content-verif
 
 #identity-popup-securityView-body > button {
   margin-inline-start: 0;
   margin-inline-end: 0;
 }
 
 /* CONTENT BLOCKING / TRACKING PROTECTION */
 
+#identity-popup-breakageReportView-footer {
+  display: flex;
+}
+
+#identity-popup-breakageReportView-footer > button {
+  flex: 1;
+}
+
+#identity-popup-breakageReportView-footer > button[default] {
+  color: white;
+  background-color: #0996f8;
+}
+
+#identity-popup-breakageReportView-footer > button[default]:hover {
+  background-color: #0675d3;
+}
+
+#identity-popup-breakageReportView-footer > button[default]:hover:active {
+  background-color: #0568ba;
+}
+
+#identity-popup-breakageReportView-heading,
+#identity-popup-breakageReportView-body {
+  padding: 16px;
+  font-size: 110%;
+}
+
+.identity-popup-breakageReportView-collection-label {
+  color: graytext;
+}
+
+#identity-popup-breakageReportView-collection-url {
+  word-wrap: break-word;
+}
+
+.identity-popup-breakageReportView-collection-section {
+  margin-bottom: 16px;
+}
+
+#identity-popup-breakageReportView-body {
+  border-top: 1px solid var(--panel-separator-color);
+}
+
+#identity-popup-breakageReportView-collection-comments {
+  height: 120px;
+}
+
 #identity-popup-content-blocking-content {
   background-image: url("chrome://browser/skin/controlcenter/tracking-protection.svg");
 }
 
 /* We can currently show either the old tracking protection-only UI, which has "Tracking Protection"
  * as a label, or the new content blocking UI which has a different label and also a list of
  * categories of blockers. This rule hides elements depending on which UI we want to show */
 #identity-popup-content-blocking-content[contentBlockingUI] #tracking-protection-label,
@@ -413,16 +461,28 @@ description#identity-popup-content-verif
 
 /* Content Blocking action button */
 
 .tracking-protection-button {
   margin: 1em 0 0;
   display: none;
 }
 
+#identity-popup-content-blocking-report-breakage {
+  margin-top: 6px;
+}
+
+/* Hide the "report breakage" button if TP is off or we have not detected any
+ * trackers (except if the user added an exception, in which case they might
+ * still (especially!) want to report the breakage). */
+#identity-popup-content-blocking-content:not([enabled]) #identity-popup-content-blocking-report-breakage,
+#identity-popup-content-blocking-content:not([detected]):not([hasException]) #identity-popup-content-blocking-report-breakage {
+  display: none;
+}
+
 /* Show the right action buttons depending on content state */
 /* Offer to temporarily add an exception in private mode. */
 #main-window:not([privatebrowsingmode]) #identity-popup-content-blocking-content[enabled][detected]:not([hasException]) > #tracking-action-unblock,
 /* Offer to permanently add an exception in normal mode. */
 #main-window[privatebrowsingmode] #identity-popup-content-blocking-content[enabled][detected]:not([hasException]) > #tracking-action-unblock-private,
 /* If there's an exception just offer to remove the exception again. */
 #identity-popup-content-blocking-content[enabled][hasException] > #tracking-action-block {
   display: -moz-box;