/* 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. */ Cu.import("resource://services-common/utils.js"); Cu.import("resource://gre/modules/ClientID.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this); Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); Cu.import("resource://gre/modules/TelemetryController.jsm", this); Cu.import("resource://gre/modules/TelemetrySession.jsm", this); Cu.import("resource://gre/modules/TelemetryStorage.jsm", this); Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this); Cu.import("resource://gre/modules/TelemetrySend.jsm", this); Cu.import("resource://gre/modules/Task.jsm", this); Cu.import("resource://gre/modules/Promise.jsm", this); Cu.import("resource://gre/modules/Preferences.jsm"); Cu.import("resource://gre/modules/osfile.jsm", this); 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 PLATFORM_VERSION = "1.9.2"; const APP_VERSION = "1"; const APP_ID = "xpcshell@tests.mozilla.org"; const APP_NAME = "XPCShell"; const IGNORE_HISTOGRAM_TO_CLONE = "MEMORY_HEAP_ALLOCATED"; const IGNORE_CLONED_HISTOGRAM = "test::ignore_me_also"; const ADDON_NAME = "Telemetry test addon"; const ADDON_HISTOGRAM = "addon-histogram"; // 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 NUMBER_OF_THREADS_TO_LAUNCH = 30; var gNumberOfThreadsLaunched = 0; const MS_IN_ONE_HOUR = 60 * 60 * 1000; const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR; const PREF_BRANCH = "toolkit.telemetry."; const PREF_SERVER = PREF_BRANCH + "server"; const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; const DATAREPORTING_DIR = "datareporting"; const ABORTED_PING_FILE_NAME = "aborted-session-ping"; const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000; XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function() { return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR); }); var gClientID = null; var gMonotonicNow = 0; function generateUUID() { let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString(); // strip {} return str.substring(1, str.length - 1); } function truncateDateToDays(date) { return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 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) { let session = Cu.import("resource://gre/modules/TelemetrySession.jsm"); session.Policy.generateSessionUUID = sessionFunc; session.Policy.generateSubsessionUUID = subsessionFunc; } function fakeIdleNotification(topic) { let session = Cu.import("resource://gre/modules/TelemetrySession.jsm"); return session.TelemetryScheduler.observe(null, topic, null); } function setupTestData() { Services.startup.interrupted = true; Telemetry.registerAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM, Telemetry.HISTOGRAM_LINEAR, 1, 5, 6); let h1 = Telemetry.getAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM); h1.add(1); 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 getSavedPingFile(basename) { let tmpDir = Services.dirsvc.get("ProfD", Ci.nsIFile); let pingFile = tmpDir.clone(); pingFile.append(basename); if (pingFile.exists()) { pingFile.remove(true); } do_register_cleanup(function () { try { pingFile.remove(true); } catch (e) { } }); return pingFile; } 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) { 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 (Services.appinfo.isOfficial) ? 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]); } // 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.ok(Date.parse(data.subsessionStartDate) >= Date.parse(data.sessionStartDate)); Assert.ok(data.profileSubsessionCounter >= data.subsessionCounter); Assert.ok(data.timezoneOffset >= -12*60, "The timezone must be in a valid range."); Assert.ok(data.timezoneOffset <= 12*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) { // 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(scalar[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(scalar[name][key]); } } } function checkEvents(processes) { // Check that the events section is available in the ping payload. const parent = processes.parent; Assert.ok("events" in parent, "The events section must be available in the parent process."); // Check that the events section has the right format. Assert.ok(Array.isArray(parent.events), "The events entry must be an array."); for (let [ts, category, method, object, value, extra] of parent.events) { Assert.equal(typeof(ts), "number", "Timestamp field should be a number."); Assert.greaterOrEqual(ts, 0, "Timestamp should be >= 0."); Assert.equal(typeof(category), "string", "Category should have the right type."); Assert.lessOrEqual(category.length, 100, "Category should have the right string length."); Assert.equal(typeof(method), "string", "Method should have the right type."); Assert.lessOrEqual(method.length, 40, "Method should have the right string length."); Assert.equal(typeof(object), "string", "Object should have the right type."); Assert.lessOrEqual(object.length, 40, "Object should have the right string length."); Assert.ok(value === null || typeof(value) === "string", "Value should be null or a string."); if (value) { Assert.lessOrEqual(value.length, 100, "Value should have the right string length."); } Assert.ok(extra === null || typeof(extra) === "object", "Extra should be null or an object."); if (extra) { let keys = Object.keys(extra); let keyTypes = keys.map(k => typeof(k)); Assert.lessOrEqual(keys.length, 20, "Should not have too many extra keys."); Assert.ok(keyTypes.every(t => t === "string"), "All extra keys should be strings."); Assert.ok(keys.every(k => k.length <= 20), "All extra keys should have the right string length."); let values = Object.values(extra); let valueTypes = values.map(v => typeof(v)); Assert.ok(valueTypes.every(t => t === "string"), "All extra values should be strings."); Assert.ok(values.every(v => v.length <= 100), "All extra values should have the right string length."); } } } function checkPayload(payload, reason, successfulPings, savedPings) { Assert.ok("info" in payload, "Payload must contain an info section."); checkPayloadInfo(payload.info); Assert.ok(payload.simpleMeasurements.totalTime >= 0); Assert.ok(payload.simpleMeasurements.uptime >= 0); Assert.equal(payload.simpleMeasurements.startupInterrupted, 1); Assert.equal(payload.simpleMeasurements.shutdownDuration, SHUTDOWN_TIME); Assert.equal(payload.simpleMeasurements.savedPings, savedPings); Assert.ok("maximalNumberOfConcurrentThreads" in payload.simpleMeasurements); Assert.ok(payload.simpleMeasurements.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched); let activeTicks = payload.simpleMeasurements.activeTicks; Assert.ok(activeTicks >= 0); 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 Components.classes); 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.equal(uneval(flag), uneval(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.equal(uneval(count), uneval(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.equal(uneval(tc), uneval(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 TelemetryController 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_JS_GC_HEAP' in payload.histograms); // UNITS_BYTES Assert.ok('MEMORY_JS_COMPARTMENTS_SYSTEM' in payload.histograms); // UNITS_COUNT // We should have included addon histograms. Assert.ok("addonHistograms" in payload); Assert.ok(ADDON_NAME in payload.addonHistograms); Assert.ok(ADDON_HISTOGRAM in payload.addonHistograms[ADDON_NAME]); Assert.ok(("mainThread" in payload.slowSQL) && ("otherThreads" in payload.slowSQL)); Assert.ok(("IceCandidatesStats" in payload.webrtc) && ("webrtc" in payload.webrtc.IceCandidatesStats)); // 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); checkEvents(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(function* test_setup() { // Addon manager needs a profile directory do_get_profile(); loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION); // Make sure we don't generate unexpected pings due to pref changes. yield setEmptyPrefWatchlist(); Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, 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(); let currentMaxNumberOfThreads = Telemetry.maximalNumberOfConcurrentThreads; do_check_true(currentMaxNumberOfThreads > 0); // Try to augment the maximal number of threads currently launched let threads = []; try { for (let i = 0; i < currentMaxNumberOfThreads + 10; ++i) { threads.push(Services.tm.newThread(0)); } } catch (ex) { // If memory is too low, it is possible that not all threads will be launched. } gNumberOfThreadsLaunched = threads.length; do_check_true(Telemetry.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched); do_register_cleanup(function() { threads.forEach(function(thread) { thread.shutdown(); }); }); yield new Promise(resolve => Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))); }); add_task(function* asyncSetup() { yield TelemetryController.testSetup(); // Load the client ID from the client ID provider to check for pings sanity. gClientID = yield ClientID.getClientID(); }); // Ensures that expired histograms are not part of the payload. add_task(function* test_expiredHistogram() { let dummy = Telemetry.getHistogramById("TELEMETRY_TEST_EXPIRED"); dummy.add(1); do_check_eq(TelemetrySession.getPayload()["histograms"]["TELEMETRY_TEST_EXPIRED"], undefined); }); // 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(function* test_noServerPing() { yield sendPing(); // We need two pings in order to make sure STARTUP_MEMORY_STORAGE_SQLIE histograms // are initialised. See bug 1131585. yield sendPing(); // Allowing Telemetry to persist unsent pings as pending. If omitted may cause // problems to the consequent tests. yield TelemetryController.testShutdown(); }); // Checks that a sent ping is correctly received by a dummy http server. add_task(function* test_simplePing() { yield TelemetryStorage.testClearPendingPings(); PingServer.start(); Preferences.set(PREF_SERVER, "http://localhost:" + PingServer.port); let now = new Date(2020, 1, 1, 12, 0, 0); let expectedDate = new Date(2020, 1, 1, 0, 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); yield 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); yield sendPing(); let ping = yield 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(generateUUID, generateUUID); }); // 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(function* test_saveLoadPing() { // Let's start out with a defined state. yield TelemetryStorage.testClearPendingPings(); yield TelemetryController.testReset(); PingServer.clearRequests(); // Setup test data and trigger pings. setupTestData(); yield TelemetrySession.testSavePendingPing(); yield sendPing(); // Get requests received by dummy server. const requests = yield 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, 1); checkPingFormat(pings[1], PING_TYPE_SAVED_SESSION, true, true); checkPayload(pings[1].payload, REASON_SAVED_SESSION, 0, 0); }); add_task(function* test_checkSubsessionScalars() { if (gIsAndroid) { // We don't support subsessions yet on Android. return; } // Clear the scalars. Telemetry.clearScalars(); yield 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 == 0, "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."); }); add_task(function* test_checkSubsessionEvents() { if (gIsAndroid) { // We don't support subsessions yet on Android. return; } // Clear the events. Telemetry.clearEvents(); yield TelemetryController.testReset(); // Record some events. let expected = [ ["telemetry.test", "test1", "object1", "a", null], ["telemetry.test", "test1", "object1", null, {key1: "value"}], ]; for (let event of expected) { Telemetry.recordEvent(...event); } // Strip off trailing null values to match the serialized events. for (let e of expected) { while ((e.length >= 3) && (e[e.length - 1] === null)) { e.pop(); } } // Check that events 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); Assert.ok("events" in classic.processes.parent, "Should have an events field in classic payload."); Assert.ok("events" in subsession.processes.parent, "Should have an events field in subsession payload."); // They should be empty in the classic payload. Assert.deepEqual(classic.processes.parent.events, [], "Events in classic payload should be empty."); // In the subsession payload, they should contain the recorded test events. let events = subsession.processes.parent.events.filter(e => e[1] === "telemetry.test"); Assert.equal(events.length, expected.length, "Should have the right amount of events in the payload."); for (let i = 0; i < expected.length; ++i) { Assert.deepEqual(events[i].slice(1), expected[i], "Should have the right event data in the ping."); } // As we cleared the subsession above, the events entry should now be empty. subsession = TelemetrySession.getPayload("environment-change", false); Assert.ok("events" in subsession.processes.parent, "Should have an events field in subsession payload."); events = subsession.processes.parent.events.filter(e => e[1] === "telemetry.test"); Assert.equal(events.length, 0, "Should have no test events in the subsession payload now."); }); add_task(function* test_checkSubsessionHistograms() { if (gIsAndroid) { // We don't support subsessions yet on Android. return; } let now = new Date(2020, 1, 1, 12, 0, 0); let expectedDate = new Date(2020, 1, 1, 0, 0, 0); fakeNow(now); yield TelemetryController.testReset(); 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); const registeredIds = new Set(Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, [])); const stableHistograms = new Set([ "TELEMETRY_TEST_FLAG", "TELEMETRY_TEST_COUNT", "TELEMETRY_TEST_RELEASE_OPTOUT", "TELEMETRY_TEST_RELEASE_OPTIN", "STARTUP_CRASH_DETECTED", ]); const stableKeyedHistograms = new Set([ "TELEMETRY_TEST_KEYED_FLAG", "TELEMETRY_TEST_KEYED_COUNT", "TELEMETRY_TEST_KEYED_RELEASE_OPTIN", "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT", ]); // Compare the two sets of histograms. // The "subsession" histograms should match the registered // "classic" histograms. However, histograms can change // between us collecting the different payloads, so we only // check for deep equality on known stable histograms. checkHistograms = (classic, subsession) => { for (let id of Object.keys(classic)) { if (!registeredIds.has(id)) { continue; } Assert.ok(id in subsession); if (stableHistograms.has(id)) { Assert.deepEqual(classic[id], subsession[id]); } else { Assert.equal(classic[id].histogram_type, subsession[id].histogram_type); } } }; // Same as above, except for keyed histograms. checkKeyedHistograms = (classic, subsession) => { for (let id of Object.keys(classic)) { if (!registeredIds.has(id)) { continue; } Assert.ok(id in subsession); if (stableKeyedHistograms.has(id)) { Assert.deepEqual(classic[id], subsession[id]); } } }; // Both classic and subsession payload histograms should start the same. // The payloads should be identical for now except for the reason. count.clear(); keyed.clear(); let classic = TelemetrySession.getPayload(); let subsession = TelemetrySession.getPayload("environment-change"); Assert.equal(classic.info.reason, "gather-payload"); Assert.equal(subsession.info.reason, "environment-change"); Assert.ok(!(COUNT_ID in classic.histograms)); Assert.ok(!(COUNT_ID in subsession.histograms)); Assert.ok(!(KEYED_ID in classic.keyedHistograms)); Assert.ok(!(KEYED_ID in subsession.keyedHistograms)); checkHistograms(classic.histograms, subsession.histograms); checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms); // Adding values should get picked up in both. count.add(1); keyed.add("a", 1); keyed.add("b", 1); classic = TelemetrySession.getPayload(); subsession = TelemetrySession.getPayload("environment-change"); Assert.ok(COUNT_ID in classic.histograms); Assert.ok(COUNT_ID in subsession.histograms); Assert.ok(KEYED_ID in classic.keyedHistograms); Assert.ok(KEYED_ID in subsession.keyedHistograms); Assert.equal(classic.histograms[COUNT_ID].sum, 1); Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 1); Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 1); checkHistograms(classic.histograms, subsession.histograms); checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms); // Values should still reset properly. count.clear(); keyed.clear(); classic = TelemetrySession.getPayload(); subsession = TelemetrySession.getPayload("environment-change"); Assert.ok(!(COUNT_ID in classic.histograms)); Assert.ok(!(COUNT_ID in subsession.histograms)); Assert.ok(!(KEYED_ID in classic.keyedHistograms)); Assert.ok(!(KEYED_ID in subsession.keyedHistograms)); checkHistograms(classic.histograms, subsession.histograms); checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms); // Adding values should get picked up in both. count.add(1); keyed.add("a", 1); keyed.add("b", 1); classic = TelemetrySession.getPayload(); subsession = TelemetrySession.getPayload("environment-change"); Assert.ok(COUNT_ID in classic.histograms); Assert.ok(COUNT_ID in subsession.histograms); Assert.ok(KEYED_ID in classic.keyedHistograms); Assert.ok(KEYED_ID in subsession.keyedHistograms); Assert.equal(classic.histograms[COUNT_ID].sum, 1); Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 1); Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 1); checkHistograms(classic.histograms, subsession.histograms); checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms); // We should be able to reset only the subsession histograms. // First check that "snapshot and clear" still returns the old state... classic = TelemetrySession.getPayload(); subsession = TelemetrySession.getPayload("environment-change", true); let subsessionStartDate = new Date(classic.info.subsessionStartDate); Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString()); subsessionStartDate = new Date(subsession.info.subsessionStartDate); Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString()); checkHistograms(classic.histograms, subsession.histograms); checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms); // ... then check that the next snapshot shows the subsession // histograms got reset. classic = TelemetrySession.getPayload(); subsession = TelemetrySession.getPayload("environment-change"); Assert.ok(COUNT_ID in classic.histograms); Assert.ok(COUNT_ID in subsession.histograms); Assert.equal(classic.histograms[COUNT_ID].sum, 1); Assert.equal(subsession.histograms[COUNT_ID].sum, 0); Assert.ok(KEYED_ID in classic.keyedHistograms); Assert.ok(!(KEYED_ID in subsession.keyedHistograms)); Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 1); Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 1); // Adding values should get picked up in both again. count.add(1); keyed.add("a", 1); keyed.add("b", 1); classic = TelemetrySession.getPayload(); subsession = TelemetrySession.getPayload("environment-change"); Assert.ok(COUNT_ID in classic.histograms); Assert.ok(COUNT_ID in subsession.histograms); Assert.equal(classic.histograms[COUNT_ID].sum, 2); Assert.equal(subsession.histograms[COUNT_ID].sum, 1); Assert.ok(KEYED_ID in classic.keyedHistograms); Assert.ok(KEYED_ID in subsession.keyedHistograms); Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 2); Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 2); Assert.equal(subsession.keyedHistograms[KEYED_ID]["a"].sum, 1); Assert.equal(subsession.keyedHistograms[KEYED_ID]["b"].sum, 1); }); add_task(function* test_checkSubsessionData() { if (gIsAndroid) { // We don't support subsessions yet on Android. return; } // Keep track of the active ticks count if the session recorder is available. let sessionRecorder = TelemetryController.getSessionRecorder(); let activeTicksAtSubsessionStart = sessionRecorder.activeTicks; let expectedActiveTicks = activeTicksAtSubsessionStart; incrementActiveTicks = () => { sessionRecorder.incrementActiveTicks(); ++expectedActiveTicks; } yield TelemetryController.testReset(); // Both classic and subsession payload data should be the same on the first subsession. incrementActiveTicks(); let classic = TelemetrySession.getPayload(); let subsession = TelemetrySession.getPayload("environment-change"); Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks, "Classic pings must count active ticks since the beginning of the session."); Assert.equal(subsession.simpleMeasurements.activeTicks, expectedActiveTicks, "Subsessions must count active ticks as classic pings on the first subsession."); // Start a new subsession and check that the active ticks are correctly reported. incrementActiveTicks(); activeTicksAtSubsessionStart = sessionRecorder.activeTicks; classic = TelemetrySession.getPayload(); subsession = TelemetrySession.getPayload("environment-change", true); Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks, "Classic pings must count active ticks since the beginning of the session."); Assert.equal(subsession.simpleMeasurements.activeTicks, expectedActiveTicks, "Pings must not loose the tick count when starting a new subsession."); // Get a new subsession payload without clearing the subsession. incrementActiveTicks(); classic = TelemetrySession.getPayload(); subsession = TelemetrySession.getPayload("environment-change"); Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks, "Classic pings must count active ticks since the beginning of the session."); Assert.equal(subsession.simpleMeasurements.activeTicks, expectedActiveTicks - activeTicksAtSubsessionStart, "Subsessions must count active ticks since the last new subsession."); }); add_task(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 nowDay = new Date(2030, 1, 1, 0, 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. yield TelemetryStorage.testClearPendingPings(); yield TelemetryController.testSetup(); 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 = nowDay; now = futureDate(nowDay, MS_IN_ONE_DAY); fakeNow(now); Assert.ok(!!schedulerTickCallback); // Run a scheduler tick: it should trigger the daily ping. yield schedulerTickCallback(); // Collect the daily ping. let ping = yield 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. yield schedulerTickCallback(); ping = yield 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, 0); 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); yield schedulerTickCallback(); ping = yield 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. yield TelemetryController.testShutdown(); }); add_task(function* test_dailyDuplication() { if (gIsAndroid) { // We don't do daily collections yet on Android. return; } yield TelemetrySend.reset(); yield 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, () => {}); yield 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); yield schedulerTickCallback(); // Get the first daily ping. let ping = yield 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); yield schedulerTickCallback(); // Shutdown to cleanup the aborted-session if it gets created. PingServer.resetPingHandler(); yield TelemetryController.testShutdown(); }); add_task(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, () => {}); yield TelemetryStorage.testClearPendingPings(); yield 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); yield 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); yield schedulerTickCallback(); // Get the first daily ping. let ping = yield 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. yield TelemetryController.testShutdown(); }); add_task(function* test_environmentChange() { if (gIsAndroid) { // We don't split subsessions on environment changes yet on Android. return; } yield 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"; Preferences.reset(PREF_TEST); const PREFS_TO_WATCH = new Map([ [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}], ]); // Setup. yield TelemetryController.testReset(); TelemetrySend.setServer("http://localhost:" + PingServer.port); 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 startDay = truncateDateToDays(now); now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE)); Preferences.set(PREF_TEST, 1); let ping = yield 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(), startDay.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. startDay = truncateDateToDays(now); gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE); now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE)); Preferences.set(PREF_TEST, 2); ping = yield 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(), startDay.toISOString()); Assert.equal(ping.payload.histograms[COUNT_ID].sum, 0); Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms)); }); add_task(function* test_savedPingsOnShutdown() { // On desktop, we expect both "saved-session" and "shutdown" pings. We only expect // the former on Android. const expectedPingCount = (gIsAndroid) ? 1 : 2; // 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; yield OS.File.removeDir(dir, {ignoreAbsent: true}); yield OS.File.makeDir(dir); yield TelemetryController.testShutdown(); PingServer.clearRequests(); yield TelemetryController.testReset(); const pings = yield PingServer.promiseNextPings(expectedPingCount); for (let ping of pings) { Assert.ok("type" in ping); let expectedReason = (ping.type == PING_TYPE_SAVED_SESSION) ? REASON_SAVED_SESSION : REASON_SHUTDOWN; checkPingFormat(ping, ping.type, true, true); Assert.equal(ping.payload.info.reason, expectedReason); Assert.equal(ping.clientId, gClientID); } }); add_task(function* test_savedSessionData() { // Create the directory which will contain the data file, if it doesn't already // exist. yield OS.File.makeDir(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 = OS.Path.join(DATAREPORTING_PATH, "session-state.json"); const sessionState = { sessionId: null, subsessionId: null, profileSubsessionCounter: 3785, }; yield CommonUtils.writeJSON(sessionState, dataFilePath); const PREF_TEST = "toolkit.telemetry.test.pref1"; Preferences.reset(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. yield 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)); TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); let changePromise = new Promise(resolve => TelemetryEnvironment.registerChangeListener("test_fake_change", resolve)); Preferences.set(PREF_TEST, 1); yield changePromise; TelemetryEnvironment.unregisterChangeListener("test_fake_change"); let payload = TelemetrySession.getPayload(); Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions); yield TelemetryController.testShutdown(); // Restore the UUID generator so we don't mess with other tests. fakeGenerateUUID(generateUUID, generateUUID); // Load back the serialised session data. let data = yield CommonUtils.readJSON(dataFilePath); Assert.equal(data.profileSubsessionCounter, expectedSubsessions); Assert.equal(data.sessionId, expectedSessionUUID); Assert.equal(data.subsessionId, expectedSubsessionUUID); }); add_task(function* test_sessionData_ShortSession() { if (gIsAndroid) { // We don't support subsessions yet on Android, so skip the next checks. return; } const SESSION_STATE_PATH = OS.Path.join(DATAREPORTING_PATH, "session-state.json"); // Shut down Telemetry and remove the session state file. yield TelemetryController.testReset(); yield OS.File.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(); yield 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(generateUUID, generateUUID); // Start TelemetryController so that it loads the session data file. We expect the profile // subsession counter to be incremented by 1 again. yield 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); }); add_task(function* test_invalidSessionData() { // Create the directory which will contain the data file, if it doesn't already // exist. yield OS.File.makeDir(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 = OS.Path.join(DATAREPORTING_PATH, "session-state.json"); const unparseableData = "{asdf:@äü"; OS.File.writeAtomic(dataFilePath, unparseableData, {encoding: "utf-8", tmpPath: dataFilePath + ".tmp"}); // Start TelemetryController so that it loads the session data file. yield 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, }; yield CommonUtils.writeJSON(sessionState, dataFilePath); // 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. yield 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); yield TelemetryController.testShutdown(); // Restore the UUID generator so we don't mess with other tests. fakeGenerateUUID(generateUUID, generateUUID); // Load back the serialised session data. let data = yield CommonUtils.readJSON(dataFilePath); Assert.equal(data.profileSubsessionCounter, expectedSubsessions); Assert.equal(data.sessionId, expectedSessionUUID); Assert.equal(data.subsessionId, expectedSubsessionUUID); }); add_task(function* test_abortedSession() { if (gIsAndroid || gIsGonk) { // We don't have the aborted session ping here. return; } const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME); // Make sure the aborted sessions directory does not exist to test its creation. yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: 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, () => {}); yield TelemetryController.testReset(); Assert.ok((yield OS.File.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. yield schedulerTickCallback(); // Check that the aborted session is due at the correct time. Assert.ok((yield OS.File.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 pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" }); let abortedSessionPing = JSON.parse(pingContent); // 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); yield schedulerTickCallback(); pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" }); let updatedAbortedSessionPing = JSON.parse(pingContent); 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); yield TelemetryController.testShutdown(); Assert.ok(!(yield OS.File.exists(ABORTED_FILE)), "No aborted session ping must be available after a shutdown."); // Write the ping to the aborted-session file. TelemetrySession will add it to the // saved pings directory when it starts. yield TelemetryStorage.savePingToFile(abortedSessionPing, ABORTED_FILE, false); Assert.ok((yield OS.File.exists(ABORTED_FILE)), "The aborted session ping must exist in the aborted session ping directory."); yield TelemetryStorage.testClearPendingPings(); PingServer.clearRequests(); yield TelemetryController.testReset(); Assert.ok(!(yield OS.File.exists(ABORTED_FILE)), "The aborted session ping must be removed from the aborted session ping directory."); // Restarting Telemetry again to trigger sending pings in TelemetrySend. yield TelemetryController.testReset(); // We should have received an aborted-session ping. const receivedPing = yield PingServer.promiseNextPing(); Assert.equal(receivedPing.type, PING_TYPE_MAIN, "Should have the correct type"); Assert.equal(receivedPing.payload.info.reason, REASON_ABORTED_SESSION, "Ping should have the correct reason"); yield TelemetryController.testShutdown(); }); add_task(function* test_abortedSession_Shutdown() { if (gIsAndroid || gIsGonk) { // We don't have the aborted session ping here. return; } const ABORTED_FILE = OS.Path.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, () => {}); yield TelemetryController.testReset(); Assert.ok((yield OS.File.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. yield schedulerTickCallback(); // Check that the aborted session is due at the correct time. Assert.ok((yield OS.File.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. yield OS.File.remove(ABORTED_FILE); yield TelemetryController.testShutdown(); }); add_task(function* test_abortedDailyCoalescing() { if (gIsAndroid || gIsGonk) { // We don't have the aborted session or the daily ping here. return; } const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME); // Make sure the aborted sessions directory does not exist to test its creation. yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: 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, () => {}); yield TelemetryStorage.testClearPendingPings(); PingServer.clearRequests(); yield TelemetryController.testReset(); Assert.ok((yield OS.File.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. yield schedulerTickCallback(); // Wait for the daily ping. let dailyPing = yield PingServer.promiseNextPing(); Assert.equal(dailyPing.payload.info.reason, REASON_DAILY); // Check that an aborted session ping was also written to disk. Assert.ok((yield OS.File.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 pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" }); let abortedSessionPing = JSON.parse(pingContent); Assert.equal(abortedSessionPing.payload.info.sessionId, dailyPing.payload.info.sessionId); Assert.equal(abortedSessionPing.payload.info.subsessionId, dailyPing.payload.info.subsessionId); yield TelemetryController.testShutdown(); }); add_task(function* test_schedulerComputerSleep() { if (gIsAndroid || gIsGonk) { // We don't have the aborted session or the daily ping here. return; } const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME); yield TelemetryStorage.testClearPendingPings(); yield TelemetryController.testReset(); PingServer.clearRequests(); // Remove any aborted-session ping from the previous tests. yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: 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, () => {}); yield 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. yield schedulerTickCallback(); let dailyPing = yield 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((yield OS.File.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", null); dailyPing = yield 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."); yield TelemetryController.testShutdown(); }); add_task(function* test_schedulerEnvironmentReschedules() { if (gIsAndroid || gIsGonk) { // We don't have the aborted session or the daily ping here. return; } // Reset the test preference. const PREF_TEST = "toolkit.telemetry.test.pref1"; Preferences.reset(PREF_TEST); const PREFS_TO_WATCH = new Map([ [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}], ]); yield TelemetryStorage.testClearPendingPings(); PingServer.clearRequests(); yield TelemetryController.testReset(); // 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, () => {}); yield TelemetryController.testReset(); 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. Preferences.set(PREF_TEST, 1); // Wait for the environment-changed ping. yield PingServer.promiseNextPing(); // We don't expect to receive any daily ping in this test, so assert if we do. PingServer.registerPingHandler((req, res) => { Assert.ok(false, "No ping should be sent/received in this test."); }); // Execute one scheduler tick. It should not trigger a daily ping. Assert.ok(!!schedulerTickCallback); yield schedulerTickCallback(); yield TelemetryController.testShutdown(); }); add_task(function* test_schedulerNothingDue() { if (gIsAndroid || gIsGonk) { // We don't have the aborted session or the daily ping here. return; } const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME); // Remove any aborted-session ping from the previous tests. yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true }); yield TelemetryStorage.testClearPendingPings(); yield TelemetryController.testReset(); // We don't expect to receive any ping in this test, so assert if we do. PingServer.registerPingHandler((req, res) => { Assert.ok(false, "No ping should be sent/received in this test."); }); // 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, () => {}); yield 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. yield schedulerTickCallback(); // Check that no aborted session ping was written to disk. Assert.ok(!(yield OS.File.exists(ABORTED_FILE))); yield TelemetryController.testShutdown(); PingServer.resetPingHandler(); }); add_task(function* test_pingExtendedStats() { const EXTENDED_PAYLOAD_FIELDS = [ "chromeHangs", "threadHangStats", "log", "slowSQL", "fileIOReports", "lateWrites", "addonHistograms", "addonDetails", "UIMeasurements", "webrtc" ]; // Reset telemetry and disable sending extended statistics. yield TelemetryStorage.testClearPendingPings(); PingServer.clearRequests(); yield TelemetryController.testReset(); Telemetry.canRecordExtended = false; yield sendPing(); let ping = yield 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 if the extended set is off."); // Restore the preference. Telemetry.canRecordExtended = true; // Send a new ping that should contain the extended data. yield sendPing(); ping = yield 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 be sent if the extended set is on."); }); add_task(function* test_schedulerUserIdle() { if (gIsAndroid || gIsGonk) { // 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; }, () => {}); yield TelemetryController.testReset(); yield 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. 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); yield TelemetryController.testShutdown(); }); add_task(function* test_DailyDueAndIdle() { if (gIsAndroid || gIsGonk) { // We don't have the aborted session or the daily ping here. return; } yield 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, () => {}); yield 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. yield tickPromise; yield 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); yield TelemetryController.testShutdown(); }); add_task(function* test_userIdleAndSchedlerTick() { if (gIsAndroid || gIsGonk) { // 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, () => {}); yield TelemetryStorage.testClearPendingPings(); yield 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); yield schedulerTickCallback(); // A new "idle" notification should not send a new daily ping. fakeIdleNotification("idle"); yield 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(); yield TelemetryController.testShutdown(); }); add_task(function* test_changeThrottling() { if (gIsAndroid) { // We don't support subsessions yet on Android. return; } let getSubsessionCount = () => { return TelemetrySession.getPayload().info.subsessionCounter; }; const PREF_TEST = "toolkit.telemetry.test.pref1"; const PREFS_TO_WATCH = new Map([ [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}], ]); Preferences.reset(PREF_TEST); let now = fakeNow(2050, 1, 2, 0, 0, 0); gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE); yield TelemetryController.testReset(); Assert.equal(getSubsessionCount(), 1); // Set the Environment preferences to watch. TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); // The first pref change should not trigger a notification. Preferences.set(PREF_TEST, 1); 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); Preferences.set(PREF_TEST, 2); 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); Preferences.set(PREF_TEST, 3); Assert.equal(getSubsessionCount(), 2); // ... for 5min. now = fakeNow(futureDate(now, 4 * MILLISECONDS_PER_MINUTE + 1)); gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 4 * MILLISECONDS_PER_MINUTE + 1); Preferences.set(PREF_TEST, 4); Assert.equal(getSubsessionCount(), 3); // Unregister the listener. TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_throttling"); }); add_task(function* stopServer() { yield PingServer.stop(); });