Bug 1863022 - Render message from browser-siteProtections.js, replace messaging system telemetry, and migrate strings r=pdahiya,omc-reviewers,pbz,fluent-reviewers,flod

The protections panel message should show once when the panel is first opened; after that it will be collapsed by default and can be shown again by clicking the "info" button on the panel

Messaging system previously sent the following pings on message show, and when the "learn more" link was clicked:

```
{“message_id”:“PROTECTIONS_PANEL_1",“event”:“IMPRESSION”,“addon_version”:“20231106094018",“locale”:“en-US”,“client_id”:“6fabd2de-3d0a-4b11-be4c-86b0ea1a1144",“browser_session_id”:“4b0f34b1-75ef-4704-907e-18d84e5187c3",“pingType”:“whats-new-panel”}
```
and

```
{“message_id”:“PROTECTIONS_PANEL_1",“event”:“CLICK”,“addon_version”:“20231106094018",“locale”:“en-US”,“client_id”:“6fabd2de-3d0a-4b11-be4c-86b0ea1a1144",“browser_session_id”:“4b0f34b1-75ef-4704-907e-18d84e5187c3",“pingType”:“whats-new-panel”}
```

This patch replaces these pings with 'RecordEvents' telemetry on the `protectionsPopup` object:

```
33153 	security.ui.protectionspopup 	open 	protectionspopup_cfr 	impression 	{"message": "PROTECTIONS_PANEL_1"}
```
and

```
34932 	security.ui.protectionspopup 	click 	protectionspopup_cfr
```

Differential Revision: https://phabricator.services.mozilla.com/D192968
This commit is contained in:
Emily McMinn 2024-01-09 22:34:35 +00:00
parent 5a73b57894
commit a240571a59
9 changed files with 297 additions and 247 deletions

View File

