tor-browser

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

commit 10837e9d630986aeda460d6edf67be7cc4b2a9cc
parent 7360a821339e6f5070fc53c8deb1616ac8707e88
Author: Robin Steuber <bytesized@mozilla.com>
Date:   Fri, 14 Nov 2025 23:01:35 +0000

Bug 1992809 - Add telemetry to be sent when restoring a backed up Firefox Profile, prior to restarting r=cdupuis,data-stewards,jhirsch

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

Diffstat:
Mbrowser/components/backup/BackupService.sys.mjs | 26++++++++++++++++++++++++++
Mbrowser/components/backup/common/backup-constants.mjs | 9+++++++++
Mbrowser/components/backup/metrics.yaml | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/backup/tests/xpcshell/test_BackupService.js | 42++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/backup/tests/xpcshell/test_BackupService_wrongPassword.js | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/backup/tests/xpcshell/xpcshell.toml | 2++
6 files changed, 250 insertions(+), 0 deletions(-)

diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs @@ -16,6 +16,7 @@ import { import { ERRORS, STEPS, + errorString, } from "chrome://browser/content/backup/backup-constants.mjs"; import { BackupError } from "resource:///modules/backup/BackupError.mjs"; @@ -1167,6 +1168,10 @@ export class BackupService extends EventTarget { if (this.#instance) { return this.#instance; } + + // If there is unsent restore telemetry, send it now. + GleanPings.profileRestore.submit(); + this.#instance = new BackupService(DefaultBackupResources); this.#instance.checkForPostRecovery(); @@ -2872,6 +2877,10 @@ export class BackupService extends EventTarget { return null; } + Glean.browserBackup.restoreStarted.record({ + restore_id: this.#_state.restoreID, + }); + try { this.#_state.recoveryInProgress = true; this.#_state.recoveryErrorCode = 0; @@ -2928,6 +2937,17 @@ export class BackupService extends EventTarget { profileRootPath, encState ); + + Glean.browserBackup.restoreComplete.record({ + restore_id: this.#_state.restoreID, + }); + // We are probably about to shutdown, so we want to submit this ASAP. + // But this will also clear out the data in this ping, which is a bit + // of a problem for testing. So fire off an event first that tests can + // listen for. + Services.obs.notifyObservers(null, "browser-backup-restore-complete"); + GleanPings.profileRestore.submit(); + return newProfile; } finally { // If we had decrypted a backup, we would have created the temporary @@ -2945,6 +2965,12 @@ export class BackupService extends EventTarget { ); } } + } catch (ex) { + Glean.browserBackup.restoreFailed.record({ + restore_id: this.#_state.restoreID, + error_type: errorString(ex.cause), + }); + throw ex; } finally { this.#_state.recoveryInProgress = false; this.stateUpdate(); diff --git a/browser/components/backup/common/backup-constants.mjs b/browser/components/backup/common/backup-constants.mjs @@ -50,6 +50,15 @@ export const ERRORS = Object.freeze({ UNSUPPORTED_APPLICATION: 14, }); +export function errorString(errorCodeToLookup) { + for (let [errorName, errorCode] of Object.entries(ERRORS)) { + if (errorCode == errorCodeToLookup) { + return errorName; + } + } + return "UNDEFINED_ERROR"; +} + /** * These are steps that the BackupService or any of its subcomponents might * be going through during configuration, creation, deletion of or restoration diff --git a/browser/components/backup/metrics.yaml b/browser/components/backup/metrics.yaml @@ -801,3 +801,68 @@ browser.backup: telemetry_enabled: type: boolean description: Whether telemetry was enabled when the backup was created. + + restore_started: + type: event + description: Dispatched when a file is chosen to be restored. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1992809 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1992809 + data_sensitivity: + - interaction + send_in_pings: + - profile-restore + notification_emails: + - drubino@mozilla.com + expires: never + extra_keys: + restore_id: + type: string + description: The identifier for the potential profile restore event. + + restore_failed: + type: event + description: Dispatched when a restore fails (prior to Firefox restart). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1992809 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1992809 + data_sensitivity: + - technical + send_in_pings: + - profile-restore + notification_emails: + - drubino@mozilla.com + expires: never + extra_keys: + restore_id: + type: string + description: The identifier for the potential profile restore event. + error_type: + type: string + description: > + The reason for the failure. Possible values include, but are not + limited to, "UNAUTHORIZED" (bad password provided by user), + "CORRUPTED_ARCHIVE", and "FILE_SYSTEM_ERROR". + + restore_complete: + type: event + description: > + Dispatched when a restore completes, just before the browser restarts into + the restored profile. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1992809 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1992809 + data_sensitivity: + - interaction + send_in_pings: + - profile-restore + notification_emails: + - drubino@mozilla.com + expires: never + extra_keys: + restore_id: + type: string + description: The identifier for the potential profile restore event. diff --git a/browser/components/backup/tests/xpcshell/test_BackupService.js b/browser/components/backup/tests/xpcshell/test_BackupService.js @@ -290,6 +290,26 @@ async function testCreateBackupHelper(sandbox, taskFn) { // make our current profile default profileSvc.defaultProfile = currentProfile; + await bs.getBackupFileInfo(backupFilePath); + const restoreID = bs.state.restoreID; + + // Intercept the telemetry that we want to check for before it gets submitted + // and cleared out. + let restoreStartedEvents; + let restoreCompleteEvents; + let restoreCompleteCallback = () => { + Services.obs.removeObserver( + restoreCompleteCallback, + "browser-backup-restore-complete" + ); + restoreStartedEvents = Glean.browserBackup.restoreStarted.testGetValue(); + restoreCompleteEvents = Glean.browserBackup.restoreComplete.testGetValue(); + }; + Services.obs.addObserver( + restoreCompleteCallback, + "browser-backup-restore-complete" + ); + let recoveredProfile = await bs.recoverFromBackupArchive( backupFilePath, null, @@ -315,6 +335,28 @@ async function testCreateBackupHelper(sandbox, taskFn) { "The new profile should now be the default" ); + Assert.equal( + restoreStartedEvents.length, + 1, + "Should be a single restore start event after we start restoring a profile" + ); + Assert.deepEqual( + restoreStartedEvents[0].extra, + { restore_id: restoreID }, + "Restore start event should have the right data" + ); + + Assert.equal( + restoreCompleteEvents.length, + 1, + "Should be a single restore complete event after we start restoring a profile" + ); + Assert.deepEqual( + restoreCompleteEvents[0].extra, + { restore_id: restoreID }, + "Restore complete event should have the right data" + ); + // Check that resources were recovered from highest to lowest backup priority. sinon.assert.callOrder( FakeBackupResource3.prototype.recover, diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_wrongPassword.js b/browser/components/backup/tests/xpcshell/test_BackupService_wrongPassword.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + ERRORS: "chrome://browser/content/backup/backup-constants.mjs", +}); + +const bs = new BackupService({ FakeBackupResource1 }); +const correctPassword = "correcthorsebatterystaple"; +const incorrectPassword = "Tr0ub4dor&3"; +let testBackupDirPath; +let testBackupPath; + +add_setup(async function () { + setupProfile(); + + let sandbox = sinon.createSandbox(); + let fakeManifestEntry = { fake: "test" }; + sandbox + .stub(FakeBackupResource1.prototype, "backup") + .resolves(fakeManifestEntry); + sandbox.stub(FakeBackupResource1.prototype, "recover").resolves(); + + testBackupDirPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "wrongPasswordTestBackup" + ); + await bs.enableEncryption(correctPassword); + testBackupPath = (await bs.createBackup({ profilePath: testBackupDirPath })) + .archivePath; + + registerCleanupFunction(async () => { + sandbox.restore(); + + await IOUtils.remove(testBackupDirPath, { recursive: true }); + }); +}); + +/** + * Tests the case where the wrong password is given when trying to restore from + * a backup. + * + * @param {string|null} passwordToUse + * The password to decrypt with, or `null` to specify no password. + */ +async function testWrongPassword(passwordToUse) { + Services.fog.testResetFOG(); + + Assert.ok(await IOUtils.exists(testBackupPath), "The backup file exists"); + + let recoveredProfilePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "wrongPasswordTestRecoveredProfile" + ); + registerCleanupFunction(async () => { + await IOUtils.remove(recoveredProfilePath, { recursive: true }); + }); + + await bs.getBackupFileInfo(testBackupPath); + const restoreID = bs.state.restoreID; + + await Assert.rejects( + bs.recoverFromBackupArchive( + testBackupPath, + passwordToUse, + false, + testBackupDirPath, + recoveredProfilePath + ), + err => err.cause == ERRORS.UNAUTHORIZED + ); + + let events = Glean.browserBackup.restoreStarted.testGetValue(); + Assert.equal( + events.length, + 1, + "Should be a single restore started event after we start restoring a profile" + ); + Assert.deepEqual( + events[0].extra, + { restore_id: restoreID }, + "Restore event should have the right data" + ); + + events = Glean.browserBackup.restoreFailed.testGetValue(); + Assert.equal( + events.length, + 1, + "Should be a single restore failed event after we fail to restore a profile" + ); + Assert.deepEqual( + events[0].extra, + { restore_id: restoreID, error_type: "UNAUTHORIZED" }, + "Restore failure event should have the right data" + ); +} + +add_task(async function test_wrong_password() { + await testWrongPassword(incorrectPassword); +}); + +add_task(async function test_no_password() { + await testWrongPassword(null); +}); diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml @@ -70,6 +70,8 @@ run-sequentially = ["true"] # Mock Windows registry interferes with normal opera ["test_BackupService_telemetry.js"] +["test_BackupService_wrongPassword.js"] + ["test_CookiesBackupResource.js"] ["test_CredentialsAndSecurityBackupResource.js"]