tor-browser

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

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

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

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

Diffstat:
Mbrowser/components/backup/BackupService.sys.mjs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mbrowser/components/backup/metrics.yaml | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/backup/pings.yaml | 14++++++++++++++
Mbrowser/components/backup/tests/xpcshell/test_BackupService.js | 5+++++
Abrowser/components/backup/tests/xpcshell/test_BackupService_internalPostRecovery.js | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/backup/tests/xpcshell/xpcshell.toml | 2++
6 files changed, 256 insertions(+), 9 deletions(-)

diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs @@ -40,6 +40,8 @@ const BACKUP_DEBUG_INFO_PREF_NAME = "browser.backup.backup-debug-info"; const MAXIMUM_NUMBER_OF_UNREMOVABLE_STAGING_ITEMS_PREF_NAME = "browser.backup.max-num-unremovable-staging-items"; const CREATED_MANAGED_PROFILES_PREF_NAME = "browser.profiles.created"; +const RESTORED_BACKUP_METADATA_PREF_NAME = + "browser.backup.restored-backup-metadata"; const SCHEMAS = Object.freeze({ BACKUP_MANIFEST: 1, @@ -1226,6 +1228,37 @@ export class BackupService extends EventTarget { }); } }, BackupService.REGENERATION_DEBOUNCE_RATE_MS); + this.#postRecoveryPromise.then(() => { + const payload = { + is_restored: + !!Services.prefs.getIntPref( + "browser.backup.profile-restoration-date", + 0 + ) && + !Services.prefs.getBoolPref("browser.profiles.profile-copied", false), + }; + if (payload.is_restored) { + let backupMetadata = {}; + try { + backupMetadata = JSON.parse( + Services.prefs.getStringPref( + RESTORED_BACKUP_METADATA_PREF_NAME, + "{}" + ) + ); + } catch {} + payload.backup_timestamp = backupMetadata.date + ? new Date(backupMetadata.date).getTime() + : null; + payload.backup_app_name = backupMetadata.appName || null; + payload.backup_app_version = backupMetadata.appVersion || null; + payload.backup_build_id = backupMetadata.buildID || null; + payload.backup_os_name = backupMetadata.osName || null; + payload.backup_os_version = backupMetadata.osVersion || null; + payload.backup_legacy_client_id = backupMetadata.legacyClientID || null; + } + Glean.browserBackup.restoredProfileData.set(payload); + }); } /** @@ -3218,6 +3251,24 @@ export class BackupService extends EventTarget { profile.rootDir.path ); + try { + postRecovery.backupServiceInternal = { + // Indicates that this is not a result of a profile copy (which uses the + // same mechanism, but doesn't go through this function). + isProfileRestore: true, + restoreID: this.#_state.restoreID, + backupMetadata: { + date: this.#_state.backupFileInfo.date, + appName: this.#_state.backupFileInfo.appName, + appVersion: this.#_state.backupFileInfo.appVersion, + buildID: this.#_state.backupFileInfo.buildID, + osName: this.#_state.backupFileInfo.osName, + osVersion: this.#_state.backupFileInfo.osVersion, + legacyClientID: this.#_state.backupFileInfo.legacyClientID, + }, + }; + } catch {} + await this.#maybeWriteEncryptedStateObject( encState, profile.rootDir.path @@ -3384,17 +3435,39 @@ export class BackupService extends EventTarget { let postRecovery = await IOUtils.readJSON(postRecoveryFile); for (let resourceKey in postRecovery) { let postRecoveryEntry = postRecovery[resourceKey]; - let resourceClass = this.#resources.get(resourceKey); - if (!resourceClass) { - lazy.logConsole.error( - `Invalid resource for post-recovery step: ${resourceKey}` + if ( + resourceKey == "backupServiceInternal" && + postRecoveryEntry.isProfileRestore + ) { + Services.prefs.setStringPref( + RESTORED_BACKUP_METADATA_PREF_NAME, + JSON.stringify(postRecoveryEntry.backupMetadata) ); - continue; - } + Glean.browserBackup.restoredProfileLaunched.record({ + restore_id: postRecoveryEntry.restoreID, + }); + // This will 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-restored-profile-telemetry-set" + ); + GleanPings.postProfileRestore.submit(); + } else { + let resourceClass = this.#resources.get(resourceKey); + if (!resourceClass) { + lazy.logConsole.error( + `Invalid resource for post-recovery step: ${resourceKey}` + ); + continue; + } - lazy.logConsole.debug(`Running post-recovery step for ${resourceKey}`); - await new resourceClass().postRecovery(postRecoveryEntry); - lazy.logConsole.debug(`Done post-recovery step for ${resourceKey}`); + lazy.logConsole.debug( + `Running post-recovery step for ${resourceKey}` + ); + await new resourceClass().postRecovery(postRecoveryEntry); + lazy.logConsole.debug(`Done post-recovery step for ${resourceKey}`); + } } } finally { await IOUtils.remove(postRecoveryFile, { @@ -4316,6 +4389,7 @@ export class BackupService extends EventTarget { osName: archiveJSON?.meta?.osName, osVersion: archiveJSON?.meta?.osVersion, healthTelemetryEnabled: archiveJSON?.meta?.healthTelemetryEnabled, + legacyClientID: archiveJSON?.meta?.legacyClientID, }; // Clear any existing recovery error from state since we've successfully diff --git a/browser/components/backup/metrics.yaml b/browser/components/backup/metrics.yaml @@ -866,3 +866,62 @@ browser.backup: restore_id: type: string description: The identifier for the potential profile restore event. + + restored_profile_launched: + type: event + description: > + Dispatched when a restore completes, after the browser restarts into the + restored profile (from that profile). Note that this will not be sent if + the new profile has telemetry disabled. + 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: + - post-profile-restore + notification_emails: + - drubino@mozilla.com + expires: never + extra_keys: + restore_id: + type: string + description: The identifier for the potential profile restore event. + + restored_profile_data: + type: object + description: > + Describes the backup that was restored to make the current profile. If + This is not a restored profile, `is_restored` will be set to `false` and + no other values will be set. + 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: + - post-profile-restore + notification_emails: + - drubino@mozilla.com + expires: never + structure: + type: object + properties: + is_restored: + type: boolean + backup_timestamp: + type: number + backup_app_name: + type: string + backup_app_version: + type: string + backup_build_id: + type: string + backup_os_name: + type: string + backup_os_version: + type: string + backup_legacy_client_id: + type: string diff --git a/browser/components/backup/pings.yaml b/browser/components/backup/pings.yaml @@ -22,3 +22,17 @@ profile-restore: - https://bugzilla.mozilla.org/show_bug.cgi?id=1992809 notification_emails: - drubino@mozilla.com + +post-profile-restore: + description: | + Only used for telemetry that is sent from a profile created by restoring a + backup. This is in a separate ping specifically to avoid associating the + client id of the restored profile with the client ids of the backed up + profile and the profile that did the restoring. + include_client_id: false + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1992809 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1992809 + notification_emails: + - drubino@mozilla.com diff --git a/browser/components/backup/tests/xpcshell/test_BackupService.js b/browser/components/backup/tests/xpcshell/test_BackupService.js @@ -43,6 +43,7 @@ const BUILD_ID = "test-build-id"; const OS_NAME = "test-os-name"; const OS_VERSION = "test-os-version"; const TELEMETRY_ENABLED = true; +const LEGACY_CLIENT_ID = "legacy-client-id"; add_setup(function () { currentProfile = setupProfile(); @@ -1101,6 +1102,7 @@ add_task(async function test_getBackupFileInfo() { osName: OS_NAME, osVersion: OS_VERSION, healthTelemetryEnabled: TELEMETRY_ENABLED, + legacyClientID: LEGACY_CLIENT_ID, }, encConfig: {}, }, @@ -1131,6 +1133,7 @@ add_task(async function test_getBackupFileInfo() { osName: OS_NAME, osVersion: OS_VERSION, healthTelemetryEnabled: TELEMETRY_ENABLED, + legacyClientID: LEGACY_CLIENT_ID, }, "State should match a subset from the archive sample." ); @@ -1194,6 +1197,7 @@ add_task(async function test_getBackupFileInfo_error_handling() { osName: OS_NAME, osVersion: OS_VERSION, healthTelemetryEnabled: TELEMETRY_ENABLED, + legacyClientID: LEGACY_CLIENT_ID, }, encConfig: {}, }, @@ -1217,6 +1221,7 @@ add_task(async function test_getBackupFileInfo_error_handling() { osName: OS_NAME, osVersion: OS_VERSION, healthTelemetryEnabled: TELEMETRY_ENABLED, + legacyClientID: LEGACY_CLIENT_ID, }, "Initial state should be set correctly" ); diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_internalPostRecovery.js b/browser/components/backup/tests/xpcshell/test_BackupService_internalPostRecovery.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let testBackupDirPath; +let recoveredProfilePath; + +add_setup(async function () { + setupProfile(); + + testBackupDirPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "internalPostRecoveryBackup" + ); + recoveredProfilePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "internalPostRecoveryRestore" + ); + registerCleanupFunction(async () => { + await IOUtils.remove(testBackupDirPath, { recursive: true }); + await IOUtils.remove(recoveredProfilePath, { recursive: true }); + }); +}); + +add_task(async function test_internal_post_recovery() { + let bs = new BackupService({}); + + const testBackupPath = ( + await bs.createBackup({ profilePath: testBackupDirPath }) + ).archivePath; + + await bs.getBackupFileInfo(testBackupPath); + const restoreID = bs.state.restoreID; + const expectedRestoreAttributes = { + is_restored: true, + backup_timestamp: new Date(bs.state.backupFileInfo.date).getTime(), + backup_app_name: bs.state.backupFileInfo.appName, + backup_app_version: bs.state.backupFileInfo.appVersion, + backup_build_id: bs.state.backupFileInfo.buildID, + backup_os_name: bs.state.backupFileInfo.osName, + backup_os_version: bs.state.backupFileInfo.osVersion, + backup_legacy_client_id: bs.state.backupFileInfo.legacyClientID, + }; + + await bs.recoverFromBackupArchive( + testBackupPath, + null, + false, + testBackupDirPath, + recoveredProfilePath + ); + + // Intercept the telemetry that we want to check for before it gets submitted + // and cleared out. + let restoredProfileLaunchedEvents; + let telemetrySetCallback = () => { + Services.obs.removeObserver( + telemetrySetCallback, + "browser-backup-restored-profile-telemetry-set" + ); + restoredProfileLaunchedEvents = + Glean.browserBackup.restoredProfileLaunched.testGetValue(); + }; + Services.obs.addObserver( + telemetrySetCallback, + "browser-backup-restored-profile-telemetry-set" + ); + + // Simulate the browser starting up into this profile + Services.prefs.setIntPref( + "browser.backup.profile-restoration-date", + Math.round(Date.now() / 1000) + ); + bs = new BackupService({}); + await bs.checkForPostRecovery(recoveredProfilePath); + + Assert.equal( + restoredProfileLaunchedEvents.length, + 1, + "Should be a single restore profile launch event after we launch a restored profile" + ); + Assert.deepEqual( + restoredProfileLaunchedEvents[0].extra, + { restore_id: restoreID }, + "Restore profile launch event should have the right data" + ); + + Assert.deepEqual( + Glean.browserBackup.restoredProfileData.testGetValue(), + expectedRestoreAttributes + ); +}); diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml @@ -42,6 +42,8 @@ support-files = [ ["test_BackupService_finalizeSingleFileArchive.js"] +["test_BackupService_internalPostRecovery.js"] + ["test_BackupService_onedrive.js"] run-if = ["os == 'win'"] run-sequentially = ["true"] # Mock Windows registry interferes with normal operations