diff options
Diffstat (limited to 'toolkit/components/telemetry/tests/unit/test_PingAPI.js')
-rw-r--r-- | toolkit/components/telemetry/tests/unit/test_PingAPI.js | 709 |
1 files changed, 709 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/test_PingAPI.js b/toolkit/components/telemetry/tests/unit/test_PingAPI.js new file mode 100644 index 0000000000..a6ef791f6a --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_PingAPI.js @@ -0,0 +1,709 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +// This tests the public Telemetry API for submitting pings. + +"use strict"; + +const { ClientID } = ChromeUtils.importESModule( + "resource://gre/modules/ClientID.sys.mjs" +); +const { TelemetryArchive } = ChromeUtils.importESModule( + "resource://gre/modules/TelemetryArchive.sys.mjs" +); + +ChromeUtils.defineLazyGetter(this, "gPingsArchivePath", function () { + return PathUtils.join(PathUtils.profileDir, "datareporting", "archived"); +}); + +/** + * Fakes the archive storage quota. + * @param {Integer} aArchiveQuota The new quota, in bytes. + */ +function fakeStorageQuota(aArchiveQuota) { + let { Policy } = ChromeUtils.importESModule( + "resource://gre/modules/TelemetryStorage.sys.mjs" + ); + Policy.getArchiveQuota = () => aArchiveQuota; +} + +/** + * Lists all the valid archived pings and their metadata, sorted by creation date. + * + * @return {Object[]} A list of objects with the extracted data in the form: + * { timestamp: <number>, + * id: <string>, + * type: <string>, + * size: <integer> } + */ +var getArchivedPingsInfo = async function () { + let archivedPings = []; + + // Iterate through the subdirs of |gPingsArchivePath|. + for (const dir of await IOUtils.getChildren(gPingsArchivePath)) { + const { type } = await IOUtils.stat(dir); + if (type != "directory") { + continue; + } + + // Then get a list of the files for the current subdir. + for (const filePath of await IOUtils.getChildren(dir)) { + const fileInfo = await IOUtils.stat(filePath); + if (fileInfo.type == "directory") { + continue; + } + let pingInfo = TelemetryStorage._testGetArchivedPingDataFromFileName( + PathUtils.filename(filePath) + ); + if (!pingInfo) { + // This is not a valid archived ping, skip it. + continue; + } + // Find the size of the ping and then add the info to the array. + pingInfo.size = fileInfo.size; + archivedPings.push(pingInfo); + } + } + + // Sort the list by creation date and then return it. + archivedPings.sort((a, b) => b.timestamp - a.timestamp); + return archivedPings; +}; + +add_task(async function test_setup() { + do_get_profile(true); + // Make sure we don't generate unexpected pings due to pref changes. + await setEmptyPrefWatchlist(); +}); + +add_task(async function test_archivedPings() { + // TelemetryController should not be fully initialized at this point. + // Submitting pings should still work fine. + + const PINGS = [ + { + type: "test-ping-api-1", + payload: { foo: "bar" }, + dateCreated: new Date(2010, 1, 1, 10, 0, 0), + }, + { + type: "test-ping-api-2", + payload: { moo: "meh" }, + dateCreated: new Date(2010, 2, 1, 10, 0, 0), + }, + ]; + + // Submit pings and check the ping list. + let expectedPingList = []; + + for (let data of PINGS) { + fakeNow(data.dateCreated); + data.id = await TelemetryController.submitExternalPing( + data.type, + data.payload + ); + let list = await TelemetryArchive.promiseArchivedPingList(); + + expectedPingList.push({ + id: data.id, + type: data.type, + timestampCreated: data.dateCreated.getTime(), + }); + Assert.deepEqual( + list, + expectedPingList, + "Archived ping list should contain submitted pings" + ); + } + + // Check loading the archived pings. + let checkLoadingPings = async function () { + for (let data of PINGS) { + let ping = await TelemetryArchive.promiseArchivedPingById(data.id); + Assert.equal(ping.id, data.id, "Archived ping should have matching id"); + Assert.equal( + ping.type, + data.type, + "Archived ping should have matching type" + ); + Assert.equal( + ping.creationDate, + data.dateCreated.toISOString(), + "Archived ping should have matching creation date" + ); + } + }; + + await checkLoadingPings(); + + // Check that we find the archived pings again by scanning after a restart. + await TelemetryController.testReset(); + + let pingList = await TelemetryArchive.promiseArchivedPingList(); + Assert.deepEqual( + expectedPingList, + pingList, + "Should have submitted pings in archive list after restart" + ); + await checkLoadingPings(); + + // Write invalid pings into the archive with both valid and invalid names. + let writeToArchivedDir = async function ( + dirname, + filename, + content, + compressed + ) { + const dirPath = PathUtils.join(gPingsArchivePath, dirname); + await IOUtils.makeDirectory(dirPath, { ignoreExisting: true }); + const filePath = PathUtils.join(dirPath, filename); + const options = { tmpPath: filePath + ".tmp", mode: "overwrite" }; + if (compressed) { + options.compress = true; + } + await IOUtils.writeUTF8(filePath, content, options); + }; + + const FAKE_ID1 = "10000000-0123-0123-0123-0123456789a1"; + const FAKE_ID2 = "20000000-0123-0123-0123-0123456789a2"; + const FAKE_ID3 = "20000000-0123-0123-0123-0123456789a3"; + const FAKE_TYPE = "foo"; + + // These should get rejected. + await writeToArchivedDir("xx", "foo.json", "{}"); + await writeToArchivedDir("2010-02", "xx.xx.xx.json", "{}"); + // This one should get picked up... + await writeToArchivedDir( + "2010-02", + "1." + FAKE_ID1 + "." + FAKE_TYPE + ".json", + "{}" + ); + // ... but get overwritten by this one. + await writeToArchivedDir( + "2010-02", + "2." + FAKE_ID1 + "." + FAKE_TYPE + ".json", + "" + ); + // This should get picked up fine. + await writeToArchivedDir( + "2010-02", + "3." + FAKE_ID2 + "." + FAKE_TYPE + ".json", + "" + ); + // This compressed ping should get picked up fine as well. + await writeToArchivedDir( + "2010-02", + "4." + FAKE_ID3 + "." + FAKE_TYPE + ".jsonlz4", + "" + ); + + expectedPingList.push({ + id: FAKE_ID1, + type: "foo", + timestampCreated: 2, + }); + expectedPingList.push({ + id: FAKE_ID2, + type: "foo", + timestampCreated: 3, + }); + expectedPingList.push({ + id: FAKE_ID3, + type: "foo", + timestampCreated: 4, + }); + expectedPingList.sort((a, b) => a.timestampCreated - b.timestampCreated); + + // Reset the TelemetryArchive so we scan the archived dir again. + await TelemetryController.testReset(); + + // Check that we are still picking up the valid archived pings on disk, + // plus the valid ones above. + pingList = await TelemetryArchive.promiseArchivedPingList(); + Assert.deepEqual( + expectedPingList, + pingList, + "Should have picked up valid archived pings" + ); + await checkLoadingPings(); + + // Now check that we fail to load the two invalid pings from above. + Assert.ok( + await promiseRejects(TelemetryArchive.promiseArchivedPingById(FAKE_ID1)), + "Should have rejected invalid ping" + ); + Assert.ok( + await promiseRejects(TelemetryArchive.promiseArchivedPingById(FAKE_ID2)), + "Should have rejected invalid ping" + ); +}); + +add_task(async function test_archiveCleanup() { + const PING_TYPE = "foo"; + + // Empty the archive. + await IOUtils.remove(gPingsArchivePath, { recursive: true }); + + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").clear(); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT").clear(); + // Also reset these histograms to make sure normal sized pings don't get counted. + Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").clear(); + Telemetry.getHistogramById( + "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB" + ).clear(); + + // Build the cache. Nothing should be evicted as there's no ping directory. + await TelemetryController.testReset(); + await TelemetryStorage.testCleanupTaskPromise(); + await TelemetryArchive.promiseArchivedPingList(); + + let h = Telemetry.getHistogramById( + "TELEMETRY_ARCHIVE_SCAN_PING_COUNT" + ).snapshot(); + Assert.equal( + h.sum, + 0, + "Telemetry must report 0 pings scanned if no archive dir exists." + ); + // One directory out of four was removed as well. + h = Telemetry.getHistogramById( + "TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS" + ).snapshot(); + Assert.equal( + h.sum, + 0, + "Telemetry must report 0 evicted dirs if no archive dir exists." + ); + + let expectedPrunedInfo = []; + let expectedNotPrunedInfo = []; + + let checkArchive = async function () { + // Check that the pruned pings are not on disk anymore. + for (let prunedInfo of expectedPrunedInfo) { + await Assert.rejects( + TelemetryArchive.promiseArchivedPingById(prunedInfo.id), + /TelemetryStorage.loadArchivedPing - no ping with id/, + "Ping " + prunedInfo.id + " should have been pruned." + ); + const pingPath = TelemetryStorage._testGetArchivedPingPath( + prunedInfo.id, + prunedInfo.creationDate, + PING_TYPE + ); + Assert.ok( + !(await IOUtils.exists(pingPath)), + "The ping should not be on the disk anymore." + ); + } + + // Check that the expected pings are there. + for (let expectedInfo of expectedNotPrunedInfo) { + Assert.ok( + await TelemetryArchive.promiseArchivedPingById(expectedInfo.id), + "Ping" + expectedInfo.id + " should be in the archive." + ); + } + }; + + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT").clear(); + + // Create a ping which should be pruned because it is past the retention period. + let date = fakeNow(2010, 1, 1, 1, 0, 0); + let firstDate = date; + let pingId = await TelemetryController.submitExternalPing(PING_TYPE, {}, {}); + expectedPrunedInfo.push({ id: pingId, creationDate: date }); + + // Create a ping which should be kept because it is within the retention period. + const oldestDirectoryDate = fakeNow(2010, 2, 1, 1, 0, 0); + pingId = await TelemetryController.submitExternalPing(PING_TYPE, {}, {}); + expectedNotPrunedInfo.push({ id: pingId, creationDate: oldestDirectoryDate }); + + // Create 20 other pings which are within the retention period, but would be affected + // by the disk quota. + for (let month of [3, 4]) { + for (let minute = 0; minute < 10; minute++) { + date = fakeNow(2010, month, 1, 1, minute, 0); + pingId = await TelemetryController.submitExternalPing(PING_TYPE, {}, {}); + expectedNotPrunedInfo.push({ id: pingId, creationDate: date }); + } + } + + // We expect all the pings we archived to be in this histogram. + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT"); + Assert.equal( + h.snapshot().sum, + 22, + "All the pings must be live-accumulated in the histogram." + ); + // Reset the histogram that will be populated by the archive scan. + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").clear(); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE").clear(); + + // Move the current date 60 days ahead of the first ping. + fakeNow(futureDate(firstDate, 60 * MILLISECONDS_PER_DAY)); + // Reset TelemetryArchive and TelemetryController to start the startup cleanup. + await TelemetryController.testReset(); + // Wait for the cleanup to finish. + await TelemetryStorage.testCleanupTaskPromise(); + // Then scan the archived dir. + await TelemetryArchive.promiseArchivedPingList(); + + // Check that the archive is in the correct state. + await checkArchive(); + + // Make sure the ping count is correct after the scan (one ping was removed). + h = Telemetry.getHistogramById( + "TELEMETRY_ARCHIVE_SCAN_PING_COUNT" + ).snapshot(); + Assert.equal( + h.sum, + 21, + "The histogram must count all the pings in the archive." + ); + // One directory out of four was removed as well. + h = Telemetry.getHistogramById( + "TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS" + ).snapshot(); + Assert.equal( + h.sum, + 1, + "Telemetry must correctly report removed archive directories." + ); + // Check that the remaining directories are correctly counted. + h = Telemetry.getHistogramById( + "TELEMETRY_ARCHIVE_DIRECTORIES_COUNT" + ).snapshot(); + Assert.equal( + h.sum, + 3, + "Telemetry must correctly report the remaining archive directories." + ); + // Check that the remaining directories are correctly counted. + const oldestAgeInMonths = 1; + h = Telemetry.getHistogramById( + "TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE" + ).snapshot(); + Assert.equal( + h.sum, + oldestAgeInMonths, + "Telemetry must correctly report age of the oldest directory in the archive." + ); + + // We need to test the archive size before we hit the quota, otherwise a special + // value is recorded. + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").clear(); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").clear(); + Telemetry.getHistogramById( + "TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS" + ).clear(); + + // Move the current date 60 days ahead of the second ping. + fakeNow(futureDate(oldestDirectoryDate, 60 * MILLISECONDS_PER_DAY)); + // Reset TelemetryController and TelemetryArchive. + await TelemetryController.testReset(); + // Wait for the cleanup to finish. + await TelemetryStorage.testCleanupTaskPromise(); + // Then scan the archived dir again. + await TelemetryArchive.promiseArchivedPingList(); + + // Move the oldest ping to the unexpected pings list. + expectedPrunedInfo.push(expectedNotPrunedInfo.shift()); + // Check that the archive is in the correct state. + await checkArchive(); + + // Find how much disk space the archive takes. + const archivedPingsInfo = await getArchivedPingsInfo(); + let archiveSizeInBytes = archivedPingsInfo.reduce( + (lastResult, element) => lastResult + element.size, + 0 + ); + + // Check that the correct values for quota probes are reported when no quota is hit. + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").snapshot(); + Assert.equal( + h.sum, + Math.round(archiveSizeInBytes / 1024 / 1024), + "Telemetry must report the correct archive size." + ); + h = Telemetry.getHistogramById( + "TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA" + ).snapshot(); + Assert.equal( + h.sum, + 0, + "Telemetry must report 0 evictions if quota is not hit." + ); + h = Telemetry.getHistogramById( + "TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS" + ).snapshot(); + Assert.equal( + h.sum, + 0, + "Telemetry must report a null elapsed time if quota is not hit." + ); + + // Set the quota to 80% of the space. + const testQuotaInBytes = archiveSizeInBytes * 0.8; + fakeStorageQuota(testQuotaInBytes); + + // The storage prunes archived pings until we reach 90% of the requested storage quota. + // Based on that, find how many pings should be kept. + const safeQuotaSize = testQuotaInBytes * 0.9; + let sizeInBytes = 0; + let pingsWithinQuota = []; + let pingsOutsideQuota = []; + + for (let pingInfo of archivedPingsInfo) { + sizeInBytes += pingInfo.size; + if (sizeInBytes >= safeQuotaSize) { + pingsOutsideQuota.push({ + id: pingInfo.id, + creationDate: new Date(pingInfo.timestamp), + }); + continue; + } + pingsWithinQuota.push({ + id: pingInfo.id, + creationDate: new Date(pingInfo.timestamp), + }); + } + + expectedNotPrunedInfo = pingsWithinQuota; + expectedPrunedInfo = expectedPrunedInfo.concat(pingsOutsideQuota); + + // Reset TelemetryArchive and TelemetryController to start the startup cleanup. + await TelemetryController.testReset(); + await TelemetryStorage.testCleanupTaskPromise(); + await TelemetryArchive.promiseArchivedPingList(); + // Check that the archive is in the correct state. + await checkArchive(); + + h = Telemetry.getHistogramById( + "TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA" + ).snapshot(); + Assert.equal( + h.sum, + pingsOutsideQuota.length, + "Telemetry must correctly report the over quota pings evicted from the archive." + ); + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").snapshot(); + Assert.equal( + h.sum, + 300, + "Archive quota was hit, a special size must be reported." + ); + + // Trigger a cleanup again and make sure we're not removing anything. + await TelemetryController.testReset(); + await TelemetryStorage.testCleanupTaskPromise(); + await TelemetryArchive.promiseArchivedPingList(); + await checkArchive(); + + const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24"; + // Create and archive an oversized, uncompressed, ping. + const OVERSIZED_PING = { + id: OVERSIZED_PING_ID, + type: PING_TYPE, + creationDate: new Date().toISOString(), + // Generate a ~2MB string to use as the payload. + payload: generateRandomString(2 * 1024 * 1024), + }; + await TelemetryArchive.promiseArchivePing(OVERSIZED_PING); + + // Get the size of the archived ping. + const oversizedPingPath = + TelemetryStorage._testGetArchivedPingPath( + OVERSIZED_PING.id, + new Date(OVERSIZED_PING.creationDate), + PING_TYPE + ) + "lz4"; + const archivedPingSizeMB = Math.floor( + (await IOUtils.stat(oversizedPingPath)).size / 1024 / 1024 + ); + + // We expect the oversized ping to be pruned when scanning the archive. + expectedPrunedInfo.push({ + id: OVERSIZED_PING_ID, + creationDate: new Date(OVERSIZED_PING.creationDate), + }); + + // Scan the archive. + await TelemetryController.testReset(); + await TelemetryStorage.testCleanupTaskPromise(); + await TelemetryArchive.promiseArchivedPingList(); + // The following also checks that non oversized pings are not removed. + await checkArchive(); + + // Make sure we're correctly updating the related histograms. + h = Telemetry.getHistogramById( + "TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED" + ).snapshot(); + Assert.equal( + h.sum, + 1, + "Telemetry must report 1 oversized ping in the archive." + ); + h = Telemetry.getHistogramById( + "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB" + ).snapshot(); + Assert.equal( + h.values[archivedPingSizeMB], + 1, + "Telemetry must report the correct size for the oversized ping." + ); +}); + +add_task(async function test_clientId() { + // Check that a ping submitted after the delayed telemetry initialization completed + // should get a valid client id. + await TelemetryController.testReset(); + const clientId = await ClientID.getClientID(); + + let id = await TelemetryController.submitExternalPing( + "test-type", + {}, + { addClientId: true } + ); + let ping = await TelemetryArchive.promiseArchivedPingById(id); + + Assert.ok(!!ping, "Should have loaded the ping."); + Assert.ok("clientId" in ping, "Ping should have a client id."); + Assert.ok(UUID_REGEX.test(ping.clientId), "Client id is in UUID format."); + Assert.equal( + ping.clientId, + clientId, + "Ping client id should match the global client id." + ); + + // We should have cached the client id now. Lets confirm that by + // checking the client id on a ping submitted before the async + // controller setup is finished. + let promiseSetup = TelemetryController.testReset(); + id = await TelemetryController.submitExternalPing( + "test-type", + {}, + { addClientId: true } + ); + ping = await TelemetryArchive.promiseArchivedPingById(id); + Assert.equal(ping.clientId, clientId); + + // Finish setup. + await promiseSetup; +}); + +add_task(async function test_InvalidPingType() { + const TYPES = [ + "a", + "-", + "¿€€€?", + "-foo-", + "-moo", + "zoo-", + ".bar", + "asfd.asdf", + ]; + + for (let type of TYPES) { + let histogram = Telemetry.getKeyedHistogramById( + "TELEMETRY_INVALID_PING_TYPE_SUBMITTED" + ); + Assert.ok( + !(type in histogram.snapshot()), + "Should not have counted this invalid ping yet: " + type + ); + Assert.ok( + promiseRejects(TelemetryController.submitExternalPing(type, {})), + "Ping type should have been rejected." + ); + Assert.equal( + histogram.snapshot()[type].sum, + 1, + "Should have counted this as an invalid ping type." + ); + } +}); + +add_task(async function test_InvalidPayloadType() { + const PAYLOAD_TYPES = [19, "string", [1, 2, 3, 4], null, undefined]; + + let histogram = Telemetry.getHistogramById( + "TELEMETRY_INVALID_PAYLOAD_SUBMITTED" + ); + for (let i = 0; i < PAYLOAD_TYPES.length; i++) { + histogram.clear(); + Assert.equal( + histogram.snapshot().sum, + 0, + "Should not have counted this invalid payload yet: " + + JSON.stringify(PAYLOAD_TYPES[i]) + ); + Assert.ok( + await promiseRejects( + TelemetryController.submitExternalPing("payload-test", PAYLOAD_TYPES[i]) + ), + "Payload type should have been rejected." + ); + Assert.equal( + histogram.snapshot().sum, + 1, + "Should have counted this as an invalid payload type." + ); + } +}); + +add_task(async function test_currentPingData() { + await TelemetryController.testSetup(); + + // Setup test data. + let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT"); + h.clear(); + h.add(1); + let k = Telemetry.getKeyedHistogramById( + "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT" + ); + k.clear(); + k.add("a", 1); + + // Get current ping data objects and check that their data is sane. + for (let subsession of [true, false]) { + let ping = TelemetryController.getCurrentPingData(subsession); + + Assert.ok(!!ping, "Should have gotten a ping."); + Assert.equal(ping.type, "main", "Ping should have correct type."); + const expectedReason = subsession + ? "gather-subsession-payload" + : "gather-payload"; + Assert.equal( + ping.payload.info.reason, + expectedReason, + "Ping should have the correct reason." + ); + + let id = "TELEMETRY_TEST_RELEASE_OPTOUT"; + Assert.ok( + id in ping.payload.histograms, + "Payload should have test count histogram." + ); + Assert.equal( + ping.payload.histograms[id].sum, + 1, + "Test count value should match." + ); + id = "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"; + Assert.ok( + id in ping.payload.keyedHistograms, + "Payload should have keyed test histogram." + ); + Assert.equal( + ping.payload.keyedHistograms[id].a.sum, + 1, + "Keyed test value should match." + ); + } +}); + +add_task(async function test_shutdown() { + await TelemetryController.testShutdown(); +}); |