tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 5692b1b549030f7788a56ce3a95cf836e06c2115
parent 8613c0008383391a3ab08187b52516409b66888c
Author: Thomas Wisniewski <twisniewski@mozilla.com>
Date:   Tue, 25 Nov 2025 20:56:23 +0000

Bug 1992050 - give users the ability to preview the data contained in a broken site report before sending it; r=Gijs,fluent-reviewers,desktop-theme-reviewers,bolsson,hjones

Differential Revision: https://phabricator.services.mozilla.com/D267154

Diffstat:
Mbrowser/components/reportbrokensite/ReportBrokenSite.sys.mjs | 302+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mbrowser/components/reportbrokensite/content/reportBrokenSitePanel.inc.xhtml | 20+++++++++++++++++++-
Mbrowser/components/reportbrokensite/test/browser/browser.toml | 5++---
Mbrowser/components/reportbrokensite/test/browser/browser_addon_data_sent.js | 2+-
Mbrowser/components/reportbrokensite/test/browser/browser_antitracking_data_sent.js | 2+-
Mbrowser/components/reportbrokensite/test/browser/browser_experiment_data_sent.js | 2+-
Abrowser/components/reportbrokensite/test/browser/browser_report_preview.js | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/reportbrokensite/test/browser/browser_send_more_info.js | 2+-
Mbrowser/components/reportbrokensite/test/browser/browser_tab_key_order.js | 1+
Mbrowser/components/reportbrokensite/test/browser/head.js | 48+++++++++++++++++++++++++++++++++++++++++++++++-
Mbrowser/components/reportbrokensite/test/browser/send.js | 29+++++++++++++++++++++++------
Mbrowser/components/reportbrokensite/test/browser/send_more_info.js | 168+++++++++++++++++++++++++++++--------------------------------------------------
Mbrowser/locales/en-US/browser/reportBrokenSite.ftl | 6++++++
Mbrowser/themes/shared/customizableui/panelUI-shared.css | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mmobile/android/fenix/app/metrics.yaml | 15+++++++++++++++
Mtoolkit/components/reportbrokensite/ReportBrokenSiteChild.sys.mjs | 405+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mtoolkit/components/reportbrokensite/metrics.yaml | 15+++++++++++++++
17 files changed, 980 insertions(+), 336 deletions(-)

