Bug 1804502 - Add test cases for telemetry events for relay integration. r=credential-management-reviewers,dimi

Depends on D163911

Differential Revision: https://phabricator.services.mozilla.com/D171190
This commit is contained in:
Issam Mani 2023-03-13 22:17:39 +00:00
Родитель 78b2232569
Коммит ab4c7cefc9
4 изменённых файлов: 548 добавлений и 11 удалений

Просмотреть файл

@ -27,7 +27,7 @@ const { TelemetryUtils } = ChromeUtils.import(
const lazy = {};
// Static configuration
const config = (function() {
const gConfig = (function() {
const baseUrl = Services.prefs.getStringPref(
"signon.firefoxRelay.base_url",
undefined
@ -66,7 +66,7 @@ if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
async function getRelayTokenAsync() {
try {
return await lazy.fxAccounts.getOAuthToken({ scope: config.scope });
return await lazy.fxAccounts.getOAuthToken({ scope: gConfig.scope });
} catch (e) {
console.error(`There was an error getting the user's token: ${e.message}`);
return undefined;
@ -118,7 +118,7 @@ async function isRelayUserAsync() {
const response = await fetchWithReauth(
null,
headers => new Request(config.profilesUrl, { headers })
headers => new Request(gConfig.profilesUrl, { headers })
);
if (!response) {
return false;
@ -139,7 +139,7 @@ async function getReusableMasksAsync(browser, _origin) {
const response = await fetchWithReauth(
browser,
headers =>
new Request(config.addressesUrl, {
new Request(gConfig.addressesUrl, {
method: "GET",
headers,
})
@ -197,7 +197,7 @@ async function showErrorAsync(browser, messageId, messageArgs) {
autofocus: true,
removeOnDismissal: true,
popupIconURL: "page-icon:https://relay.firefox.com",
learnMoreURL: config.learnMoreURL,
learnMoreURL: gConfig.learnMoreURL,
}
);
}
@ -255,7 +255,7 @@ async function showReusableMasksAsync(browser, origin, error) {
"get_unlimited_masks",
FirefoxRelay.flowId
);
browser.ownerGlobal.openWebLinkIn(config.learnMoreURL, "tab");
browser.ownerGlobal.openWebLinkIn(gConfig.learnMoreURL, "tab");
},
};
@ -357,7 +357,7 @@ async function generateUsernameAsync(browser, origin) {
const response = await fetchWithReauth(
browser,
headers =>
new Request(config.addressesUrl, {
new Request(gConfig.addressesUrl, {
method: "POST",
headers,
body,
@ -477,11 +477,11 @@ class RelayOffered {
async callback() {
lazy.log.info("user opted in to Firefox Relay integration");
feature.markAsEnabled();
fillUsername(await generateUsernameAsync(browser, origin));
FirefoxRelayTelemetry.recordRelayOptInPanelEvent(
"enabled",
FirefoxRelay.flowId
);
fillUsername(await generateUsernameAsync(browser, origin));
},
};
const postpone = {
@ -524,7 +524,7 @@ class RelayOffered {
{
autofocus: true,
removeOnDismissal: true,
learnMoreURL: config.learnMoreURL,
learnMoreURL: gConfig.learnMoreURL,
eventCallback: event => {
switch (event) {
case "shown":
@ -599,12 +599,26 @@ class RelayFeature extends OptInFeature {
static AUTH_TOKEN_ERROR_CODE = 418;
constructor() {
super(RelayOffered, RelayEnabled, RelayDisabled, config.relayFeaturePref);
super(RelayOffered, RelayEnabled, RelayDisabled, gConfig.relayFeaturePref);
Services.telemetry.setEventRecordingEnabled("relay_integration", true);
// Update the config when the signon.firefoxRelay.base_url pref is changed.
// This is added mainly for tests.
Services.prefs.addObserver(
"signon.firefoxRelay.base_url",
this.updateConfig
);
}
get learnMoreUrl() {
return config.learnMoreURL;
return gConfig.learnMoreURL;
}
updateConfig() {
const newBaseUrl = Services.prefs.getStringPref(
"signon.firefoxRelay.base_url"
);
gConfig.addressesUrl = newBaseUrl + `relayaddresses/`;
gConfig.profilesUrl = newBaseUrl + `profiles/`;
}
async autocompleteItemsAsync({ origin, scenarioName, hasInput }) {

Просмотреть файл

@ -6,6 +6,7 @@ support-files =
form_basic.html
form_basic_iframe.html
form_basic_login.html
form_basic_signup.html
form_basic_no_username.html
formless_basic.html
form_multipage.html
@ -151,3 +152,4 @@ skip-if = os == "android"
[browser_autocomplete_disabled_readonly_passwordField.js]
support-files =
form_disabled_readonly_passwordField.html
[browser_relay_telemetry.js]

Просмотреть файл

@ -0,0 +1,511 @@
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
const { getFxAccountsSingleton } = ChromeUtils.import(
"resource://gre/modules/FxAccounts.jsm"
);
const { FirefoxRelayTelemetry } = ChromeUtils.importESModule(
"resource://gre/modules/FirefoxRelayTelemetry.mjs"
);
const gFxAccounts = getFxAccountsSingleton();
let gRelayACOptionsTitles;
let gHttpServer;
const TEST_URL_PATH = `https://example.org${DIRECTORY_PATH}form_basic_signup.html`;
const MOCK_MASKS = [
{
full_address: "email1@mozilla.com",
description: "Email 1 Description",
enabled: true,
},
{
full_address: "email2@mozilla.com",
description: "Email 2 Description",
enabled: false,
},
{
full_address: "email3@mozilla.com",
description: "Email 3 Description",
enabled: true,
},
];
const SERVER_SCENARIOS = {
free_tier_limit: {
"/relayaddresses/": {
POST: (request, response) => {
response.setStatusLine(request.httpVersion, 403);
response.write(JSON.stringify({ error_code: "free_tier_limit" }));
},
GET: (_, response) => {
response.write(JSON.stringify(MOCK_MASKS));
},
},
},
unknown_error: {
"/relayaddresses/": {
default: (request, response) => {
response.setStatusLine(request.httpVersion, 408);
},
},
},
default: {
default: (request, response) => {
response.setStatusLine(request.httpVersion, 200);
response.write(JSON.stringify({ foo: "bar" }));
},
},
};
const simpleRouter = scenarioName => (request, response) => {
const routeHandler =
SERVER_SCENARIOS[scenarioName][request._path] ?? SERVER_SCENARIOS.default;
const methodHandler =
routeHandler?.[request._method] ??
routeHandler.default ??
SERVER_SCENARIOS.default.default;
methodHandler(request, response);
};
const setupServerScenario = (scenarioName = "default") =>
gHttpServer.registerPrefixHandler("/", simpleRouter(scenarioName));
const setupRelayScenario = async scenarioName => {
await SpecialPowers.pushPrefEnv({
set: [["signon.firefoxRelay.feature", scenarioName]],
});
Services.telemetry.clearEvents();
};
const waitForEvents = async expectedEvents =>
TestUtils.waitForCondition(
() => {
const snapshots = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
);
return (snapshots.parent?.length ?? 0) >= (expectedEvents.length ?? 0);
},
"Wait for telemetry to be collected",
100,
100
);
async function assertEvents(expectedEvents) {
// To avoid intermittent failures, we wait for telemetry to be collected
await waitForEvents(expectedEvents);
const events = TelemetryTestUtils.getEvents(
{ category: "relay_integration" },
{ process: "parent" }
);
for (let i = 0; i < expectedEvents.length; i++) {
const keysInExpectedEvent = Object.keys(expectedEvents[i]);
keysInExpectedEvent.forEach(key => {
const assertFn =
typeof events[i][key] === "object"
? Assert.deepEqual.bind(Assert)
: Assert.equal.bind(Assert);
assertFn(
events[i][key],
expectedEvents[i][key],
`Key value for ${key} should match`
);
});
}
}
async function openRelayAC(browser) {
// In rare cases, especially in chaos mode in verify tests, some events creep in.
// Clear them out before we start.
Services.telemetry.clearEvents();
const popup = document.getElementById("PopupAutoComplete");
await openACPopup(popup, browser, "#form-basic-username");
const popupItem = document
.querySelector("richlistitem")
.getAttribute("ac-label");
const popupItemTitle = JSON.parse(popupItem).title;
Assert.ok(
gRelayACOptionsTitles.some(title => title.value === popupItemTitle),
"AC Popup has an item Relay option shown in popup"
);
const promiseHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
popup.firstChild.getItemAtIndex(0).click();
await promiseHidden;
}
add_setup(async function() {
gHttpServer = new HttpServer();
setupServerScenario();
gHttpServer.start(-1);
const API_ENDPOINT = `http://localhost:${gHttpServer.identity.primaryPort}/`;
await SpecialPowers.pushPrefEnv({
set: [
["signon.firefoxRelay.feature", "available"],
["signon.firefoxRelay.base_url", API_ENDPOINT],
],
});
sinon.stub(gFxAccounts, "hasLocalSession").returns(true);
sinon
.stub(gFxAccounts.constructor.config, "isProductionConfig")
.returns(true);
sinon.stub(gFxAccounts, "getOAuthToken").returns("MOCK_TOKEN");
sinon.stub(gFxAccounts, "getSignedInUser").returns({
email: "example@mozilla.com",
});
const canRecordExtendedOld = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
Services.telemetry.clearEvents();
Services.telemetry.setEventRecordingEnabled("relay_integration", true);
gRelayACOptionsTitles = await new Localization([
"browser/firefoxRelay.ftl",
]).formatMessages([
"firefox-relay-opt-in-title",
"firefox-relay-generate-mask-title",
]);
registerCleanupFunction(async () => {
await new Promise(resolve => {
gHttpServer.stop(function() {
resolve();
});
});
Services.telemetry.setEventRecordingEnabled("relay_integration", false);
Services.telemetry.clearEvents();
Services.telemetry.canRecordExtended = canRecordExtendedOld;
sinon.restore();
});
});
add_task(async function test_pref_toggle() {
await setupRelayScenario("available");
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "about:preferences#privacy",
},
async function(browser) {
const relayIntegrationCheckbox = content.document.querySelector(
"checkbox#relayIntegration"
);
relayIntegrationCheckbox.click();
relayIntegrationCheckbox.click();
await assertEvents([
{ object: "pref_change", method: "enabled" },
{ object: "pref_change", method: "disabled" },
]);
}
);
});
add_task(async function test_popup_option_optin_enabled() {
await setupRelayScenario("available");
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_URL_PATH,
},
async function(browser) {
await openRelayAC(browser);
const notificationPopup = document.getElementById("notification-popup");
const notificationShown = BrowserTestUtils.waitForPopupEvent(
notificationPopup,
"shown"
);
const notificationHidden = BrowserTestUtils.waitForPopupEvent(
notificationPopup,
"hidden"
);
await notificationShown;
notificationPopup
.querySelector("button.popup-notification-primary-button")
.click();
await notificationHidden;
await BrowserTestUtils.waitForEvent(
ConfirmationHint._panel,
"popuphidden"
);
await assertEvents([
{
object: "offer_relay",
method: "shown",
extra: { is_relay_user: "true", scenario: "SignUpFormScenario" },
},
{
object: "offer_relay",
method: "clicked",
extra: { is_relay_user: "true", scenario: "SignUpFormScenario" },
},
{ object: "opt_in_panel", method: "shown" },
{ object: "opt_in_panel", method: "enabled" },
{
object: "fill_username",
method: "shown",
extra: { error_code: "0" },
},
]);
}
);
});
add_task(async function test_popup_option_optin_postponed() {
await setupRelayScenario("available");
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_URL_PATH,
},
async function(browser) {
await openRelayAC(browser);
const notificationPopup = document.getElementById("notification-popup");
const notificationShown = BrowserTestUtils.waitForPopupEvent(
notificationPopup,
"shown"
);
const notificationHidden = BrowserTestUtils.waitForPopupEvent(
notificationPopup,
"hidden"
);
await notificationShown;
notificationPopup
.querySelector("button.popup-notification-secondary-button")
.click();
await notificationHidden;
await assertEvents([
{ object: "offer_relay", method: "shown" },
{ object: "offer_relay", method: "clicked" },
{ object: "opt_in_panel", method: "shown" },
{ object: "opt_in_panel", method: "postponed" },
]);
}
);
});
add_task(async function test_popup_option_optin_disabled() {
await setupRelayScenario("available");
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_URL_PATH,
},
async function(browser) {
await openRelayAC(browser);
const notificationPopup = document.getElementById("notification-popup");
const notificationShown = BrowserTestUtils.waitForPopupEvent(
notificationPopup,
"shown"
);
const notificationHidden = BrowserTestUtils.waitForPopupEvent(
notificationPopup,
"hidden"
);
await notificationShown;
const menupopup = notificationPopup.querySelector("menupopup");
const menuitem = menupopup.querySelector("menuitem");
menuitem.click();
await notificationHidden;
await assertEvents([
{ object: "offer_relay", method: "shown" },
{ object: "offer_relay", method: "clicked" },
{ object: "opt_in_panel", method: "shown" },
{ object: "opt_in_panel", method: "disabled" },
]);
}
);
});
add_task(async function test_popup_option_fillusername() {
await setupRelayScenario("enabled");
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_URL_PATH,
},
async function(browser) {
await openRelayAC(browser);
await BrowserTestUtils.waitForEvent(
ConfirmationHint._panel,
"popuphidden"
);
await assertEvents([
{ object: "fill_username", method: "shown" },
{
object: "fill_username",
method: "clicked",
},
]);
}
);
});
add_task(async function test_fillusername_free_tier_limit() {
await setupRelayScenario("enabled");
setupServerScenario("free_tier_limit");
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_URL_PATH,
},
async function(browser) {
await openRelayAC(browser);
const notificationPopup = document.getElementById("notification-popup");
const notificationShown = BrowserTestUtils.waitForPopupEvent(
notificationPopup,
"shown"
);
const notificationHidden = BrowserTestUtils.waitForPopupEvent(
notificationPopup,
"hidden"
);
await notificationShown;
notificationPopup.querySelector(".reusable-relay-masks button").click();
await notificationHidden;
await assertEvents([
{ object: "fill_username", method: "shown" },
{
object: "fill_username",
method: "clicked",
},
{
object: "fill_username",
method: "shown",
extra: { error_code: "free_tier_limit" },
},
{
object: "reuse_panel",
method: "shown",
},
{
object: "reuse_panel",
method: "reuse_mask",
},
]);
await SpecialPowers.spawn(browser, [], async function() {
const username = content.document.getElementById("form-basic-username");
Assert.equal(
username.value,
"email1@mozilla.com",
"Username field should be filled with the first mask"
);
});
}
);
});
add_task(async function test_fillusername_error() {
await setupRelayScenario("enabled");
setupServerScenario("unknown_error");
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_URL_PATH,
},
async function(browser) {
await openRelayAC(browser);
const notificationPopup = document.getElementById("notification-popup");
const notificationShown = BrowserTestUtils.waitForPopupEvent(
notificationPopup,
"shown"
);
await notificationShown;
Assert.equal(
notificationPopup.querySelector("popupnotification").id,
"relay-integration-error-notification",
"Error message should be displayed"
);
await assertEvents([
{ object: "fill_username", method: "shown" },
{
object: "fill_username",
method: "clicked",
},
{
object: "reuse_panel",
method: "shown",
extra: { error_code: "408" },
},
]);
}
);
});
add_task(async function test_auth_token_error() {
setupRelayScenario("enabled");
gFxAccounts.getOAuthToken.restore();
const oauthTokenStub = sinon.stub(gFxAccounts, "getOAuthToken").throws();
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_URL_PATH,
},
async function(browser) {
await openRelayAC(browser);
const notificationPopup = document.getElementById("notification-popup");
const notificationShown = BrowserTestUtils.waitForPopupEvent(
notificationPopup,
"shown"
);
const notificationHidden = BrowserTestUtils.waitForPopupEvent(
notificationPopup,
"hidden"
);
await notificationShown;
notificationPopup
.querySelector("button.popup-notification-primary-button")
.click();
await notificationHidden;
await assertEvents([
{
object: "fill_username",
method: "shown",
extra: { error_code: "0" },
},
{
object: "fill_username",
method: "clicked",
extra: { error_code: "0" },
},
{
object: "fill_username",
method: "shown",
extra: { error_code: "418" },
},
]);
}
);
oauthTokenStub.restore();
});

Просмотреть файл

@ -0,0 +1,10 @@
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<form id="form-basic-signup">
<input id="form-basic-username" name="username">
<input id="form-basic-password" name="password" type="password" autocomplete="new-password">
<input id="form-basic-submit" type="submit" value="sign up">
</form>
</body></html>