summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/tests/unit/test_TelemetryController.js')
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryController.js1225
1 files changed, 1225 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
new file mode 100644
index 0000000000..5af7adf6b0
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -0,0 +1,1225 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* This testcase triggers two telemetry pings.
+ *
+ * Telemetry code keeps histograms of past telemetry pings. The first
+ * ping populates these histograms. One of those histograms is then
+ * checked in the second request.
+ */
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+);
+const { TelemetryArchive } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryArchive.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const { TelemetryArchiveTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryArchiveTesting.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ JsonSchemaValidator:
+ "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
+ jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs",
+});
+
+const PING_FORMAT_VERSION = 4;
+const DELETION_REQUEST_PING_TYPE = "deletion-request";
+const TEST_PING_TYPE = "test-ping-type";
+
+var gClientID = null;
+
+ChromeUtils.defineLazyGetter(this, "DATAREPORTING_PATH", async function () {
+ return PathUtils.join(PathUtils.profileDir, "datareporting");
+});
+
+function sendPing(aSendClientId, aSendEnvironment) {
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ } else {
+ TelemetrySend.setServer("http://doesnotexist");
+ }
+
+ let options = {
+ addClientId: aSendClientId,
+ addEnvironment: aSendEnvironment,
+ };
+ return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
+}
+
+function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {
+ const MANDATORY_PING_FIELDS = [
+ "type",
+ "id",
+ "creationDate",
+ "version",
+ "application",
+ "payload",
+ ];
+
+ const APPLICATION_TEST_DATA = {
+ buildId: gAppInfo.appBuildID,
+ name: APP_NAME,
+ version: APP_VERSION,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ // Check that the ping contains all the mandatory fields.
+ for (let f of MANDATORY_PING_FIELDS) {
+ Assert.ok(f in aPing, f + " must be available.");
+ }
+
+ Assert.equal(aPing.type, aType, "The ping must have the correct type.");
+ Assert.equal(
+ aPing.version,
+ PING_FORMAT_VERSION,
+ "The ping must have the correct version."
+ );
+
+ // Test the application section.
+ for (let f in APPLICATION_TEST_DATA) {
+ Assert.equal(
+ aPing.application[f],
+ APPLICATION_TEST_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // We can't check the values for channel and architecture. Just make
+ // sure they are in.
+ Assert.ok(
+ "architecture" in aPing.application,
+ "The application section must have an architecture field."
+ );
+ Assert.ok(
+ "channel" in aPing.application,
+ "The application section must have a channel field."
+ );
+
+ // Check the clientId and environment fields, as needed.
+ Assert.equal("clientId" in aPing, aHasClientId);
+ Assert.equal("environment" in aPing, aHasEnvironment);
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ await loadAddonManager(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "1.9.2"
+ );
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ await new Promise(resolve =>
+ Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))
+ );
+});
+
+add_task(async function asyncSetup() {
+ await TelemetryController.testSetup();
+});
+
+// Ensure that not overwriting an existing file fails silently
+add_task(async function test_overwritePing() {
+ let ping = { id: "foo" };
+ await TelemetryStorage.savePing(ping, true);
+ await TelemetryStorage.savePing(ping, false);
+ await TelemetryStorage.cleanupPingFile(ping);
+});
+
+// Checks that a sent ping is correctly received by a dummy http server.
+add_task(async function test_simplePing() {
+ PingServer.start();
+ // Update the Telemetry Server preference with the address of the local server.
+ // Otherwise we might end up sending stuff to a non-existing server after
+ // |TelemetryController.testReset| is called.
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ await sendPing(false, false);
+ let request = await PingServer.promiseNextRequest();
+
+ let ping = decodeRequestPayload(request);
+ checkPingFormat(ping, TEST_PING_TYPE, false, false);
+});
+
+add_task(async function test_disableDataUpload() {
+ const OPTIN_PROBE = "telemetry.data_upload_optin";
+ const isUnified = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.Unified,
+ false
+ );
+ if (!isUnified) {
+ // Skipping the test if unified telemetry is off, as no deletion-request ping will be generated.
+ return;
+ }
+
+ // Check that the optin probe is not set.
+ // (If there are no recorded scalars, "parent" will be undefined).
+ let snapshot = Telemetry.getSnapshotForScalars("main", false).parent || {};
+ Assert.ok(
+ !(OPTIN_PROBE in snapshot),
+ "Data optin scalar should not be set at start"
+ );
+
+ // Send a first ping to get the current used client id
+ await sendPing(true, false);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ let firstClientId = ping.clientId;
+
+ Assert.ok(firstClientId, "Test ping needs a client ID");
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ firstClientId,
+ "Client ID should be valid and random"
+ );
+
+ // The next step should trigger an event, watch for it.
+ let disableObserved = TestUtils.topicObserved(
+ TelemetryUtils.TELEMETRY_UPLOAD_DISABLED_TOPIC
+ );
+
+ // Disable FHR upload: this should trigger a deletion-request ping.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+
+ // Wait for the disable event
+ await disableObserved;
+
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_REQUEST_PING_TYPE, true, false);
+ // Wait on ping activity to settle.
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ snapshot = Telemetry.getSnapshotForScalars("main", false).parent || {};
+ Assert.ok(
+ !(OPTIN_PROBE in snapshot),
+ "Data optin scalar should not be set after opt out"
+ );
+
+ // Restore FHR Upload.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // We need to wait until the scalar is set
+ await ContentTaskUtils.waitForCondition(() => {
+ const scalarSnapshot = Telemetry.getSnapshotForScalars("main", false);
+ return (
+ Object.keys(scalarSnapshot).includes("parent") &&
+ OPTIN_PROBE in scalarSnapshot.parent
+ );
+ });
+
+ snapshot = Telemetry.getSnapshotForScalars("main", false).parent || {};
+ Assert.ok(
+ snapshot[OPTIN_PROBE],
+ "Enabling data upload should set optin probe"
+ );
+
+ // The clientId should've been reset when we restored FHR Upload.
+ let secondClientId = TelemetryController.getCurrentPingData().clientId;
+ Assert.notEqual(
+ firstClientId,
+ secondClientId,
+ "The client id must have changed"
+ );
+ // Simulate a failure in sending the deletion-request ping by disabling the HTTP server.
+ await PingServer.stop();
+
+ // Try to send a ping. It will be saved as pending and get deleted when disabling upload.
+ TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Disable FHR upload to send a deletion-request ping again.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+ // Wait for the deletion-request ping to be submitted.
+ await TelemetryController.testPromiseDeletionRequestPingSubmitted();
+
+ // Wait on sending activity to settle, as |TelemetryController.testReset()| doesn't do that.
+ await TelemetrySend.testWaitOnOutgoingPings();
+ // Wait for the pending pings to be deleted. Resetting TelemetryController doesn't
+ // trigger the shutdown, so we need to call it ourselves.
+ await TelemetryStorage.shutdown();
+ // Simulate a restart, and spin the send task.
+ await TelemetryController.testReset();
+
+ // Disabling Telemetry upload must clear out all the pending pings.
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ Assert.equal(
+ pendingPings.length,
+ 1,
+ "All the pending pings should have been deleted, except the deletion-request ping"
+ );
+
+ // Enable the ping server again.
+ PingServer.start();
+ // We set the new server using the pref, otherwise it would get reset with
+ // |TelemetryController.testReset|.
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ // Stop the sending task and then start it again.
+ await TelemetrySend.shutdown();
+ // Reset the controller to spin the ping sending task.
+ await TelemetryController.testReset();
+
+ // Re-enable Telemetry
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Send a test ping
+ await sendPing(true, false);
+
+ // We should have received the test ping first.
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+
+ // The data in the test ping should be different than before
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ ping.clientId,
+ "Client ID should be reset to a random value"
+ );
+ Assert.notEqual(
+ firstClientId,
+ ping.clientId,
+ "Client ID should be different from the previous value"
+ );
+
+ // The "deletion-request" ping should come next, as it was pending.
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_REQUEST_PING_TYPE, true, false);
+ Assert.equal(
+ secondClientId,
+ ping.clientId,
+ "Deletion must be requested for correct client id"
+ );
+
+ // Wait on ping activity to settle before moving on to the next test. If we were
+ // to shut down telemetry, even though the PingServer caught the expected pings,
+ // TelemetrySend could still be processing them (clearing pings would happen in
+ // a couple of ticks). Shutting down would cancel the request and save them as
+ // pending pings.
+ await TelemetrySend.testWaitOnOutgoingPings();
+});
+
+add_task(async function test_pingHasClientId() {
+ // Make sure we have no cached client ID for this test: we'll try to send
+ // a ping with it while Telemetry is being initialized.
+ Services.prefs.clearUserPref(TelemetryUtils.Preferences.CachedClientId);
+ await TelemetryController.testShutdown();
+ await ClientID._reset();
+ await TelemetryStorage.testClearPendingPings();
+ // And also clear the counter histogram since we're here.
+ let h = Telemetry.getHistogramById(
+ "TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID"
+ );
+ h.clear();
+
+ // Init telemetry and try to send a ping with a client ID.
+ let promisePingSetup = TelemetryController.testReset();
+ await sendPing(true, false);
+ Assert.equal(
+ h.snapshot().sum,
+ 1,
+ "We must have a ping waiting for the clientId early during startup."
+ );
+ // Wait until we are fully initialized. Pings will be assembled but won't get
+ // sent before then.
+ await promisePingSetup;
+
+ let ping = await PingServer.promiseNextPing();
+ // Fetch the client ID after initializing and fetching the the ping, so we
+ // don't unintentionally trigger its loading. We'll still need the client ID
+ // to see if the ping looks sane.
+ gClientID = await ClientID.getClientID();
+
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "The correct clientId must be reported."
+ );
+
+ // Shutdown Telemetry so we can safely restart it.
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+
+ // We should have cached the client ID now. Lets confirm that by checking it before
+ // the async ping setup is finished.
+ h.clear();
+ promisePingSetup = TelemetryController.testReset();
+ await sendPing(true, false);
+ await promisePingSetup;
+
+ // Check that we received the cached client id.
+ Assert.equal(h.snapshot().sum, 0, "We must have used the cached clientId.");
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "Telemetry should report the correct cached clientId."
+ );
+
+ // Check that sending a ping without relying on the cache, after the
+ // initialization, still works.
+ Services.prefs.clearUserPref(TelemetryUtils.Preferences.CachedClientId);
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ await sendPing(true, false);
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "The correct clientId must be reported."
+ );
+ Assert.equal(
+ h.snapshot().sum,
+ 0,
+ "No ping should have been waiting for a clientId."
+ );
+});
+
+add_task(async function test_pingHasEnvironment() {
+ // Send a ping with the environment data.
+ await sendPing(false, true);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, false, true);
+
+ // Test a field in the environment build section.
+ Assert.equal(ping.application.buildId, ping.environment.build.buildId);
+});
+
+add_task(async function test_pingHasEnvironmentAndClientId() {
+ // Send a ping with the environment data and client id.
+ await sendPing(true, true);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+
+ // Test a field in the environment build section.
+ Assert.equal(ping.application.buildId, ping.environment.build.buildId);
+ // Test that we have the correct clientId.
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "The correct clientId must be reported."
+ );
+});
+
+add_task(async function test_archivePings() {
+ let now = new Date(2009, 10, 18, 12, 0, 0);
+ fakeNow(now);
+
+ // Disable ping upload so that pings don't get sent.
+ // With unified telemetry the FHR upload pref controls this,
+ // with non-unified telemetry the Telemetry enabled pref.
+ const isUnified = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.Unified,
+ false
+ );
+ const uploadPref = isUnified
+ ? TelemetryUtils.Preferences.FhrUploadEnabled
+ : TelemetryUtils.Preferences.TelemetryEnabled;
+ Services.prefs.setBoolPref(uploadPref, false);
+
+ // If we're using unified telemetry, disabling ping upload will generate a "deletion-request" ping. Catch it.
+ if (isUnified) {
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_REQUEST_PING_TYPE, true, false);
+ }
+
+ // Register a new Ping Handler that asserts if a ping is received, then send a ping.
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Telemetry must not send pings if not allowed to.")
+ );
+ let pingId = await sendPing(true, true);
+
+ // Check that the ping was archived, even with upload disabled.
+ let ping = await TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(
+ ping.id,
+ pingId,
+ "TelemetryController should still archive pings."
+ );
+
+ // Check that pings don't get archived if not allowed to.
+ now = new Date(2010, 10, 18, 12, 0, 0);
+ fakeNow(now);
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.ArchiveEnabled, false);
+ pingId = await sendPing(true, true);
+ let promise = TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.ok(
+ await promiseRejects(promise),
+ "TelemetryController should not archive pings if the archive pref is disabled."
+ );
+
+ // Enable archiving and the upload so that pings get sent and archived again.
+ Services.prefs.setBoolPref(uploadPref, true);
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.ArchiveEnabled, true);
+
+ now = new Date(2014, 6, 18, 22, 0, 0);
+ fakeNow(now);
+ // Restore the non asserting ping handler.
+ PingServer.resetPingHandler();
+ pingId = await sendPing(true, true);
+
+ // Check that we archive pings when successfully sending them.
+ await PingServer.promiseNextPing();
+ ping = await TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(
+ ping.id,
+ pingId,
+ "TelemetryController should still archive pings if ping upload is enabled."
+ );
+});
+
+// Test that we fuzz the submission time around midnight properly
+// to avoid overloading the telemetry servers.
+add_task(async function test_midnightPingSendFuzzing() {
+ const fuzzingDelay = 60 * 60 * 1000;
+ fakeMidnightPingFuzzingDelay(fuzzingDelay);
+ let now = new Date(2030, 5, 1, 11, 0, 0);
+ fakeNow(now);
+
+ let waitForTimer = () =>
+ new Promise(resolve => {
+ fakePingSendTimer(
+ (callback, timeout) => {
+ resolve([callback, timeout]);
+ },
+ () => {}
+ );
+ });
+
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ // A ping after midnight within the fuzzing delay should not get sent.
+ now = new Date(2030, 5, 2, 0, 40, 0);
+ fakeNow(now);
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(false, "No ping should be received yet.");
+ });
+ let timerPromise = waitForTimer();
+ await sendPing(true, true);
+ let [timerCallback, timerTimeout] = await timerPromise;
+ Assert.ok(!!timerCallback);
+ Assert.deepEqual(
+ futureDate(now, timerTimeout),
+ new Date(2030, 5, 2, 1, 0, 0)
+ );
+
+ // A ping just before the end of the fuzzing delay should not get sent.
+ now = new Date(2030, 5, 2, 0, 59, 59);
+ fakeNow(now);
+ timerPromise = waitForTimer();
+ await sendPing(true, true);
+ [timerCallback, timerTimeout] = await timerPromise;
+ Assert.deepEqual(timerTimeout, 1 * 1000);
+
+ // Restore the previous ping handler.
+ PingServer.resetPingHandler();
+
+ // Setting the clock to after the fuzzing delay, we should trigger the two ping sends
+ // with the timer callback.
+ now = futureDate(now, timerTimeout);
+ fakeNow(now);
+ await timerCallback();
+ const pings = await PingServer.promiseNextPings(2);
+ for (let ping of pings) {
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ }
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Moving the clock further we should still send pings immediately.
+ now = futureDate(now, 5 * 60 * 1000);
+ await sendPing(true, true);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Check that pings shortly before midnight are immediately sent.
+ now = fakeNow(2030, 5, 3, 23, 59, 0);
+ await sendPing(true, true);
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Clean-up.
+ fakeMidnightPingFuzzingDelay(0);
+ fakePingSendTimer(
+ () => {},
+ () => {}
+ );
+});
+
+add_task(async function test_changePingAfterSubmission() {
+ // Submit a ping with a custom payload.
+ let payload = { canary: "test" };
+ let pingPromise = TelemetryController.submitExternalPing(
+ TEST_PING_TYPE,
+ payload
+ );
+
+ // Change the payload with a predefined value.
+ payload.canary = "changed";
+
+ // Wait for the ping to be archived.
+ const pingId = await pingPromise;
+
+ // Make sure our changes didn't affect the submitted payload.
+ let archivedCopy = await TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(
+ archivedCopy.payload.canary,
+ "test",
+ "The payload must not be changed after being submitted."
+ );
+});
+
+add_task(async function test_telemetryCleanFHRDatabase() {
+ const FHR_DBNAME_PREF = "datareporting.healthreport.dbName";
+ const CUSTOM_DB_NAME = "unlikely.to.be.used.sqlite";
+ const DEFAULT_DB_NAME = "healthreport.sqlite";
+
+ // Check that we're able to remove a FHR DB with a custom name.
+ const profileDir = PathUtils.profileDir;
+ const CUSTOM_DB_PATHS = [
+ PathUtils.join(profileDir, CUSTOM_DB_NAME),
+ PathUtils.join(profileDir, CUSTOM_DB_NAME + "-wal"),
+ PathUtils.join(profileDir, CUSTOM_DB_NAME + "-shm"),
+ ];
+ Services.prefs.setStringPref(FHR_DBNAME_PREF, CUSTOM_DB_NAME);
+
+ // Write fake DB files to the profile directory.
+ for (let dbFilePath of CUSTOM_DB_PATHS) {
+ await IOUtils.writeUTF8(dbFilePath, "some data");
+ }
+
+ // Trigger the cleanup and check that the files were removed.
+ await TelemetryStorage.removeFHRDatabase();
+ for (let dbFilePath of CUSTOM_DB_PATHS) {
+ try {
+ await IOUtils.read(dbFilePath);
+ } catch (e) {
+ Assert.ok(DOMException.isInstance(e));
+ Assert.equal(
+ e.name,
+ "NotFoundError",
+ "The DB must not be on the disk anymore: " + dbFilePath
+ );
+ }
+ }
+
+ // We should not break anything if there's no DB file.
+ await TelemetryStorage.removeFHRDatabase();
+
+ // Check that we're able to remove a FHR DB with the default name.
+ Services.prefs.clearUserPref(FHR_DBNAME_PREF);
+
+ const DEFAULT_DB_PATHS = [
+ PathUtils.join(profileDir, DEFAULT_DB_NAME),
+ PathUtils.join(profileDir, DEFAULT_DB_NAME + "-wal"),
+ PathUtils.join(profileDir, DEFAULT_DB_NAME + "-shm"),
+ ];
+
+ // Write fake DB files to the profile directory.
+ for (let dbFilePath of DEFAULT_DB_PATHS) {
+ await IOUtils.writeUTF8(dbFilePath, "some data");
+ }
+
+ // Trigger the cleanup and check that the files were removed.
+ await TelemetryStorage.removeFHRDatabase();
+ for (let dbFilePath of DEFAULT_DB_PATHS) {
+ try {
+ await IOUtils.read(dbFilePath);
+ } catch (e) {
+ Assert.ok(DOMException.isInstance(e));
+ Assert.equal(
+ e.name,
+ "NotFoundError",
+ "The DB must not be on the disk anymore: " + dbFilePath
+ );
+ }
+ }
+});
+
+add_task(async function test_sendNewProfile() {
+ if (
+ gIsAndroid ||
+ (AppConstants.platform == "linux" && !Services.appinfo.is64Bit)
+ ) {
+ // We don't support the pingsender on Android, yet, see bug 1335917.
+ // We also don't suppor the pingsender testing on Treeherder for
+ // Linux 32 bit (due to missing libraries). So skip it there too.
+ // See bug 1310703 comment 78.
+ return;
+ }
+
+ const NEWPROFILE_PING_TYPE = "new-profile";
+ const PREF_NEWPROFILE_ENABLED = "toolkit.telemetry.newProfilePing.enabled";
+ const PREF_NEWPROFILE_DELAY = "toolkit.telemetry.newProfilePing.delay";
+
+ // Make sure Telemetry is shut down before beginning and that we have
+ // no pending pings.
+ let resetTest = async function () {
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ };
+ await resetTest();
+
+ // Make sure to reset all the new-profile ping prefs.
+ const stateFilePath = PathUtils.join(
+ await DATAREPORTING_PATH,
+ "session-state.json"
+ );
+ await IOUtils.remove(stateFilePath);
+ Services.prefs.setIntPref(PREF_NEWPROFILE_DELAY, 1);
+ Services.prefs.setBoolPref(PREF_NEWPROFILE_ENABLED, true);
+
+ // Check that a new-profile ping is sent on the first session.
+ let nextReq = PingServer.promiseNextRequest();
+ await TelemetryController.testReset();
+ let req = await nextReq;
+ let ping = decodeRequestPayload(req);
+ checkPingFormat(ping, NEWPROFILE_PING_TYPE, true, true);
+ Assert.equal(
+ ping.payload.reason,
+ "startup",
+ "The new-profile ping generated after startup must have the correct reason"
+ );
+ Assert.ok(
+ "parent" in ping.payload.processes,
+ "The new-profile ping generated after startup must have processes.parent data"
+ );
+
+ // Check that is not sent with the pingsender during startup.
+ Assert.throws(
+ () => req.getHeader("X-PingSender-Version"),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Should not have used the pingsender."
+ );
+
+ // Make sure that the new-profile ping is sent at shutdown if it wasn't sent before.
+ await resetTest();
+ await IOUtils.remove(stateFilePath);
+ Services.prefs.clearUserPref(PREF_NEWPROFILE_DELAY);
+
+ nextReq = PingServer.promiseNextRequest();
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ req = await nextReq;
+ ping = decodeRequestPayload(req);
+ checkPingFormat(ping, NEWPROFILE_PING_TYPE, true, true);
+ Assert.equal(
+ ping.payload.reason,
+ "shutdown",
+ "The new-profile ping generated at shutdown must have the correct reason"
+ );
+ Assert.ok(
+ "parent" in ping.payload.processes,
+ "The new-profile ping generated at shutdown must have processes.parent data"
+ );
+
+ // Check that the new-profile ping is sent at shutdown using the pingsender.
+ Assert.equal(
+ req.getHeader("User-Agent"),
+ "pingsender/1.0",
+ "Should have received the correct user agent string."
+ );
+ Assert.equal(
+ req.getHeader("X-PingSender-Version"),
+ "1.0",
+ "Should have received the correct PingSender version string."
+ );
+
+ // Check that no new-profile ping is sent on second sessions, not at startup
+ // nor at shutdown.
+ await resetTest();
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "The new-profile ping must be sent only on new profiles.")
+ );
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Check that we don't send the new-profile ping if the profile already contains
+ // a state file (but no "newProfilePingSent" property).
+ await resetTest();
+ await IOUtils.remove(stateFilePath);
+ const sessionState = {
+ sessionId: null,
+ subsessionId: null,
+ profileSubsessionCounter: 3785,
+ };
+ await IOUtils.writeJSON(stateFilePath, sessionState);
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Reset the pref and restart Telemetry.
+ Services.prefs.clearUserPref(PREF_NEWPROFILE_ENABLED);
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_encryptedPing() {
+ if (gIsAndroid) {
+ // The underlying jwcrypto module being used here is not currently available on Android.
+ return;
+ }
+
+ const ECDH_PARAMS = {
+ name: "ECDH",
+ namedCurve: "P-256",
+ };
+
+ const privateKey = {
+ crv: "P-256",
+ d: "rcs093UlGDG6piwHenmSDoAxbzMIXT43JkQbkt3xEmI",
+ ext: true,
+ key_ops: ["deriveKey"],
+ kty: "EC",
+ x: "h12feyTYBZ__wO_AnM1a5-KTDlko3-YyQ_en19jyrs0",
+ y: "6GSfzo14ehDyH5E-xCOedJDAYlN0AGPMCtIgFbheLko",
+ };
+
+ const publicKey = {
+ crv: "P-256",
+ ext: true,
+ kty: "EC",
+ x: "h12feyTYBZ__wO_AnM1a5-KTDlko3-YyQ_en19jyrs0",
+ y: "6GSfzo14ehDyH5E-xCOedJDAYlN0AGPMCtIgFbheLko",
+ };
+
+ const pioneerId = "12345";
+ const schemaName = "abc";
+ const schemaNamespace = "def";
+ const schemaVersion = 2;
+
+ Services.prefs.setStringPref("toolkit.telemetry.pioneerId", pioneerId);
+
+ // Stop the sending task and then start it again.
+ await TelemetrySend.shutdown();
+ // Reset the controller to spin the ping sending task.
+ await TelemetryController.testReset();
+
+ // Submit a ping with a custom payload, which will be encrypted.
+ let payload = { canary: "test" };
+ let pingPromise = TelemetryController.submitExternalPing(
+ "pioneer-study",
+ payload,
+ {
+ studyName: "pioneer-dev-1@allizom.org",
+ addPioneerId: true,
+ useEncryption: true,
+ encryptionKeyId: "pioneer-dev-20200423",
+ publicKey,
+ schemaName,
+ schemaNamespace,
+ schemaVersion,
+ }
+ );
+
+ // Wait for the ping to be archived.
+ const pingId = await pingPromise;
+
+ let archivedCopy = await TelemetryArchive.promiseArchivedPingById(pingId);
+
+ Assert.notEqual(
+ archivedCopy.payload.encryptedData,
+ payload,
+ "The encrypted payload must not match the plaintext."
+ );
+
+ Assert.equal(
+ archivedCopy.payload.pioneerId,
+ pioneerId,
+ "Pioneer ID in ping must match the pref."
+ );
+
+ // Validate ping against schema.
+ const schema = {
+ $schema: "http://json-schema.org/draft-04/schema#",
+ properties: {
+ application: {
+ additionalProperties: false,
+ properties: {
+ architecture: {
+ type: "string",
+ },
+ buildId: {
+ pattern: "^[0-9]{10}",
+ type: "string",
+ },
+ channel: {
+ type: "string",
+ },
+ displayVersion: {
+ pattern: "^[0-9]{2,3}\\.",
+ type: "string",
+ },
+ name: {
+ type: "string",
+ },
+ platformVersion: {
+ pattern: "^[0-9]{2,3}\\.",
+ type: "string",
+ },
+ vendor: {
+ type: "string",
+ },
+ version: {
+ pattern: "^[0-9]{2,3}\\.",
+ type: "string",
+ },
+ xpcomAbi: {
+ type: "string",
+ },
+ },
+ required: [
+ "architecture",
+ "buildId",
+ "channel",
+ "name",
+ "platformVersion",
+ "version",
+ "vendor",
+ "xpcomAbi",
+ ],
+ type: "object",
+ },
+ creationDate: {
+ pattern:
+ "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$",
+ type: "string",
+ },
+ id: {
+ pattern:
+ "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
+ type: "string",
+ },
+ payload: {
+ description: "",
+ properties: {
+ encryptedData: {
+ description: "JOSE/JWE encrypted payload.",
+ type: "string",
+ },
+ encryptionKeyId: {
+ description: "JOSE/JWK key id, e.g. pioneer-20170520.",
+ type: "string",
+ },
+ pioneerId: {
+ description: "Custom pioneer id, must not be Telemetry clientId",
+ pattern:
+ "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
+ type: "string",
+ },
+ schemaName: {
+ description:
+ "Name of a schema used for validation of the encryptedData",
+ maxLength: 100,
+ minLength: 1,
+ pattern: "^\\S+$",
+ type: "string",
+ },
+ schemaNamespace: {
+ description:
+ "The namespace of the schema used for validation and routing to a dataset.",
+ maxLength: 100,
+ minLength: 1,
+ pattern: "^\\S+$",
+ type: "string",
+ },
+ schemaVersion: {
+ description: "Integer version number of the schema",
+ minimum: 1,
+ type: "integer",
+ },
+ studyName: {
+ description: "Name of a particular study. Usually the addon_id.",
+ maxLength: 100,
+ minLength: 1,
+ pattern: "^\\S+$",
+ type: "string",
+ },
+ },
+ required: [
+ "encryptedData",
+ "encryptionKeyId",
+ "pioneerId",
+ "studyName",
+ "schemaName",
+ "schemaNamespace",
+ "schemaVersion",
+ ],
+ title: "pioneer-study",
+ type: "object",
+ },
+ type: {
+ description: "doc_type, restated",
+ enum: ["pioneer-study"],
+ type: "string",
+ },
+ version: {
+ maximum: 4,
+ minimum: 4,
+ type: "integer",
+ },
+ },
+ required: [
+ "application",
+ "creationDate",
+ "id",
+ "payload",
+ "type",
+ "version",
+ ],
+ title: "pioneer-study",
+ type: "object",
+ };
+
+ const result = JsonSchemaValidator.validate(archivedCopy, schema);
+
+ Assert.ok(
+ result.valid,
+ `Archived ping should validate against schema: ${result.error}`
+ );
+
+ // check that payload can be decrypted.
+ const privateJWK = await crypto.subtle.importKey(
+ "jwk",
+ privateKey,
+ ECDH_PARAMS,
+ false,
+ ["deriveKey"]
+ );
+
+ const decryptedJWE = await jwcrypto.decryptJWE(
+ archivedCopy.payload.encryptedData,
+ privateJWK
+ );
+
+ Assert.deepEqual(
+ JSON.parse(new TextDecoder("utf-8").decode(decryptedJWE)),
+ payload,
+ "decrypted payload should match"
+ );
+});
+
+add_task(async function test_encryptedPing_overrideId() {
+ if (gIsAndroid) {
+ // The underlying jwcrypto module being used here is not currently available on Android.
+ return;
+ }
+
+ const publicKey = {
+ crv: "P-256",
+ ext: true,
+ kty: "EC",
+ x: "h12feyTYBZ__wO_AnM1a5-KTDlko3-YyQ_en19jyrs0",
+ y: "6GSfzo14ehDyH5E-xCOedJDAYlN0AGPMCtIgFbheLko",
+ };
+
+ const prefPioneerId = "12345";
+ const overriddenPioneerId = "c0ffeeaa-bbbb-abab-baba-eeff0ceeff0c";
+ const schemaName = "abc";
+ const schemaNamespace = "def";
+ const schemaVersion = 2;
+
+ Services.prefs.setStringPref("toolkit.telemetry.pioneerId", prefPioneerId);
+
+ let archiveTester = new TelemetryArchiveTesting.Checker();
+ await archiveTester.promiseInit();
+
+ // Submit a ping with a custom payload, which will be encrypted.
+ let payload = { canary: "test" };
+ let pingPromise = TelemetryController.submitExternalPing(
+ "test-pioneer-study-override",
+ payload,
+ {
+ studyName: "pioneer-dev-1@allizom.org",
+ addPioneerId: true,
+ overridePioneerId: overriddenPioneerId,
+ useEncryption: true,
+ encryptionKeyId: "pioneer-dev-20200423",
+ publicKey,
+ schemaName,
+ schemaNamespace,
+ schemaVersion,
+ }
+ );
+
+ // Wait for the ping to be submitted, to have the ping id to scan the
+ // archive for.
+ const pingId = await pingPromise;
+
+ // And then wait for the ping to be available in the archive.
+ await TestUtils.waitForCondition(
+ () => archiveTester.promiseFindPing("test-pioneer-study-override", []),
+ "Failed to find the pioneer ping"
+ );
+
+ let archivedCopy = await TelemetryArchive.promiseArchivedPingById(pingId);
+
+ Assert.notEqual(
+ archivedCopy.payload.encryptedData,
+ payload,
+ "The encrypted payload must not match the plaintext."
+ );
+
+ Assert.equal(
+ archivedCopy.payload.pioneerId,
+ overriddenPioneerId,
+ "Pioneer ID in ping must match the provided override."
+ );
+});
+
+// Testing shutdown and checking that pings sent afterwards are rejected.
+add_task(async function test_pingRejection() {
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ await sendPing(false, false).then(
+ () => Assert.ok(false, "Pings submitted after shutdown must be rejected."),
+ () => Assert.ok(true, "Ping submitted after shutdown correctly rejected.")
+ );
+});
+
+add_task(async function test_newCanRecordsMatchTheOld() {
+ Assert.equal(
+ Telemetry.canRecordBase,
+ Telemetry.canRecordReleaseData,
+ "Release Data is the new way to say Base Collection"
+ );
+ Assert.equal(
+ Telemetry.canRecordExtended,
+ Telemetry.canRecordPrereleaseData,
+ "Prerelease Data is the new way to say Extended Collection"
+ );
+});
+
+add_task(function test_histogram_filtering() {
+ const COUNT_ID = "TELEMETRY_TEST_COUNT";
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const count = Telemetry.getHistogramById(COUNT_ID);
+ const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ count.add(1);
+ keyed.add("a", 1);
+
+ let snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ let keyedSnapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ Assert.ok(COUNT_ID in snapshot, "test histogram should be snapshotted");
+ Assert.ok(
+ KEYED_ID in keyedSnapshot,
+ "test keyed histogram should be snapshotted"
+ );
+
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ keyedSnapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ Assert.ok(
+ !(COUNT_ID in snapshot),
+ "test histogram should not be snapshotted"
+ );
+ Assert.ok(
+ !(KEYED_ID in keyedSnapshot),
+ "test keyed histogram should not be snapshotted"
+ );
+});
+
+add_task(function test_scalar_filtering() {
+ const COUNT_ID = "telemetry.test.unsigned_int_kind";
+ const KEYED_ID = "telemetry.test.keyed_unsigned_int";
+
+ Telemetry.scalarSet(COUNT_ID, 2);
+ Telemetry.keyedScalarSet(KEYED_ID, "a", 2);
+
+ let snapshot = Telemetry.getSnapshotForScalars(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ let keyedSnapshot = Telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ Assert.ok(COUNT_ID in snapshot, "test scalars should be snapshotted");
+ Assert.ok(
+ KEYED_ID in keyedSnapshot,
+ "test keyed scalars should be snapshotted"
+ );
+
+ snapshot = Telemetry.getSnapshotForScalars(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ keyedSnapshot = Telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ Assert.ok(!(COUNT_ID in snapshot), "test scalars should not be snapshotted");
+ Assert.ok(
+ !(KEYED_ID in keyedSnapshot),
+ "test keyed scalars should not be snapshotted"
+ );
+});
+
+add_task(async function stopServer() {
+ await PingServer.stop();
+});