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 @@
@@ -172,7 +172,7 @@
-
+
@@ -181,7 +181,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,
},
};