commit df59ce6374fa20c338d3de7373021bbd1a3a499b
parent dbd45c0668a3a0b0031f5a42919cea1cc3af970d
Author: Duncan McIntosh <dmcintosh@mozilla.com>
Date: Wed, 29 Oct 2025 19:37:55 +0000
Bug 1992808 - Part 1: Add new events and extra keys to Firefox Backup archival telemetry. r=bytesized,kpatenio,toolkit-telemetry-reviewers
Originally based on https://phabricator.services.mozilla.com/D268168
(thanks Robin!)
Differential Revision: https://phabricator.services.mozilla.com/D268774
Diffstat:
8 files changed, 631 insertions(+), 9 deletions(-)
diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs
@@ -619,6 +619,7 @@ export class BackupService extends EventTarget {
return {
enabled: false,
reason: "Archiving a profile disabled remotely.",
+ internalReason: "nimbus",
};
}
@@ -808,6 +809,15 @@ export class BackupService extends EventTarget {
#takenMeasurements = false;
/**
+ * Stores whether backing up has been disabled at some point during this
+ * session. If it has been, the backupDisabledReason telemetry metric is set
+ * on each backup. (It cannot be unset due to Glean limitations.)
+ *
+ * @type {boolean}
+ */
+ #wasArchivePreviouslyDisabled = false;
+
+ /**
* The path of the default parent directory for saving backups.
* The current default is the Documents directory.
*
@@ -1098,10 +1108,19 @@ export class BackupService extends EventTarget {
if (!this.#backupWriteAbortController.signal.aborted) {
await this.deleteLastBackup();
if (lazy.scheduledBackupsPref) {
- await this.createBackupOnIdleDispatch();
+ await this.createBackupOnIdleDispatch({
+ reason: "user deleted some data",
+ });
}
}
}, BackupService.REGENERATION_DEBOUNCE_RATE_MS);
+
+ let backupStatus = this.archiveEnabledStatus;
+ if (!backupStatus.enabled) {
+ let internalReason = backupStatus.internalReason;
+ this.#wasArchivePreviouslyDisabled = true;
+ Glean.browserBackup.backupDisabledReason.set(internalReason);
+ }
}
/**
@@ -1469,15 +1488,25 @@ export class BackupService extends EventTarget {
* @param {string} [options.profilePath=PathUtils.profileDir]
* The path to the profile to backup. By default, this is the current
* profile.
+ * @param {string} [options.reason=unknown]
+ * The reason for starting the backup. This is sent along with the
+ * backup.backup_start event.
* @returns {Promise<CreateBackupResult|null>}
* A promise that resolves to information about the backup that was
* created, or null if the backup failed.
*/
- async createBackup({ profilePath = PathUtils.profileDir } = {}) {
- const status = this.archiveEnabledStatus;
+ async createBackup({
+ profilePath = PathUtils.profileDir,
+ reason = "unknown",
+ } = {}) {
+ let status = this.archiveEnabledStatus;
if (!status.enabled) {
lazy.logConsole.debug(status.reason);
+ this.#wasArchivePreviouslyDisabled = true;
+ Glean.browserBackup.backupDisabledReason.set(status.internalReason);
return null;
+ } else if (this.#wasArchivePreviouslyDisabled) {
+ Glean.browserBackup.backupDisabledReason.set("reenabled");
}
// createBackup does not allow re-entry or concurrent backups.
@@ -1486,6 +1515,8 @@ export class BackupService extends EventTarget {
return null;
}
+ Glean.browserBackup.backupStart.record({ reason });
+
return locks.request(
BackupService.WRITE_BACKUP_LOCK_NAME,
{ signal: this.#backupWriteAbortController.signal },
@@ -1587,7 +1618,11 @@ export class BackupService extends EventTarget {
this.#_state.lastBackupDate = nowSeconds;
Glean.browserBackup.totalBackupTime.stopAndAccumulate(backupTimer);
- Glean.browserBackup.created.record();
+ Glean.browserBackup.created.record({
+ encrypted: this.#_state.encryptionEnabled,
+ location: this.classifyLocationForTelemetry(archiveDestFolderPath),
+ size: archiveSizeBytesNearestMebibyte,
+ });
// we should reset any values that were set for retry error handling
Services.prefs.clearUserPref(DISABLED_ON_IDLE_RETRY_PREF_NAME);
@@ -1625,6 +1660,51 @@ export class BackupService extends EventTarget {
}
/**
+ * Creates a coarse name corresponding to the location where the backup will
+ * be stored. This is sent by telemetry, and aims to anonymize the data.
+ *
+ * Normally, the path should end in 'Restore Firefox'; if it doesn't, you
+ * might be passing the wrong path and will get the wrong result.
+ *
+ * This isn't private so it can be used by the tests; avoid relying on this
+ * code from elsewhere.
+ *
+ * @param {string} path The absolute path that contains the backup file.
+ * @returns {string} A coarse location to send with the telemetry.
+ */
+ classifyLocationForTelemetry(path) {
+ let knownLocations = {
+ onedrive: "OneDrPD",
+ documents: "Docs",
+ };
+
+ let location;
+ try {
+ // By default, the backup will go into a folder called 'Restore Firefox',
+ // so we actually want the parent directory.
+ location = lazy.nsLocalFile(path).parent;
+ } catch (e) {
+ // initWithPath (at least on Windows) is _really_ picky; e.g.
+ // "C:/Windows/system32" will fail. Bail out if something went wrong so
+ // this doesn't affect the backup.
+ return `Error: ${e.name ?? "Unknown error"}`;
+ }
+
+ for (let label of Object.keys(knownLocations)) {
+ try {
+ let candidate = Services.dirsvc.get(knownLocations[label], Ci.nsIFile);
+ if (candidate.equals(location)) {
+ return label;
+ }
+ } catch (e) {
+ // ignore (maybe it wasn't found?)
+ }
+ }
+
+ return "other";
+ }
+
+ /**
* Generates a string from a Date in the form of:
*
* YYYYMMDD-HHMM
@@ -3429,7 +3509,10 @@ export class BackupService extends EventTarget {
onUpdateScheduledBackups(isScheduledBackupsEnabled) {
if (this.#_state.scheduledBackupsEnabled != isScheduledBackupsEnabled) {
if (isScheduledBackupsEnabled) {
- Glean.browserBackup.toggleOn.record();
+ Glean.browserBackup.toggleOn.record({
+ encrypted: this.#_state.encryptionEnabled,
+ location: this.classifyLocationForTelemetry(lazy.backupDirPref),
+ });
} else {
Glean.browserBackup.toggleOff.record();
}
@@ -3998,8 +4081,11 @@ export class BackupService extends EventTarget {
* Calls BackupService.createBackup at the next moment when the event queue
* is not busy with higher priority events. This is intentionally broken out
* into its own method to make it easier to stub out in tests.
+ *
+ * @param {...*} args
+ * Arguments to pass through to createBackup.
*/
- createBackupOnIdleDispatch() {
+ createBackupOnIdleDispatch(...args) {
let now = Math.floor(Date.now() / 1000);
let errorStateDebugInfo = Services.prefs.getStringPref(
BACKUP_DEBUG_INFO_PREF_NAME,
@@ -4026,15 +4112,16 @@ export class BackupService extends EventTarget {
"idleDispatch fired. Attempting to create a backup."
);
- this.createBackup().catch(e => {
+ this.createBackup(...args).catch(e => {
lazy.logConsole.debug(
`There was an error creating backup on idle dispatch: ${e}`
);
BackupService.#errorRetries += 1;
if (BackupService.#errorRetries > lazy.backupRetryLimit) {
- // We've had too many error's with retries, let's only backup on next timestamp
+ // We've had too many errors with retries, let's only backup on next timestamp
Services.prefs.setBoolPref(DISABLED_ON_IDLE_RETRY_PREF_NAME, true);
+ Glean.browserBackup.backupThrottled.record();
}
});
});
diff --git a/browser/components/backup/metrics.yaml b/browser/components/backup/metrics.yaml
@@ -459,8 +459,10 @@ browser.backup:
Dispatched when scheduled backups are enabled.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1908732
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1992808
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1908732
+ - https://phabricator.services.mozilla.com/D268774
data_sensitivity:
- interaction
send_in_pings:
@@ -469,6 +471,17 @@ browser.backup:
- mconley@mozilla.com
expires: never
telemetry_mirror: BrowserBackup_ToggleOn_Backupservice
+ extra_keys:
+ encrypted:
+ type: boolean
+ description: Whether or not encryption is enabled.
+ location:
+ type: string
+ description: >
+ The location of the backup. This can be either "onedrive",
+ "documents", or "other". "other" will be sent if the parent directory
+ of the backup doesn't match the default "documents" or "onedrive"
+ save paths.
toggle_off:
type: event
@@ -493,8 +506,10 @@ browser.backup:
Dispatched when a backup is successfully created.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1908732
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1992808
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1908732
+ - https://phabricator.services.mozilla.com/D268774
data_sensitivity:
- technical
send_in_pings:
@@ -503,6 +518,22 @@ browser.backup:
- mconley@mozilla.com
expires: never
telemetry_mirror: BrowserBackup_Created_Backupservice
+ extra_keys:
+ encrypted:
+ type: boolean
+ description: Whether or not encryption is enabled for the backup.
+ location:
+ type: string
+ description: >
+ The location of the backup. This can be either "onedrive",
+ "documents", or "other". "other" will be sent if the parent directory
+ of the backup doesn't match the default "documents" or "onedrive"
+ save paths.
+ size:
+ type: quantity
+ description: >
+ The size-on-disk of the created backup. This is rounded to the
+ nearest mebibyte to reduce fingerprintability.
change_location:
type: event
@@ -598,3 +629,67 @@ browser.backup:
The string representation of the step that backup creation was in
when the error occurred.
telemetry_mirror: BrowserBackup_Error_Backupservice
+
+ backup_start:
+ type: event
+ description: >
+ Dispatched when a backup is initiated.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1992808
+ data_reviews:
+ - https://phabricator.services.mozilla.com/D268774
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - drubino@mozilla.com
+ expires: never
+ extra_keys:
+ reason:
+ type: string
+ description: >
+ The reason that the backup was started. Can be one of the following:
+ - "manual" if the user selected "Backup Now" (not implemented yet,
+ will be in Part 2);
+ - "idle" if the user was idle and sufficiently long passed since
+ the last backup (not implemented yet, will be in Part 3);
+ - "missed" if the user was idle, sufficiently long has passed since
+ the last backup, and this backup was scheduled to happen before
+ Firefox opened (not implemented yet, will be in Part 3);
+ - "user deleted some data" if cookies or other user data was
+ deleted;
+ - "encryption" if the password was added, changed, or removed (not
+ implemented yet, will be in Part 2);
+ - "location" if the location changed (not implemented yet, see
+ bug 1996377);
+ - "first" if scheduled backups were just enabled (not implemented
+ yet, see bug 1900125);
+ - or "unknown" if the reason wasn't specified.
+
+ backup_throttled:
+ type: event
+ description: >
+ Dispatched when backups are throttled due to too many errors.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1992808
+ data_reviews:
+ - https://phabricator.services.mozilla.com/D268774
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - drubino@mozilla.com
+ expires: never
+
+ backup_disabled_reason:
+ type: string
+ description: >
+ Only set if `browser.backup.enabled` is `false`. Possible reasons are
+ "nimbus", "pref" (non-Nimbus), "policy", "managedProfiles".
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1992808
+ data_reviews:
+ - https://phabricator.services.mozilla.com/D268774
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - drubino@mozilla.com
+ expires: never
diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_enabled.js b/browser/components/backup/tests/xpcshell/test_BackupService_enabled.js
@@ -60,11 +60,18 @@ add_task(async function test_archive_killswitch_enrollment() {
"`archiveEnabledStatus` should report that it is disabled by the archive killswitch."
);
+ Services.fog.testResetFOG();
let backup = await bs.createBackup();
Assert.ok(
!backup,
"Creating a backup should fail when the archive killswitch is active."
);
+ let telemetry = Glean.browserBackup.backupDisabledReason.testGetValue();
+ Assert.equal(
+ telemetry,
+ "nimbus",
+ "Telemetry identifies the backup is disabled by Nimbus."
+ );
// End the experiment.
await cleanupExperiment();
@@ -83,6 +90,13 @@ add_task(async function test_archive_killswitch_enrollment() {
await IOUtils.exists(backup.archivePath),
"Archive file should exist on disk."
);
+
+ telemetry = Glean.browserBackup.backupDisabledReason.testGetValue();
+ Assert.equal(
+ telemetry,
+ "reenabled",
+ "Telemetry identifies the backup was re-enabled."
+ );
});
add_task(async function test_restore_killswitch_enrollment() {
diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_regeneration.js b/browser/components/backup/tests/xpcshell/test_BackupService_regeneration.js
@@ -143,8 +143,13 @@ async function expectRegeneration(taskFn, msg) {
});
let createBackupDeferred = Promise.withResolvers();
- sandbox.stub(bs, "createBackupOnIdleDispatch").callsFake(() => {
+ sandbox.stub(bs, "createBackupOnIdleDispatch").callsFake(options => {
Assert.ok(true, "Saw createBackupOnIdleDispatch call");
+ Assert.equal(
+ options.reason,
+ "user deleted some data",
+ "Backup was recorded as being caused by user data deletion"
+ );
createBackupDeferred.resolve();
return Promise.resolve();
});
diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_retryHeuristic.js b/browser/components/backup/tests/xpcshell/test_BackupService_retryHeuristic.js
@@ -63,6 +63,8 @@ add_setup(async () => {
});
add_task(async function test_retry_limit() {
+ Services.fog.testResetFOG();
+
let bs = new BackupService();
let sandbox = sinon.createSandbox();
// Make createBackup fail intentionally
@@ -101,6 +103,22 @@ add_task(async function test_retry_limit() {
ERRORS.UNKNOWN,
"Error code has been set"
);
+
+ if (i < n) {
+ Assert.equal(
+ Glean.browserBackup.backupThrottled.testGetValue(),
+ null,
+ "backupThrottled telemetry was not sent yet"
+ );
+ } else {
+ // On this call, createBackup _was_ called, but the next call will be
+ // ignored. However, the telemetry ping is sent now.
+ Assert.equal(
+ Glean.browserBackup.backupThrottled.testGetValue().length,
+ 1,
+ "backupThrottled telemetry was sent"
+ );
+ }
}
// check if it switched to no longer creating backups on idle
const previousCalls = bs.createBackup.callCount;
@@ -120,6 +138,7 @@ add_task(async function test_retry_limit() {
"Disable on idle has been enabled"
);
+ Services.fog.testResetFOG();
Services.prefs.setIntPref(MINIMUM_TIME_BETWEEN_BACKUPS_SECONDS_PREF_NAME, 0);
registerCleanupFunction(() => {
Services.prefs.clearUserPref(
@@ -142,6 +161,12 @@ add_task(async function test_retry_limit() {
"createBackup was called again"
);
+ Assert.equal(
+ Glean.browserBackup.backupThrottled.testGetValue(),
+ null,
+ "backupThrottled telemetry was not sent after resuming backups"
+ );
+
// #backupInProgress is set to false
await bsInProgressStateUpdate(bs, false);
diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_telemetry.js b/browser/components/backup/tests/xpcshell/test_BackupService_telemetry.js
@@ -0,0 +1,377 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineLazyGetter(this, "nsLocalFile", () =>
+ Components.Constructor("@mozilla.org/file/local;1", "nsIFile", "initWithPath")
+);
+
+const BACKUP_DIR_PREF_NAME = "browser.backup.location";
+
+const TEST_PASSWORD = "correcthorsebatterystaple";
+
+const kKnownMappings = Object.freeze({
+ OneDrPD: "onedrive",
+ Docs: "documents",
+});
+
+const gDirectoryServiceProvider = {
+ getFile(prop, persistent) {
+ persistent.value = false;
+
+ // We only expect a narrow range of calls.
+ let folder = gBase.clone();
+ if (prop === "ProfD") {
+ return folder;
+ }
+
+ if (prop in kKnownMappings) {
+ folder.append("dirsvc");
+ folder.append(prop + "-dir");
+ return folder;
+ }
+
+ console.error(`Access to unexpected directory '${prop}'`);
+ return Cr.NS_ERROR_FAILURE;
+ },
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
+};
+
+let gBase;
+add_setup(function setup() {
+ setupProfile();
+ gBase = do_get_profile();
+
+ Services.dirsvc
+ .QueryInterface(Ci.nsIDirectoryService)
+ .registerProvider(gDirectoryServiceProvider);
+});
+
+/**
+ * Gets a telemetry event and checks that it looks the same between Glean and
+ * legacy telemetry, i.e. that the extra data is equal.
+ *
+ * @param {string} name
+ * The Glean programming name of the event, e.g. turnOn instead of turn_on.
+ * @returns {object}
+ * The extra data associated with the event.
+ */
+function assertSingleTelemetryEvent(name) {
+ let value = Glean.browserBackup[name].testGetValue();
+ Assert.equal(value.length, 1, `${name} Glean event was recorded once.`);
+
+ let snakeName = name.replace(/([A-Z])/g, "_$1").toLowerCase();
+ let legacy = TelemetryTestUtils.getEvents(
+ { category: "browser.backup", method: snakeName, object: "BackupService" },
+ { process: "parent" }
+ );
+ Assert.equal(legacy.length, 1, `${name} legacy event was recorded once.`);
+
+ Assert.deepEqual(
+ legacy[0].extra,
+ value[0].extra,
+ "Legacy telemetry measured the same data as Glean."
+ );
+ return value[0].extra;
+}
+
+/**
+ * Checks that the recorded event's 'encrypted' and 'location' extra keys
+ * match `destPath` and `encrypted`. Reset telemetry before if needed!
+ *
+ * @param {string} name
+ * The name of the Glean event that should have been recorded.
+ * @param {string} destPath
+ * The path that the backup was stored to.
+ * @param {boolean} encrypted
+ * Whether the backup was encrypted or not.
+ */
+function assertEventMatches(name, destPath, encrypted) {
+ let extra = assertSingleTelemetryEvent(name);
+ Assert.equal(
+ extra.encrypted,
+ String(encrypted),
+ `Glean event indicates the backup is ${encrypted ? "" : "NOT "}encrypted.`
+ );
+
+ // This is returned from the mock of classifyLocationForTelemetry, and
+ // checks that the correct path was passed in.
+ Assert.equal(
+ extra.location,
+ `[classifying: ${relativeToProfile(destPath)}]`,
+ "Glean event has right location"
+ );
+
+ return extra;
+}
+
+/**
+ * Determines the path to 'source' from the profile directory to reduce the
+ * length and avoid truncation within legacy telemetry.
+ *
+ * @param {string} path
+ * The file that should be pointed to.
+ * @returns {string}
+ * The relative path from 'base' to 'source'.
+ */
+function relativeToProfile(path) {
+ let file = nsLocalFile(path);
+ return file.getRelativePath(gBase);
+}
+
+add_task(function test_relativeToProfile() {
+ // This aims to check that the direction is right.
+ const file = gBase.clone();
+ file.append("abc");
+ Assert.equal(
+ relativeToProfile(file.path),
+ "abc",
+ "relativeToProfile computes the right path."
+ );
+});
+
+add_task(async function test_created_encrypted_noreason() {
+ await template("testCreatedEncryptedNoReason", true, undefined);
+});
+
+add_task(async function test_created_nonencrypted_noreason() {
+ await template("testCreatedNonencryptedNoReason", false, undefined);
+});
+
+add_task(async function test_created_encrypted_with_reason() {
+ await template("testCreatedEncryptedWithReason", true, "I said so");
+});
+
+async function template(name, encrypted, reason) {
+ let bs = new BackupService();
+ let profilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ name
+ );
+
+ const backupDir = PathUtils.join(PathUtils.tempDir, name + "_dest");
+ Services.prefs.setStringPref(BACKUP_DIR_PREF_NAME, backupDir);
+
+ if (encrypted) {
+ await bs.enableEncryption(TEST_PASSWORD, profilePath);
+ }
+
+ sinon.stub(bs, "classifyLocationForTelemetry").callsFake(file => {
+ return `[classifying: ${relativeToProfile(file)}]`;
+ });
+
+ // To ensure that the backup_start event happens before the actual backup,
+ // take the lock for ourselves. Then we can unblock the backup once we've
+ // checked the telemetry is finished.
+ let resolver = Promise.withResolvers();
+ locks.request(BackupService.WRITE_BACKUP_LOCK_NAME, () => {
+ Services.fog.testResetFOG();
+ Services.telemetry.clearEvents();
+
+ let promise = bs.createBackup({ profilePath, reason });
+
+ let startedEvents = Glean.browserBackup.backupStart.testGetValue();
+ Assert.equal(
+ startedEvents.length,
+ 1,
+ "Found the backup_start Glean event."
+ );
+ Assert.equal(
+ startedEvents[0].extra.reason,
+ reason ?? "unknown",
+ "Found the reason for starting the backup in the Glean event."
+ );
+
+ // Don't await on it, since createBackup needs the lock!
+ resolver.resolve(promise);
+ });
+
+ await resolver.promise;
+
+ let value = assertEventMatches("created", backupDir, encrypted);
+ // Not sure how big it is, and we're not testing the fuzzByteSize
+ // function, so just check that it's plausible.
+ Assert.greater(Number(value.size), 0, "Telemetry event has nonzero size");
+}
+
+add_task(async function test_toggleOn() {
+ let bs = new BackupService();
+
+ let backupDir = PathUtils.join(PathUtils.tempDir, "toggleOn_dest");
+ Services.prefs.setStringPref(BACKUP_DIR_PREF_NAME, backupDir);
+
+ let profilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "toggleOn"
+ );
+
+ if (bs.state.scheduledBackupsEnabled) {
+ // The test assumes that this is false. Do this before resetting telemetry
+ // so it doesn't affect the results.
+ bs.onUpdateScheduledBackups(false);
+ }
+
+ sinon.stub(bs, "classifyLocationForTelemetry").callsFake(file => {
+ return `[classifying: ${relativeToProfile(file)}]`;
+ });
+
+ Services.fog.testResetFOG();
+ Services.telemetry.clearEvents();
+ bs.onUpdateScheduledBackups(true);
+ assertEventMatches("toggleOn", backupDir, false);
+
+ Services.fog.testResetFOG();
+ Services.telemetry.clearEvents();
+ bs.onUpdateScheduledBackups(false);
+ assertSingleTelemetryEvent("toggleOff");
+
+ await bs.enableEncryption(TEST_PASSWORD, profilePath);
+ Services.fog.testResetFOG();
+ Services.telemetry.clearEvents();
+ bs.onUpdateScheduledBackups(true);
+ assertEventMatches("toggleOn", backupDir, true);
+
+ Services.fog.testResetFOG();
+ Services.telemetry.clearEvents();
+ bs.onUpdateScheduledBackups(false);
+ assertSingleTelemetryEvent("toggleOff");
+});
+
+add_task(async function test_classifyLocationForTelemetry() {
+ let bs = new BackupService();
+ for (const prop of Object.keys(kKnownMappings)) {
+ let file = Services.dirsvc.get(prop, Ci.nsIFile);
+ Assert.equal(
+ bs.classifyLocationForTelemetry(file.path),
+ "other",
+ `'${file.path}' was correctly classified.`
+ );
+
+ file.append("child");
+ Assert.equal(
+ bs.classifyLocationForTelemetry(file.path),
+ kKnownMappings[prop],
+ `'${file.path}' was correctly classified.`
+ );
+
+ file = file.parent.parent;
+ Assert.equal(
+ bs.classifyLocationForTelemetry(file.path),
+ "other",
+ `'${file.path}' was correctly classified.`
+ );
+ }
+
+ Assert.equal(
+ bs.classifyLocationForTelemetry(gBase.path),
+ "other",
+ "Unrelated path is not classified anywhere."
+ );
+
+ Assert.equal(
+ bs.classifyLocationForTelemetry("path"),
+ "Error: NS_ERROR_FILE_UNRECOGNIZED_PATH",
+ "Invalid path returns an error name."
+ );
+});
+
+add_task(async function test_idleDispatchPassesOptionsThrough() {
+ let bs = new BackupService();
+ let stub = sinon.stub(bs, "createBackupOnIdleDispatch").resolves();
+
+ let options = {};
+ bs.createBackupOnIdleDispatch(options);
+ Assert.equal(
+ stub.firstCall.args[0],
+ options,
+ "Options were passed as-is into createBackup."
+ );
+});
+
+add_task(async function test_backupDisableReason_reEnabled() {
+ Services.fog.testResetFOG();
+ let bs = new BackupService();
+
+ Assert.equal(
+ Glean.browserBackup.backupDisabledReason.testGetValue(),
+ null,
+ "No disable reason is reported before it is disabled."
+ );
+
+ let status = {
+ enabled: true,
+ };
+ sinon.stub(bs, "archiveEnabledStatus").get(() => status);
+
+ await bs.createBackup();
+ Assert.equal(
+ Glean.browserBackup.backupDisabledReason.testGetValue(),
+ null,
+ "No disable reason is reported on first backup when enabled."
+ );
+
+ status = {
+ enabled: false,
+ reason: "Stubbed out by test (#1)",
+ internalReason: "* test *",
+ };
+
+ await bs.createBackup();
+ Assert.equal(
+ Glean.browserBackup.backupDisabledReason.testGetValue(),
+ "* test *",
+ "Disable reason is reported."
+ );
+
+ status = {
+ enabled: true,
+ };
+
+ await bs.createBackup();
+ Assert.equal(
+ Glean.browserBackup.backupDisabledReason.testGetValue(),
+ "reenabled",
+ "Backup service reports that it has been reenabled."
+ );
+});
+
+add_task(async function test_backupDisableReason_startup() {
+ let sandbox = sinon.createSandbox();
+ let status = {};
+ sandbox
+ .stub(BackupService.prototype, "archiveEnabledStatus")
+ .get(() => status);
+
+ status = {
+ enabled: false,
+ reason: "Stubbed out by test (#2)",
+ internalReason: "* startup *",
+ };
+
+ Services.fog.testResetFOG();
+ let bs = new BackupService();
+ Assert.equal(
+ Glean.browserBackup.backupDisabledReason.testGetValue(),
+ "* startup *",
+ "Backup service reports that is is disabled at startup."
+ );
+
+ await bs.createBackup();
+ Assert.equal(
+ Glean.browserBackup.backupDisabledReason.testGetValue(),
+ "* startup *",
+ "Backup service reports that is is disabled after creating a backup."
+ );
+
+ status = {
+ enabled: true,
+ };
+
+ await bs.createBackup();
+ Assert.equal(
+ Glean.browserBackup.backupDisabledReason.testGetValue(),
+ "reenabled",
+ "Backup service reports that it is re-enabled."
+ );
+});
diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml
@@ -66,6 +66,8 @@ run-sequentially = ["true"] # Mock Windows registry interferes with normal opera
["test_BackupService_takeMeasurements.js"]
+["test_BackupService_telemetry.js"]
+
["test_CookiesBackupResource.js"]
["test_CredentialsAndSecurityBackupResource.js"]
diff --git a/toolkit/components/telemetry/Events.yaml b/toolkit/components/telemetry/Events.yaml
@@ -3864,6 +3864,13 @@ browser.backup:
record_in_processes: [main]
release_channel_collection: opt-out
expiry_version: never
+ extra_keys:
+ encrypted: Whether or not encryption is enabled for the backup.
+ location: >
+ The location of the backup. This can be either "onedrive",
+ "documents", or "other". "other" will be sent if the parent directory
+ of the backup doesn't match the default "documents" or "onedrive"
+ save paths.
toggle_off:
objects: ["BackupService"]
description: >
@@ -3890,6 +3897,16 @@ browser.backup:
record_in_processes: [main]
release_channel_collection: opt-out
expiry_version: never
+ extra_keys:
+ encrypted: Whether or not encryption is enabled for the backup.
+ location: >
+ The location of the backup. This can be either "onedrive",
+ "documents", or "other". "other" will be sent if the parent directory
+ of the backup doesn't match the default "documents" or "onedrive"
+ save paths.
+ size: >
+ The size-on-disk of the created backup. This is rounded to the
+ nearest mebibyte to reduce fingerprintability.
change_location:
objects: ["BackupService"]
description: >