tor-browser

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

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:
Mbrowser/components/backup/BackupService.sys.mjs | 103++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mbrowser/components/backup/metrics.yaml | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/backup/tests/xpcshell/test_BackupService_enabled.js | 14++++++++++++++
Mbrowser/components/backup/tests/xpcshell/test_BackupService_regeneration.js | 7++++++-
Mbrowser/components/backup/tests/xpcshell/test_BackupService_retryHeuristic.js | 25+++++++++++++++++++++++++
Abrowser/components/backup/tests/xpcshell/test_BackupService_telemetry.js | 377+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/backup/tests/xpcshell/xpcshell.toml | 2++
Mtoolkit/components/telemetry/Events.yaml | 17+++++++++++++++++
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: >