summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/tests/unit/test_EcosystemTelemetry.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/tests/unit/test_EcosystemTelemetry.js')
-rw-r--r--toolkit/components/telemetry/tests/unit/test_EcosystemTelemetry.js430
1 files changed, 430 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/test_EcosystemTelemetry.js b/toolkit/components/telemetry/tests/unit/test_EcosystemTelemetry.js
new file mode 100644
index 0000000000..44741b9cd2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_EcosystemTelemetry.js
@@ -0,0 +1,430 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/TelemetryController.jsm", this);
+ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", this);
+ChromeUtils.import("resource://gre/modules/Preferences.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ONLOGIN_NOTIFICATION: "resource://gre/modules/FxAccountsCommon.js",
+ ONLOGOUT_NOTIFICATION: "resource://gre/modules/FxAccountsCommon.js",
+ ONVERIFIED_NOTIFICATION: "resource://gre/modules/FxAccountsCommon.js",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "EcosystemTelemetry",
+ "resource://gre/modules/EcosystemTelemetry.jsm"
+);
+
+const TEST_PING_TYPE = "test-ping-type";
+
+const RE_VALID_GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
+
+function fakeIdleNotification(topic) {
+ let scheduler = ChromeUtils.import(
+ "resource://gre/modules/TelemetryScheduler.jsm",
+ null
+ );
+ return scheduler.TelemetryScheduler.observe(null, topic, null);
+}
+
+async function promiseNoPing() {
+ // We check there's not one of our pings pending by sending a test ping, then
+ // immediately fetching a pending ping and checking it's that test one.
+ TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, {});
+ let ping = await PingServer.promiseNextPing();
+ Assert.equal(ping.type, TEST_PING_TYPE, "Should be a test ping.");
+}
+
+function checkPingStructure(ping, reason) {
+ Assert.equal(
+ ping.type,
+ EcosystemTelemetry.PING_TYPE,
+ "Should be an ecosystem ping."
+ );
+
+ Assert.ok(!("clientId" in ping), "Ping must not contain a client ID.");
+ Assert.ok("environment" in ping, "Ping must contain an environment.");
+ let environment = ping.environment;
+
+ // Check that the environment is indeed minimal
+ const ALLOWED_ENVIRONMENT_KEYS = ["settings", "system", "profile"];
+ Assert.deepEqual(
+ ALLOWED_ENVIRONMENT_KEYS,
+ Object.keys(environment),
+ "Environment should only contain a limited set of keys."
+ );
+
+ // Check that fields of the environment are indeed minimal
+ Assert.deepEqual(
+ ["locale"],
+ Object.keys(environment.settings),
+ "Settings environment should only contain locale"
+ );
+ Assert.deepEqual(
+ ["cpu", "memoryMB", "os"],
+ Object.keys(environment.system).sort(),
+ "System environment should contain a limited set of keys"
+ );
+ Assert.deepEqual(
+ ["locale", "name", "version"],
+ Object.keys(environment.system.os).sort(),
+ "system.environment.os should contain a limited set of keys"
+ );
+
+ // Check the payload for required fields.
+ let payload = ping.payload;
+ Assert.equal(payload.reason, reason, "Ping reason must match.");
+ Assert.ok(
+ payload.duration >= 0,
+ "Payload must have a duration greater or equal to 0"
+ );
+ Assert.ok("ecosystemAnonId" in payload, "payload must have ecosystemAnonId");
+ Assert.ok(
+ RE_VALID_GUID.test(payload.ecosystemClientId),
+ "ecosystemClientId must be a valid GUID"
+ );
+
+ Assert.ok("scalars" in payload, "Payload must contain scalars");
+ Assert.ok("keyedScalars" in payload, "Payload must contain keyed scalars");
+ Assert.ok("histograms" in payload, "Payload must contain histograms");
+ Assert.ok(
+ "keyedHistograms" in payload,
+ "Payload must contain keyed histograms"
+ );
+}
+
+function fakeAnonId(fn) {
+ const m = ChromeUtils.import(
+ "resource://gre/modules/EcosystemTelemetry.jsm",
+ null
+ );
+ let oldFn = m.Policy.getEcosystemAnonId;
+ m.Policy.getEcosystemAnonId = fn;
+ return oldFn;
+}
+
+registerCleanupFunction(function() {
+ PingServer.stop();
+});
+
+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();
+
+ // Start the local ping server and setup Telemetry to use it during the tests.
+ PingServer.start();
+ Preferences.set(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+
+ await TelemetryController.testSetup();
+});
+
+// We make absolute sure the Ecosystem ping is never triggered on Fennec/Non-unified Telemetry
+add_task(
+ {
+ skip_if: () => !gIsAndroid,
+ },
+ async function test_no_ecosystem_ping_on_fennec() {
+ // Force preference to true, we should have an additional check on Android/Unified Telemetry
+ Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
+ EcosystemTelemetry.testReset();
+
+ // This is invoked in regular intervals by the timer.
+ // Would trigger ping sending.
+ EcosystemTelemetry.periodicPing();
+ await promiseNoPing();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_disabled_non_fxa_production() {
+ Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
+ Assert.ok(EcosystemTelemetry.enabled(), "enabled by default");
+ Preferences.set("identity.fxaccounts.autoconfig.uri", "http://");
+ Assert.ok(!EcosystemTelemetry.enabled(), "disabled if non-prod");
+ Preferences.set(
+ TelemetryUtils.Preferences.EcosystemTelemetryAllowForNonProductionFxA,
+ true
+ );
+ Assert.ok(
+ EcosystemTelemetry.enabled(),
+ "enabled for non-prod but preference override"
+ );
+ Preferences.reset("identity.fxaccounts.autoconfig.uri");
+ Preferences.reset(
+ TelemetryUtils.Preferences.EcosystemTelemetryAllowForNonProductionFxA
+ );
+ }
+);
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_nosending_if_disabled() {
+ Preferences.set(
+ TelemetryUtils.Preferences.EcosystemTelemetryEnabled,
+ false
+ );
+ EcosystemTelemetry.testReset();
+
+ // This is invoked in regular intervals by the timer.
+ // Would trigger ping sending.
+ EcosystemTelemetry.periodicPing();
+ await promiseNoPing();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_no_default_send() {
+ // No user's logged in, nothing is mocked, so nothing is sent.
+ Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
+ EcosystemTelemetry.testReset();
+
+ // This is invoked in regular intervals by the timer.
+ EcosystemTelemetry.periodicPing();
+
+ await promiseNoPing();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_login_workflow() {
+ // Fake the whole login/logout workflow by triggering the events directly.
+
+ Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
+ EcosystemTelemetry.testReset();
+
+ let originalAnonId = fakeAnonId(() => null);
+ let ping;
+
+ // 1. No user, timer invoked
+ EcosystemTelemetry.periodicPing();
+ await promiseNoPing();
+
+ // 2. User logs in, but we fail to obtain a valid uid.
+ // No ping will be generated.
+ fakeAnonId(() => null);
+ EcosystemTelemetry.observe(null, ONLOGIN_NOTIFICATION, null);
+
+ EcosystemTelemetry.periodicPing();
+ await promiseNoPing();
+
+ // Once we've failed to get the ID, we don't try again until next startup
+ // or another login-related event - so...
+ // 3. uid becomes available after verification.
+ fakeAnonId(() => "test_login_workflow:my.anon.id");
+ EcosystemTelemetry.observe(null, ONVERIFIED_NOTIFICATION, null);
+ print("triggering ping now that we have an anon-id");
+ EcosystemTelemetry.periodicPing();
+ ping = await PingServer.promiseNextPing();
+ checkPingStructure(ping, "periodic");
+ Assert.equal(
+ ping.payload.ecosystemAnonId,
+ "test_login_workflow:my.anon.id"
+ );
+ const origClientId = ping.payload.ecosystemClientId;
+
+ // 4. User disconnects account, should get an immediate ping.
+ print("user disconnects");
+ // We need to arrange for the new empty anonid before the notification.
+ fakeAnonId(() => null);
+ await EcosystemTelemetry.observe(null, ONLOGOUT_NOTIFICATION, null);
+ ping = await PingServer.promiseNextPing();
+ checkPingStructure(ping, "logout");
+ Assert.equal(
+ ping.payload.ecosystemAnonId,
+ "test_login_workflow:my.anon.id",
+ "should have been submitted with the old anonid"
+ );
+ Assert.equal(
+ ping.payload.ecosystemClientId,
+ origClientId,
+ "should have been submitted with the old clientid"
+ );
+ Assert.equal(
+ await EcosystemTelemetry.promiseEcosystemAnonId,
+ null,
+ "should resolve to null immediately after logout"
+ );
+
+ // 5. No user, timer invoked
+ print("timer fires after disconnection");
+ EcosystemTelemetry.periodicPing();
+ await promiseNoPing();
+
+ // 6. Transition back to logged in, pings should again be sent.
+ fakeAnonId(() => "test_login_workflow:my.anon.id.2");
+ EcosystemTelemetry.observe(null, ONVERIFIED_NOTIFICATION, null);
+ print("triggering ping now the user has logged back in");
+ EcosystemTelemetry.periodicPing();
+ ping = await PingServer.promiseNextPing();
+ checkPingStructure(ping, "periodic");
+ Assert.equal(
+ ping.payload.ecosystemAnonId,
+ "test_login_workflow:my.anon.id.2"
+ );
+ Assert.notEqual(
+ ping.payload.ecosystemClientId,
+ origClientId,
+ "should have a different clientid after signing out then back in"
+ );
+
+ // Reset policy.
+ fakeAnonId(originalAnonId);
+ }
+);
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_shutdown_logged_in() {
+ // Check shutdown when a user's logged in does the right thing.
+ Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
+ EcosystemTelemetry.testReset();
+
+ let originalAnonId = fakeAnonId(() =>
+ Promise.resolve("test_shutdown_logged_in:my.anon.id")
+ );
+
+ EcosystemTelemetry.observe(null, ONLOGIN_NOTIFICATION, null);
+
+ // No ping expected yet.
+ await promiseNoPing();
+
+ // Shutdown
+ EcosystemTelemetry.shutdown();
+ let ping = await PingServer.promiseNextPing();
+ checkPingStructure(ping, "shutdown");
+ Assert.equal(
+ ping.payload.ecosystemAnonId,
+ "test_shutdown_logged_in:my.anon.id",
+ "our anon ID is in the ping"
+ );
+ fakeAnonId(originalAnonId);
+ }
+);
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_shutdown_not_logged_in() {
+ // Check shutdown when no user is logged in does the right thing.
+ Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
+ EcosystemTelemetry.testReset();
+
+ let originalAnonId = fakeAnonId(() => Promise.resolve(null));
+
+ // No ping expected yet.
+ await promiseNoPing();
+
+ // Shutdown
+ EcosystemTelemetry.shutdown();
+
+ // Still no ping.
+ await promiseNoPing();
+ fakeAnonId(originalAnonId);
+ }
+);
+
+// Test that a periodic ping is triggered by the scheduler at midnight
+//
+// Based on `test_TelemetrySession#test_DailyDueAndIdle`.
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_periodic_ping() {
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let receivedPing = null;
+ // Register a ping handler that will assert when receiving multiple ecosystem pings.
+ // We can ignore other pings, such as the periodic ping.
+ PingServer.registerPingHandler(req => {
+ const ping = decodeRequestPayload(req);
+ if (ping.type == EcosystemTelemetry.PING_TYPE) {
+ Assert.ok(
+ !receivedPing,
+ "Telemetry must only send one periodic ecosystem ping."
+ );
+ receivedPing = ping;
+ }
+ });
+
+ // Faking scheduler timer has to happen before resetting TelemetryController
+ // to be effective.
+ let schedulerTickCallback = null;
+ let now = new Date(2040, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control periodic collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
+ EcosystemTelemetry.testReset();
+
+ // Have to arrange for an anon-id to be configured.
+ let originalAnonId = fakeAnonId(() => "test_periodic_ping:my.anon.id");
+ EcosystemTelemetry.observe(null, ONLOGIN_NOTIFICATION, null);
+
+ // As a sanity check we trigger a keyedHistogram and scalar declared as
+ // being in our ping, just to help ensure that the payload was assembled
+ // in the correct shape.
+ let h = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
+ h.add("test-key");
+ Telemetry.scalarSet("browser.engagement.total_uri_count", 2);
+
+ // Trigger the periodic ecosystem ping.
+ let firstPeriodicDue = new Date(2040, 1, 2, 0, 0, 0);
+ fakeNow(firstPeriodicDue);
+
+ // Run a scheduler tick: it should trigger the periodic ping.
+ Assert.ok(!!schedulerTickCallback);
+ let tickPromise = schedulerTickCallback();
+
+ // Send an idle and then an active user notification.
+ fakeIdleNotification("idle");
+ fakeIdleNotification("active");
+
+ // Wait on the tick promise.
+ await tickPromise;
+
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Decode the ping contained in the request and check that's a periodic ping.
+ Assert.ok(receivedPing, "Telemetry must send one ecosystem periodic ping.");
+ checkPingStructure(receivedPing, "periodic");
+ // And check the content we expect is there.
+ Assert.ok(receivedPing.payload.keyedHistograms.parent.SEARCH_COUNTS);
+ Assert.equal(
+ receivedPing.payload.scalars.parent["browser.engagement.total_uri_count"],
+ 2
+ );
+
+ fakeAnonId(originalAnonId);
+ }
+);