From cafcffa6a9e78618cd2812b98b7df4902b1925d7 Mon Sep 17 00:00:00 2001 From: Erica Wright Date: Thu, 8 Aug 2019 18:53:41 +0000 Subject: [PATCH] Bug 1557050 - Add basic telemetry to protection report. r=mtigley,johannh Differential Revision: https://phabricator.services.mozilla.com/D39750 --HG-- extra : moz-landing-system : lando --- browser/app/profile/firefox.js | 2 + .../about/AboutProtectionsHandler.jsm | 4 + .../protections/content/lockwise-card.js | 17 ++ .../protections/content/monitor-card.js | 23 +- .../protections/content/protections.html | 14 +- .../protections/content/protections.js | 16 + .../protections/test/browser/browser.ini | 2 + .../browser/browser_protections_telemetry.js | 285 ++++++++++++++++++ .../remotepagemanager/MessagePort.jsm | 29 +- .../RemotePageManagerChild.jsm | 3 + toolkit/components/telemetry/Events.yaml | 63 ++++ .../lib/environments/frame-script.js | 1 + 12 files changed, 450 insertions(+), 9 deletions(-) create mode 100644 browser/components/protections/test/browser/browser_protections_telemetry.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 3962401bd5ec..55c6c9cf99f6 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1610,6 +1610,8 @@ pref("browser.contentblocking.report.lockwise.enabled", true); // Enable Protections report's Monitor card by default. pref("browser.contentblocking.report.monitor.enabled", true); +pref("browser.contentblocking.report.monitor.url", "https://monitor.firefox.com"); +pref("browser.contentblocking.report.lockwise.url", "https://lockwise.firefox.com/"); // Enables the new Protections Panel. #ifdef NIGHTLY_BUILD diff --git a/browser/components/about/AboutProtectionsHandler.jsm b/browser/components/about/AboutProtectionsHandler.jsm index e42f8b28bc62..4af1e3f9c5db 100644 --- a/browser/components/about/AboutProtectionsHandler.jsm +++ b/browser/components/about/AboutProtectionsHandler.jsm @@ -78,6 +78,10 @@ var AboutProtectionsHandler = { for (let topic of this._topics) { this.pageListener.addMessageListener(topic, this.receiveMessage); } + Services.telemetry.setEventRecordingEnabled( + "security.ui.protections", + true + ); this._inited = true; }, diff --git a/browser/components/protections/content/lockwise-card.js b/browser/components/protections/content/lockwise-card.js index 5d3dc022a91e..b94e8a5a4e92 100644 --- a/browser/components/protections/content/lockwise-card.js +++ b/browser/components/protections/content/lockwise-card.js @@ -4,6 +4,11 @@ /* eslint-env mozilla/frame-script */ +const LOCKWISE_URL = RPMGetStringPref( + "browser.contentblocking.report.lockwise.url", + "" +); + export default class LockwiseCard { constructor(document) { this.doc = document; @@ -17,15 +22,27 @@ export default class LockwiseCard { "open-about-logins-button" ); openAboutLoginsButton.addEventListener("click", () => { + this.doc.sendTelemetryEvent("click", "lw_open_button"); RPMSendAsyncMessage("OpenAboutLogins"); }); const syncLink = this.doc.querySelector(".synced-devices-text a"); // Register a click handler for the anchor since it's not possible to navigate to about:preferences via href syncLink.addEventListener("click", () => { + this.doc.sendTelemetryEvent("click", "lw_app_link"); RPMSendAsyncMessage("OpenSyncPreferences"); }); + const lockwiseAppLink = this.doc.getElementById("lockwise-inline-link"); + lockwiseAppLink.href = LOCKWISE_URL; + lockwiseAppLink.addEventListener("click", () => { + this.doc.sendTelemetryEvent("click", "lw_sync_link"); + }); + const lockwiseReportLink = this.doc.getElementById("lockwise-how-it-works"); + lockwiseReportLink.addEventListener("click", () => { + this.doc.sendTelemetryEvent("click", "lw_about_link"); + }); + RPMAddMessageListener("SendUserLoginsData", ({ data }) => { // Once data for the user is retrieved, display the lockwise card. this.buildContent(data); diff --git a/browser/components/protections/content/monitor-card.js b/browser/components/protections/content/monitor-card.js index 5e981e3f3015..ca4536f15822 100644 --- a/browser/components/protections/content/monitor-card.js +++ b/browser/components/protections/content/monitor-card.js @@ -4,7 +4,10 @@ /* eslint-env mozilla/frame-script */ -const MONITOR_SIGN_IN_URL = "https://monitor.firefox.com"; +const MONITOR_SIGN_IN_URL = RPMGetStringPref( + "browser.contentblocking.report.monitor.url", + "" +); export default class MonitorClass { constructor(document) { @@ -17,6 +20,21 @@ export default class MonitorClass { this.getMonitorData(data); RPMSendAsyncMessage("FetchMonitorData"); }); + + let monitorReportLink = this.doc.getElementById("full-report-link"); + monitorReportLink.addEventListener("click", () => { + this.doc.sendTelemetryEvent("click", "mtr_report_link"); + }); + + let monitorAboutLink = this.doc.getElementById("monitor-link"); + monitorAboutLink.addEventListener("click", () => { + this.doc.sendTelemetryEvent("click", "mtr_about_link"); + }); + + let openLockwise = this.doc.getElementById("lockwise-link"); + openLockwise.addEventListener("click", () => { + this.doc.sendTelemetryEvent("click", "lw_open_breach_link"); + }); } /** @@ -58,6 +76,9 @@ export default class MonitorClass { signUpForMonitorLink.href = this.buildMonitorUrl(monitorData.userEmail); signUpForMonitorLink.setAttribute("data-l10n-id", "monitor-sign-up"); headerContent.setAttribute("data-l10n-id", "monitor-header-content"); + signUpForMonitorLink.addEventListener("click", () => { + this.doc.sendTelemetryEvent("click", "mtr_signup_button"); + }); } } diff --git a/browser/components/protections/content/protections.html b/browser/components/protections/content/protections.html index e8c25a51a6d9..a71d392ad0b4 100644 --- a/browser/components/protections/content/protections.html +++ b/browser/components/protections/content/protections.html @@ -91,7 +91,7 @@ - +

