fune/toolkit/components/telemetry/tests/unit/test_HealthPing.js

403 lines
12 KiB
JavaScript

/* 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]: {
"<unknown>": 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]: {
"<unknown>": 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();
});