summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/tests/unit/test_TelemetrySession.js')
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySession.js2389
1 files changed, 2389 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
new file mode 100644
index 0000000000..9267c96ce1
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -0,0 +1,2389 @@
+/* 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 { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+const { TelemetryReportingPolicy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+);
+
+const PING_FORMAT_VERSION = 4;
+const PING_TYPE_MAIN = "main";
+const PING_TYPE_SAVED_SESSION = "saved-session";
+
+const REASON_ABORTED_SESSION = "aborted-session";
+const REASON_SAVED_SESSION = "saved-session";
+const REASON_SHUTDOWN = "shutdown";
+const REASON_TEST_PING = "test-ping";
+const REASON_DAILY = "daily";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+
+const IGNORE_CLONED_HISTOGRAM = "test::ignore_me_also";
+// Add some unicode characters here to ensure that sending them works correctly.
+const SHUTDOWN_TIME = 10000;
+const FAILED_PROFILE_LOCK_ATTEMPTS = 2;
+
+// Constants from prio.h for nsIFileOutputStream.init
+const PR_WRONLY = 0x2;
+const PR_CREATE_FILE = 0x8;
+const PR_TRUNCATE = 0x20;
+const RW_OWNER = parseInt("0600", 8);
+
+const MS_IN_ONE_HOUR = 60 * 60 * 1000;
+const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR;
+
+const DATAREPORTING_DIR = "datareporting";
+const ABORTED_PING_FILE_NAME = "aborted-session-ping";
+const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
+
+ChromeUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function () {
+ return PathUtils.join(PathUtils.profileDir, DATAREPORTING_DIR);
+});
+
+var gClientID = null;
+var gMonotonicNow = 0;
+
+function sendPing() {
+ TelemetrySession.gatherStartup();
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ return TelemetrySession.testPing();
+ }
+ TelemetrySend.setServer("http://doesnotexist");
+ return TelemetrySession.testPing();
+}
+
+function fakeGenerateUUID(sessionFunc, subsessionFunc) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+ );
+ Policy.generateSessionUUID = sessionFunc;
+ Policy.generateSubsessionUUID = subsessionFunc;
+}
+
+function fakeIdleNotification(topic) {
+ return TelemetryScheduler.observe(null, topic, null);
+}
+
+function setupTestData() {
+ Services.startup.interrupted = true;
+ let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ h2.add();
+
+ let k1 = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
+ k1.add("a");
+ k1.add("a");
+ k1.add("b");
+}
+
+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,
+ 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);
+}
+
+function checkPayloadInfo(data, reason) {
+ const ALLOWED_REASONS = [
+ "environment-change",
+ "shutdown",
+ "daily",
+ "saved-session",
+ "test-ping",
+ ];
+ let numberCheck = arg => {
+ return typeof arg == "number";
+ };
+ let positiveNumberCheck = arg => {
+ return numberCheck(arg) && arg >= 0;
+ };
+ let stringCheck = arg => {
+ return typeof arg == "string" && arg != "";
+ };
+ let revisionCheck = arg => {
+ return AppConstants.MOZILLA_OFFICIAL
+ ? stringCheck(arg)
+ : typeof arg == "string";
+ };
+ let uuidCheck = arg => {
+ return UUID_REGEX.test(arg);
+ };
+ let isoDateCheck = arg => {
+ // We expect use of this version of the ISO format:
+ // 2015-04-12T18:51:19.1+00:00
+ const isoDateRegEx =
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{2}:\d{2}$/;
+ return (
+ stringCheck(arg) &&
+ !Number.isNaN(Date.parse(arg)) &&
+ isoDateRegEx.test(arg)
+ );
+ };
+
+ const EXPECTED_INFO_FIELDS_TYPES = {
+ reason: stringCheck,
+ revision: revisionCheck,
+ timezoneOffset: numberCheck,
+ sessionId: uuidCheck,
+ subsessionId: uuidCheck,
+ // Special cases: previousSessionId and previousSubsessionId are null on first run.
+ previousSessionId: arg => {
+ return arg ? uuidCheck(arg) : true;
+ },
+ previousSubsessionId: arg => {
+ return arg ? uuidCheck(arg) : true;
+ },
+ subsessionCounter: positiveNumberCheck,
+ profileSubsessionCounter: positiveNumberCheck,
+ sessionStartDate: isoDateCheck,
+ subsessionStartDate: isoDateCheck,
+ subsessionLength: positiveNumberCheck,
+ };
+
+ for (let f in EXPECTED_INFO_FIELDS_TYPES) {
+ Assert.ok(f in data, f + " must be available.");
+
+ let checkFunc = EXPECTED_INFO_FIELDS_TYPES[f];
+ Assert.ok(
+ checkFunc(data[f]),
+ f + " must have the correct type and valid data " + data[f]
+ );
+ }
+
+ // Check for a valid revision.
+ if (data.revision != "") {
+ const revisionUrlRegEx =
+ /^http[s]?:\/\/hg.mozilla.org(\/[a-z\S]+)+(\/rev\/[0-9a-z]+)$/g;
+ Assert.ok(revisionUrlRegEx.test(data.revision));
+ }
+
+ // Previous buildId is not mandatory.
+ if (data.previousBuildId) {
+ Assert.ok(stringCheck(data.previousBuildId));
+ }
+
+ Assert.ok(
+ ALLOWED_REASONS.find(r => r == data.reason),
+ "Payload must contain an allowed reason."
+ );
+ Assert.equal(data.reason, reason, "Payload reason must match expected.");
+
+ Assert.ok(
+ Date.parse(data.subsessionStartDate) >= Date.parse(data.sessionStartDate)
+ );
+ Assert.ok(data.profileSubsessionCounter >= data.subsessionCounter);
+
+ // According to https://en.wikipedia.org/wiki/List_of_UTC_time_offsets,
+ // UTC offsets range from -12 to +14 hours.
+ // Don't think the extremes of the range are affected by further
+ // daylight-savings adjustments, but it is possible.
+ Assert.ok(
+ data.timezoneOffset >= -12 * 60,
+ "The timezone must be in a valid range."
+ );
+ Assert.ok(
+ data.timezoneOffset <= 14 * 60,
+ "The timezone must be in a valid range."
+ );
+}
+
+function checkScalars(processes) {
+ // Check that the scalars section is available in the ping payload.
+ const parentProcess = processes.parent;
+ Assert.ok(
+ "scalars" in parentProcess,
+ "The scalars section must be available in the parent process."
+ );
+ Assert.ok(
+ "keyedScalars" in parentProcess,
+ "The keyedScalars section must be available in the parent process."
+ );
+ Assert.equal(
+ typeof parentProcess.scalars,
+ "object",
+ "The scalars entry must be an object."
+ );
+ Assert.equal(
+ typeof parentProcess.keyedScalars,
+ "object",
+ "The keyedScalars entry must be an object."
+ );
+
+ let checkScalar = function (scalar, name) {
+ // Check if the value is of a supported type.
+ const valueType = typeof scalar;
+ switch (valueType) {
+ case "string":
+ Assert.ok(
+ scalar.length <= 50,
+ "String values can't have more than 50 characters"
+ );
+ break;
+ case "number":
+ Assert.ok(
+ scalar >= 0,
+ "We only support unsigned integer values in scalars."
+ );
+ break;
+ case "boolean":
+ Assert.ok(true, "Boolean scalar found.");
+ break;
+ default:
+ Assert.ok(
+ false,
+ name + " contains an unsupported value type (" + valueType + ")"
+ );
+ }
+ };
+
+ // Check that we have valid scalar entries.
+ const scalars = parentProcess.scalars;
+ for (let name in scalars) {
+ Assert.equal(typeof name, "string", "Scalar names must be strings.");
+ checkScalar(scalars[name], name);
+ }
+
+ // Check that we have valid keyed scalar entries.
+ const keyedScalars = parentProcess.keyedScalars;
+ for (let name in keyedScalars) {
+ Assert.equal(typeof name, "string", "Scalar names must be strings.");
+ Assert.ok(
+ Object.keys(keyedScalars[name]).length,
+ "The reported keyed scalars must contain at least 1 key."
+ );
+ for (let key in keyedScalars[name]) {
+ Assert.equal(typeof key, "string", "Keyed scalar keys must be strings.");
+ Assert.ok(
+ key.length <= 70,
+ "Keyed scalar keys can't have more than 70 characters."
+ );
+ checkScalar(scalars[name][key], name);
+ }
+ }
+}
+
+function checkPayload(payload, reason, successfulPings) {
+ Assert.ok("info" in payload, "Payload must contain an info section.");
+ checkPayloadInfo(payload.info, reason);
+
+ Assert.ok(payload.simpleMeasurements.totalTime >= 0);
+ Assert.equal(payload.simpleMeasurements.startupInterrupted, 1);
+ Assert.equal(payload.simpleMeasurements.shutdownDuration, SHUTDOWN_TIME);
+
+ let activeTicks = payload.simpleMeasurements.activeTicks;
+ Assert.ok(activeTicks >= 0);
+
+ if ("browser.timings.last_shutdown" in payload.processes.parent.scalars) {
+ Assert.equal(
+ payload.processes.parent.scalars["browser.timings.last_shutdown"],
+ SHUTDOWN_TIME
+ );
+ }
+
+ Assert.equal(
+ payload.simpleMeasurements.failedProfileLockCount,
+ FAILED_PROFILE_LOCK_ATTEMPTS
+ );
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let failedProfileLocksFile = profileDirectory.clone();
+ failedProfileLocksFile.append("Telemetry.FailedProfileLocks.txt");
+ Assert.ok(!failedProfileLocksFile.exists());
+
+ let isWindows = "@mozilla.org/windows-registry-key;1" in Cc;
+ if (isWindows) {
+ Assert.ok(payload.simpleMeasurements.startupSessionRestoreReadBytes > 0);
+ Assert.ok(payload.simpleMeasurements.startupSessionRestoreWriteBytes > 0);
+ }
+
+ const TELEMETRY_SEND_SUCCESS = "TELEMETRY_SEND_SUCCESS";
+ const TELEMETRY_SUCCESS = "TELEMETRY_SUCCESS";
+ const TELEMETRY_TEST_FLAG = "TELEMETRY_TEST_FLAG";
+ const TELEMETRY_TEST_COUNT = "TELEMETRY_TEST_COUNT";
+ const TELEMETRY_TEST_KEYED_FLAG = "TELEMETRY_TEST_KEYED_FLAG";
+ const TELEMETRY_TEST_KEYED_COUNT = "TELEMETRY_TEST_KEYED_COUNT";
+
+ if (successfulPings > 0) {
+ Assert.ok(TELEMETRY_SEND_SUCCESS in payload.histograms);
+ }
+ Assert.ok(TELEMETRY_TEST_FLAG in payload.histograms);
+ Assert.ok(TELEMETRY_TEST_COUNT in payload.histograms);
+
+ Assert.ok(!(IGNORE_CLONED_HISTOGRAM in payload.histograms));
+
+ // Flag histograms should automagically spring to life.
+ const expected_flag = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 3,
+ values: { 0: 1, 1: 0 },
+ sum: 0,
+ };
+ let flag = payload.histograms[TELEMETRY_TEST_FLAG];
+ Assert.deepEqual(flag, expected_flag);
+
+ // We should have a test count.
+ const expected_count = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: { 0: 1, 1: 0 },
+ sum: 1,
+ };
+ let count = payload.histograms[TELEMETRY_TEST_COUNT];
+ Assert.deepEqual(count, expected_count);
+
+ // There should be one successful report from the previous telemetry ping.
+ if (successfulPings > 0) {
+ const expected_tc = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 2,
+ values: { 0: 2, 1: successfulPings, 2: 0 },
+ sum: successfulPings,
+ };
+ let tc = payload.histograms[TELEMETRY_SUCCESS];
+ Assert.deepEqual(tc, expected_tc);
+ }
+
+ // The ping should include data from memory reporters. We can't check that
+ // this data is correct, because we can't control the values returned by the
+ // memory reporters. But we can at least check that the data is there.
+ //
+ // It's important to check for the presence of reporters with a mix of units,
+ // because MemoryTelemetry has separate logic for each one. But we can't
+ // currently check UNITS_COUNT_CUMULATIVE or UNITS_PERCENTAGE because
+ // Telemetry doesn't touch a memory reporter with these units that's
+ // available on all platforms.
+
+ Assert.ok("MEMORY_TOTAL" in payload.histograms); // UNITS_BYTES
+ Assert.ok("MEMORY_JS_GC_HEAP" in payload.histograms); // UNITS_BYTES
+ Assert.ok("MEMORY_JS_COMPARTMENTS_SYSTEM" in payload.histograms); // UNITS_COUNT
+
+ Assert.ok(
+ "mainThread" in payload.slowSQL && "otherThreads" in payload.slowSQL
+ );
+
+ // Check keyed histogram payload.
+
+ Assert.ok("keyedHistograms" in payload);
+ let keyedHistograms = payload.keyedHistograms;
+ Assert.ok(!(TELEMETRY_TEST_KEYED_FLAG in keyedHistograms));
+ Assert.ok(TELEMETRY_TEST_KEYED_COUNT in keyedHistograms);
+
+ const expected_keyed_count = {
+ a: {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: { 0: 2, 1: 0 },
+ sum: 2,
+ },
+ b: {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: { 0: 1, 1: 0 },
+ sum: 1,
+ },
+ };
+ Assert.deepEqual(
+ expected_keyed_count,
+ keyedHistograms[TELEMETRY_TEST_KEYED_COUNT]
+ );
+
+ Assert.ok(
+ "processes" in payload,
+ "The payload must have a processes section."
+ );
+ Assert.ok(
+ "parent" in payload.processes,
+ "There must be at least a parent process."
+ );
+
+ checkScalars(payload.processes);
+}
+
+function writeStringToFile(file, contents) {
+ let ostream = Cc[
+ "@mozilla.org/network/safe-file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(
+ file,
+ PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ RW_OWNER,
+ ostream.DEFER_OPEN
+ );
+ ostream.write(contents, contents.length);
+ ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ ostream.close();
+}
+
+function write_fake_shutdown_file() {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append("Telemetry.ShutdownTime.txt");
+ let contents = "" + SHUTDOWN_TIME;
+ writeStringToFile(file, contents);
+}
+
+function write_fake_failedprofilelocks_file() {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append("Telemetry.FailedProfileLocks.txt");
+ let contents = "" + FAILED_PROFILE_LOCK_ATTEMPTS;
+ writeStringToFile(file, contents);
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Make it look like we've previously failed to lock a profile a couple times.
+ write_fake_failedprofilelocks_file();
+
+ // Make it look like we've shutdown before.
+ write_fake_shutdown_file();
+
+ await new Promise(resolve =>
+ Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))
+ );
+});
+
+add_task(async function asyncSetup() {
+ await TelemetryController.testSetup();
+ // Load the client ID from the client ID provider to check for pings sanity.
+ gClientID = await ClientID.getClientID();
+});
+
+// Ensures that expired histograms are not part of the payload.
+add_task(async function test_expiredHistogram() {
+ let dummy = Telemetry.getHistogramById("TELEMETRY_TEST_EXPIRED");
+
+ dummy.add(1);
+
+ Assert.equal(
+ TelemetrySession.getPayload().histograms.TELEMETRY_TEST_EXPIRED,
+ undefined
+ );
+});
+
+add_task(async function sessionTimeExcludingAndIncludingSuspend() {
+ if (gIsAndroid) {
+ // We don't support this new probe on android at the moment.
+ return;
+ }
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+ await TelemetryController.testReset();
+ let subsession = TelemetrySession.getPayload("environment-change", true);
+ let parentScalars = subsession.processes.parent.scalars;
+
+ let withSuspend =
+ parentScalars["browser.engagement.session_time_including_suspend"];
+ let withoutSuspend =
+ parentScalars["browser.engagement.session_time_excluding_suspend"];
+
+ Assert.ok(
+ withSuspend > 0,
+ "The session time including suspend should be positive"
+ );
+
+ Assert.ok(
+ withoutSuspend > 0,
+ "The session time excluding suspend should be positive"
+ );
+
+ // Two things about the next assertion:
+ // 1. The two calls to get the two different uptime values are made
+ // separately, so we can't guarantee equality, even if we know the machine
+ // has not been suspended (for example because it's running in infra and
+ // was just booted). In this case the value should be close to each other.
+ // 2. This test will fail if the device running this has been suspended in
+ // between booting the Firefox process running this test, and doing the
+ // following assertion test, but that's unlikely in practice.
+ const max_delta_ms = 100;
+
+ Assert.ok(
+ withSuspend - withoutSuspend <= max_delta_ms,
+ "In test condition, the two uptimes should be close to each other"
+ );
+
+ // This however should always hold.
+ Assert.greaterOrEqual(
+ withSuspend,
+ withoutSuspend,
+ `The uptime with suspend must always been greater or equal to the uptime
+ without suspend`
+ );
+
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ false
+ );
+});
+
+// Sends a ping to a non existing server. If we remove this test, we won't get
+// all the histograms we need in the main ping.
+add_task(async function test_noServerPing() {
+ await sendPing();
+ // We need two pings in order to make sure STARTUP_MEMORY_STORAGE_SQLIE histograms
+ // are initialised. See bug 1131585.
+ await sendPing();
+ // Allowing Telemetry to persist unsent pings as pending. If omitted may cause
+ // problems to the consequent tests.
+ await TelemetryController.testShutdown();
+});
+
+// Checks that a sent ping is correctly received by a dummy http server.
+add_task(async function test_simplePing() {
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.start();
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ let now = new Date(2020, 1, 1, 12, 5, 6);
+ let expectedDate = new Date(2020, 1, 1, 12, 0, 0);
+ fakeNow(now);
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 5000);
+
+ const expectedSessionUUID = "bd314d15-95bf-4356-b682-b6c4a8942202";
+ const expectedSubsessionUUID = "3e2e5f6c-74ba-4e4d-a93f-a48af238a8c7";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+ await TelemetryController.testReset();
+
+ // Session and subsession start dates are faked during TelemetrySession setup. We can
+ // now fake the session duration.
+ const SESSION_DURATION_IN_MINUTES = 15;
+ fakeNow(new Date(2020, 1, 1, 12, SESSION_DURATION_IN_MINUTES, 0));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + SESSION_DURATION_IN_MINUTES * 60 * 1000
+ );
+
+ await sendPing();
+ let ping = await PingServer.promiseNextPing();
+
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that we get the data we expect.
+ let payload = ping.payload;
+ Assert.equal(payload.info.sessionId, expectedSessionUUID);
+ Assert.equal(payload.info.subsessionId, expectedSubsessionUUID);
+ let sessionStartDate = new Date(payload.info.sessionStartDate);
+ Assert.equal(sessionStartDate.toISOString(), expectedDate.toISOString());
+ let subsessionStartDate = new Date(payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+ Assert.equal(payload.info.subsessionLength, SESSION_DURATION_IN_MINUTES * 60);
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ await TelemetryController.testShutdown();
+});
+
+// Saves the current session histograms, reloads them, performs a ping
+// and checks that the dummy http server received both the previously
+// saved ping and the new one.
+add_task(async function test_saveLoadPing() {
+ // Let's start out with a defined state.
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ PingServer.clearRequests();
+
+ // Setup test data and trigger pings.
+ setupTestData();
+ await TelemetrySession.testSavePendingPing();
+ await sendPing();
+
+ // Get requests received by dummy server.
+ const requests = await PingServer.promiseNextRequests(2);
+
+ for (let req of requests) {
+ Assert.equal(
+ req.getHeader("content-type"),
+ "application/json; charset=UTF-8",
+ "The request must have the correct content-type."
+ );
+ }
+
+ // We decode both requests to check for the |reason|.
+ let pings = Array.from(requests, decodeRequestPayload);
+
+ // Check we have the correct two requests. Ordering is not guaranteed. The ping type
+ // is encoded in the URL.
+ if (pings[0].type != PING_TYPE_MAIN) {
+ pings.reverse();
+ }
+
+ checkPingFormat(pings[0], PING_TYPE_MAIN, true, true);
+ checkPayload(pings[0].payload, REASON_TEST_PING, 0);
+ checkPingFormat(pings[1], PING_TYPE_SAVED_SESSION, true, true);
+ checkPayload(pings[1].payload, REASON_SAVED_SESSION, 0);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_checkSubsessionScalars() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android.
+ return;
+ }
+
+ // Clear the scalars.
+ Telemetry.clearScalars();
+ await TelemetryController.testReset();
+
+ // Set some scalars.
+ const UINT_SCALAR = "telemetry.test.unsigned_int_kind";
+ const STRING_SCALAR = "telemetry.test.string_kind";
+ let expectedUint = 37;
+ let expectedString = "Test value. Yay.";
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.scalarSet(STRING_SCALAR, expectedString);
+
+ // Check that scalars are not available in classic pings but are in subsession
+ // pings. Also clear the subsession.
+ let classic = TelemetrySession.getPayload();
+ let subsession = TelemetrySession.getPayload("environment-change", true);
+
+ const TEST_SCALARS = [UINT_SCALAR, STRING_SCALAR];
+ for (let name of TEST_SCALARS) {
+ // Scalar must be reported in subsession pings (e.g. main).
+ Assert.ok(
+ name in subsession.processes.parent.scalars,
+ name + " must be reported in a subsession ping."
+ );
+ }
+ // No scalar must be reported in classic pings (e.g. saved-session).
+ Assert.ok(
+ !Object.keys(classic.processes.parent.scalars).length,
+ "Scalars must not be reported in a classic ping."
+ );
+
+ // And make sure that we're getting the right values in the
+ // subsession ping.
+ Assert.equal(
+ subsession.processes.parent.scalars[UINT_SCALAR],
+ expectedUint,
+ UINT_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ subsession.processes.parent.scalars[STRING_SCALAR],
+ expectedString,
+ STRING_SCALAR + " must contain the expected value."
+ );
+
+ // Since we cleared the subsession in the last getPayload(), check that
+ // breaking subsessions clears the scalars.
+ subsession = TelemetrySession.getPayload("environment-change");
+ for (let name of TEST_SCALARS) {
+ Assert.ok(
+ !(name in subsession.processes.parent.scalars),
+ name + " must be cleared with the new subsession."
+ );
+ }
+
+ // Check if setting the scalars again works as expected.
+ expectedUint = 85;
+ expectedString = "A creative different value";
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.scalarSet(STRING_SCALAR, expectedString);
+ subsession = TelemetrySession.getPayload("environment-change");
+ Assert.equal(
+ subsession.processes.parent.scalars[UINT_SCALAR],
+ expectedUint,
+ UINT_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ subsession.processes.parent.scalars[STRING_SCALAR],
+ expectedString,
+ STRING_SCALAR + " must contain the expected value."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_dailyCollection() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ let now = new Date(2030, 1, 1, 12, 0, 0);
+ let nowHour = new Date(2030, 1, 1, 12, 0, 0);
+ let schedulerTickCallback = null;
+
+ PingServer.clearRequests();
+
+ fakeNow(now);
+
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+
+ // Init and check timer.
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+
+ // Set histograms to expected state.
+ 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.clear();
+ keyed.clear();
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+ keyed.add("b", 1);
+
+ // Make sure the daily ping gets triggered.
+ let expectedDate = nowHour;
+ now = futureDate(nowHour, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ Assert.ok(!!schedulerTickCallback);
+ // Run a scheduler tick: it should trigger the daily ping.
+ await schedulerTickCallback();
+
+ // Collect the daily ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].b.sum, 2);
+
+ // The daily ping is rescheduled for "tomorrow".
+ expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
+ now = futureDate(now, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ // Run a scheduler tick. Trigger and collect another ping. The histograms should be reset.
+ await schedulerTickCallback();
+
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.ok(!(COUNT_ID in ping.payload.histograms));
+ Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms));
+
+ // Trigger and collect another daily ping, with the histograms being set again.
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+
+ // The daily ping is rescheduled for "tomorrow".
+ expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
+ now = futureDate(now, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ await schedulerTickCallback();
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].b.sum, 1);
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_dailyDuplication() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ await TelemetrySend.reset();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Make sure the daily ping gets triggered at midnight.
+ // We need to make sure that we trigger this after the period where we wait for
+ // the user to become idle.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Get the first daily ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+
+ // We don't expect to receive any other daily ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(
+ false,
+ "No more daily pings should be sent/received in this test."
+ );
+ });
+
+ // Set the current time to a bit after midnight.
+ let secondDailyDue = new Date(firstDailyDue);
+ secondDailyDue.setHours(0);
+ secondDailyDue.setMinutes(15);
+ fakeNow(secondDailyDue);
+
+ // Run a scheduler tick: it should NOT trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ PingServer.resetPingHandler();
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_dailyOverdue() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 11, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+
+ // Skip one hour ahead: nothing should be due.
+ now.setHours(now.getHours() + 1);
+ fakeNow(now);
+
+ // Assert if we receive something!
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(false, "No daily ping should be received if not overdue!.");
+ });
+
+ // This tick should not trigger any daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Restore the non asserting ping handler.
+ PingServer.resetPingHandler();
+ PingServer.clearRequests();
+
+ // Simulate an overdue ping: we're not close to midnight, but the last daily ping
+ // time is too long ago.
+ let dailyOverdue = new Date(2030, 1, 2, 13, 0, 0);
+ fakeNow(dailyOverdue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Get the first daily ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_environmentChange() {
+ if (gIsAndroid) {
+ // We don't split subsessions on environment changes yet on Android.
+ return;
+ }
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let now = fakeNow(2040, 1, 1, 12, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Services.prefs.clearUserPref(PREF_TEST);
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ ]);
+
+ // Setup.
+ await TelemetryController.testReset();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ // Set histograms to expected state.
+ 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.clear();
+ keyed.clear();
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+
+ // Trigger and collect environment-change ping.
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ let startHour = TelemetryUtils.truncateToHours(now);
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+
+ Services.prefs.setIntPref(PREF_TEST, 1);
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], undefined);
+ Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
+ let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), startHour.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);
+
+ // Trigger and collect another ping. The histograms should be reset.
+ startHour = TelemetryUtils.truncateToHours(now);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+
+ Services.prefs.setIntPref(PREF_TEST, 2);
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], 1);
+ Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), startHour.toISOString());
+
+ Assert.ok(!(COUNT_ID in ping.payload.histograms));
+ Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms));
+
+ // Trigger and collect another ping. The histograms should be reset.
+ startHour = TelemetryUtils.truncateToHours(now);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_experimentAnnotations_subsession() {
+ if (gIsAndroid) {
+ // We don't split subsessions on environment changes yet on Android.
+ return;
+ }
+
+ const EXPERIMENT1 = "experiment-1";
+ const EXPERIMENT1_BRANCH = "nice-branch";
+ const EXPERIMENT2 = "experiment-2";
+ const EXPERIMENT2_BRANCH = "other-branch";
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let now = fakeNow(2040, 1, 1, 12, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+
+ // Setup.
+ await TelemetryController.testReset();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ Assert.equal(TelemetrySession.getPayload().info.subsessionCounter, 1);
+
+ // Trigger a subsession split with a telemetry annotation.
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ let futureTestDate = futureDate(now, 10 * MILLISECONDS_PER_MINUTE);
+ now = fakeNow(futureTestDate);
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT1, EXPERIMENT1_BRANCH);
+
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping, "A ping must be received.");
+
+ Assert.equal(
+ ping.type,
+ PING_TYPE_MAIN,
+ "The received ping must be a 'main' ping."
+ );
+ Assert.equal(
+ ping.payload.info.reason,
+ REASON_ENVIRONMENT_CHANGE,
+ "The 'main' ping must be triggered by a change in the environment."
+ );
+ // We expect the current experiments to be reported in the next ping, not this
+ // one.
+ Assert.ok(
+ !("experiments" in ping.environment),
+ "The old environment must contain no active experiments."
+ );
+ // Since this change wasn't throttled, the subsession counter must increase.
+ Assert.equal(
+ TelemetrySession.getPayload().info.subsessionCounter,
+ 2,
+ "The experiment annotation must trigger a new subsession."
+ );
+
+ // Add another annotation to the environment. We're not advancing the fake
+ // timer, so no subsession split should happen due to throttling.
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT2, EXPERIMENT2_BRANCH);
+ Assert.equal(
+ TelemetrySession.getPayload().info.subsessionCounter,
+ 2,
+ "The experiment annotation must not trigger a new subsession " +
+ "if throttling happens."
+ );
+ let oldExperiments = TelemetryEnvironment.getActiveExperiments();
+
+ // Fake the timer and remove an annotation, we expect a new subsession split.
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+ TelemetryEnvironment.setExperimentInactive(EXPERIMENT1, EXPERIMENT1_BRANCH);
+
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping, "A ping must be received.");
+
+ Assert.equal(
+ ping.type,
+ PING_TYPE_MAIN,
+ "The received ping must be a 'main' ping."
+ );
+ Assert.equal(
+ ping.payload.info.reason,
+ REASON_ENVIRONMENT_CHANGE,
+ "The 'main' ping must be triggered by a change in the environment."
+ );
+ // We expect both experiments to be in this environment.
+ Assert.deepEqual(
+ ping.environment.experiments,
+ oldExperiments,
+ "The environment must contain both the experiments."
+ );
+ Assert.equal(
+ TelemetrySession.getPayload().info.subsessionCounter,
+ 3,
+ "The removing an experiment annotation must trigger a new subsession."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_savedPingsOnShutdown() {
+ await TelemetryController.testReset();
+
+ // Assure that we store the ping properly when saving sessions on shutdown.
+ // We make the TelemetryController shutdown to trigger a session save.
+ const dir = TelemetryStorage.pingDirectoryPath;
+ await IOUtils.remove(dir, { ignoreAbsent: true, recursive: true });
+ await IOUtils.makeDirectory(dir);
+ await TelemetryController.testShutdown();
+
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ const ping = await PingServer.promiseNextPing();
+
+ let expectedType = gIsAndroid ? PING_TYPE_SAVED_SESSION : PING_TYPE_MAIN;
+ let expectedReason = gIsAndroid ? REASON_SAVED_SESSION : REASON_SHUTDOWN;
+
+ checkPingFormat(ping, expectedType, true, true);
+ Assert.equal(ping.payload.info.reason, expectedReason);
+ Assert.equal(ping.clientId, gClientID);
+});
+
+add_task(async function test_sendShutdownPing() {
+ 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;
+ }
+
+ let checkPendingShutdownPing = async function () {
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ Assert.equal(pendingPings.length, 1, "We expect 1 pending ping: shutdown.");
+ // Load the pings off the disk.
+ const shutdownPing = await TelemetryStorage.loadPendingPing(
+ pendingPings[0].id
+ );
+ Assert.ok(shutdownPing, "The 'shutdown' ping must be saved to disk.");
+ Assert.equal(
+ "shutdown",
+ shutdownPing.payload.info.reason,
+ "The 'shutdown' ping must be saved to disk."
+ );
+ };
+
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ true
+ );
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, false);
+ // Make sure the reporting policy picks up the updated pref.
+ TelemetryReportingPolicy.testUpdateFirstRun();
+ PingServer.clearRequests();
+ Telemetry.clearScalars();
+
+ // Shutdown telemetry and wait for an incoming ping.
+ let nextPing = PingServer.promiseNextPing();
+ await TelemetryController.testShutdown();
+ let ping = await nextPing;
+
+ // Check that we received a shutdown ping.
+ checkPingFormat(ping, ping.type, true, true);
+ Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
+ Assert.equal(ping.clientId, gClientID);
+ // Try again, this time disable ping upload. The PingSender
+ // should not be sending any ping!
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Telemetry must not send pings if not allowed to.")
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+
+ // Enable ping upload and signal an OS shutdown. The pingsender
+ // will not be spawned and no ping will be sent.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(null, "quit-application-forced");
+ await TelemetryController.testShutdown();
+ // After re-enabling FHR, wait for the new client ID
+ gClientID = await ClientID.getClientID();
+
+ // Check that the "shutdown" ping was correctly saved to disk.
+ await checkPendingShutdownPing();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+ Telemetry.clearScalars();
+
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(
+ null,
+ "quit-application-granted",
+ "syncShutdown"
+ );
+ await TelemetryController.testShutdown();
+ await checkPendingShutdownPing();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+
+ // Disable the "submission policy". The shutdown ping must not be sent.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ false
+ );
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+
+ // We cannot reset the BypassNotification pref, as we need it to be
+ // |true| in tests.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ true
+ );
+
+ // With both upload enabled and the policy shown, make sure we don't
+ // send the shutdown ping using the pingsender on the first
+ // subsession.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
+ // Make sure the reporting policy picks up the updated pref.
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Clear the state and prepare for the next test.
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ PingServer.resetPingHandler();
+
+ // Check that we're able to send the shutdown ping using the pingsender
+ // from the first session if the related pref is on.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ true
+ );
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ // Restart/shutdown telemetry and wait for an incoming ping.
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ ping = await PingServer.promiseNextPing();
+
+ // Check that we received a shutdown ping.
+ checkPingFormat(ping, ping.type, true, true);
+ Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
+ Assert.equal(ping.clientId, gClientID);
+
+ // Reset the pref and restart Telemetry.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ false
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ );
+ Services.prefs.clearUserPref(TelemetryUtils.Preferences.FirstRun);
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_sendFirstShutdownPing() {
+ 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;
+ }
+
+ let storageContainsFirstShutdown = async function () {
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ let pings = await Promise.all(
+ pendingPings.map(async p => {
+ return TelemetryStorage.loadPendingPing(p.id);
+ })
+ );
+ return pings.find(p => p.type == "first-shutdown");
+ };
+
+ let checkShutdownNotSent = async function () {
+ // The failure-mode of the ping-sender is used to check that a ping was
+ // *not* sent. This can be combined with the state of the storage to infer
+ // the appropriate behavior from the preference flags.
+
+ // Assert failure if we recive a ping.
+ PingServer.registerPingHandler((req, res) => {
+ const receivedPing = decodeRequestPayload(req);
+ Assert.ok(
+ false,
+ `No ping should be received in this test (got ${receivedPing.id}).`
+ );
+ });
+
+ // Assert that pings are sent on first run, forcing a forced application
+ // quit. This should be equivalent to the first test in this suite.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(null, "quit-application-forced");
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ await storageContainsFirstShutdown(),
+ "The 'first-shutdown' ping must be saved to disk."
+ );
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that it's not sent during subsequent runs
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, false);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(null, "quit-application-forced");
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await storageContainsFirstShutdown()),
+ "The 'first-shutdown' ping should only be written during first run."
+ );
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that the the ping is only sent if the flag is enabled.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FirstShutdownPingEnabled,
+ false
+ );
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await storageContainsFirstShutdown()),
+ "The 'first-shutdown' ping should only be written if enabled"
+ );
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that the the ping is not collected when the ping-sender is disabled.
+ // The information would be made irrelevant by the main-ping in the second session.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FirstShutdownPingEnabled,
+ true
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ false
+ );
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await storageContainsFirstShutdown()),
+ "The 'first-shutdown' ping should only be written if ping-sender is enabled"
+ );
+
+ // Clear the state and prepare for the next test.
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ PingServer.resetPingHandler();
+ };
+
+ // Remove leftover pending pings from other tests
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ Telemetry.clearScalars();
+
+ // Set testing invariants for FirstShutdownPingEnabled
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ true
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ );
+
+ // Set primary conditions of the 'first-shutdown' ping
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FirstShutdownPingEnabled,
+ true
+ );
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ // Assert general 'first-shutdown' use-case.
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, "first-shutdown", true, true);
+ Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
+ Assert.equal(ping.clientId, gClientID);
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that the shutdown is not sent under various conditions
+ await checkShutdownNotSent();
+
+ // Reset the pref and restart Telemetry.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ false
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FirstShutdownPingEnabled,
+ false
+ );
+ Services.prefs.clearUserPref(TelemetryUtils.Preferences.FirstRun);
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_savedSessionData() {
+ // Create the directory which will contain the data file, if it doesn't already
+ // exist.
+ await IOUtils.makeDirectory(DATAREPORTING_PATH);
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ // Write test data to the session data file.
+ const dataFilePath = PathUtils.join(DATAREPORTING_PATH, "session-state.json");
+ const sessionState = {
+ sessionId: null,
+ subsessionId: null,
+ profileSubsessionCounter: 3785,
+ };
+ await IOUtils.writeJSON(dataFilePath, sessionState);
+
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Services.prefs.clearUserPref(PREF_TEST);
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ ]);
+
+ // We expect one new subsession when starting TelemetrySession and one after triggering
+ // an environment change.
+ const expectedSubsessions = sessionState.profileSubsessionCounter + 2;
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android, so skip the next checks.
+ return;
+ }
+
+ // Start TelemetrySession so that it loads the session data file.
+ await TelemetryController.testReset();
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Watch a test preference, trigger and environment change and wait for it to propagate.
+ // _watchPreferences triggers a subsession notification
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ fakeNow(new Date(2050, 1, 1, 12, 0, 0));
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let changePromise = new Promise(resolve =>
+ TelemetryEnvironment.registerChangeListener("test_fake_change", resolve)
+ );
+ Services.prefs.setIntPref(PREF_TEST, 1);
+ await changePromise;
+ TelemetryEnvironment.unregisterChangeListener("test_fake_change");
+
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
+ await TelemetryController.testShutdown();
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ // Load back the serialised session data.
+ let data = await IOUtils.readJSON(dataFilePath);
+ Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(data.sessionId, expectedSessionUUID);
+ Assert.equal(data.subsessionId, expectedSubsessionUUID);
+});
+
+add_task(async function test_sessionData_ShortSession() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android, so skip the next checks.
+ return;
+ }
+
+ const SESSION_STATE_PATH = PathUtils.join(
+ DATAREPORTING_PATH,
+ "session-state.json"
+ );
+
+ // Remove the session state file.
+ await IOUtils.remove(SESSION_STATE_PATH, { ignoreAbsent: true });
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+
+ // We intentionally don't wait for the setup to complete and shut down to simulate
+ // short sessions. We expect the profile subsession counter to be 1.
+ TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Restore the UUID generation functions.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ // Start TelemetryController so that it loads the session data file. We expect the profile
+ // subsession counter to be incremented by 1 again.
+ await TelemetryController.testReset();
+
+ // We expect 2 profile subsession counter updates.
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, 2);
+ Assert.equal(payload.info.previousSessionId, expectedSessionUUID);
+ Assert.equal(payload.info.previousSubsessionId, expectedSubsessionUUID);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_invalidSessionData() {
+ // Create the directory which will contain the data file, if it doesn't already
+ // exist.
+ await IOUtils.makeDirectory(DATAREPORTING_PATH);
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ // Write test data to the session data file. This should fail to parse.
+ const dataFilePath = PathUtils.join(DATAREPORTING_PATH, "session-state.json");
+ const unparseableData = "{asdf:@äü";
+ await IOUtils.writeUTF8(dataFilePath, unparseableData, {
+ tmpPath: `${dataFilePath}.tmp`,
+ });
+
+ // Start TelemetryController so that it loads the session data file.
+ await TelemetryController.testReset();
+
+ // The session data file should not load. Only expect the current subsession.
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Write test data to the session data file. This should fail validation.
+ const sessionState = {
+ profileSubsessionCounter: "not-a-number?",
+ someOtherField: 12,
+ };
+ await IOUtils.writeJSON(dataFilePath, sessionState);
+
+ // The session data file should not load. Only expect the current subsession.
+ const expectedSubsessions = 1;
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+
+ // Start TelemetryController so that it loads the session data file.
+ await TelemetryController.testShutdown();
+ await TelemetryController.testReset();
+
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ await TelemetryController.testShutdown();
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ // Load back the serialised session data.
+ let data = await IOUtils.readJSON(dataFilePath);
+ Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(data.sessionId, expectedSessionUUID);
+ Assert.equal(data.subsessionId, expectedSubsessionUUID);
+});
+
+add_task(async function test_abortedSession() {
+ if (gIsAndroid) {
+ // We don't have the aborted session ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2040, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // This ping is not yet in the pending pings folder, so we can't access it using
+ // TelemetryStorage.popPendingPings().
+ let abortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);
+
+ // Validate the ping.
+ checkPingFormat(abortedSessionPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(abortedSessionPing.payload.info.reason, REASON_ABORTED_SESSION);
+
+ // Trigger a another aborted-session ping and check that it overwrites the previous one.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ await schedulerTickCallback();
+
+ let updatedAbortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);
+ checkPingFormat(updatedAbortedSessionPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(
+ updatedAbortedSessionPing.payload.info.reason,
+ REASON_ABORTED_SESSION
+ );
+ Assert.notEqual(abortedSessionPing.id, updatedAbortedSessionPing.id);
+ Assert.notEqual(
+ abortedSessionPing.creationDate,
+ updatedAbortedSessionPing.creationDate
+ );
+
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await IOUtils.exists(ABORTED_FILE)),
+ "No aborted session ping must be available after a shutdown."
+ );
+});
+
+add_task(async function test_abortedSession_Shutdown() {
+ if (gIsAndroid) {
+ // We don't have the aborted session ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ let schedulerTickCallback = null;
+ let now = fakeNow(2040, 1, 1, 0, 0, 0);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ fakeNow(futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS));
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // Remove the aborted session file and then shut down to make sure exceptions (e.g file
+ // not found) do not compromise the shutdown.
+ await IOUtils.remove(ABORTED_FILE);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_abortedDailyCoalescing() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ let schedulerTickCallback = null;
+ PingServer.clearRequests();
+
+ let nowDate = new Date(2009, 10, 18, 0, 0, 0);
+ fakeNow(nowDate);
+
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Delay the callback around midnight so that the aborted-session ping gets merged with the
+ // daily ping.
+ let dailyDueDate = futureDate(nowDate, MS_IN_ONE_DAY);
+ fakeNow(dailyDueDate);
+ // Trigger both the daily ping and the saved-session.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+
+ // Wait for the daily ping.
+ let dailyPing = await PingServer.promiseNextPing();
+ Assert.equal(dailyPing.payload.info.reason, REASON_DAILY);
+
+ // Check that an aborted session ping was also written to disk.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // Read aborted session ping and check that the session/subsession ids equal the
+ // ones in the daily ping.
+ let abortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);
+ Assert.equal(
+ abortedSessionPing.payload.info.sessionId,
+ dailyPing.payload.info.sessionId
+ );
+ Assert.equal(
+ abortedSessionPing.payload.info.subsessionId,
+ dailyPing.payload.info.subsessionId
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerComputerSleep() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ // Remove any aborted-session ping from the previous tests.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ // Set a fake current date and start Telemetry.
+ let nowDate = fakeNow(2009, 10, 18, 0, 0, 0);
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Set the current time 3 days in the future at midnight, before running the callback.
+ nowDate = fakeNow(futureDate(nowDate, 3 * MS_IN_ONE_DAY));
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+
+ let dailyPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ dailyPing.payload.info.reason,
+ REASON_DAILY,
+ "The wake notification should have triggered a daily ping."
+ );
+ Assert.equal(
+ dailyPing.creationDate,
+ nowDate.toISOString(),
+ "The daily ping date should be correct."
+ );
+
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // Now also test if we are sending a daily ping if we wake up on the next
+ // day even when the timer doesn't trigger.
+ // This can happen due to timeouts not running out during sleep times,
+ // see bug 1262386, bug 1204823 et al.
+ // Note that we don't get wake notifications on Linux due to bug 758848.
+ nowDate = fakeNow(futureDate(nowDate, 1 * MS_IN_ONE_DAY));
+
+ // We emulate the mentioned timeout behavior by sending the wake notification
+ // instead of triggering the timeout callback.
+ // This should trigger a daily ping, because we passed midnight.
+ Services.obs.notifyObservers(null, "wake_notification");
+
+ dailyPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ dailyPing.payload.info.reason,
+ REASON_DAILY,
+ "The wake notification should have triggered a daily ping."
+ );
+ Assert.equal(
+ dailyPing.creationDate,
+ nowDate.toISOString(),
+ "The daily ping date should be correct."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerEnvironmentReschedules() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ // Reset the test preference.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Services.prefs.clearUserPref(PREF_TEST);
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ ]);
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ // bug 1829855 - Sometimes the idle dispatch from a previous test interferes.
+ TelemetryScheduler.testReset();
+ PingServer.clearRequests();
+
+ // Set a fake current date and start Telemetry.
+ let nowDate = fakeNow(2060, 10, 18, 0, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ // Set the current time at midnight.
+ fakeNow(futureDate(nowDate, MS_IN_ONE_DAY));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+
+ // Trigger the environment change.
+ Services.prefs.setIntPref(PREF_TEST, 1);
+
+ // Wait for the environment-changed ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.equal(ping.type, "main", `Expected 'main' ping on ${ping.id}`);
+ Assert.equal(
+ ping.payload.info.reason,
+ "environment-change",
+ `Expected 'environment-change' reason on ${ping.id}`
+ );
+
+ // We don't expect to receive any daily ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ const receivedPing = decodeRequestPayload(req);
+ Assert.ok(
+ false,
+ `No ping should be received in this test (got ${receivedPing.id} type: ${receivedPing.type} reason: ${receivedPing.payload.info.reason}).`
+ );
+ });
+
+ // Execute one scheduler tick. It should not trigger a daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerNothingDue() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Remove any aborted-session ping from the previous tests.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+
+ // We don't expect to receive any ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ const receivedPing = decodeRequestPayload(req);
+ Assert.ok(
+ false,
+ `No ping should be received in this test (got ${receivedPing.id}).`
+ );
+ });
+
+ // Set a current date/time away from midnight, so that the daily ping doesn't get
+ // sent.
+ let nowDate = new Date(2009, 10, 18, 11, 0, 0);
+ fakeNow(nowDate);
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Delay the callback execution to a time when no ping should be due.
+ let nothingDueDate = futureDate(
+ nowDate,
+ ABORTED_SESSION_UPDATE_INTERVAL_MS / 2
+ );
+ fakeNow(nothingDueDate);
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+
+ // Check that no aborted session ping was written to disk.
+ Assert.ok(!(await IOUtils.exists(ABORTED_FILE)));
+
+ await TelemetryController.testShutdown();
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_pingExtendedStats() {
+ const EXTENDED_PAYLOAD_FIELDS = [
+ "log",
+ "slowSQL",
+ "fileIOReports",
+ "lateWrites",
+ "addonDetails",
+ ];
+
+ // Reset telemetry and disable sending extended statistics.
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+ Telemetry.canRecordExtended = false;
+
+ await sendPing();
+
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that the payload does not contain extended statistics fields.
+ for (let f in EXTENDED_PAYLOAD_FIELDS) {
+ Assert.ok(
+ !(EXTENDED_PAYLOAD_FIELDS[f] in ping.payload),
+ EXTENDED_PAYLOAD_FIELDS[f] +
+ " must not be in the payload if the extended set is off."
+ );
+ }
+
+ // We check this one separately so that we can reuse EXTENDED_PAYLOAD_FIELDS below, since
+ // slowSQLStartup might not be there.
+ Assert.ok(
+ !("slowSQLStartup" in ping.payload),
+ "slowSQLStartup must not be sent if the extended set is off"
+ );
+
+ Assert.ok(
+ !("addonManager" in ping.payload.simpleMeasurements),
+ "addonManager must not be sent if the extended set is off."
+ );
+ Assert.ok(
+ !("UITelemetry" in ping.payload.simpleMeasurements),
+ "UITelemetry must not be sent."
+ );
+
+ // Restore the preference.
+ Telemetry.canRecordExtended = true;
+
+ // Send a new ping that should contain the extended data.
+ await sendPing();
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that the payload now contains extended statistics fields.
+ for (let f in EXTENDED_PAYLOAD_FIELDS) {
+ Assert.ok(
+ EXTENDED_PAYLOAD_FIELDS[f] in ping.payload,
+ EXTENDED_PAYLOAD_FIELDS[f] +
+ " must be in the payload if the extended set is on."
+ );
+ }
+
+ Assert.ok(
+ "addonManager" in ping.payload.simpleMeasurements,
+ "addonManager must be sent if the extended set is on."
+ );
+ Assert.ok(
+ !("UITelemetry" in ping.payload.simpleMeasurements),
+ "UITelemetry must not be sent."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerUserIdle() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const SCHEDULER_TICK_INTERVAL_MS = 5 * 60 * 1000;
+ const SCHEDULER_TICK_IDLE_INTERVAL_MS = 60 * 60 * 1000;
+
+ let now = new Date(2010, 1, 1, 11, 0, 0);
+ fakeNow(now);
+
+ let schedulerTimeout = 0;
+ fakeSchedulerTimer(
+ (callback, timeout) => {
+ schedulerTimeout = timeout;
+ },
+ () => {}
+ );
+ await TelemetryController.testReset();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ // When not idle, the scheduler should have a 5 minutes tick interval.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);
+
+ // Send an "idle" notification to the scheduler.
+ fakeIdleNotification("idle");
+
+ // When idle, the scheduler should have a 1hr tick interval.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_IDLE_INTERVAL_MS);
+
+ // Send an "active" notification to the scheduler.
+ await fakeIdleNotification("active");
+
+ // When user is back active, the scheduler tick should be 5 minutes again.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);
+
+ // We should not miss midnight when going to idle.
+ now.setHours(23);
+ now.setMinutes(50);
+ fakeNow(now);
+ fakeIdleNotification("idle");
+ Assert.equal(schedulerTimeout, 10 * 60 * 1000);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_DailyDueAndIdle() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let receivedPingRequest = null;
+ // Register a ping handler that will assert when receiving multiple daily pings.
+ PingServer.registerPingHandler(req => {
+ Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
+ receivedPingRequest = req;
+ });
+
+ // Faking scheduler timer has to happen before resetting TelemetryController
+ // to be effective.
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Trigger the daily ping.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ let tickPromise = schedulerTickCallback();
+
+ // Send an idle and then an active user notification.
+ fakeIdleNotification("idle");
+ fakeIdleNotification("active");
+
+ // Wait on the tick promise.
+ await tickPromise;
+
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Decode the ping contained in the request and check that's a daily ping.
+ Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
+ const receivedPing = decodeRequestPayload(receivedPingRequest);
+ checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_userIdleAndSchedlerTick() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ let receivedPingRequest = null;
+ // Register a ping handler that will assert when receiving multiple daily pings.
+ PingServer.registerPingHandler(req => {
+ Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
+ receivedPingRequest = req;
+ });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ PingServer.clearRequests();
+
+ // Move the current date/time to midnight.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // The active notification should trigger a scheduler tick. The latter will send the
+ // due daily ping.
+ fakeIdleNotification("active");
+
+ // Immediately running another tick should not send a daily ping again.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // A new "idle" notification should not send a new daily ping.
+ fakeIdleNotification("idle");
+
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Decode the ping contained in the request and check that's a daily ping.
+ Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
+ const receivedPing = decodeRequestPayload(receivedPingRequest);
+ checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);
+
+ PingServer.resetPingHandler();
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_changeThrottling() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android.
+ return;
+ }
+
+ let getSubsessionCount = () => {
+ return TelemetrySession.getPayload().info.subsessionCounter;
+ };
+
+ let now = fakeNow(2050, 1, 2, 0, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ await TelemetryController.testReset();
+ Assert.equal(getSubsessionCount(), 1);
+
+ // The first pref change should not trigger a notification.
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 1);
+
+ // We should get a change notification after the 5min throttling interval.
+ fakeNow(futureDate(now, 5 * MILLISECONDS_PER_MINUTE + 1));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 5 * MILLISECONDS_PER_MINUTE + 1
+ );
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 2);
+
+ // After that, changes should be throttled again.
+ now = fakeNow(futureDate(now, 1 * MILLISECONDS_PER_MINUTE));
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 1 * MILLISECONDS_PER_MINUTE);
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 2);
+
+ // ... for 5min.
+ now = fakeNow(futureDate(now, 4 * MILLISECONDS_PER_MINUTE + 1));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 4 * MILLISECONDS_PER_MINUTE + 1
+ );
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 3);
+});
+
+add_task(async function stopServer() {
+ // It is important to shut down the TelemetryController first as, due to test
+ // environment changes, failure to upload pings here during shutdown results
+ // in an infinite loop of send failures and retries.
+ await TelemetryController.testShutdown();
+ await PingServer.stop();
+});