test_BackupService_telemetry.js (8475B)
1 /* Any copyright is dedicated to the Public Domain. 2 * https://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 ChromeUtils.defineLazyGetter(this, "nsLocalFile", () => 7 Components.Constructor("@mozilla.org/file/local;1", "nsIFile", "initWithPath") 8 ); 9 10 const BACKUP_DIR_PREF_NAME = "browser.backup.location"; 11 12 const TEST_PASSWORD = "correcthorsebatterystaple"; 13 14 const kKnownMappings = Object.freeze({ 15 OneDrPD: "onedrive", 16 Docs: "documents", 17 }); 18 19 const gDirectoryServiceProvider = { 20 getFile(prop, persistent) { 21 persistent.value = false; 22 23 // We only expect a narrow range of calls. 24 let folder = gBase.clone(); 25 if (prop === "ProfD") { 26 return folder; 27 } 28 29 if (prop in kKnownMappings) { 30 folder.append("dirsvc"); 31 folder.append(prop + "-dir"); 32 return folder; 33 } 34 35 console.error(`Access to unexpected directory '${prop}'`); 36 return Cr.NS_ERROR_FAILURE; 37 }, 38 QueryInterface: ChromeUtils.generateQI([Ci.nsIDirectoryServiceProvider]), 39 }; 40 41 let gBase; 42 add_setup(function setup() { 43 setupProfile(); 44 gBase = do_get_profile(); 45 46 Services.dirsvc 47 .QueryInterface(Ci.nsIDirectoryService) 48 .registerProvider(gDirectoryServiceProvider); 49 }); 50 51 /** 52 * Gets a telemetry event and checks that it looks the same between Glean and 53 * legacy telemetry, i.e. that the extra data is equal. 54 * 55 * @param {string} name 56 * The Glean programming name of the event, e.g. turnOn instead of turn_on. 57 * @returns {object} 58 * The extra data associated with the event. 59 */ 60 function assertSingleTelemetryEvent(name) { 61 let value = Glean.browserBackup[name].testGetValue(); 62 Assert.equal(value.length, 1, `${name} Glean event was recorded once.`); 63 64 let snakeName = name.replace(/([A-Z])/g, "_$1").toLowerCase(); 65 let legacy = TelemetryTestUtils.getEvents( 66 { category: "browser.backup", method: snakeName, object: "BackupService" }, 67 { process: "parent" } 68 ); 69 Assert.equal(legacy.length, 1, `${name} legacy event was recorded once.`); 70 71 Assert.deepEqual( 72 legacy[0].extra, 73 value[0].extra, 74 "Legacy telemetry measured the same data as Glean." 75 ); 76 return value[0].extra; 77 } 78 79 /** 80 * Checks that the recorded event's 'encrypted' and 'location' extra keys 81 * match `destPath` and `encrypted`. Reset telemetry before if needed! 82 * 83 * @param {string} name 84 * The name of the Glean event that should have been recorded. 85 * @param {string} destPath 86 * The path that the backup was stored to. 87 * @param {boolean} encrypted 88 * Whether the backup was encrypted or not. 89 */ 90 function assertEventMatches(name, destPath, encrypted) { 91 let extra = assertSingleTelemetryEvent(name); 92 Assert.equal( 93 extra.encrypted, 94 String(encrypted), 95 `Glean event indicates the backup is ${encrypted ? "" : "NOT "}encrypted.` 96 ); 97 98 // This is returned from the mock of classifyLocationForTelemetry, and 99 // checks that the correct path was passed in. 100 Assert.equal( 101 extra.location, 102 `[classifying: ${relativeToProfile(destPath)}]`, 103 "Glean event has right location" 104 ); 105 106 return extra; 107 } 108 109 /** 110 * Determines the path to 'source' from the profile directory to reduce the 111 * length and avoid truncation within legacy telemetry. 112 * 113 * @param {string} path 114 * The file that should be pointed to. 115 * @returns {string} 116 * The relative path from 'base' to 'source'. 117 */ 118 function relativeToProfile(path) { 119 let file = nsLocalFile(path); 120 return file.getRelativePath(gBase); 121 } 122 123 add_task(function test_relativeToProfile() { 124 // This aims to check that the direction is right. 125 const file = gBase.clone(); 126 file.append("abc"); 127 Assert.equal( 128 relativeToProfile(file.path), 129 "abc", 130 "relativeToProfile computes the right path." 131 ); 132 }); 133 134 add_task(async function test_created_encrypted_noreason() { 135 await template("testCreatedEncryptedNoReason", true, undefined); 136 }); 137 138 add_task(async function test_created_nonencrypted_noreason() { 139 await template("testCreatedNonencryptedNoReason", false, undefined); 140 }); 141 142 add_task(async function test_created_encrypted_with_reason() { 143 await template("testCreatedEncryptedWithReason", true, "I said so"); 144 }); 145 146 async function template(name, encrypted, reason) { 147 let bs = new BackupService(); 148 let profilePath = await IOUtils.createUniqueDirectory( 149 PathUtils.tempDir, 150 name 151 ); 152 153 const backupDir = PathUtils.join(PathUtils.tempDir, name + "_dest"); 154 Services.prefs.setStringPref(BACKUP_DIR_PREF_NAME, backupDir); 155 156 if (encrypted) { 157 await bs.enableEncryption(TEST_PASSWORD, profilePath); 158 } 159 160 sinon.stub(bs, "classifyLocationForTelemetry").callsFake(file => { 161 return `[classifying: ${relativeToProfile(file)}]`; 162 }); 163 164 // To ensure that the backup_start event happens before the actual backup, 165 // take the lock for ourselves. Then we can unblock the backup once we've 166 // checked the telemetry is finished. 167 let resolver = Promise.withResolvers(); 168 locks.request(BackupService.WRITE_BACKUP_LOCK_NAME, () => { 169 Services.fog.testResetFOG(); 170 Services.telemetry.clearEvents(); 171 172 let promise = bs.createBackup({ profilePath, reason }); 173 174 let startedEvents = Glean.browserBackup.backupStart.testGetValue(); 175 Assert.equal( 176 startedEvents.length, 177 1, 178 "Found the backup_start Glean event." 179 ); 180 Assert.equal( 181 startedEvents[0].extra.reason, 182 reason ?? "unknown", 183 "Found the reason for starting the backup in the Glean event." 184 ); 185 186 // Don't await on it, since createBackup needs the lock! 187 resolver.resolve(promise); 188 }); 189 190 await resolver.promise; 191 192 let value = assertEventMatches("created", backupDir, encrypted); 193 // Not sure how big it is, and we're not testing the fuzzByteSize 194 // function, so just check that it's plausible. 195 Assert.greater(Number(value.size), 0, "Telemetry event has nonzero size"); 196 } 197 198 add_task(async function test_toggleOn() { 199 let bs = new BackupService(); 200 201 let backupDir = PathUtils.join(PathUtils.tempDir, "toggleOn_dest"); 202 Services.prefs.setStringPref(BACKUP_DIR_PREF_NAME, backupDir); 203 204 let profilePath = await IOUtils.createUniqueDirectory( 205 PathUtils.tempDir, 206 "toggleOn" 207 ); 208 209 if (bs.state.scheduledBackupsEnabled) { 210 // The test assumes that this is false. Do this before resetting telemetry 211 // so it doesn't affect the results. 212 bs.onUpdateScheduledBackups(false); 213 } 214 215 sinon.stub(bs, "classifyLocationForTelemetry").callsFake(file => { 216 return `[classifying: ${relativeToProfile(file)}]`; 217 }); 218 219 Services.fog.testResetFOG(); 220 Services.telemetry.clearEvents(); 221 bs.onUpdateScheduledBackups(true); 222 assertEventMatches("toggleOn", backupDir, false); 223 224 Services.fog.testResetFOG(); 225 Services.telemetry.clearEvents(); 226 bs.onUpdateScheduledBackups(false); 227 assertSingleTelemetryEvent("toggleOff"); 228 229 await bs.enableEncryption(TEST_PASSWORD, profilePath); 230 Services.fog.testResetFOG(); 231 Services.telemetry.clearEvents(); 232 bs.onUpdateScheduledBackups(true); 233 assertEventMatches("toggleOn", backupDir, true); 234 235 Services.fog.testResetFOG(); 236 Services.telemetry.clearEvents(); 237 bs.onUpdateScheduledBackups(false); 238 assertSingleTelemetryEvent("toggleOff"); 239 }); 240 241 add_task(async function test_classifyLocationForTelemetry() { 242 let bs = new BackupService(); 243 for (const prop of Object.keys(kKnownMappings)) { 244 let file = Services.dirsvc.get(prop, Ci.nsIFile); 245 Assert.equal( 246 bs.classifyLocationForTelemetry(file.path), 247 "other", 248 `'${file.path}' was correctly classified.` 249 ); 250 251 file.append("child"); 252 Assert.equal( 253 bs.classifyLocationForTelemetry(file.path), 254 kKnownMappings[prop], 255 `'${file.path}' was correctly classified.` 256 ); 257 258 file = file.parent.parent; 259 Assert.equal( 260 bs.classifyLocationForTelemetry(file.path), 261 "other", 262 `'${file.path}' was correctly classified.` 263 ); 264 } 265 266 Assert.equal( 267 bs.classifyLocationForTelemetry(gBase.path), 268 "other", 269 "Unrelated path is not classified anywhere." 270 ); 271 272 Assert.equal( 273 bs.classifyLocationForTelemetry("path"), 274 "Error: NS_ERROR_FILE_UNRECOGNIZED_PATH", 275 "Invalid path returns an error name." 276 ); 277 }); 278 279 add_task(async function test_idleDispatchPassesOptionsThrough() { 280 let bs = new BackupService(); 281 let stub = sinon.stub(bs, "createBackupOnIdleDispatch").resolves(); 282 283 let options = {}; 284 bs.createBackupOnIdleDispatch(options); 285 Assert.equal( 286 stub.firstCall.args[0], 287 options, 288 "Options were passed as-is into createBackup." 289 ); 290 });