tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 45f78eb8a4ce94ff423616cb3f0b286e16391541
parent ed88a0583f7e9b59b52398d664d8019677a298b3
Author: Jason Prickett <jprickett@mozilla.com>
Date:   Thu, 16 Oct 2025 18:29:28 +0000

Bug 1993550 - Add utm params to restore component support links when used in about welcome r=omc-reviewers,sthompson,mviar

Differential Revision: https://phabricator.services.mozilla.com/D268752

Diffstat:
Mbrowser/components/backup/content/restore-from-backup.mjs | 88++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mbrowser/components/backup/tests/browser/browser_settings_restore_from_backup.js | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/backup/tests/chrome/test_restore_from_backup.html | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 253 insertions(+), 16 deletions(-)

diff --git a/browser/components/backup/content/restore-from-backup.mjs b/browser/components/backup/content/restore-from-backup.mjs @@ -2,7 +2,11 @@ * 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/. */ -import { html, styleMap } from "chrome://global/content/vendor/lit.all.mjs"; +import { + html, + ifDefined, + styleMap, +} from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; import { ERRORS } from "chrome://browser/content/backup/backup-constants.mjs"; import { getErrorL10nId } from "chrome://browser/content/backup/backup-errors.mjs"; @@ -55,7 +59,7 @@ export default class RestoreFromBackup extends MozLitElement { scheduledBackupsEnabled: false, lastBackupDate: null, lastBackupFileName: "", - supportBaseLink: "", + supportBaseLink: "https://support.mozilla.org/", backupInProgress: false, recoveryInProgress: false, recoveryErrorCode: ERRORS.NONE, @@ -208,6 +212,64 @@ export default class RestoreFromBackup extends MozLitElement { } } + /** + * Constructs a support URL with UTM parameters for use + * when embedded in about:welcome + * + * @param {string} supportPage - The support page slug + * @returns {string} The full support URL including UTM params + */ + + getSupportURLWithUTM(supportPage) { + let supportURL = new URL( + supportPage, + this.backupServiceState.supportBaseLink + ); + supportURL.searchParams.set("utm_medium", "firefox-desktop"); + supportURL.searchParams.set("utm_source", "npo"); + supportURL.searchParams.set("utm_campaign", "fx-backup-restore"); + supportURL.searchParams.set("utm_content", "restore-error"); + return supportURL.href; + } + + /** + * Returns a support link anchor element, either with UTM params for use in + * about:welcome, or falling back to moz-support-link otherwise + * + * @param {object} options - Link configuration options + * @param {string} options.id - The element id + * @param {string} options.l10nId - The fluent l10n id + * @param {string} options.l10nName - The fluent l10n name + * @param {string} options.supportPage - The support page slug + * @returns {TemplateResult} The link template + */ + + getSupportLinkAnchor({ + id, + l10nId, + l10nName, + supportPage = "firefox-backup", + }) { + if (this.aboutWelcomeEmbedded) { + return html`<a + id=${id} + target="_blank" + href=${this.getSupportURLWithUTM(supportPage)} + data-l10n-id=${ifDefined(l10nId)} + data-l10n-name=${ifDefined(l10nName)} + ></a>`; + } + + return html`<a + id=${id} + slot="support-link" + is="moz-support-link" + support-page=${supportPage} + data-l10n-id=${ifDefined(l10nId)} + data-l10n-name=${ifDefined(l10nName)} + ></a>`; + } + applyContentCustomizations() { if (this.aboutWelcomeEmbedded) { this.style.setProperty("--label-font-weight", "600"); @@ -241,13 +303,10 @@ export default class RestoreFromBackup extends MozLitElement { </div> ${!this.backupServiceState?.backupFileInfo - ? html`<a - id="restore-from-backup-no-backup-file-link" - slot="support-link" - is="moz-support-link" - support-page="firefox-backup" - data-l10n-id="restore-from-backup-no-backup-file-link" - ></a>` + ? this.getSupportLinkAnchor({ + id: "restore-from-backup-no-backup-file-link", + l10nId: "restore-from-backup-no-backup-file-link", + }) : null} ${this.backupServiceState?.backupFileInfo ? html`<p @@ -331,13 +390,10 @@ export default class RestoreFromBackup extends MozLitElement { class="field-error" data-l10n-id="backup-service-error-incorrect-password" > - <a - id="backup-incorrect-password-support-link" - slot="support-link" - is="moz-support-link" - support-page="firefox-backup" - data-l10n-name="incorrect-password-support-link" - ></a> + ${this.getSupportLinkAnchor({ + id: "backup-incorrect-password-support-link", + l10nName: "incorrect-password-support-link", + })} </span> ` : html`<label diff --git a/browser/components/backup/tests/browser/browser_settings_restore_from_backup.js b/browser/components/backup/tests/browser/browser_settings_restore_from_backup.js @@ -3,6 +3,10 @@ "use strict"; +const { ERRORS } = ChromeUtils.importESModule( + "chrome://browser/content/backup/backup-constants.mjs" +); + let TEST_PROFILE_PATH; add_setup(async () => { @@ -460,3 +464,88 @@ add_task(async function test_restore_backup_file_info_display() { ); }); }); + +/** + * Helper function to test that a support link has the appropriate attributes + * + * @param {Element} link - The support link element to test + * @param {string} linkName - The name of the link to test + */ + +function assertNonEmbeddedSupportLink(link, linkName) { + Assert.ok(link, `${linkName} should be present`); + Assert.equal( + link.getAttribute("is"), + "moz-support-link", + `${linkName} should use moz-support-link when not embedded` + ); + Assert.equal( + link.getAttribute("support-page"), + "firefox-backup", + `${linkName} should have support-page attribute` + ); + Assert.ok( + !link.href.includes("utm_source"), + `${linkName} should not have UTM params when not embedded` + ); +} + +/** + * Tests that support links use moz-support-link when aboutWelcomeEmbedded is falsy + */ +add_task(async function test_support_links_non_embedded() { + await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => { + let settings = browser.contentDocument.querySelector("backup-settings"); + await settings.updateComplete; + + settings.restoreFromBackupButtonEl.click(); + await settings.updateComplete; + + let restoreFromBackup = settings.restoreFromBackupEl; + Assert.ok(restoreFromBackup, "restore-from-backup should be found"); + + Assert.ok( + !restoreFromBackup.aboutWelcomeEmbedded, + "aboutWelcomeEmbedded should be falsy" + ); + + // Test the 'no backup file' link + let noBackupFileLink = restoreFromBackup.shadowRoot.querySelector( + "#restore-from-backup-no-backup-file-link" + ); + assertNonEmbeddedSupportLink(noBackupFileLink, "'No backup file' link"); + + // Test the description link + restoreFromBackup.backupServiceState = { + ...restoreFromBackup.backupServiceState, + backupFileInfo: { + date: new Date(), + deviceName: "test-device", + isEncrypted: false, + }, + }; + await restoreFromBackup.updateComplete; + + let descriptionLink = restoreFromBackup.shadowRoot.querySelector( + "#restore-from-backup-learn-more-link" + ); + assertNonEmbeddedSupportLink(descriptionLink, "Description link"); + + // Test the incorrect password link + restoreFromBackup.backupServiceState = { + ...restoreFromBackup.backupServiceState, + backupFileInfo: { + date: new Date(), + deviceName: "test-device", + isEncrypted: true, + }, + recoveryErrorCode: ERRORS.UNAUTHORIZED, + }; + await restoreFromBackup.updateComplete; + + let passwordErrorLink = restoreFromBackup.shadowRoot.querySelector( + "#backup-incorrect-password-support-link" + ); + assertNonEmbeddedSupportLink(passwordErrorLink, "Password error link"); + }); +}); diff --git a/browser/components/backup/tests/chrome/test_restore_from_backup.html b/browser/components/backup/tests/chrome/test_restore_from_backup.html @@ -262,6 +262,98 @@ } restoreFromBackup.remove(); }); + + /** + * Helper function to test that a support link has correct attributes + * and UTM params when used with aboutWelcomeEmbedded + * + * @param {Element} link - The support link element to test + * @param {string} linkName - The name of the link to test + */ + + function assertEmbeddedSupportLink(link, linkName) { + ok(link, `${linkName} should be present`); + ok( + !link.hasAttribute("is"), + `${linkName} should not have 'is' attribute` + ); + ok( + !link.hasAttribute("support-page"), + `${linkName} should not have support-page attribute` + ); + ok( + link.hasAttribute("href"), + `${linkName} should have href attribute` + ); + + let url = new URL(link.getAttribute("href")); + + is( + url.searchParams.get("utm_medium"), + "firefox-desktop", + `${linkName} should have correct utm_medium` + ); + is( + url.searchParams.get("utm_source"), + "npo", + `${linkName} should have correct utm_source` + ); + is( + url.searchParams.get("utm_campaign"), + "fx-backup-restore", + `${linkName} should have correct utm_campaign` + ); + is( + url.searchParams.get("utm_content"), + "restore-error", + `${linkName} should have correct utm_content` + ); + is( + link.getAttribute("target"), + "_blank", + `${linkName} should have target='_blank'` + ); + } + + /** + * Tests that support links have UTM parameters when aboutWelcomeEmbedded is true + */ + add_task(async function test_support_links_with_utm_params() { + let content = document.getElementById("content"); + let restoreFromBackup = document.createElement("restore-from-backup"); + content.appendChild(restoreFromBackup); + + // Set up the support base link for testing, otherwise links will be broken + restoreFromBackup.backupServiceState = { + ...restoreFromBackup.backupServiceState, + supportBaseLink: "https://support.mozilla.org/", + }; + restoreFromBackup.aboutWelcomeEmbedded = true; + await restoreFromBackup.updateComplete; + + // Test the "no backup file" link + let noBackupFileLink = restoreFromBackup.shadowRoot.querySelector( + "#restore-from-backup-no-backup-file-link" + ); + assertEmbeddedSupportLink(noBackupFileLink, "No backup file link"); + + // Test the incorrect password support link + restoreFromBackup.backupServiceState = { + ...restoreFromBackup.backupServiceState, + backupFileInfo: { + date: new Date(), + isEncrypted: true, + }, + recoveryErrorCode: ERRORS.UNAUTHORIZED, + }; + await restoreFromBackup.updateComplete; + let passwordErrorLink = restoreFromBackup.shadowRoot.querySelector( + "#backup-incorrect-password-support-link" + ); + assertEmbeddedSupportLink(passwordErrorLink, "Password error link"); + + restoreFromBackup.remove(); + }); </script> </head> <body>