summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/tests/unit/test_TelemetrySend.js')
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySend.js427
1 files changed, 427 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
new file mode 100644
index 000000000..88ff8cf44
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
@@ -0,0 +1,427 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// This tests the public Telemetry API for submitting pings.
+
+"use strict";
+
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+const PREF_TELEMETRY_SERVER = "toolkit.telemetry.server";
+
+const MS_IN_A_MINUTE = 60 * 1000;
+
+function countPingTypes(pings) {
+ let countByType = new Map();
+ for (let p of pings) {
+ countByType.set(p.type, 1 + (countByType.get(p.type) || 0));
+ }
+ return countByType;
+}
+
+function setPingLastModified(id, timestamp) {
+ const path = OS.Path.join(TelemetryStorage.pingDirectoryPath, id);
+ return OS.File.setDates(path, null, timestamp);
+}
+
+// Mock out the send timer activity.
+function waitForTimer() {
+ return new Promise(resolve => {
+ fakePingSendTimer((callback, timeout) => {
+ resolve([callback, timeout]);
+ }, () => {});
+ });
+}
+
+// Allow easy faking of readable ping ids.
+// This helps with debugging issues with e.g. ordering in the send logic.
+function fakePingId(type, number) {
+ const HEAD = "93bd0011-2c8f-4e1c-bee0-";
+ const TAIL = "000000000000";
+ const N = String(number);
+ const id = HEAD + type + TAIL.slice(type.length, - N.length) + N;
+ fakeGeneratePingId(() => id);
+ return id;
+}
+
+var checkPingsSaved = Task.async(function* (pingIds) {
+ let allFound = true;
+ for (let id of pingIds) {
+ const path = OS.Path.join(TelemetryStorage.pingDirectoryPath, id);
+ let exists = false;
+ try {
+ exists = yield OS.File.exists(path);
+ } catch (ex) {}
+
+ if (!exists) {
+ dump("checkPingsSaved - failed to find ping: " + path + "\n");
+ allFound = false;
+ }
+ }
+
+ return allFound;
+});
+
+function histogramValueCount(h) {
+ return h.counts.reduce((a, b) => a + b);
+}
+
+add_task(function* test_setup() {
+ // Trigger a proper telemetry init.
+ do_get_profile(true);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+ Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+});
+
+// Test the ping sending logic.
+add_task(function* test_sendPendingPings() {
+ const TYPE_PREFIX = "test-sendPendingPings-";
+ const TEST_TYPE_A = TYPE_PREFIX + "A";
+ const TEST_TYPE_B = TYPE_PREFIX + "B";
+
+ const TYPE_A_COUNT = 20;
+ const TYPE_B_COUNT = 5;
+
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS");
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ histSuccess.clear();
+ histSendTimeSuccess.clear();
+ histSendTimeFail.clear();
+
+ // Fake a current date.
+ let now = TelemetryUtils.truncateToDays(new Date());
+ now = fakeNow(futureDate(now, 10 * 60 * MS_IN_A_MINUTE));
+
+ // Enable test-mode for TelemetrySend, otherwise we won't store pending pings
+ // before the module is fully initialized later.
+ TelemetrySend.setTestModeEnabled(true);
+
+ // Submit some pings without the server and telemetry started yet.
+ for (let i = 0; i < TYPE_A_COUNT; ++i) {
+ fakePingId("a", i);
+ const id = yield TelemetryController.submitExternalPing(TEST_TYPE_A, {});
+ yield setPingLastModified(id, now.getTime() + (i * 1000));
+ }
+
+ Assert.equal(TelemetrySend.pendingPingCount, TYPE_A_COUNT,
+ "Should have correct pending ping count");
+
+ // Submit some more pings of a different type.
+ now = fakeNow(futureDate(now, 5 * MS_IN_A_MINUTE));
+ for (let i = 0; i < TYPE_B_COUNT; ++i) {
+ fakePingId("b", i);
+ const id = yield TelemetryController.submitExternalPing(TEST_TYPE_B, {});
+ yield setPingLastModified(id, now.getTime() + (i * 1000));
+ }
+
+ Assert.equal(TelemetrySend.pendingPingCount, TYPE_A_COUNT + TYPE_B_COUNT,
+ "Should have correct pending ping count");
+
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 0, 0],
+ "Should not have recorded any sending in histograms yet.");
+ Assert.equal(histSendTimeSuccess.snapshot().sum, 0,
+ "Should not have recorded any sending in histograms yet.");
+ Assert.equal(histSendTimeFail.snapshot().sum, 0,
+ "Should not have recorded any sending in histograms yet.");
+
+ // Now enable sending to the ping server.
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ PingServer.start();
+ Preferences.set(PREF_TELEMETRY_SERVER, "http://localhost:" + PingServer.port);
+
+ let timerPromise = waitForTimer();
+ yield TelemetryController.testReset();
+ let [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+ Assert.ok(!!pingSendTimerCallback, "Should have a timer callback");
+
+ // We should have received 10 pings from the first send batch:
+ // 5 of type B and 5 of type A, as sending is newest-first.
+ // The other pings should be delayed by the 10-pings-per-minute limit.
+ let pings = yield PingServer.promiseNextPings(10);
+ Assert.equal(TelemetrySend.pendingPingCount, TYPE_A_COUNT - 5,
+ "Should have correct pending ping count");
+ PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now"));
+ let countByType = countPingTypes(pings);
+
+ Assert.equal(countByType.get(TEST_TYPE_B), TYPE_B_COUNT,
+ "Should have received the correct amount of type B pings");
+ Assert.equal(countByType.get(TEST_TYPE_A), 10 - TYPE_B_COUNT,
+ "Should have received the correct amount of type A pings");
+
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 10, 0],
+ "Should have recorded sending success in histograms.");
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 10,
+ "Should have recorded successful send times in histograms.");
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0,
+ "Should not have recorded any failed sending in histograms yet.");
+
+ // As we hit the ping send limit and still have pending pings, a send tick should
+ // be scheduled in a minute.
+ Assert.ok(!!pingSendTimerCallback, "Timer callback should be set");
+ Assert.equal(pingSendTimeout, MS_IN_A_MINUTE, "Send tick timeout should be correct");
+
+ // Trigger the next tick - we should receive the next 10 type A pings.
+ PingServer.resetPingHandler();
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ timerPromise = waitForTimer();
+ pingSendTimerCallback();
+ [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+
+ pings = yield PingServer.promiseNextPings(10);
+ PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now"));
+ countByType = countPingTypes(pings);
+
+ Assert.equal(countByType.get(TEST_TYPE_A), 10, "Should have received the correct amount of type A pings");
+
+ // We hit the ping send limit again and still have pending pings, a send tick should
+ // be scheduled in a minute.
+ Assert.equal(pingSendTimeout, MS_IN_A_MINUTE, "Send tick timeout should be correct");
+
+ // Trigger the next tick - we should receive the remaining type A pings.
+ PingServer.resetPingHandler();
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ yield pingSendTimerCallback();
+
+ pings = yield PingServer.promiseNextPings(5);
+ PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now"));
+ countByType = countPingTypes(pings);
+
+ Assert.equal(countByType.get(TEST_TYPE_A), 5, "Should have received the correct amount of type A pings");
+
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ PingServer.resetPingHandler();
+});
+
+add_task(function* test_sendDateHeader() {
+ fakeNow(new Date(Date.UTC(2011, 1, 1, 11, 0, 0)));
+ yield TelemetrySend.reset();
+
+ let pingId = yield TelemetryController.submitExternalPing("test-send-date-header", {});
+ let req = yield PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+ Assert.equal(req.getHeader("Date"), "Tue, 01 Feb 2011 11:00:00 GMT",
+ "Telemetry should send the correct Date header with requests.");
+ Assert.equal(ping.id, pingId, "Should have received the correct ping id.");
+});
+
+// Test the backoff timeout behavior after send failures.
+add_task(function* test_backoffTimeout() {
+ const TYPE_PREFIX = "test-backoffTimeout-";
+ const TEST_TYPE_C = TYPE_PREFIX + "C";
+ const TEST_TYPE_D = TYPE_PREFIX + "D";
+ const TEST_TYPE_E = TYPE_PREFIX + "E";
+
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS");
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+
+ // Failing a ping send now should trigger backoff behavior.
+ let now = fakeNow(2010, 1, 1, 11, 0, 0);
+ yield TelemetrySend.reset();
+ PingServer.stop();
+
+ histSuccess.clear();
+ histSendTimeSuccess.clear();
+ histSendTimeFail.clear();
+
+ fakePingId("c", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ let sendAttempts = 0;
+ let timerPromise = waitForTimer();
+ yield TelemetryController.submitExternalPing(TEST_TYPE_C, {});
+ let [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+ Assert.equal(TelemetrySend.pendingPingCount, 1, "Should have one pending ping.");
+ ++sendAttempts;
+
+ const MAX_BACKOFF_TIMEOUT = 120 * MS_IN_A_MINUTE;
+ for (let timeout = 2 * MS_IN_A_MINUTE; timeout <= MAX_BACKOFF_TIMEOUT; timeout *= 2) {
+ Assert.ok(!!pingSendTimerCallback, "Should have received a timer callback");
+ Assert.equal(pingSendTimeout, timeout, "Send tick timeout should be correct");
+
+ let callback = pingSendTimerCallback;
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ timerPromise = waitForTimer();
+ yield callback();
+ [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+ ++sendAttempts;
+ }
+
+ timerPromise = waitForTimer();
+ yield pingSendTimerCallback();
+ [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+ Assert.equal(pingSendTimeout, MAX_BACKOFF_TIMEOUT, "Tick timeout should be capped");
+ ++sendAttempts;
+
+ Assert.deepEqual(histSuccess.snapshot().counts, [sendAttempts, 0, 0],
+ "Should have recorded sending failure in histograms.");
+ Assert.equal(histSendTimeSuccess.snapshot().sum, 0,
+ "Should not have recorded any sending success in histograms yet.");
+ Assert.greater(histSendTimeFail.snapshot().sum, 0,
+ "Should have recorded send failure times in histograms.");
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), sendAttempts,
+ "Should have recorded send failure times in histograms.");
+
+ // Submitting a new ping should reset the backoff behavior.
+ fakePingId("d", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ timerPromise = waitForTimer();
+ yield TelemetryController.submitExternalPing(TEST_TYPE_D, {});
+ [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+ Assert.equal(pingSendTimeout, 2 * MS_IN_A_MINUTE, "Send tick timeout should be correct");
+ sendAttempts += 2;
+
+ // With the server running again, we should send out the pending pings immediately
+ // when a new ping is submitted.
+ PingServer.start();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ fakePingId("e", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ timerPromise = waitForTimer();
+ yield TelemetryController.submitExternalPing(TEST_TYPE_E, {});
+
+ let pings = yield PingServer.promiseNextPings(3);
+ let countByType = countPingTypes(pings);
+
+ Assert.equal(countByType.get(TEST_TYPE_C), 1, "Should have received the correct amount of type C pings");
+ Assert.equal(countByType.get(TEST_TYPE_D), 1, "Should have received the correct amount of type D pings");
+ Assert.equal(countByType.get(TEST_TYPE_E), 1, "Should have received the correct amount of type E pings");
+
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ Assert.equal(TelemetrySend.pendingPingCount, 0, "Should have no pending pings left");
+
+ Assert.deepEqual(histSuccess.snapshot().counts, [sendAttempts, 3, 0],
+ "Should have recorded sending failure in histograms.");
+ Assert.greater(histSendTimeSuccess.snapshot().sum, 0,
+ "Should have recorded sending success in histograms.");
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 3,
+ "Should have recorded sending success in histograms.");
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), sendAttempts,
+ "Should have recorded send failure times in histograms.");
+});
+
+add_task(function* test_discardBigPings() {
+ const TEST_PING_TYPE = "test-ping-type";
+
+ let histSizeExceeded = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_SEND");
+ let histDiscardedSize = Telemetry.getHistogramById("TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB");
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS");
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ for (let h of [histSizeExceeded, histDiscardedSize, histSuccess, histSendTimeSuccess, histSendTimeFail]) {
+ h.clear();
+ }
+
+ // Generate a 2MB string and create an oversized payload.
+ const OVERSIZED_PAYLOAD = {"data": generateRandomString(2 * 1024 * 1024)};
+
+ // Reset the histograms.
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_SEND").clear();
+ Telemetry.getHistogramById("TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB").clear();
+
+ // Submit a ping of a normal size and check that we don't count it in the histogram.
+ yield TelemetryController.submitExternalPing(TEST_PING_TYPE, { test: "test" });
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ Assert.equal(histSizeExceeded.snapshot().sum, 0, "Telemetry must report no oversized ping submitted.");
+ Assert.equal(histDiscardedSize.snapshot().sum, 0, "Telemetry must report no oversized pings.");
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 1, 0], "Should have recorded sending success.");
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1, "Should have recorded send success time.");
+ Assert.greater(histSendTimeSuccess.snapshot().sum, 0, "Should have recorded send success time.");
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0, "Should not have recorded send failure time.");
+
+ // Submit an oversized ping and check that it gets discarded.
+ yield TelemetryController.submitExternalPing(TEST_PING_TYPE, OVERSIZED_PAYLOAD);
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ Assert.equal(histSizeExceeded.snapshot().sum, 1, "Telemetry must report 1 oversized ping submitted.");
+ Assert.equal(histDiscardedSize.snapshot().counts[2], 1, "Telemetry must report a 2MB, oversized, ping submitted.");
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 1, 0], "Should have recorded sending success.");
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1, "Should have recorded send success time.");
+ Assert.greater(histSendTimeSuccess.snapshot().sum, 0, "Should have recorded send success time.");
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0, "Should not have recorded send failure time.");
+});
+
+add_task(function* test_evictedOnServerErrors() {
+ const TEST_TYPE = "test-evicted";
+
+ yield TelemetrySend.reset();
+
+ let histEvicted = Telemetry.getHistogramById("TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS");
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS");
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ for (let h of [histEvicted, histSuccess, histSendTimeSuccess, histSendTimeFail]) {
+ h.clear();
+ }
+
+ // Write a custom ping handler which will return 403. This will trigger ping eviction
+ // on client side.
+ PingServer.registerPingHandler((req, res) => {
+ res.setStatusLine(null, 403, "Forbidden");
+ res.processAsync();
+ res.finish();
+ });
+
+ // Clear the histogram and submit a ping.
+ let pingId = yield TelemetryController.submitExternalPing(TEST_TYPE, {});
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ Assert.equal(histEvicted.snapshot().sum, 1,
+ "Telemetry must report a ping evicted due to server errors");
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 1, 0]);
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1);
+ Assert.greater(histSendTimeSuccess.snapshot().sum, 0);
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0);
+
+ // The ping should not be persisted.
+ yield Assert.rejects(TelemetryStorage.loadPendingPing(pingId), "The ping must not be persisted.");
+
+ // Reset the ping handler and submit a new ping.
+ PingServer.resetPingHandler();
+ pingId = yield TelemetryController.submitExternalPing(TEST_TYPE, {});
+
+ let ping = yield PingServer.promiseNextPings(1);
+ Assert.equal(ping[0].id, pingId, "The correct ping must be received");
+
+ // We should not have updated the error histogram.
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ Assert.equal(histEvicted.snapshot().sum, 1, "Telemetry must report only one ping evicted due to server errors");
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 2, 0]);
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 2);
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0);
+});
+
+// Test that the current, non-persisted pending pings are properly saved on shutdown.
+add_task(function* test_persistCurrentPingsOnShutdown() {
+ const TEST_TYPE = "test-persistCurrentPingsOnShutdown";
+ const PING_COUNT = 5;
+ yield TelemetrySend.reset();
+ PingServer.stop();
+ Assert.equal(TelemetrySend.pendingPingCount, 0, "Should have no pending pings yet");
+
+ // Submit new pings that shouldn't be persisted yet.
+ let ids = [];
+ for (let i=0; i<5; ++i) {
+ ids.push(fakePingId("f", i));
+ TelemetryController.submitExternalPing(TEST_TYPE, {});
+ }
+
+ Assert.equal(TelemetrySend.pendingPingCount, PING_COUNT, "Should have the correct pending ping count");
+
+ // Triggering a shutdown should persist the pings.
+ yield TelemetrySend.shutdown();
+ Assert.ok((yield checkPingsSaved(ids)), "All pending pings should have been persisted");
+
+ // After a restart the pings should have been found when scanning.
+ yield TelemetrySend.reset();
+ Assert.equal(TelemetrySend.pendingPingCount, PING_COUNT, "Should have the correct pending ping count");
+});