@@ -129,15 +129,15 @@ -
- +
@@ -163,7 +163,7 @@ diff --git a/browser/components/protections/content/protections.js b/browser/components/protections/content/protections.js index 5cbff5947dd3..a1738f03ff72 100644 --- a/browser/components/protections/content/protections.js +++ b/browser/components/protections/content/protections.js @@ -7,6 +7,11 @@ import LockwiseCard from "./lockwise-card.js"; import MonitorCard from "./monitor-card.js"; +// We need to send the close telemetry before unload while we still have a connection to RPM. +window.addEventListener("beforeunload", () => { + document.sendTelemetryEvent("close", "protection_report"); +}); + document.addEventListener("DOMContentLoaded", e => { let todayInMs = Date.now(); let weekAgoInMs = todayInMs - 7 * 24 * 60 * 60 * 1000; @@ -53,6 +58,17 @@ document.addEventListener("DOMContentLoaded", e => { legend.style.gridTemplateAreas = "'social cookie tracker fingerprinter cryptominer'"; + document.sendTelemetryEvent = (action, object) => { + // eslint-disable-next-line no-undef + // eslint-disable-next-line no-undef + RPMRecordTelemetryEvent("security.ui.protections", action, object, "", { + category: cbCategory, + }); + }; + + // Send telemetry on arriving and closing this page + document.sendTelemetryEvent("show", "protection_report"); + let createGraph = data => { // All of our dates are recorded as 00:00 GMT, add 12 hours to the timestamp // to ensure we display the correct date no matter the user's location. diff --git a/browser/components/protections/test/browser/browser.ini b/browser/components/protections/test/browser/browser.ini index 17b099998530..c567bf2a5fce 100644 --- a/browser/components/protections/test/browser/browser.ini +++ b/browser/components/protections/test/browser/browser.ini @@ -6,3 +6,5 @@ support-files = [browser_protections_lockwise.js] [browser_protections_monitor.js] [browser_protections_report_ui.js] +[browser_protections_telemetry.js] +skip-if = true # see Bug 1572188 diff --git a/browser/components/protections/test/browser/browser_protections_telemetry.js b/browser/components/protections/test/browser/browser_protections_telemetry.js new file mode 100644 index 000000000000..a835f40f3211 --- /dev/null +++ b/browser/components/protections/test/browser/browser_protections_telemetry.js @@ -0,0 +1,285 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.contentblocking.database.enabled", true], + ["browser.contentblocking.report.monitor.enabled", true], + ["browser.contentblocking.report.lockwise.enabled", true], + ["browser.contentblocking.report.proxy.enabled", true], + // Change the endpoints to prevent non-local network connections when landing on the page. + ["browser.contentblocking.report.monitor.url", ""], + ["browser.contentblocking.report.lockwise.url", ""], + ], + }); + + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + registerCleanupFunction(() => { + Services.telemetry.canRecordExtended = oldCanRecord; + }); +}); + +add_task(async function checkTelemetryLoadEvents() { + // There's an arbitrary interval of 2 seconds in which the content + // processes sync their event data with the parent process, we wait + // this out to ensure that we clear everything that is left over from + // previous tests and don't receive random events in the middle of our tests. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(c => setTimeout(c, 2000)); + + // Clear everything. + Services.telemetry.clearEvents(); + await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + return !events || !events.length; + }); + + Services.telemetry.setEventRecordingEnabled("security.ui.protections", true); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + let loadEvents = await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + if (events && events.length) { + events = events.filter( + e => e[1] == "security.ui.protections" && e[2] == "show" + ); + if (events.length == 1) { + return events; + } + } + return null; + }, "recorded telemetry for showing the report"); + + is(loadEvents.length, 1, `recorded telemetry for showing the report`); + await reloadTab(tab); + loadEvents = await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + if (events && events.length) { + events = events.filter( + e => e[1] == "security.ui.protections" && e[2] == "close" + ); + if (events.length == 1) { + return events; + } + } + return null; + }, "recorded telemetry for closing the report"); + + is(loadEvents.length, 1, `recorded telemetry for closing the report`); + + await BrowserTestUtils.removeTab(tab); +}); + +function waitForTelemetryEventCount(count) { + info("waiting for telemetry event count of " + count); + return TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ).content; + info("got " + (events && events.length) + " events"); + if (events && events.length == count) { + return events; + } + return null; + }, "waiting for telemetry event count of: " + count); +} + +add_task(async function checkTelemetryClickEvents() { + // There's an arbitrary interval of 2 seconds in which the content + // processes sync their event data with the parent process, we wait + // this out to ensure that we clear everything that is left over from + // previous tests and don't receive random events in the middle of our tests. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(c => setTimeout(c, 2000)); + + // Clear everything. + Services.telemetry.clearEvents(); + await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + return !events || !events.length; + }); + + Services.telemetry.setEventRecordingEnabled("security.ui.protections", true); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + await ContentTask.spawn(tab.linkedBrowser, {}, async function() { + // Show all elements, so we can click on them, even though our user is not logged in. + let hidden_elements = content.document.querySelectorAll(".hidden"); + for (let el of hidden_elements) { + el.style.display = "block "; + } + + const syncLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("turn-on-sync"); + }, "syncLink exists"); + + syncLink.click(); + }); + + let events = await waitForTelemetryEventCount(2); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "lw_app_link" + ); + is(events.length, 1, `recorded telemetry for lw_app_link`); + + await ContentTask.spawn(tab.linkedBrowser, {}, async function() { + const openAboutLogins = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("open-about-logins-button"); + }, "openAboutLogins exists"); + + openAboutLogins.click(); + }); + + events = await waitForTelemetryEventCount(3); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "lw_open_button" + ); + is(events.length, 1, `recorded telemetry for lw_open_button`); + + await ContentTask.spawn(tab.linkedBrowser, {}, async function() { + const lockwiseAppLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("lockwise-inline-link"); + }, "lockwiseAppLink exists"); + + lockwiseAppLink.click(); + }); + + events = await waitForTelemetryEventCount(4); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "lw_sync_link" + ); + is(events.length, 1, `recorded telemetry for lw_sync_link`); + + await ContentTask.spawn(tab.linkedBrowser, {}, async function() { + const lockwiseReportLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("lockwise-how-it-works"); + }, "lockwiseReportLink exists"); + + lockwiseReportLink.click(); + }); + + events = await waitForTelemetryEventCount(5); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "lw_about_link" + ); + is(events.length, 1, `recorded telemetry for lw_about_link`); + + await ContentTask.spawn(tab.linkedBrowser, {}, async function() { + let openLockwise = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("lockwise-link"); + }, "openLockwise exists"); + + openLockwise.click(); + }); + + events = await waitForTelemetryEventCount(6); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "lw_open_breach_link" + ); + is(events.length, 1, `recorded telemetry for lw_open_breach_link`); + + await ContentTask.spawn(tab.linkedBrowser, {}, async function() { + let monitorReportLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("monitor-inline-link"); + }, "monitorReportLink exists"); + + monitorReportLink.click(); + }); + + events = await waitForTelemetryEventCount(7); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_report_link" + ); + is(events.length, 1, `recorded telemetry for mtr_report_link`); + + await ContentTask.spawn(tab.linkedBrowser, {}, async function() { + let monitorAboutLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("monitor-link"); + }, "monitorAboutLink exists"); + + monitorAboutLink.click(); + }); + + events = await waitForTelemetryEventCount(8); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_about_link" + ); + is(events.length, 1, `recorded telemetry for mtr_about_link`); + + await ContentTask.spawn(tab.linkedBrowser, {}, async function() { + const signUpForMonitorLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("sign-up-for-monitor-link"); + }, "signUpForMonitorLink exists"); + + signUpForMonitorLink.click(); + }); + + events = await waitForTelemetryEventCount(9); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_signup_button" + ); + is(events.length, 1, `recorded telemetry for mtr_signup_button`); + + await BrowserTestUtils.removeTab(tab); + // We open two extra tabs with the click events. + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + await BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/toolkit/components/remotepagemanager/MessagePort.jsm b/toolkit/components/remotepagemanager/MessagePort.jsm index ecfdd027e674..c5823e02232b 100644 --- a/toolkit/components/remotepagemanager/MessagePort.jsm +++ b/toolkit/components/remotepagemanager/MessagePort.jsm @@ -62,7 +62,12 @@ let RPMAccessManager = { "browser.contentblocking.report.lockwise.enabled", "browser.contentblocking.report.monitor.enabled", ], - getStringPref: ["browser.contentblocking.category"], + getStringPref: [ + "browser.contentblocking.category", + "browser.contentblocking.report.lockwise.url", + "browser.contentblocking.report.monitor.url", + ], + recordTelemetryEvent: ["yes"], }, "about:newinstall": { getUpdateChannel: ["yes"], @@ -479,4 +484,26 @@ class MessagePort { return this.sendRequest("FxAccountsEndpoint", aEntrypoint); } + + recordTelemetryEvent(category, event, object, value, extra) { + let principal = this.window.document.nodePrincipal; + if ( + !RPMAccessManager.checkAllowAccess( + principal, + "recordTelemetryEvent", + "yes" + ) + ) { + throw new Error( + "RPMAccessManager does not allow access to recordTelemetryEvent" + ); + } + return Services.telemetry.recordEvent( + category, + event, + object, + value, + extra + ); + } } diff --git a/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm b/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm index fa484f5a3175..0b9a44742a9e 100644 --- a/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm +++ b/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm @@ -59,6 +59,9 @@ class ChildMessagePort extends MessagePort { Cu.exportFunction(this.getFxAccountsEndpoint.bind(this), window, { defineAs: "RPMGetFxAccountsEndpoint", }); + Cu.exportFunction(this.recordTelemetryEvent.bind(this), window, { + defineAs: "RPMRecordTelemetryEvent", + }); // Send a message for load events let loadListener = () => { diff --git a/toolkit/components/telemetry/Events.yaml b/toolkit/components/telemetry/Events.yaml index 338e4f2bba27..e981e508ec14 100644 --- a/toolkit/components/telemetry/Events.yaml +++ b/toolkit/components/telemetry/Events.yaml @@ -1494,6 +1494,69 @@ security.ui.certerror: 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.protections: + show: + objects: [ + "protection_report", + ] + bug_numbers: + - 1557050 + description: > + User arrived on the protection report. + expiry_version: "75" + record_in_processes: ["content"] + release_channel_collection: opt-out + notification_emails: + - chsiang@mozilla.com + - seceng-telemetry@mozilla.com + products: + - firefox + extra_keys: + category: The category of protections the user is in, standard, strict or custom. + close: + objects: [ + "protection_report", + ] + bug_numbers: + - 1557050 + description: > + User closed on the protection report. + expiry_version: "75" + record_in_processes: ["content"] + release_channel_collection: opt-out + notification_emails: + - chsiang@mozilla.com + - seceng-telemetry@mozilla.com + products: + - firefox + extra_keys: + category: The category of protections the user is in, standard, strict or custom. + click: + bug_numbers: + - 1557050 + description: > + User interaction by click events on the protection report. + objects: [ + "lw_app_link", + "lw_open_button", + "lw_sync_link", + "lw_about_link", + "lw_open_breach_link", + "mtr_report_link", + "mtr_about_link", + "mtr_signup_button", + ] + expiry_version: "75" + record_in_processes: ["content"] + release_channel_collection: opt-out + notification_emails: + - chsiang@mozilla.com + - seceng-telemetry@mozilla.com + products: + - firefox + extra_keys: + category: The category of protections the user is in, standard, strict or custom. + security.ui.identitypopup: open: objects: ["identity_popup"] diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js index 321fa0b49c6d..850c77b52c6f 100644 --- a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js @@ -34,6 +34,7 @@ module.exports = { RPMIsWindowPrivate: false, RPMSendAsyncMessage: false, RPMAddMessageListener: false, + RPMRecordTelemetryEvent: false, RPMRemoveMessageListener: false, }, };