diff options
Diffstat (limited to 'toolkit/components/telemetry/tests/unit/test_EcosystemTelemetry.js')
-rw-r--r-- | toolkit/components/telemetry/tests/unit/test_EcosystemTelemetry.js | 430 |
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); + } +); |