Bug 1804502 - Add relevant telemetry events to relay integration. r=credential-management-reviewers,dimi,sgalich,settings-reviewers,mconley

Differential Revision: https://phabricator.services.mozilla.com/D163911
This commit is contained in:
issammani
2023-03-06 23:50:44 +00:00
parent ced50aecd5
commit 7b4986bae9
7 changed files with 253 additions and 17 deletions

View File

@@ -42,6 +42,10 @@ var { Weave } = ChromeUtils.importESModule(
"resource://services-sync/main.sys.mjs" "resource://services-sync/main.sys.mjs"
); );
var { FirefoxRelayTelemetry } = ChromeUtils.importESModule(
"resource://gre/modules/FirefoxRelayTelemetry.mjs"
);
var { FxAccounts, getFxAccountsSingleton } = ChromeUtils.importESModule( var { FxAccounts, getFxAccountsSingleton } = ChromeUtils.importESModule(
"resource://gre/modules/FxAccounts.sys.mjs" "resource://gre/modules/FxAccounts.sys.mjs"
); );

View File

@@ -2486,11 +2486,12 @@ var gPrivacyPane = {
toggleRelayIntegration() { toggleRelayIntegration() {
const checkbox = document.getElementById("relayIntegration"); const checkbox = document.getElementById("relayIntegration");
if (checkbox.checked) { if (checkbox.checked) {
FirefoxRelay.markAsEnabled(); FirefoxRelay.markAsEnabled();
FirefoxRelayTelemetry.recordRelayPrefEvent("enabled");
} else { } else {
FirefoxRelay.markAsDisabled(); FirefoxRelay.markAsDisabled();
FirefoxRelayTelemetry.recordRelayPrefEvent("disabled");
} }
}, },

View File

@@ -9,12 +9,21 @@ const EXPORTED_SYMBOLS = ["FirefoxRelay"];
const { XPCOMUtils } = ChromeUtils.importESModule( const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs" "resource://gre/modules/XPCOMUtils.sys.mjs"
); );
const { FirefoxRelayTelemetry } = ChromeUtils.importESModule(
"resource://gre/modules/FirefoxRelayTelemetry.mjs"
);
const { const {
LoginHelper, LoginHelper,
OptInFeature, OptInFeature,
ParentAutocompleteOption, ParentAutocompleteOption,
} = ChromeUtils.import("resource://gre/modules/LoginHelper.jsm"); } = ChromeUtils.import("resource://gre/modules/LoginHelper.jsm");
const { TelemetryUtils } = ChromeUtils.import(
"resource://gre/modules/TelemetryUtils.jsm"
);
const lazy = {}; const lazy = {};
// Static configuration // Static configuration
@@ -137,11 +146,13 @@ async function getReusableMasksAsync(browser, _origin) {
); );
if (!response) { if (!response) {
return undefined; // fetchWithReauth only returns undefined if login / obtaining a token failed.
// Otherwise, it will return a response object.
return [undefined, RelayFeature.AUTH_TOKEN_ERROR_CODE];
} }
if (response.ok) { if (response.ok) {
return response.json(); return [await response.json(), response.status];
} }
lazy.log.error( lazy.log.error(
@@ -150,9 +161,8 @@ async function getReusableMasksAsync(browser, _origin) {
await showErrorAsync(browser, "firefox-relay-get-reusable-masks-failed", { await showErrorAsync(browser, "firefox-relay-get-reusable-masks-failed", {
status: response.status, status: response.status,
}); });
// Services.telemetry.recordEvent("pwmgr", "make_relay_fail", "relay");
return undefined; return [undefined, response.status];
} }
/** /**
@@ -220,9 +230,14 @@ async function formatMessages(...ids) {
}); });
} }
async function showReusableMasksAsync(browser, origin, errorMessage) { async function showReusableMasksAsync(browser, origin, error) {
const reusableMasks = await getReusableMasksAsync(browser, origin); const [reusableMasks, status] = await getReusableMasksAsync(browser, origin);
if (!reusableMasks) { if (!reusableMasks) {
FirefoxRelayTelemetry.recordRelayReusePanelEvent(
"shown",
FirefoxRelay.flowId,
status
);
return null; return null;
} }
@@ -236,6 +251,10 @@ async function showReusableMasksAsync(browser, origin, errorMessage) {
accessKey: getUnlimitedMasksStrings.accesskey, accessKey: getUnlimitedMasksStrings.accesskey,
dismiss: true, dismiss: true,
async callback() { async callback() {
FirefoxRelayTelemetry.recordRelayReusePanelEvent(
"get_unlimited_masks",
FirefoxRelay.flowId
);
browser.ownerGlobal.openWebLinkIn(config.learnMoreURL, "tab"); browser.ownerGlobal.openWebLinkIn(config.learnMoreURL, "tab");
}, },
}; };
@@ -253,7 +272,7 @@ async function showReusableMasksAsync(browser, origin, errorMessage) {
notification.owner.panel.getElementsByClassName( notification.owner.panel.getElementsByClassName(
"error-message" "error-message"
)[0].textContent = errorMessage; )[0].textContent = error.detail || "";
// rebuild "reuse mask" buttons list // rebuild "reuse mask" buttons list
const list = getReusableMasksList(); const list = getReusableMasksList();
@@ -279,6 +298,10 @@ async function showReusableMasksAsync(browser, origin, errorMessage) {
notification.remove(); notification.remove();
lazy.log.info("Reusing Relay mask"); lazy.log.info("Reusing Relay mask");
fillUsername(mask.full_address); fillUsername(mask.full_address);
FirefoxRelayTelemetry.recordRelayReusePanelEvent(
"reuse_mask",
FirefoxRelay.flowId
);
}); });
fragment.appendChild(button); fragment.appendChild(button);
}); });
@@ -297,6 +320,10 @@ async function showReusableMasksAsync(browser, origin, errorMessage) {
break; break;
case "shown": case "shown":
notificationShown(); notificationShown();
FirefoxRelayTelemetry.recordRelayReusePanelEvent(
"shown",
FirefoxRelay.flowId
);
break; break;
} }
} }
@@ -338,6 +365,11 @@ async function generateUsernameAsync(browser, origin) {
); );
if (!response) { if (!response) {
FirefoxRelayTelemetry.recordRelayUsernameFilledEvent(
"shown",
FirefoxRelay.flowId,
RelayFeature.AUTH_TOKEN_ERROR_CODE
);
return undefined; return undefined;
} }
@@ -345,14 +377,18 @@ async function generateUsernameAsync(browser, origin) {
lazy.log.info(`generated Relay mask`); lazy.log.info(`generated Relay mask`);
const result = await response.json(); const result = await response.json();
showConfirmation(browser, "confirmation-hint-firefox-relay-mask-generated"); showConfirmation(browser, "confirmation-hint-firefox-relay-mask-generated");
// Services.telemetry.recordEvent("pwmgr", "make_relay", "relay");
return result.full_address; return result.full_address;
} }
if (response.status == 403) { if (response.status == 403) {
const error = await response.json(); const error = await response.json();
if (error?.error_code == "free_tier_limit") { if (error?.error_code == "free_tier_limit") {
return showReusableMasksAsync(browser, origin, error.detail); FirefoxRelayTelemetry.recordRelayUsernameFilledEvent(
"shown",
FirefoxRelay.flowId,
error?.error_code
);
return showReusableMasksAsync(browser, origin, error);
} }
} }
@@ -363,7 +399,12 @@ async function generateUsernameAsync(browser, origin) {
await showErrorAsync(browser, "firefox-relay-mask-generation-failed", { await showErrorAsync(browser, "firefox-relay-mask-generation-failed", {
status: response.status, status: response.status,
}); });
// Services.telemetry.recordEvent("pwmgr", "make_relay_fail", "relay");
FirefoxRelayTelemetry.recordRelayReusePanelEvent(
"shown",
FirefoxRelay.flowId,
response.status
);
return undefined; return undefined;
} }
@@ -391,7 +432,19 @@ class RelayOffered {
title, title,
subtitle, subtitle,
"PasswordManager:offerRelayIntegration", "PasswordManager:offerRelayIntegration",
null {
telemetry: {
flowId: FirefoxRelay.flowId,
isRelayUser: this.#isRelayUser,
scenarioName,
},
}
);
FirefoxRelayTelemetry.recordRelayOfferedEvent(
"shown",
FirefoxRelay.flowId,
scenarioName,
this.#isRelayUser
); );
} }
} }
@@ -399,6 +452,7 @@ class RelayOffered {
async offerRelayIntegration(feature, browser, origin) { async offerRelayIntegration(feature, browser, origin) {
const fxaUser = await lazy.fxAccounts.getSignedInUser(); const fxaUser = await lazy.fxAccounts.getSignedInUser();
if (!fxaUser) { if (!fxaUser) {
return null; return null;
} }
@@ -424,6 +478,10 @@ class RelayOffered {
lazy.log.info("user opted in to Firefox Relay integration"); lazy.log.info("user opted in to Firefox Relay integration");
feature.markAsEnabled(); feature.markAsEnabled();
fillUsername(await generateUsernameAsync(browser, origin)); fillUsername(await generateUsernameAsync(browser, origin));
FirefoxRelayTelemetry.recordRelayOptInPanelEvent(
"enabled",
FirefoxRelay.flowId
);
}, },
}; };
const postpone = { const postpone = {
@@ -435,6 +493,10 @@ class RelayOffered {
"user decided not to decide about Firefox Relay integration" "user decided not to decide about Firefox Relay integration"
); );
feature.markAsOffered(); feature.markAsOffered();
FirefoxRelayTelemetry.recordRelayOptInPanelEvent(
"postponed",
FirefoxRelay.flowId
);
}, },
}; };
const disableIntegration = { const disableIntegration = {
@@ -444,6 +506,10 @@ class RelayOffered {
callback() { callback() {
lazy.log.info("user opted out from Firefox Relay integration"); lazy.log.info("user opted out from Firefox Relay integration");
feature.markAsDisabled(); feature.markAsDisabled();
FirefoxRelayTelemetry.recordRelayOptInPanelEvent(
"disabled",
FirefoxRelay.flowId
);
}, },
}; };
let notification; let notification;
@@ -478,6 +544,10 @@ class RelayOffered {
useremail: fxaUser.email, useremail: fxaUser.email,
} }
); );
FirefoxRelayTelemetry.recordRelayOptInPanelEvent(
"shown",
FirefoxRelay.flowId
);
break; break;
} }
}, },
@@ -504,7 +574,15 @@ class RelayEnabled {
title, title,
subtitle, subtitle,
"PasswordManager:generateRelayUsername", "PasswordManager:generateRelayUsername",
origin {
telemetry: {
flowId: FirefoxRelay.flowId,
},
}
);
FirefoxRelayTelemetry.recordRelayUsernameFilledEvent(
"shown",
FirefoxRelay.flowId
); );
} }
} }
@@ -517,8 +595,12 @@ class RelayEnabled {
class RelayDisabled {} class RelayDisabled {}
class RelayFeature extends OptInFeature { class RelayFeature extends OptInFeature {
// Using 418 to avoid conflict with other standard http error code
static AUTH_TOKEN_ERROR_CODE = 418;
constructor() { constructor() {
super(RelayOffered, RelayEnabled, RelayDisabled, config.relayFeaturePref); super(RelayOffered, RelayEnabled, RelayDisabled, config.relayFeaturePref);
Services.telemetry.setEventRecordingEnabled("relay_integration", true);
} }
get learnMoreUrl() { get learnMoreUrl() {
@@ -528,6 +610,13 @@ class RelayFeature extends OptInFeature {
async autocompleteItemsAsync({ origin, scenarioName, hasInput }) { async autocompleteItemsAsync({ origin, scenarioName, hasInput }) {
const result = []; const result = [];
// Generate a flowID to unique identify a series of user action. FlowId
// allows us to link users' interaction on different UI component (Ex. autocomplete, notification)
// We can use flowID to build the Funnel Diagram
// This value need to always be regenerated in the entry point of an user
// action so we overwrite the previous one.
this.flowId = TelemetryUtils.generateUUID();
if (this.implementation.autocompleteItemsAsync) { if (this.implementation.autocompleteItemsAsync) {
for await (const item of this.implementation.autocompleteItemsAsync( for await (const item of this.implementation.autocompleteItemsAsync(
origin, origin,

View File

@@ -0,0 +1,74 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export const FirefoxRelayTelemetry = {
recordRelayIntegrationTelemetryEvent(
eventObject,
eventMethod,
eventFlowId,
eventExtras
) {
Services.telemetry.recordEvent(
"relay_integration",
eventMethod,
eventObject,
eventFlowId ?? "",
eventExtras ?? {}
);
},
recordRelayPrefEvent(eventMethod, eventFlowId, eventExtras) {
this.recordRelayIntegrationTelemetryEvent(
"pref_change",
eventMethod,
eventFlowId,
eventExtras
);
},
recordRelayOfferedEvent(eventMethod, eventFlowId, scenarioName, isRelayUser) {
return this.recordRelayIntegrationTelemetryEvent(
"offer_relay",
eventMethod,
eventFlowId,
{
scenario: scenarioName,
is_relay_user: (isRelayUser ?? "") + "",
}
);
},
recordRelayUsernameFilledEvent(eventMethod, eventFlowId, errorCode = 0) {
return this.recordRelayIntegrationTelemetryEvent(
"fill_username",
eventMethod,
eventFlowId,
{
error_code: errorCode + "",
}
);
},
recordRelayReusePanelEvent(eventMethod, eventFlowId, errorCode = 0) {
return this.recordRelayIntegrationTelemetryEvent(
"reuse_panel",
eventMethod,
eventFlowId,
{
error_code: errorCode + "",
}
);
},
recordRelayOptInPanelEvent(eventMethod, eventFlowId, eventExtras) {
return this.recordRelayIntegrationTelemetryEvent(
"opt_in_panel",
eventMethod,
eventFlowId,
eventExtras
);
},
};
export default FirefoxRelayTelemetry;

View File

@@ -8,6 +8,10 @@ const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs" "resource://gre/modules/XPCOMUtils.sys.mjs"
); );
const { FirefoxRelayTelemetry } = ChromeUtils.importESModule(
"resource://gre/modules/FirefoxRelayTelemetry.mjs"
);
const LoginInfo = new Components.Constructor( const LoginInfo = new Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1", "@mozilla.org/login-manager/loginInfo;1",
Ci.nsILoginInfo, Ci.nsILoginInfo,
@@ -351,10 +355,20 @@ class LoginManagerParent extends JSWindowActorParent {
} }
case "PasswordManager:offerRelayIntegration": { case "PasswordManager:offerRelayIntegration": {
FirefoxRelayTelemetry.recordRelayOfferedEvent(
"clicked",
data.telemetry.flowId,
data.telemetry.scenarioName,
data.telemetry.isRelayUser
);
return this.#offerRelayIntegration(context.origin); return this.#offerRelayIntegration(context.origin);
} }
case "PasswordManager:generateRelayUsername": { case "PasswordManager:generateRelayUsername": {
FirefoxRelayTelemetry.recordRelayUsernameFilledEvent(
"clicked",
data.telemetry.flowId
);
return this.#generateRelayUsername(context.origin); return this.#generateRelayUsername(context.origin);
} }
} }
@@ -778,6 +792,7 @@ class LoginManagerParent extends JSWindowActorParent {
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
// doesn't support structured cloning. // doesn't support structured cloning.
let jsLogins = lazy.LoginHelper.loginsToVanillaObjects(matchingLogins); let jsLogins = lazy.LoginHelper.loginsToVanillaObjects(matchingLogins);
return { return {
generatedPassword, generatedPassword,
importable: await getImportableLogins(formOrigin), importable: await getImportableLogins(formOrigin),

View File

@@ -32,6 +32,7 @@ XPIDL_MODULE = "loginmgr"
EXTRA_JS_MODULES += [ EXTRA_JS_MODULES += [
"crypto-SDR.js", "crypto-SDR.js",
"FirefoxRelay.jsm", "FirefoxRelay.jsm",
"FirefoxRelayTelemetry.mjs",
"InsecurePasswordUtils.jsm", "InsecurePasswordUtils.jsm",
"LoginAutoComplete.jsm", "LoginAutoComplete.jsm",
"LoginFormFactory.jsm", "LoginFormFactory.jsm",

View File

@@ -1316,6 +1316,62 @@ pwmgr:
Whether or not the saved/updated password was selected by the user choosing a suggested Whether or not the saved/updated password was selected by the user choosing a suggested
value from the autocomplete popup. value from the autocomplete popup.
# Record telemetry based on individual Firefox relay UI (autocomplete popup, notification panel)
relay_integration:
popup_option:
description: >
Firefox relay integration autocomplete popup
objects: ["offer_relay", "fill_username"]
methods: ["shown", "clicked"]
bug_numbers: [1804502]
expiry_version: "never"
products: ["firefox"]
record_in_processes: [main]
notification_emails: ["passwords-dev@mozilla.org"]
extra_keys:
scenario: Describes the auth context for now only SignupForm is supported
error_code: >
The error code after users click the fill username autocomplete entry.
Only present if the object is "fill_username".
When the event is successful, the error_code is 0.
is_relay_user: >
Whether the user is a relay user or not.
Only present if the object is "offer_relay"
mask_panel:
description: >
Panels to show the state of the email alias generation
objects: ["reuse_panel"]
methods: ["shown", "get_unlimited_masks", "reuse_mask"]
bug_numbers: [1804502]
expiry_version: "never"
products: ["firefox"]
record_in_processes: [main]
notification_emails: ["passwords-dev@mozilla.org"]
extra_keys:
error_code: >
The error code after users click the email alias generation panel.
When the event is successful, the error_code is 0.
opt_in_panel:
description: >
Panel to opt-in Firefox Relay Integration
objects: ["opt_in_panel"]
methods: ["shown", "enabled", "postponed", "disabled"]
bug_numbers: [1804502]
expiry_version: "never"
products: ["firefox"]
record_in_processes: [main]
notification_emails: ["passwords-dev@mozilla.org"]
pref_change:
description: >
Checkbox in the settings page to enable/disable relay
objects: ["pref_change"]
methods: ["enabled", "disabled"]
bug_numbers: [1804502]
expiry_version: "never"
products: ["firefox"]
record_in_processes: [main]
notification_emails: ["passwords-dev@mozilla.org"]
jsonfile: jsonfile:
load: load:
description: > description: >
@@ -2985,8 +3041,6 @@ security.ui.certerror:
products: products:
- "firefox" - "firefox"
record_in_processes: ["content"] record_in_processes: ["content"]
products:
- firefox
extra_keys: extra_keys:
is_frame: If the error page is loaded in an iframe. is_frame: If the error page is loaded in an iframe.
has_sts: If the error page is for a site with HSTS headers or with a pinned key. has_sts: If the error page is for a site with HSTS headers or with a pinned key.
@@ -3018,8 +3072,6 @@ security.ui.certerror:
products: products:
- "firefox" - "firefox"
record_in_processes: ["content"] record_in_processes: ["content"]
products:
- firefox
extra_keys: extra_keys:
is_frame: If the error page is loaded in an iframe. is_frame: If the error page is loaded in an iframe.
has_sts: If the error page is for a site with HSTS headers or with a pinned key. has_sts: If the error page is for a site with HSTS headers or with a pinned key.