diff --git a/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs b/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs @@ -14,9 +14,10 @@ ChromeUtils.defineESModuleGetters(lazy, { const gDescriptionCheckRE = /\S/; -class ViewState { +export class ViewState { #doc; #mainView; + #previewView; #reportSentView; #formElement; #reasonOptions; @@ -31,6 +32,10 @@ class ViewState { this.#doc, "report-broken-site-popup-mainView" ); + this.#previewView = doc.ownerGlobal.PanelMultiView.getViewNode( + this.#doc, + "report-broken-site-popup-previewView" + ); this.#reportSentView = doc.ownerGlobal.PanelMultiView.getViewNode( this.#doc, "report-broken-site-popup-reportSentView" @@ -56,6 +61,10 @@ class ViewState { return this.#mainView; } + get previewPanelview() { + return this.#mainView; + } + get reportSentPanelview() { return this.#reportSentView; } @@ -112,6 +121,14 @@ class ViewState { return reason == "choose" ? undefined : reason; } + get reasonText() { + const { reasonInput } = this; + if (!reasonInput.selectedIndex) { + return ""; + } + return reasonInput.selectedOptions[0]?.label; + } + set reason(value) { this.reasonInput.selectedIndex = this.#mainView.querySelector( `#${ViewState.REASON_CHOICES_ID_PREFIX}${value}` @@ -166,6 +183,7 @@ class ViewState { this.currentTabWebcompatDetailsPromise = undefined; this.form.reset(); this.blockedTrackersCheckbox.checked = false; + delete this.cachedPreviewData; this.resetURLToCurrentTab(); } @@ -200,6 +218,10 @@ class ViewState { ); } + createElement(name) { + return this.#doc.createElement(name); + } + #focusMainViewElement(toFocus) { const panelview = this.#doc.ownerGlobal.PanelView.forNode(this.#mainView); panelview.selectedElement = toFocus; @@ -278,6 +300,24 @@ class ViewState { "#report-broken-site-popup-okay-button" ); } + + get copyButton() { + return this.#previewView.querySelector( + "#report-broken-site-popup-preview-copy-button" + ); + } + + get previewBox() { + return this.#previewView.querySelector( + "#report-broken-site-panel-preview-items" + ); + } + + get previewButton() { + return this.#mainView.querySelector( + "#report-broken-site-popup-preview-button" + ); + } } export var ReportBrokenSite = new (class ReportBrokenSite { @@ -310,6 +350,7 @@ export var ReportBrokenSite = new (class ReportBrokenSite { static MAIN_PANELVIEW_ID = "report-broken-site-popup-mainView"; static SENT_PANELVIEW_ID = "report-broken-site-popup-reportSentView"; + static PREVIEW_PANELVIEW_ID = "report-broken-site-popup-previewView"; #_enabled = false; get enabled() { @@ -394,6 +435,7 @@ export var ReportBrokenSite = new (class ReportBrokenSite { const state = ViewState.get(document); this.#initMainView(state); + this.#initPreviewView(state); this.#initReportSentView(state); for (const id of ["menu_HelpPopup", "appMenu-popup"]) { @@ -441,11 +483,7 @@ export var ReportBrokenSite = new (class ReportBrokenSite { } else { const tabbrowser = e.target.ownerGlobal.gBrowser; state.resetURLToCurrentTab(); - state.currentTabWebcompatDetailsPromise = this.#queryActor( - "GetWebCompatInfo", - undefined, - tabbrowser.selectedBrowser - ); + this.promiseWebCompatInfo(state, tabbrowser.selectedBrowser); this.#openWebCompatTab(tabbrowser) .catch(err => { console.error("Report Broken Site: unexpected error", err); @@ -561,6 +599,63 @@ export var ReportBrokenSite = new (class ReportBrokenSite { event.target.ownerGlobal.CustomizableUI.hidePanelForNode(event.target); }); }); + + state.previewButton.addEventListener("click", event => { + state.currentTabWebcompatDetailsPromise + ?.catch(_ => {}) + .then(info => { + this.generatePreviewMarkup(state, info); + + // Update the live data on the preview which the user can edit in the reporter. + const { description, previewBox, reasonText } = state; + if (state.cachedPreviewData) { + state.cachedPreviewData.basic.description = description; + state.cachedPreviewData.basic.reason = reasonText; + } + previewBox.querySelector( + ".preview_description" + ).nextSibling.innerText = JSON.stringify(description); + previewBox.querySelector(".preview_reason").nextSibling.innerText = + JSON.stringify(reasonText ?? ""); + + const multiview = event.target.closest("panelmultiview"); + multiview.showSubView( + ReportBrokenSite.PREVIEW_PANELVIEW_ID, + event.target + ); + this.#recordGleanEvent("previewed"); + }); + }); + } + + #initPreviewView(state) { + state.copyButton.addEventListener("click", event => { + event.preventDefault(); + const { cachedPreviewData } = state; + if (!cachedPreviewData) { + console.error("Report Broken Site: No cached preview data to copy?"); + return; + } + this.#setClipboardToString(JSON.stringify(cachedPreviewData)); + }); + } + + #setClipboardToString(string) { + let str = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + str.data = string; + const xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + xferable.init(null); + xferable.addDataFlavor("text/plain"); + xferable.setTransferData("text/plain", str); + Services.clipboard.setData( + xferable, + null, + Ci.nsIClipboard.kGlobalClipboard + ); } #initReportSentView(state) { @@ -608,14 +703,88 @@ export var ReportBrokenSite = new (class ReportBrokenSite { this.#recordGleanEvent("opened", { source }); if (didReset || !state.currentTabWebcompatDetailsPromise) { - state.currentTabWebcompatDetailsPromise = this.#queryActor( - "GetWebCompatInfo", - undefined, - selectedBrowser - ).catch(err => { - console.error("Report Broken Site: unexpected error", err); - state.currentTabWebcompatDetailsPromise = undefined; - }); + this.promiseWebCompatInfo(state, selectedBrowser); + } + } + + promiseWebCompatInfo(state, selectedBrowser) { + state.currentTabWebcompatDetailsPromise = this.#queryActor( + "GetBrokenSiteReport", + undefined, + selectedBrowser + ).catch(err => { + console.error("Report Broken Site: unexpected error", err); + state.currentTabWebcompatDetailsPromise = undefined; + }); + } + + cachePreviewData(state, brokenSiteReportData) { + const { blockedTrackersCheckbox, description, reasonText, url } = state; + + const previewData = Object.assign({ + basic: { + description, + reason: reasonText, + url, + }, + }); + + if (brokenSiteReportData) { + for (const [category, values] of Object.entries(brokenSiteReportData)) { + previewData[category] = Object.fromEntries( + Object.entries(values) + .filter(([_, { do_not_preview }]) => !do_not_preview) + .map(([name, { value }]) => [name, value]) + ); + } + } + + if (!blockedTrackersCheckbox.checked && previewData.antitracking) { + delete previewData.antitracking.blockedOrigins; + } + + state.cachedPreviewData = previewData; + return previewData; + } + + generatePreviewMarkup(state, reportData) { + // If we have already cached preview data, we have already generated the markup as well. + if (this.cachedPreviewData) { + return; + } + const previewData = this.cachePreviewData(state, reportData); + const preview = state.previewBox; + preview.innerHTML = ""; + for (const [name, value] of Object.entries(previewData)) { + const details = state.createElement("details"); + + const summary = state.createElement("summary"); + summary.innerText = name; + summary.dataset.capturesFocus = "true"; + details.appendChild(summary); + + const info = state.createElement("div"); + info.className = "data"; + for (const [k, v] of Object.entries(value)) { + const div = state.createElement("div"); + div.className = "entry"; + const span_name = state.createElement("span"); + const span_value = state.createElement("span"); + span_name.className = `preview_${k}`; + span_name.innerText = `${k}:`; + // Add some extra word-wrapping opportunities to the data by adding spaces, + // so users don't have to horizontally scroll as much. + span_value.innerText = JSON.stringify(v)?.replace(/[,:]/g, "$& ") ?? ""; + div.append(span_name, span_value); + info.appendChild(div); + } + details.appendChild(info); + + preview.appendChild(details); + } + const first = preview.querySelector("details"); + if (first) { + first.setAttribute("open", ""); } } @@ -680,14 +849,6 @@ export var ReportBrokenSite = new (class ReportBrokenSite { url, }) { const gBase = Glean.brokenSiteReport; - const gTabInfo = Glean.brokenSiteReportTabInfo; - const gAntitracking = Glean.brokenSiteReportTabInfoAntitracking; - const gFrameworks = Glean.brokenSiteReportTabInfoFrameworks; - const gBrowserInfo = Glean.brokenSiteReportBrowserInfo; - const gApp = Glean.brokenSiteReportBrowserInfoApp; - const gGraphics = Glean.brokenSiteReportBrowserInfoGraphics; - const gPrefs = Glean.brokenSiteReportBrowserInfoPrefs; - const gSystem = Glean.brokenSiteReportBrowserInfoSystem; if (reason) { gBase.breakageCategory.set(reason); @@ -703,89 +864,32 @@ export var ReportBrokenSite = new (class ReportBrokenSite { return; } - const { - antitracking, - browser, - devicePixelRatio, - frameworks, - languages, - userAgent, - } = details; - - gTabInfo.languages.set(languages); - gTabInfo.useragentString.set(userAgent); - gGraphics.devicePixelRatio.set(devicePixelRatio); - - for (const [name, value] of Object.entries(antitracking)) { - if (name !== "blockedOrigins" || blockedTrackersCheckbox.checked) { - gAntitracking[name].set(value); - } + if (!blockedTrackersCheckbox.checked) { + delete details.antitracking.blockedOrigins; } - for (const [name, value] of Object.entries(frameworks)) { - gFrameworks[name].set(value); - } - - const { - addons, - app, - experiments, - graphics, - locales, - platform, - prefs, - security, - } = browser; - - gBrowserInfo.addons.set(addons); - gBrowserInfo.experiments.set(experiments); - - gApp.defaultLocales.set(locales); - gApp.defaultUseragentString.set(app.defaultUserAgent); - - const { fissionEnabled, isTablet, memoryMB } = platform; - gApp.fissionEnabled.set(fissionEnabled); - gSystem.isTablet.set(isTablet ?? false); - gSystem.memory.set(memoryMB); - - gPrefs.cookieBehavior.set(prefs["network.cookie.cookieBehavior"]); - gPrefs.forcedAcceleratedLayers.set( - prefs["layers.acceleration.force-enabled"] - ); - gPrefs.globalPrivacyControlEnabled.set( - prefs["privacy.globalprivacycontrol.enabled"] - ); - gPrefs.installtriggerEnabled.set( - prefs["extensions.InstallTrigger.enabled"] - ); - gPrefs.opaqueResponseBlocking.set(prefs["browser.opaqueResponseBlocking"]); - gPrefs.resistFingerprintingEnabled.set( - prefs["privacy.resistFingerprinting"] - ); - gPrefs.softwareWebrender.set(prefs["gfx.webrender.software"]); - gPrefs.thirdPartyCookieBlockingEnabled.set( - prefs["network.cookie.cookieBehavior.optInPartitioning"] - ); - gPrefs.thirdPartyCookieBlockingEnabledInPbm.set( - prefs["network.cookie.cookieBehavior.optInPartitioning.pbmode"] - ); - - if (security) { - for (const [name, value] of Object.entries(security)) { - if (value?.length) { - Glean.brokenSiteReportBrowserInfoSecurity[name].set(value); + for (const categoryItems of Object.values(details)) { + for (let [name, { glean, json, value }] of Object.entries( + categoryItems + )) { + if (!glean) { + continue; + } + // Transform glean=xx.yy.zz to brokenSiteReportXxYyZz. + glean = + "brokenSiteReport" + + glean + .split(".") + .map(v => `${v[0].toUpperCase()}${v.substr(1)}`) + .join(""); + if (json) { + name = `${name}Json`; + value = JSON.stringify(value); } + Glean[glean][name].set(value); } } - const { devices, drivers, features, hasTouchScreen, monitors } = graphics; - - gGraphics.devicesJson.set(JSON.stringify(devices)); - gGraphics.driversJson.set(JSON.stringify(drivers)); - gGraphics.featuresJson.set(JSON.stringify(features)); - gGraphics.hasTouchScreen.set(hasTouchScreen); - gGraphics.monitorsJson.set(JSON.stringify(monitors)); - GleanPings.brokenSiteReport.submit(); } diff --git a/browser/components/reportbrokensite/content/reportBrokenSitePanel.inc.xhtml b/browser/components/reportbrokensite/content/reportBrokenSitePanel.inc.xhtml @@ -97,7 +97,7 @@ form="report-broken-site-panel-form" allow-arrow-navigation="true" rows="4"></html:textarea> - <vbox> + <vbox id="report-broken-site-popup-blocked-trackers"> <checkbox id="report-broken-site-popup-blocked-trackers-checkbox" data-l10n-id="report-broken-site-panel-blocked-trackers-label" /> <html:p data-l10n-id="report-broken-site-panel-blocked-trackers-description"/> </vbox> @@ -106,6 +106,8 @@ href="#" data-l10n-id="report-broken-site-panel-send-more-info-link"/> </hbox> + <html:moz-box-button id="report-broken-site-popup-preview-button" + data-l10n-id="report-broken-site-panel-preview-button" /> </vbox> <html:moz-button-group class="panel-footer"> <button id="report-broken-site-popup-cancel-button" @@ -118,6 +120,22 @@ </html:moz-button-group> </panelview> +<panelview id="report-broken-site-popup-previewView" + class="report-broken-site-view preview-view PanelUI-subView" + role="dialog" + data-l10n-id="report-broken-site-panel-preview-header" + data-l10n-attrs="title" + aria-describedby="report-broken-site-panel-preview-label"> + <vbox class="panel-subview-body"> + <vbox id="report-broken-site-panel-preview-items"/> + </vbox> + <html:moz-button-group class="panel-footer"> + <html:moz-button id="report-broken-site-popup-preview-copy-button" + type="primary" + data-l10n-id="report-broken-site-panel-button-copy"/> + </html:moz-button-group> +</panelview> + <panelview id="report-broken-site-popup-reportSentView" class="report-broken-site-view sent-view PanelUI-subView" role="dialog" diff --git a/browser/components/reportbrokensite/test/browser/browser.toml b/browser/components/reportbrokensite/test/browser/browser.toml @@ -8,9 +8,6 @@ support-files = [ ["browser_addon_data_sent.js"] support-files = [ "send_more_info.js" ] -skip-if = [ - "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && opt", # Bug 1955805 -] ["browser_antitracking_data_sent.js"] support-files = [ "send_more_info.js" ] @@ -38,6 +35,8 @@ skip-if = [ ["browser_reason_dropdown.js"] +["browser_report_preview.js"] + ["browser_report_send.js"] support-files = [ "send.js" ] diff --git a/browser/components/reportbrokensite/test/browser/browser_addon_data_sent.js b/browser/components/reportbrokensite/test/browser/browser_addon_data_sent.js @@ -90,7 +90,7 @@ add_task(async function testSendingMoreInfo() { const tab = await openTab(REPORTABLE_PAGE_URL); - await testSendMoreInfo(tab, HelpMenu(), { + await testSendMoreInfo(tab, AppMenu(), { addons: EXPECTED_ADDONS, }); diff --git a/browser/components/reportbrokensite/test/browser/browser_antitracking_data_sent.js b/browser/components/reportbrokensite/test/browser/browser_antitracking_data_sent.js @@ -67,7 +67,7 @@ add_task(async function testSendingMoreInfo() { const tab = await openTab(REPORTABLE_PAGE_URL3, win); await blockedPromise; - await testSendMoreInfo(tab, HelpMenu(win), { + await testSendMoreInfo(tab, AppMenu(win), { antitracking: { blockList: "strict", blockedOrigins: ["https://trackertest.org"], diff --git a/browser/components/reportbrokensite/test/browser/browser_experiment_data_sent.js b/browser/components/reportbrokensite/test/browser/browser_experiment_data_sent.js @@ -74,7 +74,7 @@ add_task(async function testSendingMoreInfo() { const tab = await openTab(REPORTABLE_PAGE_URL); - await testSendMoreInfo(tab, HelpMenu(), { + await testSendMoreInfo(tab, AppMenu(), { experiments: EXPECTED_EXPERIMENTS_IN_REPORT, }); diff --git a/browser/components/reportbrokensite/test/browser/browser_report_preview.js b/browser/components/reportbrokensite/test/browser/browser_report_preview.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* Helper methods for testing sending reports with + * the Report Broken Site feature. + */ + +/* import-globals-from head.js */ + +function setClipboard(string) { + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(string); +} + +function getClipboardAsString() { + let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(window.docShell.QueryInterface(Ci.nsILoadContext)); + trans.addDataFlavor("text/plain"); + Services.clipboard.getData( + trans, + Ci.nsIClipboard.kGlobalClipboard, + SpecialPowers.wrap(window).browsingContext.currentWindowContext + ); + let data = {}; + trans.getTransferData("text/plain", data); + data = data.value.QueryInterface(Ci.nsISupportsString); + return data.toString(); +} + +async function checkPreviewPanel(rbs, basic) { + const items = rbs.previewItems.querySelectorAll("details"); + + let target = await pressKeyAndGetFocus("VK_TAB"); + ok( + target.matches("toolbarbutton.subviewbutton-back"), + "First focus is on back button" + ); + const previewData = {}; + for (const [idx, item] of items.entries()) { + target = await pressKeyAndGetFocus("VK_TAB"); + const summary = item.querySelector("summary"); + is(target, summary, "Next focus is on next preview item"); + + is( + item.getAttribute("open"), + idx ? null : "", + `Next preview item starts off ${idx ? "closed" : "open"}` + ); + let text = item.querySelector(".data").innerText; + + EventUtils.synthesizeKey("VK_SPACE"); + is( + item.getAttribute("open"), + idx ? "" : null, + `Next preview item properly ${idx ? "opens" : "closes"}` + ); + + text ||= item.querySelector(".data").innerText; + + EventUtils.synthesizeKey("VK_SPACE"); + is( + item.getAttribute("open"), + idx ? null : "", + `Next preview item properly ${idx ? "closes" : "opens"} again` + ); + + previewData[summary.innerText] = Object.fromEntries( + text.split("\n").map(line => { + const s = line.split(":"); + const label = s.shift(); + const value = s.join(":"); + return [label, JSON.parse(value)]; + }) + ); + } + target = await pressKeyAndGetFocus("VK_TAB"); + is(target, rbs.previewCopyButton, "Final focus is on copy button"); + + function adjustForWrapping(value) { + // match what we do to strings when generating the preview markup. + return typeof value === "string" + ? value.replaceAll(":", ": ").replaceAll(",", ", ") + : value; + } + + const expectedPreviewData = { + basic: Object.fromEntries( + Object.entries(basic).map(([n, v]) => [n, adjustForWrapping(v)]) + ), + }; + const rawReportData = structuredClone( + await ViewState.get(document).currentTabWebcompatDetailsPromise + ); + if (!rbs.blockedTrackersCheckbox.checked) { + delete rawReportData.antitracking.blockedOrigins; + } + for (const [category, values] of Object.entries(rawReportData)) { + expectedPreviewData[category] = Object.fromEntries( + Object.entries(values) + .filter(([_, { do_not_preview }]) => !do_not_preview) + .map(([name, { value }]) => [name, adjustForWrapping(value)]) + ); + } + ok( + areObjectsEqual(previewData, expectedPreviewData), + "Preview had the expected information" + ); + + setClipboard("did not copy"); + rbs.clickPreviewCopy(); + const copied = JSON.parse(getClipboardAsString()); + + // we don't adjust the copied text for wrapping like we do the markup. + const expectedCopyData = { basic }; + for (const [category, values] of Object.entries(rawReportData)) { + expectedCopyData[category] = Object.fromEntries( + Object.entries(values) + .filter(([_, { do_not_preview }]) => !do_not_preview) + .map(([name, { value }]) => [name, value]) + ); + } + + ok( + areObjectsEqual(copied, expectedCopyData), + "Copied the expected information" + ); +} + +add_task(async function testPreview() { + ensureReportBrokenSitePreffedOn(); + + const tab = await openTab(REPORTABLE_PAGE_URL); + + ViewState.get(document).reset(); + + const menu = AppMenu(); + const url = menu.win.gBrowser.currentURI.spec; + let rbs = await menu.openAndPrefillReportBrokenSite(); + + await rbs.clickPreview(); + await checkPreviewPanel(rbs, { + description: "", + reason: "", + url, + }); + + await rbs.clickPreviewBack(); + const url2 = `${url}?test`; + rbs.setURL(url2); + rbs.setDescription("description"); + rbs.chooseReason("slow"); + rbs.blockedTrackersCheckbox = true; + await rbs.clickPreview(); + await checkPreviewPanel(rbs, { + description: "description", + reason: ViewState.get(document).reasonText, + url: url2, + }); + + await rbs.close(); + + ViewState.get(document).reset(); + + closeTab(tab); +}); diff --git a/browser/components/reportbrokensite/test/browser/browser_send_more_info.js b/browser/components/reportbrokensite/test/browser/browser_send_more_info.js @@ -58,7 +58,7 @@ add_task(async function testSendingMoreInfo() { // also load a video to ensure system codec // information is loaded and properly sent const tab2 = await openTab(VIDEO_URL); - await testSendMoreInfo(tab2, HelpMenu()); + await testSendMoreInfo(tab2, AppMenu()); closeTab(tab2); closeTab(tab); diff --git a/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js b/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js @@ -59,6 +59,7 @@ async function ensureExpectedTabOrder( if (expectSendMoreInfo) { order.push("#report-broken-site-popup-send-more-info-link"); } + order.push("#report-broken-site-popup-preview-button"); // moz-button-groups swap the order of buttons to follow // platform conventions, so the order of send/cancel will vary. order.push([ diff --git a/browser/components/reportbrokensite/test/browser/head.js b/browser/components/reportbrokensite/test/browser/head.js @@ -14,7 +14,7 @@ const { UrlClassifierTestUtils } = ChromeUtils.importESModule( "resource://testing-common/UrlClassifierTestUtils.sys.mjs" ); -const { ReportBrokenSite } = ChromeUtils.importESModule( +const { ReportBrokenSite, ViewState } = ChromeUtils.importESModule( "moz-src:///browser/components/reportbrokensite/ReportBrokenSite.sys.mjs" ); @@ -255,6 +255,10 @@ class ReportBrokenSiteHelper { return this.getViewNode("report-broken-site-popup-mainView"); } + get previewView() { + return this.getViewNode("report-broken-site-popup-previewView"); + } + get sentView() { return this.getViewNode("report-broken-site-popup-reportSentView"); } @@ -307,6 +311,13 @@ class ReportBrokenSiteHelper { await Promise.all(promises); } + async awaitPreviewViewOpened() { + await Promise.all([ + BrowserTestUtils.waitForEvent(this.sentView, "ViewShown"), + BrowserTestUtils.waitForEvent(this.okayButton, "focus"), + ]); + } + async awaitReportSentViewOpened() { await Promise.all([ BrowserTestUtils.waitForEvent(this.sentView, "ViewShown"), @@ -354,6 +365,25 @@ class ReportBrokenSiteHelper { await this.#assertClickAndViewChanges(this.okayButton, this.sentView); } + async clickPreviewCopy() { + EventUtils.synthesizeMouseAtCenter(this.previewCopyButton, {}, this.win); + } + + async clickPreview() { + await this.#assertClickAndViewChanges( + this.previewButton, + this.mainView, + this.previewView + ); + } + + async clickPreviewBack() { + await this.#assertClickAndViewChanges( + this.previewBackButton, + this.sourceMenu.popup + ); + } + async clickBack() { await this.#assertClickAndViewChanges( this.backButton, @@ -418,6 +448,10 @@ class ReportBrokenSiteHelper { return this.mainView.querySelector(".subviewbutton-back"); } + get previewBackButton() { + return this.previewView.querySelector(".subviewbutton-back"); + } + get blockedTrackersCheckbox() { return this.getViewNode( "report-broken-site-popup-blocked-trackers-checkbox" @@ -440,6 +474,18 @@ class ReportBrokenSiteHelper { return this.getViewNode("report-broken-site-popup-okay-button"); } + get previewButton() { + return this.getViewNode("report-broken-site-popup-preview-button"); + } + + get previewCopyButton() { + return this.getViewNode("report-broken-site-popup-preview-copy-button"); + } + + get previewItems() { + return this.getViewNode("report-broken-site-panel-preview-items"); + } + // Test helpers #setInput(input, value) { diff --git a/browser/components/reportbrokensite/test/browser/send.js b/browser/components/reportbrokensite/test/browser/send.js @@ -188,16 +188,25 @@ async function getExpectedWebCompatInfo(tab, snapshot, fullAppData = false) { false ), }, - security: { - antispyware: securityStringToArray(registeredAntiSpyware), - antivirus: securityStringToArray(registeredAntiVirus), - firewall: securityStringToArray(registeredFirewall), - }, system: { isTablet: getSysinfoProperty("tablet", false), memory: Math.round(memorySizeBytes / 1024 / 1024), }, }; + if (registeredAntiSpyware) { + browserInfo.security ??= {}; + browserInfo.security.antispyware = securityStringToArray( + registeredAntiSpyware + ); + } + if (registeredAntiVirus) { + browserInfo.security ??= {}; + browserInfo.security.antivirus = securityStringToArray(registeredAntiVirus); + } + if (registeredFirewall) { + browserInfo.security ??= {}; + browserInfo.security.firewall = securityStringToArray(registeredFirewall); + } const tabInfo = await tab.linkedBrowser.ownerGlobal.SpecialPowers.spawn( tab.linkedBrowser, @@ -257,9 +266,17 @@ function extractBrokenSiteReportFromGleanPing(Glean) { Glean.brokenSiteReportBrowserInfo.experiments.testGetValue() ), prefs: extractPingData(Glean.brokenSiteReportBrowserInfoPrefs), - security: extractPingData(Glean.brokenSiteReportBrowserInfoSecurity), system: extractPingData(Glean.brokenSiteReportBrowserInfoSystem), }; + const security = extractPingData(Glean.brokenSiteReportBrowserInfoSecurity); + for (const [k, v] of Object.entries(security)) { + if (v === null) { + delete security[k]; + } + } + if (Object.keys(security).length) { + ping.browserInfo.security = security; + } return ping; } diff --git a/browser/components/reportbrokensite/test/browser/send_more_info.js b/browser/components/reportbrokensite/test/browser/send_more_info.js @@ -65,70 +65,65 @@ async function reformatExpectedWebCompatInfo(tab, overrides) { // ignore the console log unless explicily testing for it. const consoleLog = overrides.consoleLog ?? (() => true); - const finalPrefs = {}; - for (const [key, pref] of Object.entries({ - cookieBehavior: "network.cookie.cookieBehavior", - forcedAcceleratedLayers: "layers.acceleration.force-enabled", - globalPrivacyControlEnabled: "privacy.globalprivacycontrol.enabled", - installtriggerEnabled: "extensions.InstallTrigger.enabled", - opaqueResponseBlocking: "browser.opaqueResponseBlocking", - resistFingerprintingEnabled: "privacy.resistFingerprinting", - softwareWebrender: "gfx.webrender.software", - thirdPartyCookieBlockingEnabled: - "network.cookie.cookieBehavior.optInPartitioning", - thirdPartyCookieBlockingEnabledInPbm: - "network.cookie.cookieBehavior.optInPartitioning.pbmode", - })) { - if (key in prefs) { - finalPrefs[pref] = prefs[key]; - } - } - const reformatted = { blockList, details: { additionalData: { - addons, - applicationName, - blockList, - blockedOrigins, - buildId: snapshot.application.buildID, - devicePixelRatio: parseInt(devicePixelRatio), - experiments, - finalUserAgent: useragentString, - fissionEnabled, - gfxData: { - devices(actual) { - const devices = getExpectedGraphicsDevices(snapshot); - return compareGraphicsDevices(devices, actual); + browserInfo: { + addons, + app: { + applicationName, + buildId: snapshot.application.buildID, + defaultLocales: snapshot.intl.localeService.available, + defaultUseragentString, + fissionEnabled, + osArchitecture, + osName, + osVersion, + updateChannel, + version, }, - drivers(actual) { - const drvs = getExpectedGraphicsDrivers(snapshot); - return compareGraphicsDrivers(drvs, actual); + experiments, + graphics: { + devicePixelRatio: parseInt(devicePixelRatio), + devices(actual) { + const devices = getExpectedGraphicsDevices(snapshot); + return compareGraphicsDevices(devices, actual); + }, + drivers(actual) { + const drvs = getExpectedGraphicsDrivers(snapshot); + return compareGraphicsDrivers(drvs, actual); + }, + features(actual) { + const features = getExpectedGraphicsFeatures(snapshot); + return areObjectsEqual(actual, features); + }, + hasTouchScreen, + monitors(actual) { + return areObjectsEqual(actual, gfxInfo.getMonitors()); + }, }, - features(actual) { - const features = getExpectedGraphicsFeatures(snapshot); - return areObjectsEqual(actual, features); + prefs, + system: { + isTablet: getSysinfoProperty("tablet", false), + memory: browserInfo.system.memory, }, - hasTouchScreen, - monitors(actual) { - return areObjectsEqual(actual, gfxInfo.getMonitors()); + }, + tabInfo: { + antitracking: { + blockList, + blockedOrigins, + btpHasPurgedSite, + etpCategory, + hasMixedActiveContentBlocked, + hasMixedDisplayContentBlocked, + hasTrackingContentBlocked, + isPrivateBrowsing, }, + frameworks, + languages, + useragentString, }, - hasMixedActiveContentBlocked, - hasMixedDisplayContentBlocked, - hasTrackingContentBlocked, - btpHasPurgedSite, - isPB: isPrivateBrowsing, - etpCategory, - languages, - locales: snapshot.intl.localeService.available, - memoryMB: browserInfo.system.memory, - osArchitecture, - osName, - osVersion, - prefs: finalPrefs, - version, }, blockList, channel: updateChannel, @@ -150,56 +145,16 @@ async function reformatExpectedWebCompatInfo(tab, overrides) { utm_source: "desktop-reporter", }; - const { gfxData } = reformatted.details.additionalData; - for (const optional of [ - "directWriteEnabled", - "directWriteVersion", - "clearTypeParameters", - "targetFrameRate", - ]) { - if (optional in snapshot.graphics) { - gfxData[optional] = snapshot.graphics[optional]; - } - } - // We only care about this pref on Linux right now on webcompat.com. if (AppConstants.platform != "linux") { - delete finalPrefs["layers.acceleration.force-enabled"]; + delete prefs.forcedAcceleratedLayers; } else { reformatted.details["layers.acceleration.force-enabled"] = - finalPrefs["layers.acceleration.force-enabled"]; - } - - // Only bother adding the security key if it has any data - if (Object.values(security).filter(e => e).length) { - reformatted.details.additionalData.sec = security; + prefs.forcedAcceleratedLayers; } - const expectedCodecs = snapshot.media.codecSupportInfo - .replaceAll(" NONE", "") - .split("\n") - .sort() - .join("\n"); - if (expectedCodecs) { - reformatted.details.additionalData.gfxData.codecSupport = rawActual => { - const actual = Object.entries(rawActual) - .map( - ([ - name, - { hardwareDecode, softwareDecode, hardwareEncode, softwareEncode }, - ]) => - ( - `${name} ` + - `${softwareDecode ? "SWDEC " : ""}` + - `${hardwareDecode ? "HWDEC " : ""}` + - `${softwareEncode ? "SWENC " : ""}` + - `${hardwareEncode ? "HWENC " : ""}` - ).trim() - ) - .sort() - .join("\n"); - return areObjectsEqual(actual, expectedCodecs); - }; + if (security) { + reformatted.details.additionalData.browserInfo.security = security; } if (blockList != "basic") { @@ -280,18 +235,19 @@ async function checkWebcompatComPayload( const { additionalData } = details; ok(message.url?.length, "Got a URL"); ok(["basic", "strict"].includes(details.blockList), "Got a blockList"); - ok(additionalData.applicationName?.length, "Got an app name"); - ok(additionalData.osArchitecture?.length, "Got an OS arch"); - ok(additionalData.osName?.length, "Got an OS name"); - ok(additionalData.osVersion?.length, "Got an OS version"); - ok(additionalData.version?.length, "Got an app version"); + const { app } = additionalData.browserInfo; + ok(app.applicationName?.length, "Got an app name"); + ok(app.osArchitecture?.length, "Got an OS arch"); + ok(app.osName?.length, "Got an OS name"); + ok(app.osVersion?.length, "Got an OS version"); + ok(app.version?.length, "Got an app version"); ok(details.channel?.length, "Got an app channel"); ok(details.defaultUserAgent?.length, "Got a default UA string"); - ok(additionalData.finalUserAgent?.length, "Got a final UA string"); + ok(additionalData.tabInfo.useragentString?.length, "Got a final UA string"); // If we're sending any tab-specific data (which includes console logs), // check that there is also a valid screenshot. - if ("consoleLog" in details) { + if (details.consoleLog) { const isScreenshotValid = await new Promise(done => { var image = new Image(); image.onload = () => done(image.width > 0); diff --git a/browser/locales/en-US/browser/reportBrokenSite.ftl b/browser/locales/en-US/browser/reportBrokenSite.ftl @@ -65,3 +65,9 @@ report-broken-site-panel-missing-reason-label = Please choose a reason report-broken-site-panel-blocked-trackers-label = .label = Send URLs blocked by tracking protection report-broken-site-panel-blocked-trackers-description = Enhanced Tracking Protection may block trackers and scripts that some websites need to work properly. + +report-broken-site-panel-preview-button = + .label = Preview report +report-broken-site-panel-preview-header = + .label = Preview report +report-broken-site-panel-button-copy = Copy diff --git a/browser/themes/shared/customizableui/panelUI-shared.css b/browser/themes/shared/customizableui/panelUI-shared.css @@ -2155,6 +2155,8 @@ radiogroup:focus-visible > .subviewradio[focused="true"] { } .report-broken-site-view { + max-height: 68vh; + description, label, menulist, @@ -2163,6 +2165,120 @@ radiogroup:focus-visible > .subviewradio[focused="true"] { margin: 0; } + > .panel-subview-body { + toolbarbutton { + margin-inline: calc(-1 * var(--arrowpanel-menuitem-padding-inline)); + } + + toolbarbutton::after { + position: absolute; + inset-inline-end: calc(2 * var(--arrowpanel-menuitem-padding-inline)); + } + } + + #report-broken-site-panel-preview-items { + margin: var(--arrowpanel-menuitem-padding); + display: flex; + flex-direction: column; + align-items: flex-start; + border: var(--border-width) solid var(--border-color-card); + border-radius: var(--border-radius-medium); + background-color: var(--background-color-box); + box-sizing: border-box; + + summary { + cursor: pointer; + padding-block: var(--space-large); + } + + .data { + padding: var(--arrowpanel-menuitem-padding); + background: + linear-gradient(0deg, color-mix(in srgb, currentColor 7%, transparent) 0, transparent 16px), + linear-gradient(180deg, color-mix(in srgb, currentColor 7%, transparent) 0, transparent 16px); + overflow: auto; + + .entry { + text-indent: var(--space-large) hanging; + } + + .entry > span:first-child { + margin-inline-end: var(--space-small); + } + } + + details { + & { + width: 100%; + } + + &:not(:first-of-type) > summary { + border-top: 1px solid var(--border-color-card); + } + + > summary { + list-style: none; + cursor: pointer; + width: -moz-available; + padding: var(--arrowpanel-menuitem-padding); + padding-inline-start: calc(16px + calc(2 * var(--arrowpanel-menuitem-padding-block))); + background-image: url("chrome://global/skin/icons/arrow-down.svg"); + background-position: var(--arrowpanel-menuitem-padding-block) var(--arrowpanel-menuitem-padding-inline); + background-repeat: no-repeat; + -moz-context-properties: fill; + fill: currentColor; + } + + &[open] > summary { + background-image: url("chrome://global/skin/icons/arrow-up.svg"); + } + } + + details > summary { + color: var(--button-text-color); + background-color: var(--button-background-color-ghost); + + &:hover { + background-color: var(--button-background-color-hover); + color: var(--button-text-color-hover); + } + + &:active { + background-color: var(--button-background-color-active); + color: var(--button-text-color-active); + } + + &:focus-visible { + outline: var(--focus-outline); + } + + @media (prefers-contrast) { + outline: var(--button-border-color) solid var(--border-width); + } + } + + details > summary { + border-radius: calc(var(--border-radius-medium) - var(--border-width)); + } + + details:not(:first-of-type):not(:last-of-type) > summary { + border-radius: 0; + } + + details:first-of-type, + details:last-of-type[open] { + > summary { + border-end-end-radius: 0; + border-end-start-radius: 0; + } + } + + details:last-of-type > summary { + border-start-start-radius: 0; + border-start-end-radius: 0; + } + } + p { line-height: 1.5; } @@ -2171,7 +2287,7 @@ radiogroup:focus-visible > .subviewradio[focused="true"] { margin-block-start: var(--space-large); } - vbox > p { + #report-broken-site-popup-blocked-trackers > p { font-size: 0.9em; color: var(--text-color-deemphasized); line-height: 1.2em; @@ -2213,7 +2329,11 @@ radiogroup:focus-visible > .subviewradio[focused="true"] { } a { - margin-top: 1.5em; + margin-block: 1.5em 1em; + } + + hbox:has(a#report-broken-site-popup-send-more-info-link[hidden]) + #report-broken-site-popup-preview-button { + margin-block-start: 1.5em; } } diff --git a/mobile/android/fenix/app/metrics.yaml b/mobile/android/fenix/app/metrics.yaml @@ -13934,6 +13934,21 @@ broken_site_report.browser_info.security: - broken-site-report webcompatreporting: + previewed: + type: event + description: | + The user clicked the report preview button. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1992050 + data_reviews: + - https://phabricator.services.mozilla.com/D267154 + data_sensitivity: + - interaction + notification_emails: + - twisniewski@mozilla.com + - android-probes@mozilla.com + - webcompat-reporting-tool-telemetry@mozilla.com + expires: never reason_dropdown: type: string description: | diff --git a/toolkit/components/reportbrokensite/ReportBrokenSiteChild.sys.mjs b/toolkit/components/reportbrokensite/ReportBrokenSiteChild.sys.mjs @@ -217,24 +217,261 @@ const FrameworkDetector = { }; export class ReportBrokenSiteChild extends JSWindowActorChild { + async #getBrokenSiteReport(docShell) { + let consoleLog = []; + try { + consoleLog = await this.#getConsoleLogs(docShell); + } catch (_) {} + + const { frameworks, languages, userAgent, url } = + this.#getInfoFromChild(docShell); + + const { antitracking, browser, devicePixelRatio, screenshot } = + await this.sendQuery( + "GetWebcompatInfoFromParentProcess", + SCREENSHOT_FORMAT + ); + + const reportData = { + tabInfo: { + consoleLog: { + value: consoleLog, + do_not_preview: true, + // Only sent to webcompat.com with send more info, not with Glean. + }, + languages: { + value: languages, + glean: "tabInfo", + }, + screenshot: { + value: screenshot, + do_not_preview: true, + // Binary data not sent by Glean + }, + url: { + value: url, + do_not_preview: true, + // Duplicate value used only for sanity-checking. + }, + useragentString: { + value: userAgent, + glean: "tabInfo", + }, + }, + graphics: { + devicePixelRatio: { + value: devicePixelRatio, + glean: "browserInfo.graphics", + }, + devices: { + json: true, + value: browser.graphics.devices, + glean: "browserInfo.graphics", + }, + drivers: { + json: true, + value: browser.graphics.drivers, + glean: "browserInfo.graphics", + }, + features: { + json: true, + value: browser.graphics.features, + glean: "browserInfo.graphics", + }, + hasTouchScreen: { + value: browser.graphics.hasTouchScreen, + glean: "browserInfo.graphics", + }, + monitors: { + json: true, + value: browser.graphics.monitors, + glean: "browserInfo.graphics", + }, + }, + antitracking: { + blockList: { + value: antitracking.blockList, + glean: "tabInfo.antitracking", + }, + blockedOrigins: { + value: antitracking.blockedOrigins, + glean: "tabInfo.antitracking", + }, + isPrivateBrowsing: { + value: antitracking.isPrivateBrowsing, + glean: "tabInfo.antitracking", + }, + hasMixedActiveContentBlocked: { + value: antitracking.hasMixedActiveContentBlocked, + glean: "tabInfo.antitracking", + }, + hasMixedDisplayContentBlocked: { + value: antitracking.hasMixedDisplayContentBlocked, + glean: "tabInfo.antitracking", + }, + hasTrackingContentBlocked: { + value: antitracking.hasTrackingContentBlocked, + glean: "tabInfo.antitracking", + }, + btpHasPurgedSite: { + value: antitracking.btpHasPurgedSite, + glean: "tabInfo.antitracking", + }, + etpCategory: { + value: antitracking.etpCategory, + glean: "tabInfo.antitracking", + }, + }, + frameworks: { + fastclick: { + value: frameworks.fastclick, + glean: "tabInfo.frameworks", + }, + marfeel: { + value: frameworks.marfeel, + glean: "tabInfo.frameworks", + }, + mobify: { + value: frameworks.mobify, + glean: "tabInfo.frameworks", + }, + }, + browserInfo: { + addons: { + value: browser.addons, + glean: "browserInfo", + }, + experiments: { + value: browser.experiments, + glean: "browserInfo", + }, + }, + app: { + applicationName: { + value: browser.app.applicationName, + // Gleans sends this for us in the base ping + }, + buildId: { + value: browser.app.buildId, + // Gleans sends this for us in the base ping + }, + defaultLocales: { + value: browser.locales, + glean: "browserInfo.app", + }, + defaultUseragentString: { + value: browser.app.defaultUserAgent, + glean: "browserInfo.app", + }, + fissionEnabled: { + value: browser.platform.fissionEnabled, + glean: "browserInfo.app", + }, + platform: { + do_not_preview: true, + value: browser.platform.name, + // Gleans sends this for us in the base ping + }, + updateChannel: { + value: browser.app.updateChannel, + // Gleans sends this for us in the base ping + }, + version: { + value: browser.app.version, + // Gleans sends this for us in the base ping + }, + }, + system: { + isTablet: { + value: browser.platform.isTablet ?? false, + glean: "browserInfo.system", + }, + memory: { + value: browser.platform.memoryMB, + glean: "browserInfo.system", + }, + osArchitecture: { + value: browser.platform.osArchitecture, + // Gleans sends this for us in the base ping + }, + osName: { + value: browser.platform.osName, + // Gleans sends this for us in the base ping + }, + osVersion: { + value: browser.platform.osVersion, + // Gleans sends this for us in the base ping + }, + }, + prefs: {}, + }; + + for (const [label, pref] of Object.entries({ + cookieBehavior: "network.cookie.cookieBehavior", + forcedAcceleratedLayers: "layers.acceleration.force-enabled", + globalPrivacyControlEnabled: "privacy.globalprivacycontrol.enabled", + installtriggerEnabled: "extensions.InstallTrigger.enabled", + opaqueResponseBlocking: "browser.opaqueResponseBlocking", + resistFingerprintingEnabled: "privacy.resistFingerprinting", + softwareWebrender: "gfx.webrender.software", + thirdPartyCookieBlockingEnabled: + "network.cookie.cookieBehavior.optInPartitioning", + thirdPartyCookieBlockingEnabledInPbm: + "network.cookie.cookieBehavior.optInPartitioning.pbmode", + })) { + const value = browser.prefs[pref]; + if (value !== undefined) { + reportData.prefs[label] = { + value, + glean: "browserInfo.prefs", + }; + } + } + + if (browser.security) { + const actuallySet = {}; + for (const name of ["antispyware", "antivirus", "firewall"]) { + if (browser.security[name]?.length) { + actuallySet[name] = { + value: browser.security[name], + glean: "browserInfo.security", + }; + } + } + if (Object.keys(actuallySet).length) { + reportData.security = actuallySet; + } + } + + return reportData; + } + + #getInfoFromChild(docShell) { + const win = docShell.domWindow; + + const frameworks = FrameworkDetector.checkWindow(win); + const { languages, userAgent } = win.navigator; + + return { + frameworks, + languages, + url: win.location.href, + userAgent, + }; + } + #getWebCompatInfo(docShell) { return Promise.all([ this.#getConsoleLogs(docShell), this.sendQuery("GetWebcompatInfoFromParentProcess", SCREENSHOT_FORMAT), ]) .then(([consoleLog, infoFromParent]) => { + const { frameworks, languages, userAgent, url } = + this.#getInfoFromChild(docShell); + const { antitracking, browser, devicePixelRatio, screenshot } = infoFromParent; - const win = docShell.domWindow; - - const frameworks = FrameworkDetector.checkWindow(win); - const { languages, userAgent } = win.navigator; - - if (browser.platform.name !== "linux") { - delete browser.prefs["layers.acceleration.force-enabled"]; - } - return { antitracking, browser, @@ -243,7 +480,7 @@ export class ReportBrokenSiteChild extends JSWindowActorChild { frameworks, languages, screenshot, - url: win.location.href, + url, userAgent, }; }) @@ -292,112 +529,51 @@ export class ReportBrokenSiteChild extends JSWindowActorChild { }; if (webcompatInfo) { - const { - antitracking, - browser, - devicePixelRatio, - consoleLog, - frameworks, - languages, - screenshot, - url, - userAgent, - } = webcompatInfo; - - const { - blockList, - blockedOrigins, - isPrivateBrowsing, - hasMixedActiveContentBlocked, - hasMixedDisplayContentBlocked, - hasTrackingContentBlocked, - btpHasPurgedSite, - etpCategory, - } = antitracking; - - message.blockList = blockList; - - const { - addons, - app, - experiments, - graphics, - locales, - prefs, - platform, - security, - } = browser; - - const { - applicationName, - buildId, - defaultUserAgent, - updateChannel, - version, - } = app; - - const { - fissionEnabled, - memoryMB, - osArchitecture, - osName, - osVersion, - device, - isTablet, - } = platform; - - const additionalData = { - addons, - applicationName, - blockList, - blockedOrigins, - buildId, - devicePixelRatio, - experiments, - finalUserAgent: userAgent, - fissionEnabled, - gfxData: graphics, - hasMixedActiveContentBlocked, - hasMixedDisplayContentBlocked, - hasTrackingContentBlocked, - btpHasPurgedSite, - isPB: isPrivateBrowsing, - etpCategory, - languages, - locales, - memoryMB, - osArchitecture, - osName, - osVersion, - prefs, - version, - }; - if (security !== undefined && Object.keys(security).length) { - additionalData.sec = security; - } - if (device !== undefined) { - additionalData.device = device; - } - if (isTablet !== undefined) { - additionalData.isTablet = isTablet; + // Copy the full report data into additionalData, reformatting it nicely. + const additionalData = {}; + for (const category of Object.values(webcompatInfo)) { + for (const [name, { do_not_preview, glean, value }] of Object.entries( + category + )) { + if (do_not_preview) { + continue; + } + let target = additionalData; + for (const step of (glean ?? "browserInfo.app").split(".")) { + target[step] ??= {}; + target = target[step]; + } + target[name] = value; + } } - const specialPrefs = {}; - for (const pref of [ - "layers.acceleration.force-enabled", - "gfx.webrender.software", - ]) { - specialPrefs[pref] = prefs[pref]; - } + const { browserInfo, tabInfo } = additionalData; + const { app, graphics } = browserInfo; + const { antitracking, frameworks } = tabInfo; + const { blockList } = antitracking; + + const consoleLog = webcompatInfo.tabInfo.consoleLog.value; + const screenshot = webcompatInfo.tabInfo.screenshot.value; + const url = webcompatInfo.tabInfo.url.value; - const details = Object.assign(message.details, specialPrefs, { + message.blockList = blockList; + const details = Object.assign(message.details, { additionalData, blockList, - channel: updateChannel, - defaultUserAgent, - hasTouchScreen: browser.graphics.hasTouchScreen, + channel: app.updateChannel, + defaultUserAgent: app.defaultUseragentString, + "gfx.webrender.software": webcompatInfo.prefs.softwareWebrender.value, + hasTouchScreen: graphics.hasTouchScreen, }); + // We only care about this pref on Linux right now on webcompat.com. + if (webcompatInfo.app.platform.value === "linux") { + details["layers.acceleration.force-enabled"] = + webcompatInfo.prefs.forcedAcceleratedLayers.value; + } else { + delete details.additionalData.browserInfo.prefs.forcedAcceleratedLayers; + } + // If the user enters a URL unrelated to the current tab, // don't bother sending a screenshot or logs/etc let sendRecordedPageSpecificDetails = false; @@ -420,17 +596,15 @@ export class ReportBrokenSiteChild extends JSWindowActorChild { antitracking.hasMixedDisplayContentBlocked; details["tracking content blocked"] = antitracking.hasTrackingContentBlocked - ? `true (${antitracking.blockList})` + ? `true (${blockList})` : "false"; details["btp has purged site"] = antitracking.btpHasPurgedSite; if (antitracking.hasTrackingContentBlocked) { - extra_labels.push( - `type-tracking-protection-${antitracking.blockList}` - ); + extra_labels.push(`type-tracking-protection-${blockList}`); } - for (const [framework, active] of Object.entries(frameworks)) { + for (const [framework, active] of Object.entries(tabInfo.frameworks)) { if (!active) { continue; } @@ -484,6 +658,9 @@ export class ReportBrokenSiteChild extends JSWindowActorChild { } return null; } + case "GetBrokenSiteReport": { + return this.#getBrokenSiteReport(docShell); + } case "GetWebCompatInfo": { return this.#getWebCompatInfo(docShell); } diff --git a/toolkit/components/reportbrokensite/metrics.yaml b/toolkit/components/reportbrokensite/metrics.yaml @@ -796,6 +796,21 @@ webcompatreporting: * `hamburgerMenu` * `ETPShieldIconMenu` type: string + previewed: + type: event + description: | + The user clicked the report preview button. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1992050 + data_reviews: + - https://phabricator.services.mozilla.com/D267154 + data_sensitivity: + - interaction + notification_emails: + - twisniewski@mozilla.com + - android-probes@mozilla.com + - webcompat-reporting-tool-telemetry@mozilla.com + expires: never reason_dropdown: type: event description: >