@ -7,10 +7,8 @@
ChromeUtils.defineESModuleGetters(this, {
ContentBlockingAllowList:
"resource://gre/modules/ContentBlockingAllowList.sys.mjs",
});
XPCOMUtils.defineLazyModuleGetters(this, {
ToolbarPanelHub: "resource://activity-stream/lib/ToolbarPanelHub.jsm",
SpecialMessageActions:
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetter(
@ -1657,6 +1655,13 @@ var gProtectionsHandler = {
() => this.maybeSetMilestoneCounterText()
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"protectionsPanelMessageSeen",
"browser.protections_panel.infoMessage.seen",
false
);
for (let blocker of Object.values(this.blockers)) {
if (blocker.init) {
blocker.init();
@ -1813,7 +1818,7 @@ var gProtectionsHandler = {
// Insert the info message if needed. This will be shown once and then
// remain collapsed.
ToolbarPanelHub.insertProtectionPanelMessage(event);
this._insertProtectionsPanelInfoMessage(event);
if (!event.target.hasAttribute("toast")) {
Services.telemetry.recordEvent(
@ -2693,4 +2698,188 @@ var gProtectionsHandler = {
this._earliestRecordedDate = date;
}
},
_sendUserEventTelemetry(event, value = null, options = {}) {
// Only send telemetry for non private browsing windows
if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
Services.telemetry.recordEvent(
"security.ui.protectionspopup",
event,
"protectionspopup_cfr",
value,
options
);
}
},
/**
* Dispatch the action defined in the message and user telemetry event.
*/
_dispatchUserAction(message) {
let url;
try {
// Set platform specific path variables for SUMO articles
url = Services.urlFormatter.formatURL(message.content.cta_url);
} catch (e) {
console.error(e);
url = message.content.cta_url;
}
SpecialMessageActions.handleAction(
{
type: message.content.cta_type,
data: {
args: url,
where: message.content.cta_where || "tabshifted",
},
},
window.browser
);
this._sendUserEventTelemetry("click", "learn_more_link", {
message: message.id,
});
},
/**
* Attach event listener to dispatch message defined action.
*/
_attachCommandListener(element, message) {
// Add event listener for `mouseup` not to overlap with the
// `mousedown` & `click` events dispatched from PanelMultiView.sys.mjs
// https://searchfox.org/mozilla-central/rev/7531325c8660cfa61bf71725f83501028178cbb9/browser/components/customizableui/PanelMultiView.jsm#1830-1837
element.addEventListener("mouseup", () => {
this._dispatchUserAction(message);
});
element.addEventListener("keyup", e => {
if (e.key === "Enter" || e.key === " ") {
this._dispatchUserAction(message);
}
});
},
/**
* Inserts a message into the Protections Panel. The message is visible once
* and afterwards set in a collapsed state. It can be shown again using the
* info button in the panel header.
*/
_insertProtectionsPanelInfoMessage(event) {
// const PROTECTIONS_PANEL_INFOMSG_PREF =
// "browser.protections_panel.infoMessage.seen";
const message = {
id: "PROTECTIONS_PANEL_1",
content: {
title: { string_id: "cfr-protections-panel-header" },
body: { string_id: "cfr-protections-panel-body" },
link_text: { string_id: "cfr-protections-panel-link-text" },
cta_url: `${Services.urlFormatter.formatURLPref(
"app.support.baseURL"
)}etp-promotions?as=u&utm_source=inproduct`,
cta_type: "OPEN_URL",
},
};
const doc = event.target.ownerDocument;
const container = doc.getElementById("messaging-system-message-container");
const infoButton = doc.getElementById("protections-popup-info-button");
const panelContainer = doc.getElementById("protections-popup");
const toggleMessage = () => {
const learnMoreLink = doc.querySelector(
"#messaging-system-message-container .text-link"
);
if (learnMoreLink) {
container.toggleAttribute("disabled");
infoButton.toggleAttribute("checked");
panelContainer.toggleAttribute("infoMessageShowing");
learnMoreLink.disabled = !learnMoreLink.disabled;
}
// If the message panel is opened, send impression telemetry
if (panelContainer.hasAttribute("infoMessageShowing")) {
this._sendUserEventTelemetry("open", "impression", {
message: message.id,
});
}
};
if (!container.childElementCount) {
const messageEl = this._createHeroElement(doc, message);
container.appendChild(messageEl);
infoButton.addEventListener("click", toggleMessage);
}
// Message is collapsed by default. If it was never shown before we want
// to expand it
if (
!this.protectionsPanelMessageSeen &&
container.hasAttribute("disabled")
) {
toggleMessage(message);
}
// Save state that we displayed the message
if (!this.protectionsPanelMessageSeen) {
Services.prefs.setBoolPref(
"browser.protections_panel.infoMessage.seen",
true
);
}
// Collapse the message after the panel is hidden so we don't get the
// animation when opening the panel
panelContainer.addEventListener(
"popuphidden",
() => {
if (
this.protectionsPanelMessageSeen &&
!container.hasAttribute("disabled")
) {
toggleMessage(message);
}
},
{
once: true,
}
);
},
_createElement(doc, elem, options = {}) {
const node = doc.createElementNS("http://www.w3.org/1999/xhtml", elem);
if (options.classList) {
node.classList.add(options.classList);
}
if (options.content) {
doc.l10n.setAttributes(node, options.content.string_id);
}
return node;
},
_createHeroElement(doc, message) {
const messageEl = this._createElement(doc, "div");
messageEl.setAttribute("id", "protections-popup-message");
messageEl.classList.add("whatsNew-hero-message");
const wrapperEl = this._createElement(doc, "div");
wrapperEl.classList.add("whatsNew-message-body");
messageEl.appendChild(wrapperEl);
wrapperEl.appendChild(
this._createElement(doc, "h2", {
classList: "whatsNew-message-title",
content: message.content.title,
})
);
wrapperEl.appendChild(
this._createElement(doc, "p", { content: message.content.body })
);
if (message.content.link_text) {
let linkEl = this._createElement(doc, "a", {
classList: "text-link",
content: message.content.link_text,
});
linkEl.disabled = true;
wrapperEl.appendChild(linkEl);
this._attachCommandListener(linkEl, message);
} else {
this._attachCommandListener(wrapperEl, message);
}
return messageEl;
},
};

View File

@ -47,6 +47,65 @@ async function clickToggle(toggle) {
await changed;
}
add_task(async function testPanelInfoMessage() {
const PROTECTIONS_PANEL_INFOMSG_PREF =
"browser.protections_panel.infoMessage.seen";
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
TRACKING_PAGE
);
// Set the infomessage pref to ensure the message is displayed every time
Services.prefs.setBoolPref(PROTECTIONS_PANEL_INFOMSG_PREF, false);
await openProtectionsPanel();
await BrowserTestUtils.waitForMutationCondition(
gProtectionsHandler._protectionsPopup,
{ attributes: true, attributeFilter: ["infoMessageShowing"] },
() =>
!gProtectionsHandler._protectionsPopup.hasAttribute("infoMessageShowing")
);
// Test that the info message is displayed when the panel opens
let container = document.getElementById("messaging-system-message-container");
let message = document.getElementById("protections-popup-message");
let learnMoreLink = document.querySelector(
"#messaging-system-message-container .text-link"
);
// Check the visibility of the info message.
ok(
BrowserTestUtils.is_visible(container),
"The message container should exist."
);
ok(BrowserTestUtils.is_visible(message), "The message should be visible.");
ok(BrowserTestUtils.is_visible(learnMoreLink), "The link should be visible.");
// Check telemetry for the info message
let events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).parent;
let messageEvents = events.filter(
e =>
e[1] == "security.ui.protectionspopup" &&
e[2] == "open" &&
e[3] == "protectionspopup_cfr" &&
e[4] == "impression"
);
is(
messageEvents.length,
1,
"recorded telemetry for showing the info message"
);
Services.telemetry.clearEvents();
BrowserTestUtils.removeTab(tab);
});
add_task(async function testToggleSwitch() {
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,

View File

@ -170,6 +170,12 @@ module.exports = function (config) {
functions: 0,
branches: 0,
},
"lib/ToolbarPanelHub.jsm": {
statements: 88,
lines: 88,
functions: 94,
branches: 84,
},
"lib/*.jsm": {
statements: 100,
lines: 100,

View File

@ -50,8 +50,6 @@ class _ToolbarPanelHub {
this._hideAppmenuButton = this._hideAppmenuButton.bind(this);
this._showToolbarButton = this._showToolbarButton.bind(this);
this._hideToolbarButton = this._hideToolbarButton.bind(this);
this.insertProtectionPanelMessage =
this.insertProtectionPanelMessage.bind(this);
this.state = {};
this._initialized = false;
@ -511,73 +509,6 @@ class _ToolbarPanelHub {
}
}
/**
* Inserts a message into the Protections Panel. The message is visible once
* and afterwards set in a collapsed state. It can be shown again using the
* info button in the panel header.
*/
async insertProtectionPanelMessage(event) {
const win = event.target.ownerGlobal;
this.maybeInsertFTL(win);
const doc = event.target.ownerDocument;
const container = doc.getElementById("messaging-system-message-container");
const infoButton = doc.getElementById("protections-popup-info-button");
const panelContainer = doc.getElementById("protections-popup");
const toggleMessage = () => {
const learnMoreLink = doc.querySelector(
"#messaging-system-message-container .text-link"
);
if (learnMoreLink) {
container.toggleAttribute("disabled");
infoButton.toggleAttribute("checked");
panelContainer.toggleAttribute("infoMessageShowing");
learnMoreLink.disabled = !learnMoreLink.disabled;
}
};
if (!container.childElementCount) {
const message = await this._getMessages({
template: "protections_panel",
triggerId: "protectionsPanelOpen",
});
if (message) {
const messageEl = this._createHeroElement(win, doc, message);
container.appendChild(messageEl);
infoButton.addEventListener("click", toggleMessage);
this.sendUserEventTelemetry(win, "IMPRESSION", message);
}
}
// Message is collapsed by default. If it was never shown before we want
// to expand it
if (
!this.state.protectionPanelMessageSeen &&
container.hasAttribute("disabled")
) {
toggleMessage();
}
// Save state that we displayed the message
if (!this.state.protectionPanelMessageSeen) {
Services.prefs.setBoolPref(PROTECTIONS_PANEL_INFOMSG_PREF, true);
this.state.protectionPanelMessageSeen = true;
}
// Collapse the message after the panel is hidden so we don't get the
// animation when opening the panel
panelContainer.addEventListener(
"popuphidden",
() => {
if (
this.state.protectionPanelMessageSeen &&
!container.hasAttribute("disabled")
) {
toggleMessage();
}
},
{
once: true,
}
);
}
/**
* @param {object} [browser] MessageChannel target argument as a response to a
* user action. No message is shown if undefined.

View File

@ -1,6 +1,5 @@
import { _ToolbarPanelHub } from "lib/ToolbarPanelHub.jsm";
import { GlobalOverrider } from "test/unit/utils";
import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
import { PanelTestProvider } from "lib/PanelTestProvider.sys.mjs";
describe("ToolbarPanelHub", () => {
@ -760,169 +759,4 @@ describe("ToolbarPanelHub", () => {
});
});
});
describe("#insertProtectionPanelMessage", () => {
const fakeInsert = () =>
instance.insertProtectionPanelMessage({
target: { ownerGlobal: fakeWindow, ownerDocument: fakeDocument },
});
let getMessagesStub;
beforeEach(async () => {
const onboardingMsgs =
await OnboardingMessageProvider.getUntranslatedMessages();
getMessagesStub = sandbox
.stub()
.resolves(
onboardingMsgs.find(msg => msg.template === "protections_panel")
);
await instance.init(waitForInitializedStub, {
sendTelemetry: fakeSendTelemetry,
getMessages: getMessagesStub,
});
});
it("should remember it showed", async () => {
await fakeInsert();
assert.calledWithExactly(
setBoolPrefStub,
"browser.protections_panel.infoMessage.seen",
true
);
});
it("should toggle/expand when default collapsed/disabled", async () => {
fakeElementById.hasAttribute.returns(true);
await fakeInsert();
assert.calledThrice(fakeElementById.toggleAttribute);
});
it("should toggle again when popup hides", async () => {
fakeElementById.addEventListener.callsArg(1);
await fakeInsert();
assert.callCount(fakeElementById.toggleAttribute, 6);
});
it("should open link on click (separate link element)", async () => {
const sendTelemetryStub = sandbox.stub(
instance,
"sendUserEventTelemetry"
);
const onboardingMsgs =
await OnboardingMessageProvider.getUntranslatedMessages();
const msg = onboardingMsgs.find(m => m.template === "protections_panel");
await fakeInsert();
assert.calledOnce(sendTelemetryStub);
assert.calledWithExactly(
sendTelemetryStub,
fakeWindow,
"IMPRESSION",
msg
);
eventListeners.mouseup();
assert.calledOnce(global.SpecialMessageActions.handleAction);
assert.calledWithExactly(
global.SpecialMessageActions.handleAction,
{
type: "OPEN_URL",
data: {
args: sinon.match.string,
where: "tabshifted",
},
},
fakeWindow.browser
);
});
it("should format the url", async () => {
const stub = sandbox
.stub(global.Services.urlFormatter, "formatURL")
.returns("formattedURL");
const onboardingMsgs =
await OnboardingMessageProvider.getUntranslatedMessages();
const msg = onboardingMsgs.find(m => m.template === "protections_panel");
await fakeInsert();
eventListeners.mouseup();
assert.calledOnce(stub);
assert.calledWithExactly(stub, msg.content.cta_url);
assert.calledOnce(global.SpecialMessageActions.handleAction);
assert.calledWithExactly(
global.SpecialMessageActions.handleAction,
{
type: "OPEN_URL",
data: {
args: "formattedURL",
where: "tabshifted",
},
},
fakeWindow.browser
);
});
it("should report format url errors", async () => {
const stub = sandbox
.stub(global.Services.urlFormatter, "formatURL")
.throws();
const onboardingMsgs =
await OnboardingMessageProvider.getUntranslatedMessages();
const msg = onboardingMsgs.find(m => m.template === "protections_panel");
sandbox.spy(global.console, "error");
await fakeInsert();
eventListeners.mouseup();
assert.calledOnce(stub);
assert.calledOnce(global.console.error);
assert.calledOnce(global.SpecialMessageActions.handleAction);
assert.calledWithExactly(
global.SpecialMessageActions.handleAction,
{
type: "OPEN_URL",
data: {
args: msg.content.cta_url,
where: "tabshifted",
},
},
fakeWindow.browser
);
});
it("should open link on click (directly attached to the message)", async () => {
const onboardingMsgs =
await OnboardingMessageProvider.getUntranslatedMessages();
const msg = onboardingMsgs.find(m => m.template === "protections_panel");
getMessagesStub.resolves({
...msg,
content: { ...msg.content, link_text: null },
});
await fakeInsert();
eventListeners.mouseup();
assert.calledOnce(global.SpecialMessageActions.handleAction);
assert.calledWithExactly(
global.SpecialMessageActions.handleAction,
{
type: "OPEN_URL",
data: {
args: sinon.match.string,
where: "tabshifted",
},
},
fakeWindow.browser
);
});
it("should handle user actions from mouseup and keyup", async () => {
await fakeInsert();
eventListeners.mouseup();
eventListeners.keyup({ key: "Enter" });
eventListeners.keyup({ key: " " });
assert.calledThrice(global.SpecialMessageActions.handleAction);
});
});
});

View File

@ -75,12 +75,6 @@ cfr-doorhanger-bookmark-fxa-close-btn-tooltip =
.aria-label = Close button
.title = Close
## Protections panel
cfr-protections-panel-header = Browse without being followed
cfr-protections-panel-body = Keep your data to yourself. { -brand-short-name } protects you from many of the most common trackers that follow what you do online.
cfr-protections-panel-link-text = Learn more
## What's New toolbar button and panel
# This string is used by screen readers to offer a text based alternative for

View File

@ -153,3 +153,9 @@ protections-panel-cookie-banner-view-turn-on-label =
protections-panel-report-broken-site =
.label = Report broken site
.title = Report broken site
## Protections panel info message
cfr-protections-panel-header = Browse without being followed
cfr-protections-panel-body = Keep your data to yourself. { -brand-short-name } protects you from many of the most common trackers that follow what you do online.
cfr-protections-panel-link-text = Learn more

View File

@ -0,0 +1,24 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
from fluent.migrate.helpers import transforms_from
def migrate(ctx):
"""Bug 1863022 - Move Protection Panel Message to calling code, part {index}"""
source = "browser/browser/newtab/asrouter.ftl"
target = "browser/browser/protectionsPanel.ftl"
ctx.add_transforms(
target,
target,
transforms_from(
"""
cfr-protections-panel-header = {COPY_PATTERN(from_path, "cfr-protections-panel-header")}
cfr-protections-panel-body = {COPY_PATTERN(from_path, "cfr-protections-panel-body")}
cfr-protections-panel-link-text = {COPY_PATTERN(from_path, "cfr-protections-panel-link-text")}
""",
from_path=source,
),
)

View File

@ -2497,7 +2497,10 @@ security.ui.app_menu:
security.ui.protectionspopup:
open:
objects: ["protections_popup"]
objects: ["protections_popup", "protectionspopup_cfr",]
extra_keys:
message: >
For protectionspopup_cfr, the message ID.
bug_numbers:
- 1560327
- 1607488
@ -2534,7 +2537,11 @@ security.ui.protectionspopup:
"milestone_message",
"cookieb_toggle_on",
"cookieb_toggle_off",
"protectionspopup_cfr",
]
extra_keys:
message: >
For protectionspopup_cfr, the message ID.
bug_numbers:
- 1560327
- 1602015