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