/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ // This tests the public Telemetry API for submitting Health pings. "use strict"; ChromeUtils.import("resource://gre/modules/TelemetryController.jsm", this); ChromeUtils.import("resource://gre/modules/TelemetryStorage.jsm", this); ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", this); ChromeUtils.import("resource://gre/modules/Preferences.jsm", this); ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this); ChromeUtils.import( "resource://testing-common/TelemetryArchiveTesting.jsm", this ); ChromeUtils.defineModuleGetter( this, "TelemetryHealthPing", "resource://gre/modules/HealthPing.jsm" ); function checkHealthPingStructure(ping, expectedFailuresDict) { let payload = ping.payload; Assert.equal( ping.type, TelemetryHealthPing.HEALTH_PING_TYPE, "Should have recorded a health ping." ); for (let [key, value] of Object.entries(expectedFailuresDict)) { Assert.deepEqual( payload[key], value, "Should have recorded correct entry with key: " + key ); } } function fakeHealthSchedulerTimer(set, clear) { let telemetryHealthPing = ChromeUtils.import( "resource://gre/modules/HealthPing.jsm", null ); telemetryHealthPing.Policy.setSchedulerTickTimeout = set; telemetryHealthPing.Policy.clearSchedulerTickTimeout = clear; } async function waitForConditionWithPromise( promiseFn, timeoutMsg, tryCount = 30 ) { const SINGLE_TRY_TIMEOUT = 100; let tries = 0; do { try { return await promiseFn(); } catch (ex) {} await new Promise(resolve => do_timeout(SINGLE_TRY_TIMEOUT, resolve)); } while (++tries <= tryCount); throw new Error(timeoutMsg); } function fakeSendSubmissionTimeout(timeOut) { let telemetryHealthPing = ChromeUtils.import( "resource://gre/modules/TelemetrySend.jsm", null ); telemetryHealthPing.Policy.pingSubmissionTimeout = () => timeOut; } add_task(async function setup() { // Trigger a proper telemetry init. do_get_profile(true); // Make sure we don't generate unexpected pings due to pref changes. await setEmptyPrefWatchlist(); Preferences.set(TelemetryUtils.Preferences.HealthPingEnabled, true); await TelemetryController.testSetup(); PingServer.start(); TelemetrySend.setServer("http://localhost:" + PingServer.port); Preferences.set( TelemetryUtils.Preferences.Server, "http://localhost:" + PingServer.port ); }); add_task(async function test_sendImmediately() { PingServer.clearRequests(); TelemetryHealthPing.testReset(); await TelemetryHealthPing.recordSendFailure("testProblem"); let ping = await PingServer.promiseNextPing(); checkHealthPingStructure(ping, { [TelemetryHealthPing.FailureType.SEND_FAILURE]: { testProblem: 1, }, os: TelemetryHealthPing.OsInfo, reason: TelemetryHealthPing.Reason.IMMEDIATE, }); }); add_task(async function test_sendOnDelay() { PingServer.clearRequests(); TelemetryHealthPing.testReset(); // This first failure should immediately trigger a ping. After this, subsequent failures should be throttled. await TelemetryHealthPing.recordSendFailure("testFailure"); let testPing = await PingServer.promiseNextPing(); Assert.equal( testPing.type, TelemetryHealthPing.HEALTH_PING_TYPE, "Should have recorded a health ping." ); // Retrieve delayed call back. let pingSubmissionCallBack = null; fakeHealthSchedulerTimer( callBack => (pingSubmissionCallBack = callBack), () => {} ); // Record two failures, health ping must not be send now. await TelemetryHealthPing.recordSendFailure("testFailure"); await TelemetryHealthPing.recordSendFailure("testFailure"); // Wait for sending delayed health ping. await pingSubmissionCallBack(); let ping = await PingServer.promiseNextPing(); checkHealthPingStructure(ping, { [TelemetryHealthPing.FailureType.SEND_FAILURE]: { testFailure: 2, }, os: TelemetryHealthPing.OsInfo, reason: TelemetryHealthPing.Reason.DELAYED, }); }); add_task(async function test_sendOverSizedPing() { TelemetryHealthPing.testReset(); PingServer.clearRequests(); let OVER_SIZED_PING_TYPE = "over-sized-ping"; let overSizedData = generateRandomString(2 * 1024 * 1024); await TelemetryController.submitExternalPing(OVER_SIZED_PING_TYPE, { data: overSizedData, }); let ping = await PingServer.promiseNextPing(); checkHealthPingStructure(ping, { [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: { [OVER_SIZED_PING_TYPE]: 1, }, os: TelemetryHealthPing.OsInfo, reason: TelemetryHealthPing.Reason.IMMEDIATE, }); }); add_task(async function test_healthPingOnTop() { PingServer.clearRequests(); TelemetryHealthPing.testReset(); let PING_TYPE = "priority-ping"; // Fake now to be in throttled state. let now = fakeNow(2050, 1, 2, 0, 0, 0); fakeMidnightPingFuzzingDelay(60 * 1000); for (let value of [PING_TYPE, PING_TYPE, "health", PING_TYPE]) { TelemetryController.submitExternalPing(value, {}); } // Now trigger sending pings again. fakeNow(futureDate(now, 5 * 60 * 1000)); await TelemetrySend.notifyCanUpload(); let scheduler = ChromeUtils.import( "resource://gre/modules/TelemetrySend.jsm", null ); scheduler.SendScheduler.triggerSendingPings(true); let pings = await PingServer.promiseNextPings(4); Assert.equal( pings[0].type, "health", "Should have received the health ping first." ); }); add_task(async function test_sendOnTimeout() { TelemetryHealthPing.testReset(); await TelemetrySend.reset(); PingServer.clearRequests(); let PING_TYPE = "ping-on-timeout"; // Disable send retry to make this test more deterministic. fakePingSendTimer( () => {}, () => {} ); // Set up small ping submission timeout to always have timeout error. fakeSendSubmissionTimeout(2); await TelemetryController.submitExternalPing(PING_TYPE, {}); let response; PingServer.registerPingHandler((req, res) => { PingServer.resetPingHandler(); // We don't finish the response yet to make sure to trigger a timeout. res.processAsync(); response = res; }); // Wait for health ping. let ac = new TelemetryArchiveTesting.Checker(); await ac.promiseInit(); await waitForConditionWithPromise(() => { ac.promiseFindPing("health", []); }, "Failed to find health ping"); if (response) { response.finish(); } let telemetryHealthPing = ChromeUtils.import( "resource://gre/modules/TelemetrySend.jsm", null ); fakeSendSubmissionTimeout(telemetryHealthPing.PING_SUBMIT_TIMEOUT_MS); PingServer.resetPingHandler(); TelemetrySend.notifyCanUpload(); let pings = await PingServer.promiseNextPings(2); let healthPing = pings.find(ping => ping.type === "health"); checkHealthPingStructure(healthPing, { [TelemetryHealthPing.FailureType.SEND_FAILURE]: { timeout: 1, }, os: TelemetryHealthPing.OsInfo, reason: TelemetryHealthPing.Reason.IMMEDIATE, }); await TelemetryStorage.testClearPendingPings(); }); add_task(async function test_sendOnlyTopTenDiscardedPings() { TelemetryHealthPing.testReset(); await TelemetrySend.reset(); PingServer.clearRequests(); let PING_TYPE = "sort-discarded"; // This first failure should immediately trigger a ping. After this, subsequent failures should be throttled. await TelemetryHealthPing.recordSendFailure("testFailure"); let testPing = await PingServer.promiseNextPing(); Assert.equal( testPing.type, TelemetryHealthPing.HEALTH_PING_TYPE, "Should have recorded a health ping." ); // Retrieve delayed call back. let pingSubmissionCallBack = null; fakeHealthSchedulerTimer( callBack => (pingSubmissionCallBack = callBack), () => {} ); // Add failures for (let i = 1; i < 12; i++) { for (let j = 1; j < i; j++) { TelemetryHealthPing.recordDiscardedPing(PING_TYPE + i); } } await TelemetrySend.reset(); await pingSubmissionCallBack(); let ping = await PingServer.promiseNextPing(); checkHealthPingStructure(ping, { os: TelemetryHealthPing.OsInfo, reason: TelemetryHealthPing.Reason.DELAYED, [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: { [PING_TYPE + 11]: 10, [PING_TYPE + 10]: 9, [PING_TYPE + 9]: 8, [PING_TYPE + 8]: 7, [PING_TYPE + 7]: 6, [PING_TYPE + 6]: 5, [PING_TYPE + 5]: 4, [PING_TYPE + 4]: 3, [PING_TYPE + 3]: 2, [PING_TYPE + 2]: 1, }, }); }); add_task(async function test_discardedForSizePending() { TelemetryHealthPing.testReset(); PingServer.clearRequests(); const PING_TYPE = "discarded-for-size-pending"; const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24"; // Create a pending oversized ping. let overSizedPayload = generateRandomString(2 * 1024 * 1024); const OVERSIZED_PING = { id: OVERSIZED_PING_ID, type: PING_TYPE, creationDate: new Date().toISOString(), // Generate a 2MB string to use as the ping payload. payload: overSizedPayload, }; // Test loadPendingPing. await TelemetryStorage.savePendingPing(OVERSIZED_PING); // Try to manually load the oversized ping. await Assert.rejects( TelemetryStorage.loadPendingPing(OVERSIZED_PING_ID), /loadPendingPing - exceeded the maximum ping size/, "The oversized ping should have been pruned." ); let ping = await PingServer.promiseNextPing(); checkHealthPingStructure(ping, { [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: { "": 1, }, os: TelemetryHealthPing.OsInfo, reason: TelemetryHealthPing.Reason.IMMEDIATE, }); // Test _scanPendingPings. TelemetryHealthPing.testReset(); await TelemetryStorage.savePendingPing(OVERSIZED_PING); await TelemetryStorage.loadPendingPingList(); ping = await PingServer.promiseNextPing(); checkHealthPingStructure(ping, { [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: { "": 1, }, os: TelemetryHealthPing.OsInfo, reason: TelemetryHealthPing.Reason.IMMEDIATE, }); }); add_task(async function test_usePingSenderOnShutdown() { if ( gIsAndroid || (AppConstants.platform == "linux" && OS.Constants.Sys.bits == 32) ) { // We don't support the pingsender on Android, yet, see bug 1335917. // We also don't support the pingsender testing on Treeherder for // Linux 32 bit (due to missing libraries). So skip it there too. // See bug 1310703 comment 78. return; } TelemetryHealthPing.testReset(); await TelemetrySend.reset(); PingServer.clearRequests(); // This first failure should immediately trigger a ping. // After this, subsequent failures should be throttled. await TelemetryHealthPing.recordSendFailure("testFailure"); await PingServer.promiseNextPing(); TelemetryHealthPing.recordSendFailure("testFailure"); let nextRequest = PingServer.promiseNextRequest(); await TelemetryController.testReset(); await TelemetryController.testShutdown(); let request = await nextRequest; let ping = decodeRequestPayload(request); checkHealthPingStructure(ping, { [TelemetryHealthPing.FailureType.SEND_FAILURE]: { testFailure: 1, }, os: TelemetryHealthPing.OsInfo, reason: TelemetryHealthPing.Reason.SHUT_DOWN, }); // Check that the health ping is sent at shutdown using the pingsender. Assert.equal( request.getHeader("User-Agent"), "pingsender/1.0", "Should have received the correct user agent string." ); Assert.equal( request.getHeader("X-PingSender-Version"), "1.0", "Should have received the correct PingSender version string." ); }); add_task(async function cleanup() { await PingServer.stop(); });