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:
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