diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/components/newtab/test/xpcshell/test_TelemetryFeed.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/newtab/test/xpcshell/test_TelemetryFeed.js')
-rw-r--r-- | browser/components/newtab/test/xpcshell/test_TelemetryFeed.js | 3285 |
1 files changed, 3285 insertions, 0 deletions
diff --git a/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js b/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js new file mode 100644 index 0000000000..b54d6094ad --- /dev/null +++ b/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js @@ -0,0 +1,3285 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule( + "resource://activity-stream/common/Actions.sys.mjs" +); + +const { MESSAGE_TYPE_HASH: msg } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ActorConstants.sys.mjs" +); + +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); + +const { TelemetryFeed, USER_PREFS_ENCODING } = ChromeUtils.importESModule( + "resource://activity-stream/lib/TelemetryFeed.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + AboutWelcomeTelemetry: + "resource:///modules/aboutwelcome/AboutWelcomeTelemetry.sys.mjs", + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + ExtensionSettingsStore: + "resource://gre/modules/ExtensionSettingsStore.sys.mjs", + HomePage: "resource:///modules/HomePage.sys.mjs", + JsonSchemaValidator: + "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", + TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs", + TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", + UTEventReporting: "resource://activity-stream/lib/UTEventReporting.sys.mjs", +}); + +const FAKE_UUID = "{foo-123-foo}"; +const PREF_IMPRESSION_ID = "browser.newtabpage.activity-stream.impressionId"; +const PREF_TELEMETRY = "browser.newtabpage.activity-stream.telemetry"; +const PREF_EVENT_TELEMETRY = + "browser.newtabpage.activity-stream.telemetry.ut.events"; + +let ASRouterEventPingSchemaPromise; +let BasePingSchemaPromise; +let SessionPingSchemaPromise; +let UserEventPingSchemaPromise; + +function assertPingMatchesSchema(pingKind, ping, schema) { + // Unlike the validator from JsonSchema.sys.mjs, JsonSchemaValidator + // lets us opt-in to having "undefined" properties, which are then + // ignored. This is fine because the ping is sent as a JSON string + // over an XHR, and undefined properties are culled as part of the + // JSON encoding process. + let result = JsonSchemaValidator.validate(ping, schema, { + allowExplicitUndefinedProperties: true, + }); + + if (!result.valid) { + info(`${pingKind} failed to validate against the schema: ${result.error}`); + } + + Assert.ok(result.valid, `${pingKind} is valid against the schema.`); +} + +async function assertSessionPingValid(ping) { + let schema = await SessionPingSchemaPromise; + assertPingMatchesSchema("SessionPing", ping, schema); +} + +async function assertBasePingValid(ping) { + let schema = await BasePingSchemaPromise; + assertPingMatchesSchema("BasePing", ping, schema); +} + +async function assertUserEventPingValid(ping) { + let schema = await UserEventPingSchemaPromise; + assertPingMatchesSchema("UserEventPing", ping, schema); +} + +async function assertASRouterEventPingValid(ping) { + let schema = await ASRouterEventPingSchemaPromise; + assertPingMatchesSchema("ASRouterEventPing", ping, schema); +} + +add_setup(async function setup() { + ASRouterEventPingSchemaPromise = IOUtils.readJSON( + do_get_file("../schemas/asrouter_event_ping.schema.json").path + ); + + BasePingSchemaPromise = IOUtils.readJSON( + do_get_file("../schemas/base_ping.schema.json").path + ); + + SessionPingSchemaPromise = IOUtils.readJSON( + do_get_file("../schemas/session_ping.schema.json").path + ); + + UserEventPingSchemaPromise = IOUtils.readJSON( + do_get_file("../schemas/user_event_ping.schema.json").path + ); + + do_get_profile(); + // FOG needs to be initialized in order for data to flow. + Services.fog.initializeFOG(); + + await TelemetryController.testReset(); + + updateAppInfo({ + name: "XPCShell", + ID: "xpcshell@tests.mozilla.org", + version: "122", + platformVersion: "122", + }); + + Services.prefs.setCharPref( + "browser.contextual-services.contextId", + FAKE_UUID + ); +}); + +add_task(async function test_construction() { + let testInstance = new TelemetryFeed(); + Assert.ok( + testInstance, + "Should have been able to create an instance of TelemetryFeed." + ); + Assert.ok( + testInstance.utEvents instanceof UTEventReporting, + "Should add .utEvents, a UTEventReporting instance." + ); + Assert.ok( + testInstance._impressionId, + "Should create impression id if none exists" + ); +}); + +add_task(async function test_load_impressionId() { + info( + "Constructing a TelemetryFeed should use a saved impression ID if one exists." + ); + const FAKE_IMPRESSION_ID = "{some-fake-impression-ID}"; + const IMPRESSION_PREF = "browser.newtabpage.activity-stream.impressionId"; + Services.prefs.setCharPref(IMPRESSION_PREF, FAKE_IMPRESSION_ID); + Assert.equal(new TelemetryFeed()._impressionId, FAKE_IMPRESSION_ID); + Services.prefs.clearUserPref(IMPRESSION_PREF); +}); + +add_task(async function test_init() { + info( + "init should make this.browserOpenNewtabStart() observe browser-open-newtab-start" + ); + let sandbox = sinon.createSandbox(); + sandbox + .stub(TelemetryFeed.prototype, "getOrCreateImpressionId") + .returns(FAKE_UUID); + + let instance = new TelemetryFeed(); + sandbox.stub(instance, "browserOpenNewtabStart"); + instance.init(); + + Services.obs.notifyObservers(null, "browser-open-newtab-start"); + Assert.ok( + instance.browserOpenNewtabStart.calledOnce, + "browserOpenNewtabStart called once." + ); + + info("init should create impression id if none exists"); + Assert.equal(instance._impressionId, FAKE_UUID); + + instance.uninit(); + sandbox.restore(); +}); + +add_task(async function test_saved_impression_id() { + const FAKE_IMPRESSION_ID = "fakeImpressionId"; + Services.prefs.setCharPref(PREF_IMPRESSION_ID, FAKE_IMPRESSION_ID); + Assert.equal(new TelemetryFeed()._impressionId, FAKE_IMPRESSION_ID); + Services.prefs.clearUserPref(PREF_IMPRESSION_ID); +}); + +add_task(async function test_telemetry_prefs() { + info("Telemetry pref changes from false to true"); + Services.prefs.setBoolPref(PREF_TELEMETRY, false); + let instance = new TelemetryFeed(); + Assert.ok(!instance.telemetryEnabled, "Telemetry disabled"); + + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + Assert.ok(instance.telemetryEnabled, "Telemetry enabled"); + + info("Event telemetry pref changes from false to true"); + + Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, false); + Assert.ok(!instance.eventTelemetryEnabled, "Event telemetry disabled"); + + Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true); + Assert.ok(instance.eventTelemetryEnabled, "Event telemetry enabled"); + + Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY); + Services.prefs.clearUserPref(PREF_TELEMETRY); +}); + +add_task(async function test_deletionRequest_scalars() { + info("TelemetryFeed.init should set two scalars for deletion-request"); + + Services.telemetry.clearScalars(); + let instance = new TelemetryFeed(); + instance.init(); + + let snapshot = Services.telemetry.getSnapshotForScalars( + "deletion-request", + false + ).parent; + TelemetryTestUtils.assertScalar( + snapshot, + "deletion.request.impression_id", + instance._impressionId + ); + TelemetryTestUtils.assertScalar( + snapshot, + "deletion.request.context_id", + FAKE_UUID + ); + instance.uninit(); +}); + +add_task(async function test_metrics_on_initialization() { + info("TelemetryFeed.init should record initial metrics from newtab prefs"); + Services.fog.testResetFOG(); + const ENABLED_SETTING = true; + const TOP_SITES_ROWS = 3; + const BLOCKED_SPONSORS = ["mozilla"]; + + Services.prefs.setBoolPref( + "browser.newtabpage.activity-stream.feeds.topsites", + ENABLED_SETTING + ); + Services.prefs.setIntPref( + "browser.newtabpage.activity-stream.topSitesRows", + TOP_SITES_ROWS + ); + Services.prefs.setCharPref( + "browser.topsites.blockedSponsors", + JSON.stringify(BLOCKED_SPONSORS) + ); + + let instance = new TelemetryFeed(); + instance.init(); + + Assert.equal(Glean.topsites.enabled.testGetValue(), ENABLED_SETTING); + Assert.equal(Glean.topsites.rows.testGetValue(), TOP_SITES_ROWS); + Assert.deepEqual( + Glean.newtab.blockedSponsors.testGetValue(), + BLOCKED_SPONSORS + ); + + instance.uninit(); + + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.feeds.topsites" + ); + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.topSitesRows" + ); + Services.prefs.clearUserPref("browser.topsites.blockedSponsors"); +}); + +add_task(async function test_metrics_with_bad_json() { + info( + "TelemetryFeed.init should not record blocked sponsor metrics when " + + "bad json string is passed" + ); + Services.fog.testResetFOG(); + Services.prefs.setCharPref("browser.topsites.blockedSponsors", "BAD[JSON]"); + + let instance = new TelemetryFeed(); + instance.init(); + + Assert.equal(Glean.newtab.blockedSponsors.testGetValue(), null); + + instance.uninit(); + + Services.prefs.clearUserPref("browser.topsites.blockedSponsors"); +}); + +add_task(async function test_metrics_on_pref_changes() { + info("TelemetryFeed.init should record new metrics for newtab pref changes"); + const INITIAL_TOP_SITES_ROWS = 3; + const INITIAL_BLOCKED_SPONSORS = []; + Services.fog.testResetFOG(); + Services.prefs.setIntPref( + "browser.newtabpage.activity-stream.topSitesRows", + INITIAL_TOP_SITES_ROWS + ); + Services.prefs.setCharPref( + "browser.topsites.blockedSponsors", + JSON.stringify(INITIAL_BLOCKED_SPONSORS) + ); + + let instance = new TelemetryFeed(); + instance.init(); + + Assert.equal(Glean.topsites.rows.testGetValue(), INITIAL_TOP_SITES_ROWS); + Assert.deepEqual( + Glean.newtab.blockedSponsors.testGetValue(), + INITIAL_BLOCKED_SPONSORS + ); + + const NEXT_TOP_SITES_ROWS = 2; + const NEXT_BLOCKED_SPONSORS = ["mozilla"]; + + Services.prefs.setIntPref( + "browser.newtabpage.activity-stream.topSitesRows", + NEXT_TOP_SITES_ROWS + ); + + Services.prefs.setStringPref( + "browser.topsites.blockedSponsors", + JSON.stringify(NEXT_BLOCKED_SPONSORS) + ); + + Assert.equal(Glean.topsites.rows.testGetValue(), NEXT_TOP_SITES_ROWS); + Assert.deepEqual( + Glean.newtab.blockedSponsors.testGetValue(), + NEXT_BLOCKED_SPONSORS + ); + + instance.uninit(); + + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.topSitesRows" + ); + Services.prefs.clearUserPref("browser.topsites.blockedSponsors"); +}); + +add_task(async function test_events_on_pref_changes() { + info("TelemetryFeed.init should record events for some newtab pref changes"); + // We only record events for browser.newtabpage.activity-stream.feeds.topsites and + // browser.newtabpage.activity-stream.showSponsoredTopSites being changed. + const INITIAL_TOPSITES_ENABLED = false; + const INITIAL_SHOW_SPONSORED_TOP_SITES = true; + Services.fog.testResetFOG(); + Services.prefs.setBoolPref( + "browser.newtabpage.activity-stream.feeds.topsites", + INITIAL_TOPSITES_ENABLED + ); + Services.prefs.setBoolPref( + "browser.newtabpage.activity-stream.showSponsoredTopSites", + INITIAL_SHOW_SPONSORED_TOP_SITES + ); + + let instance = new TelemetryFeed(); + instance.init(); + + const NEXT_TOPSITES_ENABLED = true; + const NEXT_SHOW_SPONSORED_TOP_SITES = false; + + Services.prefs.setBoolPref( + "browser.newtabpage.activity-stream.feeds.topsites", + NEXT_TOPSITES_ENABLED + ); + Services.prefs.setBoolPref( + "browser.newtabpage.activity-stream.showSponsoredTopSites", + NEXT_SHOW_SPONSORED_TOP_SITES + ); + + let prefChangeEvents = Glean.topsites.prefChanged.testGetValue(); + Assert.deepEqual(prefChangeEvents[0].extra, { + pref_name: "browser.newtabpage.activity-stream.feeds.topsites", + new_value: String(NEXT_TOPSITES_ENABLED), + }); + Assert.deepEqual(prefChangeEvents[1].extra, { + pref_name: "browser.newtabpage.activity-stream.showSponsoredTopSites", + new_value: String(NEXT_SHOW_SPONSORED_TOP_SITES), + }); + + instance.uninit(); + + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.feeds.topsites" + ); + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.showSponsoredTopSites" + ); +}); + +add_task(async function test_browserOpenNewtabStart() { + info( + "TelemetryFeed.browserOpenNewtabStart should call " + + "ChromeUtils.addProfilerMarker with browser-open-newtab-start" + ); + + let instance = new TelemetryFeed(); + + let entries = 10000; + let interval = 1; + let threads = ["GeckoMain"]; + let features = []; + await Services.profiler.StartProfiler(entries, interval, features, threads); + instance.browserOpenNewtabStart(); + + let profileArrayBuffer = + await Services.profiler.getProfileDataAsArrayBuffer(); + await Services.profiler.StopProfiler(); + + let profileUint8Array = new Uint8Array(profileArrayBuffer); + let textDecoder = new TextDecoder("utf-8", { fatal: true }); + let profileString = textDecoder.decode(profileUint8Array); + let profile = JSON.parse(profileString); + Assert.ok(profile.threads); + Assert.equal(profile.threads.length, 1); + + let foundMarker = profile.threads[0].markers.data.find(marker => { + return marker[5]?.name === "browser-open-newtab-start"; + }); + + Assert.ok(foundMarker, "Found the browser-open-newtab-start marker"); +}); + +add_task(async function test_addSession_and_get_session() { + info("TelemetryFeed.addSession should add a session and return it"); + let instance = new TelemetryFeed(); + let session = instance.addSession("foo"); + + Assert.equal(instance.sessions.get("foo"), session); + + info("TelemetryFeed.addSession should set a session_id"); + Assert.ok(session.session_id, "Should have a session_id set"); +}); + +add_task(async function test_addSession_url_param() { + info("TelemetryFeed.addSession should set the page if a url param is given"); + let instance = new TelemetryFeed(); + let session = instance.addSession("foo", "about:monkeys"); + Assert.equal(session.page, "about:monkeys"); + + info( + "TelemetryFeed.assSession should set the page prop to 'unknown' " + + "if no URL param given" + ); + session = instance.addSession("test2"); + Assert.equal(session.page, "unknown"); +}); + +add_task(async function test_addSession_perf_properties() { + info( + "TelemetryFeed.addSession should set the perf type when lacking " + + "timestamp" + ); + let instance = new TelemetryFeed(); + let session = instance.addSession("foo"); + Assert.equal(session.perf.load_trigger_type, "unexpected"); + + info( + "TelemetryFeed.addSession should set load_trigger_type to " + + "first_window_opened on the first about:home seen" + ); + session = instance.addSession("test2", "about:home"); + Assert.equal(session.perf.load_trigger_type, "first_window_opened"); + + info( + "TelemetryFeed.addSession should set load_trigger_ts to the " + + "value of the process start timestamp" + ); + Assert.equal( + session.perf.load_trigger_ts, + Services.startup.getStartupInfo().process.getTime(), + "Should have set a timestamp to be the process start time" + ); + + info( + "TelemetryFeed.addSession should NOT set load_trigger_type to " + + "first_window_opened on the second about:home seen" + ); + let session2 = instance.addSession("test2", "about:home"); + Assert.notEqual(session2.perf.load_trigger_type, "first_window_opened"); +}); + +add_task(async function test_addSession_valid_ping_on_first_abouthome() { + info( + "TelemetryFeed.addSession should create a valid session ping " + + "on the first about:home seen" + ); + let instance = new TelemetryFeed(); + // Add a session + const PORT_ID = "foo"; + let session = instance.addSession(PORT_ID, "about:home"); + + // Create a ping referencing the session + let ping = instance.createSessionEndEvent(session); + await assertSessionPingValid(ping); +}); + +add_task(async function test_addSession_valid_ping_data_late_by_ms() { + info( + "TelemetryFeed.addSession should create a valid session ping " + + "with the data_late_by_ms perf" + ); + let instance = new TelemetryFeed(); + // Add a session + const PORT_ID = "foo"; + let session = instance.addSession(PORT_ID, "about:home"); + + const TOPSITES_LATE_BY_MS = 10; + const HIGHLIGHTS_LATE_BY_MS = 20; + instance.saveSessionPerfData("foo", { + topsites_data_late_by_ms: TOPSITES_LATE_BY_MS, + }); + instance.saveSessionPerfData("foo", { + highlights_data_late_by_ms: HIGHLIGHTS_LATE_BY_MS, + }); + + // Create a ping referencing the session + let ping = instance.createSessionEndEvent(session); + await assertSessionPingValid(ping); + Assert.equal(session.perf.topsites_data_late_by_ms, TOPSITES_LATE_BY_MS); + Assert.equal(session.perf.highlights_data_late_by_ms, HIGHLIGHTS_LATE_BY_MS); +}); + +add_task(async function test_addSession_valid_ping_topsites_stats_perf() { + info( + "TelemetryFeed.addSession should create a valid session ping " + + "with the topsites stats perf" + ); + let instance = new TelemetryFeed(); + // Add a session + const PORT_ID = "foo"; + let session = instance.addSession(PORT_ID, "about:home"); + + const SCREENSHOT_WITH_ICON = 2; + const TOPSITES_PINNED = 3; + const TOPSITES_SEARCH_SHORTCUTS = 2; + + instance.saveSessionPerfData("foo", { + topsites_icon_stats: { + custom_screenshot: 0, + screenshot_with_icon: SCREENSHOT_WITH_ICON, + screenshot: 1, + tippytop: 2, + rich_icon: 1, + no_image: 0, + }, + topsites_pinned: TOPSITES_PINNED, + topsites_search_shortcuts: TOPSITES_SEARCH_SHORTCUTS, + }); + + // Create a ping referencing the session + let ping = instance.createSessionEndEvent(session); + await assertSessionPingValid(ping); + Assert.equal( + instance.sessions.get("foo").perf.topsites_icon_stats.screenshot_with_icon, + SCREENSHOT_WITH_ICON + ); + Assert.equal( + instance.sessions.get("foo").perf.topsites_pinned, + TOPSITES_PINNED + ); + Assert.equal( + instance.sessions.get("foo").perf.topsites_search_shortcuts, + TOPSITES_SEARCH_SHORTCUTS + ); +}); + +add_task(async function test_endSession_no_throw_on_bad_session() { + info( + "TelemetryFeed.endSession should not throw if there is no " + + "session for a given port ID" + ); + let instance = new TelemetryFeed(); + try { + instance.endSession("doesn't exist"); + Assert.ok(true, "Did not throw."); + } catch (e) { + Assert.ok(false, "Should not have thrown."); + } +}); + +add_task(async function test_endSession_session_duration() { + info( + "TelemetryFeed.endSession should add a session_duration integer " + + "if there is a visibility_event_rcvd_ts" + ); + let instance = new TelemetryFeed(); + let session = instance.addSession("foo"); + session.perf.visibility_event_rcvd_ts = 444.4732; + instance.endSession("foo"); + + Assert.ok( + Number.isInteger(session.session_duration), + "session_duration should be an integer" + ); +}); + +add_task(async function test_endSession_no_ping_on_no_visibility_event() { + info( + "TelemetryFeed.endSession shouldn't send session ping if there's " + + "no visibility_event_rcvd_ts" + ); + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true); + let instance = new TelemetryFeed(); + instance.addSession("foo"); + + Services.telemetry.clearEvents(); + instance.endSession("foo"); + TelemetryTestUtils.assertNumberOfEvents(0); + + info("TelemetryFeed.endSession should remove the session from .sessions"); + Assert.ok(!instance.sessions.has("foo")); + + Services.prefs.clearUserPref(PREF_TELEMETRY); + Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY); +}); + +add_task(async function test_endSession_send_ping() { + info( + "TelemetryFeed.endSession should call createSessionSendEvent with the " + + "session if visibilty_event_rcvd_ts was set" + ); + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true); + let instance = new TelemetryFeed(); + + let sandbox = sinon.createSandbox(); + sandbox.stub(instance, "createSessionEndEvent"); + sandbox.stub(instance.utEvents, "sendSessionEndEvent"); + + let session = instance.addSession("foo"); + + session.perf.visibility_event_rcvd_ts = 444.4732; + instance.endSession("foo"); + + Assert.ok(instance.createSessionEndEvent.calledWith(session)); + let sessionEndEvent = instance.createSessionEndEvent.firstCall.returnValue; + Assert.ok(instance.utEvents.sendSessionEndEvent.calledWith(sessionEndEvent)); + + info("TelemetryFeed.endSession should remove the session from .sessions"); + Assert.ok(!instance.sessions.has("foo")); + + Services.prefs.clearUserPref(PREF_TELEMETRY); + Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY); + + sandbox.restore(); +}); + +add_task(async function test_createPing_valid_base_if_no_portID() { + info( + "TelemetryFeed.createPing should create a valid base ping " + + "without a session if no portID is supplied" + ); + let instance = new TelemetryFeed(); + let ping = await instance.createPing(); + await assertBasePingValid(ping); + Assert.ok(!ping.session_id); + Assert.ok(!ping.page); +}); + +add_task(async function test_createPing_valid_base_if_portID() { + info( + "TelemetryFeed.createPing should create a valid base ping " + + "with session info if a portID is supplied" + ); + // Add a session + const PORT_ID = "foo"; + let instance = new TelemetryFeed(); + instance.addSession(PORT_ID, "about:home"); + let sessionID = instance.sessions.get(PORT_ID).session_id; + + // Create a ping referencing the session + let ping = await instance.createPing(PORT_ID); + await assertBasePingValid(ping); + + // Make sure we added the right session-related stuff to the ping + Assert.equal(ping.session_id, sessionID); + Assert.equal(ping.page, "about:home"); +}); + +add_task(async function test_createPing_no_session_yet_portID() { + info( + "TelemetryFeed.createPing should create an 'unexpected' base ping " + + "if no session yet portID is supplied" + ); + let instance = new TelemetryFeed(); + let ping = await instance.createPing("foo"); + await assertBasePingValid(ping); + + Assert.equal(ping.page, "unknown"); + Assert.equal( + instance.sessions.get("foo").perf.load_trigger_type, + "unexpected" + ); +}); + +add_task(async function test_createPing_includes_userPrefs() { + info("TelemetryFeed.createPing should create a base ping with user_prefs"); + let expectedUserPrefs = 0; + + for (let pref of Object.keys(USER_PREFS_ENCODING)) { + Services.prefs.setBoolPref( + `browser.newtabpage.activity-stream.${pref}`, + true + ); + expectedUserPrefs |= USER_PREFS_ENCODING[pref]; + } + + let instance = new TelemetryFeed(); + let ping = await instance.createPing("foo"); + await assertBasePingValid(ping); + Assert.equal(ping.user_prefs, expectedUserPrefs); + + for (const pref of Object.keys(USER_PREFS_ENCODING)) { + Services.prefs.clearUserPref(`browser.newtabpage.activity-stream.${pref}`); + } +}); + +add_task(async function test_createUserEvent_is_valid() { + info( + "TelemetryFeed.createUserEvent should create a valid user event ping " + + "with the right session_id" + ); + const PORT_ID = "foo"; + + let instance = new TelemetryFeed(); + let data = { source: "TOP_SITES", event: "CLICK" }; + let action = ac.AlsoToMain(ac.UserEvent(data), PORT_ID); + let session = instance.addSession(PORT_ID); + + let ping = await instance.createUserEvent(action); + + // Is it valid? + await assertUserEventPingValid(ping); + // Does it have the right session_id? + Assert.equal(ping.session_id, session.session_id); +}); + +add_task(async function test_createSessionEndEvent_is_valid() { + info( + "TelemetryFeed.createSessionEndEvent should create a valid session ping" + ); + const FAKE_DURATION = 12345; + let instance = new TelemetryFeed(); + let ping = await instance.createSessionEndEvent({ + session_id: FAKE_UUID, + page: "about:newtab", + session_duration: FAKE_DURATION, + perf: { + load_trigger_ts: 10, + load_trigger_type: "menu_plus_or_keyboard", + visibility_event_rcvd_ts: 20, + is_preloaded: true, + }, + }); + + // Is it valid? + await assertSessionPingValid(ping); + Assert.equal(ping.session_id, FAKE_UUID); + Assert.equal(ping.page, "about:newtab"); + Assert.equal(ping.session_duration, FAKE_DURATION); +}); + +add_task(async function test_createSessionEndEvent_with_unexpected_is_valid() { + info( + "TelemetryFeed.createSessionEndEvent should create a valid 'unexpected' " + + "session ping" + ); + const FAKE_DURATION = 12345; + const FAKE_TRIGGER_TYPE = "unexpected"; + + let instance = new TelemetryFeed(); + let ping = await instance.createSessionEndEvent({ + session_id: FAKE_UUID, + page: "about:newtab", + session_duration: FAKE_DURATION, + perf: { + load_trigger_type: FAKE_TRIGGER_TYPE, + is_preloaded: true, + }, + }); + + // Is it valid? + await assertSessionPingValid(ping); + Assert.equal(ping.session_id, FAKE_UUID); + Assert.equal(ping.page, "about:newtab"); + Assert.equal(ping.session_duration, FAKE_DURATION); + Assert.equal(ping.perf.load_trigger_type, FAKE_TRIGGER_TYPE); +}); + +add_task(async function test_applyCFRPolicy_prerelease() { + info( + "TelemetryFeed.applyCFRPolicy should use client_id and message_id " + + "in prerelease" + ); + let sandbox = sinon.createSandbox(); + sandbox.stub(UpdateUtils, "getUpdateChannel").returns("nightly"); + + let instance = new TelemetryFeed(); + + let data = { + action: "cfr_user_event", + event: "IMPRESSION", + message_id: "cfr_message_01", + bucket_id: "cfr_bucket_01", + }; + let { ping, pingType } = await instance.applyCFRPolicy(data); + + Assert.equal(pingType, "cfr"); + Assert.equal(ping.impression_id, undefined); + Assert.equal( + ping.client_id, + Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") + ); + Assert.equal(ping.bucket_id, "cfr_bucket_01"); + Assert.equal(ping.message_id, "cfr_message_01"); + + sandbox.restore(); +}); + +add_task(async function test_applyCFRPolicy_release() { + info( + "TelemetryFeed.applyCFRPolicy should use impression_id and bucket_id " + + "in release" + ); + let sandbox = sinon.createSandbox(); + sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release"); + sandbox + .stub(TelemetryFeed.prototype, "getOrCreateImpressionId") + .returns(FAKE_UUID); + + let instance = new TelemetryFeed(); + + let data = { + action: "cfr_user_event", + event: "IMPRESSION", + message_id: "cfr_message_01", + bucket_id: "cfr_bucket_01", + }; + let { ping, pingType } = await instance.applyCFRPolicy(data); + + Assert.equal(pingType, "cfr"); + Assert.equal(ping.impression_id, FAKE_UUID); + Assert.equal(ping.client_id, undefined); + Assert.equal(ping.bucket_id, "cfr_bucket_01"); + Assert.equal(ping.message_id, "n/a"); + + sandbox.restore(); +}); + +add_task(async function test_applyCFRPolicy_experiment_release() { + info( + "TelemetryFeed.applyCFRPolicy should use impression_id and bucket_id " + + "in release" + ); + let sandbox = sinon.createSandbox(); + sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release"); + sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({ + slug: "SOME-CFR-EXP", + }); + + let instance = new TelemetryFeed(); + + let data = { + action: "cfr_user_event", + event: "IMPRESSION", + message_id: "cfr_message_01", + bucket_id: "cfr_bucket_01", + }; + let { ping, pingType } = await instance.applyCFRPolicy(data); + + Assert.equal(pingType, "cfr"); + Assert.equal(ping.impression_id, undefined); + Assert.equal( + ping.client_id, + Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") + ); + Assert.equal(ping.bucket_id, "cfr_bucket_01"); + Assert.equal(ping.message_id, "cfr_message_01"); + + sandbox.restore(); +}); + +add_task(async function test_applyCFRPolicy_release_private_browsing() { + info( + "TelemetryFeed.applyCFRPolicy should use impression_id and bucket_id " + + "in Private Browsing in release" + ); + let sandbox = sinon.createSandbox(); + sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release"); + sandbox + .stub(TelemetryFeed.prototype, "getOrCreateImpressionId") + .returns(FAKE_UUID); + + let instance = new TelemetryFeed(); + + let data = { + action: "cfr_user_event", + event: "IMPRESSION", + is_private: true, + message_id: "cfr_message_01", + bucket_id: "cfr_bucket_01", + }; + let { ping, pingType } = await instance.applyCFRPolicy(data); + + Assert.equal(pingType, "cfr"); + Assert.equal(ping.impression_id, FAKE_UUID); + Assert.equal(ping.client_id, undefined); + Assert.equal(ping.bucket_id, "cfr_bucket_01"); + Assert.equal(ping.message_id, "n/a"); + + sandbox.restore(); +}); + +add_task( + async function test_applyCFRPolicy_release_experiment_private_browsing() { + info( + "TelemetryFeed.applyCFRPolicy should use client_id and message_id in the " + + "experiment cohort in Private Browsing in release" + ); + let sandbox = sinon.createSandbox(); + sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release"); + sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({ + slug: "SOME-CFR-EXP", + }); + + let instance = new TelemetryFeed(); + + let data = { + action: "cfr_user_event", + event: "IMPRESSION", + is_private: true, + message_id: "cfr_message_01", + bucket_id: "cfr_bucket_01", + }; + let { ping, pingType } = await instance.applyCFRPolicy(data); + + Assert.equal(pingType, "cfr"); + Assert.equal(ping.impression_id, undefined); + Assert.equal( + ping.client_id, + Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") + ); + Assert.equal(ping.bucket_id, "cfr_bucket_01"); + Assert.equal(ping.message_id, "cfr_message_01"); + + sandbox.restore(); + } +); + +add_task(async function test_applyWhatsNewPolicy() { + info( + "TelemetryFeed.applyWhatsNewPolicy should set client_id and set pingType" + ); + let instance = new TelemetryFeed(); + let { ping, pingType } = await instance.applyWhatsNewPolicy({}); + + Assert.equal( + ping.client_id, + Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") + ); + Assert.equal(pingType, "whats-new-panel"); +}); + +add_task(async function test_applyInfoBarPolicy() { + info( + "TelemetryFeed.applyInfoBarPolicy should set client_id and set pingType" + ); + let instance = new TelemetryFeed(); + let { ping, pingType } = await instance.applyInfoBarPolicy({}); + + Assert.equal( + ping.client_id, + Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") + ); + Assert.equal(pingType, "infobar"); +}); + +add_task(async function test_applyToastNotificationPolicy() { + info( + "TelemetryFeed.applyToastNotificationPolicy should set client_id " + + "and set pingType" + ); + let instance = new TelemetryFeed(); + let { ping, pingType } = await instance.applyToastNotificationPolicy({}); + + Assert.equal( + ping.client_id, + Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") + ); + Assert.equal(pingType, "toast_notification"); +}); + +add_task(async function test_applySpotlightPolicy() { + info( + "TelemetryFeed.applySpotlightPolicy should set client_id " + + "and set pingType" + ); + let instance = new TelemetryFeed(); + let { ping, pingType } = await instance.applySpotlightPolicy({ + action: "foo", + }); + + Assert.equal( + ping.client_id, + Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") + ); + Assert.equal(pingType, "spotlight"); + Assert.equal(ping.action, undefined); +}); + +add_task(async function test_applyMomentsPolicy_prerelease() { + info( + "TelemetryFeed.applyMomentsPolicy should use client_id and " + + "message_id in prerelease" + ); + let sandbox = sinon.createSandbox(); + sandbox.stub(UpdateUtils, "getUpdateChannel").returns("nightly"); + + let instance = new TelemetryFeed(); + let data = { + action: "moments_user_event", + event: "IMPRESSION", + message_id: "moments_message_01", + bucket_id: "moments_bucket_01", + }; + let { ping, pingType } = await instance.applyMomentsPolicy(data); + + Assert.equal(pingType, "moments"); + Assert.equal(ping.impression_id, undefined); + Assert.equal( + ping.client_id, + Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") + ); + Assert.equal(ping.bucket_id, "moments_bucket_01"); + Assert.equal(ping.message_id, "moments_message_01"); + + sandbox.restore(); +}); + +add_task(async function test_applyMomentsPolicy_release() { + info( + "TelemetryFeed.applyMomentsPolicy should use impression_id and " + + "bucket_id in release" + ); + let sandbox = sinon.createSandbox(); + sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release"); + sandbox + .stub(TelemetryFeed.prototype, "getOrCreateImpressionId") + .returns(FAKE_UUID); + + let instance = new TelemetryFeed(); + let data = { + action: "moments_user_event", + event: "IMPRESSION", + message_id: "moments_message_01", + bucket_id: "moments_bucket_01", + }; + let { ping, pingType } = await instance.applyMomentsPolicy(data); + + Assert.equal(pingType, "moments"); + Assert.equal(ping.impression_id, FAKE_UUID); + Assert.equal(ping.client_id, undefined); + Assert.equal(ping.bucket_id, "moments_bucket_01"); + Assert.equal(ping.message_id, "n/a"); + + sandbox.restore(); +}); + +add_task(async function test_applyMomentsPolicy_experiment_release() { + info( + "TelemetryFeed.applyMomentsPolicy client_id and message_id in " + + "the experiment cohort in release" + ); + let sandbox = sinon.createSandbox(); + sandbox.stub(UpdateUtils, "getUpdateChannel").returns("release"); + sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({ + slug: "SOME-CFR-EXP", + }); + + let instance = new TelemetryFeed(); + let data = { + action: "moments_user_event", + event: "IMPRESSION", + message_id: "moments_message_01", + bucket_id: "moments_bucket_01", + }; + let { ping, pingType } = await instance.applyMomentsPolicy(data); + + Assert.equal(pingType, "moments"); + Assert.equal(ping.impression_id, undefined); + Assert.equal( + ping.client_id, + Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") + ); + Assert.equal(ping.bucket_id, "moments_bucket_01"); + Assert.equal(ping.message_id, "moments_message_01"); + + sandbox.restore(); +}); + +add_task(async function test_applyOnboardingPolicy_includes_client_id() { + info("TelemetryFeed.applyOnboardingPolicy should include client_id"); + let instance = new TelemetryFeed(); + let data = { + action: "onboarding_user_event", + event: "CLICK_BUTTION", + message_id: "onboarding_message_01", + }; + let { ping, pingType } = await instance.applyOnboardingPolicy(data); + + Assert.equal(pingType, "onboarding"); + Assert.equal( + ping.client_id, + Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") + ); + Assert.equal(ping.message_id, "onboarding_message_01"); + Assert.equal( + ping.browser_session_id, + TelemetrySession.getMetadata("").sessionId + ); +}); + +add_task(async function test_applyOnboardingPolicy_with_session() { + info( + "TelemetryFeed.applyOnboardingPolicy should include page to " + + "event_context if there is a session" + ); + let instance = new TelemetryFeed(); + let data = { + action: "onboarding_user_event", + event: "CLICK_BUTTION", + message_id: "onboarding_message_01", + }; + let session = { page: "about:welcome" }; + let { ping, pingType } = await instance.applyOnboardingPolicy(data, session); + + Assert.equal(pingType, "onboarding"); + Assert.equal(ping.event_context, JSON.stringify({ page: "about:welcome" })); + Assert.equal(ping.message_id, "onboarding_message_01"); +}); + +add_task(async function test_applyOnboardingPolicy_only_allowed_pages() { + info( + "TelemetryFeed.applyOnboardingPolicy should not set page if it is " + + "not in ONBOARDING_ALLOWED_PAGE_VALUES" + ); + let instance = new TelemetryFeed(); + let data = { + action: "onboarding_user_event", + event: "CLICK_BUTTION", + message_id: "onboarding_message_01", + }; + let session = { page: "foo" }; + let { ping, pingType } = await instance.applyOnboardingPolicy(data, session); + + Assert.equal(pingType, "onboarding"); + Assert.equal(ping.event_context, JSON.stringify({})); + Assert.equal(ping.message_id, "onboarding_message_01"); +}); + +add_task( + async function test_applyOnboardingPolicy_append_page_to_event_context() { + info( + "TelemetryFeed.applyOnboardingPolicy should append page to event_context " + + "if it is not empty" + ); + let instance = new TelemetryFeed(); + let data = { + action: "onboarding_user_event", + event: "CLICK_BUTTION", + message_id: "onboarding_message_01", + event_context: JSON.stringify({ foo: "bar" }), + }; + let session = { page: "about:welcome" }; + let { ping, pingType } = await instance.applyOnboardingPolicy( + data, + session + ); + + Assert.equal(pingType, "onboarding"); + Assert.equal( + ping.event_context, + JSON.stringify({ foo: "bar", page: "about:welcome" }) + ); + Assert.equal(ping.message_id, "onboarding_message_01"); + } +); + +add_task( + async function test_applyOnboardingPolicy_append_page_to_event_context() { + info( + "TelemetryFeed.applyOnboardingPolicy should append page to event_context " + + "if it is not a JSON serialized string" + ); + let instance = new TelemetryFeed(); + let data = { + action: "onboarding_user_event", + event: "CLICK_BUTTION", + message_id: "onboarding_message_01", + event_context: "foo", + }; + let session = { page: "about:welcome" }; + let { ping, pingType } = await instance.applyOnboardingPolicy( + data, + session + ); + + Assert.equal(pingType, "onboarding"); + Assert.equal( + ping.event_context, + JSON.stringify({ value: "foo", page: "about:welcome" }) + ); + Assert.equal(ping.message_id, "onboarding_message_01"); + } +); + +add_task(async function test_applyUndesiredEventPolicy() { + info( + "TelemetryFeed.applyUndesiredEventPolicy should exclude client_id " + + "and use impression_id" + ); + let sandbox = sinon.createSandbox(); + sandbox + .stub(TelemetryFeed.prototype, "getOrCreateImpressionId") + .returns(FAKE_UUID); + + let instance = new TelemetryFeed(); + let data = { + action: "asrouter_undesired_event", + event: "RS_MISSING_DATA", + }; + let { ping, pingType } = await instance.applyUndesiredEventPolicy(data); + + Assert.equal(pingType, "undesired-events"); + Assert.equal(ping.client_id, undefined); + Assert.equal(ping.impression_id, FAKE_UUID); + + sandbox.restore(); +}); + +add_task(async function test_createASRouterEvent_valid_ping() { + info( + "TelemetryFeed.createASRouterEvent should create a valid " + + "ASRouterEventPing ping" + ); + let instance = new TelemetryFeed(); + let data = { + action: "cfr_user_event", + event: "CLICK", + message_id: "cfr_message_01", + }; + let action = ac.ASRouterUserEvent(data); + let { ping } = await instance.createASRouterEvent(action); + + await assertASRouterEventPingValid(ping); + Assert.equal(ping.event, "CLICK"); +}); + +add_task(async function test_createASRouterEvent_call_correctPolicy() { + let testCallCorrectPolicy = async (expectedPolicyFnName, data) => { + info( + `TelemetryFeed.createASRouterEvent should call ${expectedPolicyFnName} ` + + `on action ${data.action} and event ${data.event}` + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + sandbox.stub(instance, expectedPolicyFnName); + + let action = ac.ASRouterUserEvent(data); + await instance.createASRouterEvent(action); + Assert.ok( + instance[expectedPolicyFnName].calledOnce, + `TelemetryFeed.${expectedPolicyFnName} called` + ); + + sandbox.restore(); + }; + + testCallCorrectPolicy("applyCFRPolicy", { + action: "cfr_user_event", + event: "IMPRESSION", + message_id: "cfr_message_01", + }); + + testCallCorrectPolicy("applyOnboardingPolicy", { + action: "onboarding_user_event", + event: "CLICK_BUTTON", + message_id: "onboarding_message_01", + }); + + testCallCorrectPolicy("applyWhatsNewPolicy", { + action: "whats-new-panel_user_event", + event: "CLICK_BUTTON", + message_id: "whats-new-panel_message_01", + }); + + testCallCorrectPolicy("applyMomentsPolicy", { + action: "moments_user_event", + event: "CLICK_BUTTON", + message_id: "moments_message_01", + }); + + testCallCorrectPolicy("applySpotlightPolicy", { + action: "spotlight_user_event", + event: "CLICK", + message_id: "SPOTLIGHT_MESSAGE_93", + }); + + testCallCorrectPolicy("applyToastNotificationPolicy", { + action: "toast_notification_user_event", + event: "IMPRESSION", + message_id: "TEST_TOAST_NOTIFICATION1", + }); + + testCallCorrectPolicy("applyUndesiredEventPolicy", { + action: "asrouter_undesired_event", + event: "UNDESIRED_EVENT", + }); +}); + +add_task(async function test_createASRouterEvent_stringify_event_context() { + info( + "TelemetryFeed.createASRouterEvent should stringify event_context if " + + "it is an Object" + ); + let instance = new TelemetryFeed(); + let data = { + action: "asrouter_undesired_event", + event: "UNDESIRED_EVENT", + event_context: { foo: "bar" }, + }; + let action = ac.ASRouterUserEvent(data); + let { ping } = await instance.createASRouterEvent(action); + + Assert.equal(ping.event_context, JSON.stringify({ foo: "bar" })); +}); + +add_task(async function test_createASRouterEvent_not_stringify_event_context() { + info( + "TelemetryFeed.createASRouterEvent should not stringify event_context " + + "if it is a String" + ); + let instance = new TelemetryFeed(); + let data = { + action: "asrouter_undesired_event", + event: "UNDESIRED_EVENT", + event_context: "foo", + }; + let action = ac.ASRouterUserEvent(data); + let { ping } = await instance.createASRouterEvent(action); + + Assert.equal(ping.event_context, "foo"); +}); + +add_task(async function test_sendUTEvent_call_right_function() { + info("TelemetryFeed.sendUTEvent should call the UT event function passed in"); + let sandbox = sinon.createSandbox(); + + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true); + + let event = {}; + let instance = new TelemetryFeed(); + sandbox.stub(instance.utEvents, "sendUserEvent"); + instance.addSession("foo"); + + await instance.sendUTEvent(event, instance.utEvents.sendUserEvent); + Assert.ok(instance.utEvents.sendUserEvent.calledWith(event)); + + Services.prefs.clearUserPref(PREF_TELEMETRY); + Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY); + sandbox.restore(); +}); + +add_task(async function test_setLoadTriggerInfo() { + info( + "TelemetryFeed.setLoadTriggerInfo should call saveSessionPerfData " + + "w/load_trigger_{ts,type} data" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + sandbox.stub(instance, "saveSessionPerfData"); + + instance.browserOpenNewtabStart(); + instance.addSession("port123"); + instance.setLoadTriggerInfo("port123"); + + Assert.ok( + instance.saveSessionPerfData.calledWith( + "port123", + sinon.match({ + load_trigger_type: "menu_plus_or_keyboard", + load_trigger_ts: sinon.match.number, + }) + ), + "TelemetryFeed.saveSessionPerfData was called with the right arguments" + ); + + sandbox.restore(); +}); + +add_task(async function test_setLoadTriggerInfo_no_saveSessionPerfData() { + info( + "TelemetryFeed.setLoadTriggerInfo should not call saveSessionPerfData " + + "when getting mark throws" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + sandbox.stub(instance, "saveSessionPerfData"); + + instance.addSession("port123"); + instance.setLoadTriggerInfo("port123"); + + Assert.ok( + instance.saveSessionPerfData.notCalled, + "TelemetryFeed.saveSessionPerfData was not called" + ); + + sandbox.restore(); +}); + +add_task(async function test_saveSessionPerfData_updates_session_with_data() { + info( + "TelemetryFeed.saveSessionPerfData should update the given session " + + "with the given data" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + instance.addSession("port123"); + Assert.equal(instance.sessions.get("port123").fake_ts, undefined); + let data = { fake_ts: 456, other_fake_ts: 789 }; + instance.saveSessionPerfData("port123", data); + + let sessionPerfData = instance.sessions.get("port123").perf; + Assert.equal(sessionPerfData.fake_ts, 456); + Assert.equal(sessionPerfData.other_fake_ts, 789); + + sandbox.restore(); +}); + +add_task(async function test_saveSessionPerfData_calls_setLoadTriggerInfo() { + info( + "TelemetryFeed.saveSessionPerfData should call setLoadTriggerInfo if " + + "data has visibility_event_rcvd_ts" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + sandbox.stub(instance, "setLoadTriggerInfo"); + instance.addSession("port123"); + let data = { visibility_event_rcvd_ts: 444455 }; + + instance.saveSessionPerfData("port123", data); + + Assert.ok( + instance.setLoadTriggerInfo.calledOnce, + "TelemetryFeed.setLoadTriggerInfo was called once" + ); + Assert.ok(instance.setLoadTriggerInfo.calledWithExactly("port123")); + + Assert.equal( + instance.sessions.get("port123").perf.visibility_event_rcvd_ts, + 444455 + ); + + sandbox.restore(); +}); + +add_task( + async function test_saveSessionPerfData_does_not_call_setLoadTriggerInfo() { + info( + "TelemetryFeed.saveSessionPerfData shouldn't call setLoadTriggerInfo if " + + "data has no visibility_event_rcvd_ts" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + sandbox.stub(instance, "setLoadTriggerInfo"); + instance.addSession("port123"); + instance.saveSessionPerfData("port123", { monkeys_ts: 444455 }); + + Assert.ok( + instance.setLoadTriggerInfo.notCalled, + "TelemetryFeed.setLoadTriggerInfo was not called" + ); + + sandbox.restore(); + } +); + +add_task( + async function test_saveSessionPerfData_does_not_call_setLoadTriggerInfo_about_home() { + info( + "TelemetryFeed.saveSessionPerfData should not call setLoadTriggerInfo when " + + "url is about:home" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + sandbox.stub(instance, "setLoadTriggerInfo"); + instance.addSession("port123", "about:home"); + let data = { visibility_event_rcvd_ts: 444455 }; + instance.saveSessionPerfData("port123", data); + + Assert.ok( + instance.setLoadTriggerInfo.notCalled, + "TelemetryFeed.setLoadTriggerInfo was not called" + ); + + sandbox.restore(); + } +); + +add_task( + async function test_saveSessionPerfData_calls_maybeRecordTopsitesPainted() { + info( + "TelemetryFeed.saveSessionPerfData should call maybeRecordTopsitesPainted " + + "when url is about:home and topsites_first_painted_ts is given" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + const TOPSITES_FIRST_PAINTED_TS = 44455; + let data = { topsites_first_painted_ts: TOPSITES_FIRST_PAINTED_TS }; + + sandbox.stub(AboutNewTab, "maybeRecordTopsitesPainted"); + instance.addSession("port123", "about:home"); + instance.saveSessionPerfData("port123", data); + + Assert.ok( + AboutNewTab.maybeRecordTopsitesPainted.calledOnce, + "AboutNewTab.maybeRecordTopsitesPainted called once" + ); + Assert.ok( + AboutNewTab.maybeRecordTopsitesPainted.calledWith( + TOPSITES_FIRST_PAINTED_TS + ) + ); + sandbox.restore(); + } +); + +add_task( + async function test_saveSessionPerfData_records_Glean_newtab_opened_event() { + info( + "TelemetryFeed.saveSessionPerfData should record a Glean newtab.opened event " + + "with the correct visit_id when visibility event received" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + const SESSION_ID = "decafc0ffee"; + const PAGE = "about:newtab"; + let session = { page: PAGE, perf: {}, session_id: SESSION_ID }; + let data = { visibility_event_rcvd_ts: 444455 }; + + sandbox.stub(instance.sessions, "get").returns(session); + instance.saveSessionPerfData("port123", data); + + let newtabOpenedEvents = Glean.newtab.opened.testGetValue(); + Assert.deepEqual(newtabOpenedEvents[0].extra, { + newtab_visit_id: SESSION_ID, + source: PAGE, + }); + + sandbox.restore(); + } +); + +add_task(async function test_uninit_calls_utEvents_uninit() { + info("TelemetryFeed.uninit should call .utEvents.uninit"); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + sandbox.stub(instance.utEvents, "uninit"); + + instance.uninit(); + Assert.ok( + instance.utEvents.uninit.calledOnce, + "TelemetryFeed.utEvents.uninit should be called" + ); + sandbox.restore(); +}); + +add_task(async function test_uninit_deregisters_observer() { + info( + "TelemetryFeed.uninit should make this.browserOpenNewtabStart() stop " + + "observing browser-open-newtab-start" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + let countObservers = () => { + return [...Services.obs.enumerateObservers("browser-open-newtab-start")] + .length; + }; + + const ORIGINAL_COUNT = countObservers(); + instance.init(); + Assert.equal(countObservers(), ORIGINAL_COUNT + 1, "Observer was added"); + + instance.uninit(); + Assert.equal(countObservers(), ORIGINAL_COUNT, "Observer was removed"); + + sandbox.restore(); +}); + +add_task(async function test_onAction_basic_actions() { + let browser = Services.appShell + .createWindowlessBrowser(false) + .document.createElement("browser"); + + let testOnAction = (setupFn, action, checkFn) => { + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + setupFn(sandbox, instance); + + instance.onAction(action); + checkFn(instance); + sandbox.restore(); + }; + + info("TelemetryFeed.onAction should call .init() on an INIT action"); + testOnAction( + (sandbox, instance) => { + sandbox.stub(instance, "init"); + sandbox.stub(instance, "sendPageTakeoverData"); + }, + { type: at.INIT }, + instance => { + Assert.ok(instance.init.calledOnce, "TelemetryFeed.init called once"); + Assert.ok( + instance.sendPageTakeoverData.calledOnce, + "TelemetryFeed.sendPageTakeoverData called once" + ); + } + ); + + info("TelemetryFeed.onAction should call .uninit() on an UNINIT action"); + testOnAction( + (sandbox, instance) => { + sandbox.stub(instance, "uninit"); + }, + { type: at.UNINIT }, + instance => { + Assert.ok(instance.uninit.calledOnce, "TelemetryFeed.uninit called once"); + } + ); + + info( + "TelemetryFeed.onAction should call .handleNewTabInit on a " + + "NEW_TAB_INIT action" + ); + testOnAction( + (sandbox, instance) => { + sandbox.stub(instance, "handleNewTabInit"); + }, + ac.AlsoToMain({ + type: at.NEW_TAB_INIT, + data: { url: "about:newtab", browser }, + }), + instance => { + Assert.ok( + instance.handleNewTabInit.calledOnce, + "TelemetryFeed.handleNewTabInit called once" + ); + } + ); + + info( + "TelemetryFeed.onAction should call .addSession() on a " + + "NEW_TAB_INIT action" + ); + testOnAction( + (sandbox, instance) => { + sandbox.stub(instance, "addSession").returns({ perf: {} }); + sandbox.stub(instance, "setLoadTriggerInfo"); + }, + ac.AlsoToMain( + { + type: at.NEW_TAB_INIT, + data: { url: "about:monkeys", browser }, + }, + "port123" + ), + instance => { + Assert.ok( + instance.addSession.calledOnce, + "TelemetryFeed.addSession called once" + ); + Assert.ok(instance.addSession.calledWith("port123", "about:monkeys")); + } + ); + + info( + "TelemetryFeed.onAction should call .endSession() on a " + + "NEW_TAB_UNLOAD action" + ); + testOnAction( + (sandbox, instance) => { + sandbox.stub(instance, "endSession"); + }, + ac.AlsoToMain({ type: at.NEW_TAB_UNLOAD }, "port123"), + instance => { + Assert.ok( + instance.endSession.calledOnce, + "TelemetryFeed.endSession called once" + ); + Assert.ok(instance.endSession.calledWith("port123")); + } + ); + + info( + "TelemetryFeed.onAction should call .saveSessionPerfData " + + "on SAVE_SESSION_PERF_DATA" + ); + testOnAction( + (sandbox, instance) => { + sandbox.stub(instance, "saveSessionPerfData"); + }, + ac.AlsoToMain( + { type: at.SAVE_SESSION_PERF_DATA, data: { some_ts: 10 } }, + "port123" + ), + instance => { + Assert.ok( + instance.saveSessionPerfData.calledOnce, + "TelemetryFeed.saveSessionPerfData called once" + ); + Assert.ok( + instance.saveSessionPerfData.calledWith("port123", { some_ts: 10 }) + ); + } + ); + + info( + "TelemetryFeed.onAction should send an event on a TELEMETRY_USER_EVENT " + + "action" + ); + Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true); + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + testOnAction( + (sandbox, instance) => { + sandbox.stub(instance, "createUserEvent"); + sandbox.stub(instance.utEvents, "sendUserEvent"); + }, + { type: at.TELEMETRY_USER_EVENT }, + instance => { + Assert.ok( + instance.createUserEvent.calledOnce, + "TelemetryFeed.createUserEvent called once" + ); + Assert.ok( + instance.createUserEvent.calledWith({ type: at.TELEMETRY_USER_EVENT }) + ); + Assert.ok( + instance.utEvents.sendUserEvent.calledOnce, + "TelemetryFeed.utEvents.sendUserEvent called once" + ); + Assert.ok( + instance.utEvents.sendUserEvent.calledWith( + instance.createUserEvent.returnValue + ) + ); + } + ); + Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY); + Services.prefs.clearUserPref(PREF_TELEMETRY); + + info( + "TelemetryFeed.onAction should send an event on a " + + "DISCOVERY_STREAM_USER_EVENT action" + ); + Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true); + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + testOnAction( + (sandbox, instance) => { + sandbox.stub(instance, "createUserEvent"); + sandbox.stub(instance.utEvents, "sendUserEvent"); + }, + { type: at.DISCOVERY_STREAM_USER_EVENT }, + instance => { + Assert.ok( + instance.createUserEvent.calledOnce, + "TelemetryFeed.createUserEvent called once" + ); + Assert.ok( + instance.createUserEvent.calledWith({ + type: at.DISCOVERY_STREAM_USER_EVENT, + data: { + value: { + pocket_logged_in_status: Glean.pocket.isSignedIn.testGetValue(), + }, + }, + }) + ); + Assert.ok( + instance.utEvents.sendUserEvent.calledOnce, + "TelemetryFeed.utEvents.sendUserEvent called once" + ); + Assert.ok( + instance.utEvents.sendUserEvent.calledWith( + instance.createUserEvent.returnValue + ) + ); + } + ); + Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY); + Services.prefs.clearUserPref(PREF_TELEMETRY); +}); + +add_task(async function test_onAction_calls_handleASRouterUserEvent() { + let actions = [ + at.AS_ROUTER_TELEMETRY_USER_EVENT, + msg.TOOLBAR_BADGE_TELEMETRY, + msg.TOOLBAR_PANEL_TELEMETRY, + msg.MOMENTS_PAGE_TELEMETRY, + msg.DOORHANGER_TELEMETRY, + ]; + Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true); + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + actions.forEach(type => { + info(`Testing ${type} action`); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + const eventHandler = sandbox.spy(instance, "handleASRouterUserEvent"); + const action = { + type, + data: { event: "CLICK" }, + }; + + instance.onAction(action); + + Assert.ok(eventHandler.calledWith(action)); + sandbox.restore(); + }); + Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY); + Services.prefs.clearUserPref(PREF_TELEMETRY); +}); + +add_task( + async function test_onAction_calls_handleDiscoveryStreamImpressionStats_ds() { + info( + "TelemetryFeed.onAction should call " + + ".handleDiscoveryStreamImpressionStats on a " + + "DISCOVERY_STREAM_IMPRESSION_STATS action" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + let session = {}; + sandbox.stub(instance.sessions, "get").returns(session); + let data = { source: "foo", tiles: [{ id: 1 }] }; + let action = { type: at.DISCOVERY_STREAM_IMPRESSION_STATS, data }; + sandbox.spy(instance, "handleDiscoveryStreamImpressionStats"); + + instance.onAction(ac.AlsoToMain(action, "port123")); + + Assert.ok( + instance.handleDiscoveryStreamImpressionStats.calledWith("port123", data) + ); + + sandbox.restore(); + } +); + +add_task( + async function test_onAction_calls_handleTopSitesSponsoredImpressionStats() { + info( + "TelemetryFeed.onAction should call " + + ".handleTopSitesSponsoredImpressionStats on a " + + "TOP_SITES_SPONSORED_IMPRESSION_STATS action" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + let session = {}; + sandbox.stub(instance.sessions, "get").returns(session); + let data = { type: "impression", tile_id: 42, position: 1 }; + let action = { type: at.TOP_SITES_SPONSORED_IMPRESSION_STATS, data }; + sandbox.spy(instance, "handleTopSitesSponsoredImpressionStats"); + + instance.onAction(ac.AlsoToMain(action)); + + Assert.ok( + instance.handleTopSitesSponsoredImpressionStats.calledOnce, + "TelemetryFeed.handleTopSitesSponsoredImpressionStats called once" + ); + Assert.deepEqual( + instance.handleTopSitesSponsoredImpressionStats.firstCall.args[0].data, + data + ); + + sandbox.restore(); + } +); + +add_task(async function test_onAction_calls_handleAboutSponsoredTopSites() { + info( + "TelemetryFeed.onAction should call " + + ".handleAboutSponsoredTopSites on a " + + "ABOUT_SPONSORED_TOP_SITES action" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + let data = { position: 0, advertiser_name: "moo", tile_id: 42 }; + let action = { type: at.ABOUT_SPONSORED_TOP_SITES, data }; + sandbox.spy(instance, "handleAboutSponsoredTopSites"); + + instance.onAction(ac.AlsoToMain(action)); + + Assert.ok( + instance.handleAboutSponsoredTopSites.calledOnce, + "TelemetryFeed.handleAboutSponsoredTopSites called once" + ); + + sandbox.restore(); +}); + +add_task(async function test_onAction_calls_handleBlockUrl() { + info( + "TelemetryFeed.onAction should call #handleBlockUrl on a BLOCK_URL action" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + let data = { position: 0, advertiser_name: "moo", tile_id: 42 }; + let action = { type: at.BLOCK_URL, data }; + sandbox.spy(instance, "handleBlockUrl"); + + instance.onAction(ac.AlsoToMain(action)); + + Assert.ok( + instance.handleBlockUrl.calledOnce, + "TelemetryFeed.handleBlockUrl called once" + ); + + sandbox.restore(); +}); + +add_task( + async function test_onAction_calls_handleTopSitesOrganicImpressionStats() { + info( + "TelemetryFeed.onAction should call .handleTopSitesOrganicImpressionStats " + + "on a TOP_SITES_ORGANIC_IMPRESSION_STATS action" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + let session = {}; + sandbox.stub(instance.sessions, "get").returns(session); + + let data = { type: "impression", position: 1 }; + let action = { type: at.TOP_SITES_ORGANIC_IMPRESSION_STATS, data }; + sandbox.spy(instance, "handleTopSitesOrganicImpressionStats"); + + instance.onAction(ac.AlsoToMain(action)); + + Assert.ok( + instance.handleTopSitesOrganicImpressionStats.calledOnce, + "TelemetryFeed.handleTopSitesOrganicImpressionStats called once" + ); + Assert.deepEqual( + instance.handleTopSitesOrganicImpressionStats.firstCall.args[0].data, + data + ); + + sandbox.restore(); + } +); + +add_task(async function test_handleNewTabInit_sets_preloaded_session() { + info( + "TelemetryFeed.handleNewTabInit should set the session as preloaded " + + "if the browser is preloaded" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + let session = { perf: {} }; + let preloadedBrowser = { + getAttribute() { + return "preloaded"; + }, + }; + sandbox.stub(instance, "addSession").returns(session); + + instance.onAction( + ac.AlsoToMain({ + type: at.NEW_TAB_INIT, + data: { url: "about:newtab", browser: preloadedBrowser }, + }) + ); + + Assert.ok(session.perf.is_preloaded, "is_preloaded property was set"); + + sandbox.restore(); +}); + +add_task(async function test_handleNewTabInit_sets_nonpreloaded_session() { + info( + "TelemetryFeed.handleNewTabInit should set the session as non-preloaded " + + "if the browser is non-preloaded" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + let session = { perf: {} }; + let preloadedBrowser = { + getAttribute() { + return ""; + }, + }; + sandbox.stub(instance, "addSession").returns(session); + + instance.onAction( + ac.AlsoToMain({ + type: at.NEW_TAB_INIT, + data: { url: "about:newtab", browser: preloadedBrowser }, + }) + ); + + Assert.ok(!session.perf.is_preloaded, "is_preloaded property is not true"); + + sandbox.restore(); +}); + +add_task( + async function test_SendASRouterUndesiredEvent_calls_handleASRouterUserEvent() { + info( + "TelemetryFeed.SendASRouterUndesiredEvent should call " + + "handleASRouterUserEvent" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + sandbox.stub(instance, "handleASRouterUserEvent"); + + instance.SendASRouterUndesiredEvent({ foo: "bar" }); + + Assert.ok( + instance.handleASRouterUserEvent.calledOnce, + "TelemetryFeed.handleASRouterUserEvent was called once" + ); + let [payload] = instance.handleASRouterUserEvent.firstCall.args; + Assert.equal(payload.data.action, "asrouter_undesired_event"); + Assert.equal(payload.data.foo, "bar"); + + sandbox.restore(); + } +); + +add_task(async function test_sendPageTakeoverData_homepage_category() { + info( + "TelemetryFeed.sendPageTakeoverData should call " + + "handleASRouterUserEvent" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + sandbox.stub(HomePage, "get").returns("https://searchprovider.com"); + instance._classifySite = () => Promise.resolve("other"); + + await instance.sendPageTakeoverData(); + Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "other"); + + Services.prefs.clearUserPref(PREF_TELEMETRY); + sandbox.restore(); +}); + +add_task(async function test_sendPageTakeoverData_newtab_category_custom() { + info( + "TelemetryFeed.sendPageTakeoverData should send correct newtab " + + "category for about:newtab set to custom URL" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + sandbox.stub(AboutNewTab, "newTabURLOverridden").get(() => true); + sandbox + .stub(AboutNewTab, "newTabURL") + .get(() => "https://searchprovider.com"); + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + instance._classifySite = () => Promise.resolve("other"); + + await instance.sendPageTakeoverData(); + Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "other"); + + Services.prefs.clearUserPref(PREF_TELEMETRY); + sandbox.restore(); +}); + +add_task(async function test_sendPageTakeoverData_newtab_category_custom() { + info( + "TelemetryFeed.sendPageTakeoverData should not set home|newtab " + + "category if neither about:{home,newtab} are set to custom URL" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + instance._classifySite = () => Promise.resolve("other"); + + await instance.sendPageTakeoverData(); + Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "enabled"); + Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "enabled"); + + Services.prefs.clearUserPref(PREF_TELEMETRY); + sandbox.restore(); +}); + +add_task(async function test_sendPageTakeoverData_newtab_category_extension() { + info( + "TelemetryFeed.sendPageTakeoverData should set correct home|newtab " + + "category when changed by extension" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + const ID = "{abc-foo-bar}"; + sandbox.stub(ExtensionSettingsStore, "getSetting").returns({ id: ID }); + + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + instance._classifySite = () => Promise.resolve("other"); + + await instance.sendPageTakeoverData(); + Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "extension"); + Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "extension"); + + Services.prefs.clearUserPref(PREF_TELEMETRY); + sandbox.restore(); +}); + +add_task(async function test_sendPageTakeoverData_newtab_disabled() { + info( + "TelemetryFeed.sendPageTakeoverData instruments when newtab is disabled" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + Services.prefs.setBoolPref("browser.newtabpage.enabled", false); + instance._classifySite = () => Promise.resolve("other"); + + await instance.sendPageTakeoverData(); + Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "disabled"); + + Services.prefs.clearUserPref(PREF_TELEMETRY); + Services.prefs.clearUserPref("browser.newtabpage.enabled"); + sandbox.restore(); +}); + +add_task(async function test_sendPageTakeoverData_homepage_disabled() { + info( + "TelemetryFeed.sendPageTakeoverData instruments when homepage is disabled" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + sandbox.stub(HomePage, "overridden").get(() => true); + + await instance.sendPageTakeoverData(); + Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "disabled"); + + Services.prefs.clearUserPref(PREF_TELEMETRY); + sandbox.restore(); +}); + +add_task(async function test_sendPageTakeoverData_newtab_ping() { + info("TelemetryFeed.sendPageTakeoverData should send a 'newtab' ping"); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + + let pingSubmitted = new Promise(resolve => { + GleanPings.newtab.testBeforeNextSubmit(reason => { + Assert.equal(reason, "component_init"); + resolve(); + }); + }); + + await instance.sendPageTakeoverData(); + await pingSubmitted; + + Services.prefs.clearUserPref(PREF_TELEMETRY); + sandbox.restore(); +}); + +add_task( + async function test_handleDiscoveryStreamImpressionStats_should_throw() { + info( + "TelemetryFeed.handleDiscoveryStreamImpressionStats should throw " + + "for a missing session" + ); + + let instance = new TelemetryFeed(); + try { + instance.handleDiscoveryStreamImpressionStats("a_missing_port", {}); + Assert.ok(false, "Should not have reached here."); + } catch (e) { + Assert.ok(true, "Should have thrown for a missing session."); + } + } +); + +add_task( + async function test_handleDiscoveryStreamImpressionStats_instrument_pocket_impressions() { + info( + "TelemetryFeed.handleDiscoveryStreamImpressionStats should throw " + + "for a missing session" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + const SESSION_ID = "1337cafe"; + const POS_1 = 1; + const POS_2 = 4; + const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ="; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + let pingSubmitted = new Promise(resolve => { + GleanPings.spoc.testBeforeNextSubmit(reason => { + Assert.equal(reason, "impression"); + + let pocketImpressions = Glean.pocket.impression.testGetValue(); + Assert.equal(pocketImpressions.length, 2); + Assert.deepEqual(pocketImpressions[0].extra, { + newtab_visit_id: SESSION_ID, + is_sponsored: String(false), + position: String(POS_1), + recommendation_id: "decaf-c0ff33", + tile_id: String(1), + }); + Assert.deepEqual(pocketImpressions[1].extra, { + newtab_visit_id: SESSION_ID, + is_sponsored: String(true), + position: String(POS_2), + tile_id: String(2), + }); + Assert.equal(Glean.pocket.shim.testGetValue(), SHIM); + + resolve(); + }); + }); + + instance.handleDiscoveryStreamImpressionStats("_", { + source: "foo", + tiles: [ + { + id: 1, + pos: POS_1, + type: "organic", + recommendation_id: "decaf-c0ff33", + }, + { + id: 2, + pos: POS_2, + type: "spoc", + recommendation_id: undefined, + shim: SHIM, + }, + ], + window_inner_width: 1000, + window_inner_height: 900, + }); + + await pingSubmitted; + + sandbox.restore(); + } +); + +add_task( + async function test_handleASRouterUserEvent_calls_submitGleanPingForPing() { + info( + "TelemetryFeed.handleASRouterUserEvent should call " + + "submitGleanPingForPing on known pingTypes when telemetry is enabled" + ); + + let data = { + action: "onboarding_user_event", + event: "IMPRESSION", + message_id: "12345", + }; + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + + sandbox.spy(AboutWelcomeTelemetry.prototype, "submitGleanPingForPing"); + + await instance.handleASRouterUserEvent({ data }); + + Assert.ok( + AboutWelcomeTelemetry.prototype.submitGleanPingForPing.calledOnce, + "AboutWelcomeTelemetry.submitGleanPingForPing called once" + ); + + Services.prefs.clearUserPref(PREF_TELEMETRY); + sandbox.restore(); + } +); + +add_task( + async function test_handleASRouterUserEvent_no_submit_unknown_pingTypes() { + info( + "TelemetryFeed.handleASRouterUserEvent not submit pings on unknown pingTypes" + ); + + let data = { + action: "unknown_event", + event: "IMPRESSION", + message_id: "12345", + }; + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + Services.prefs.setBoolPref(PREF_TELEMETRY, true); + + sandbox.spy(AboutWelcomeTelemetry.prototype, "submitGleanPingForPing"); + + await instance.handleASRouterUserEvent({ data }); + + Assert.ok( + AboutWelcomeTelemetry.prototype.submitGleanPingForPing.notCalled, + "AboutWelcomeTelemetry.submitGleanPingForPing not called" + ); + + Services.prefs.clearUserPref(PREF_TELEMETRY); + sandbox.restore(); + } +); + +add_task( + async function test_isInCFRCohort_return_false_for_no_CFR_experiment() { + info( + "TelemetryFeed.isInCFRCohort should return false if there " + + "is no CFR experiment registered" + ); + let instance = new TelemetryFeed(); + Assert.ok( + !instance.isInCFRCohort, + "Should not be in CFR cohort by default" + ); + } +); + +add_task( + async function test_isInCFRCohort_return_true_for_registered_CFR_experiment() { + info( + "TelemetryFeed.isInCFRCohort should return true if there " + + "is a CFR experiment registered" + ); + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + + sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({ + slug: "SOME-CFR-EXP", + }); + + Assert.ok(instance.isInCFRCohort, "Should be in a CFR cohort"); + Assert.equal( + ExperimentAPI.getExperimentMetaData.firstCall.args[0].featureId, + "cfr" + ); + + sandbox.restore(); + } +); + +add_task( + async function test_handleTopSitesSponsoredImpressionStats_add_keyed_scalar() { + info( + "TelemetryFeed.handleTopSitesSponsoredImpressionStats should add to " + + "keyed scalar on an impression event" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.telemetry.clearScalars(); + + let data = { + type: "impression", + tile_id: 42, + source: "newtab", + position: 0, + reporting_url: "https://test.reporting.net/", + }; + await instance.handleTopSitesSponsoredImpressionStats({ data }); + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "contextual.services.topsites.impression", + "newtab_1", + 1 + ); + + sandbox.restore(); + } +); + +add_task( + async function test_handleTopSitesSponsoredImpressionStats_add_keyed_scalar_click() { + info( + "TelemetryFeed.handleTopSitesSponsoredImpressionStats should add to " + + "keyed scalar on a click event" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.telemetry.clearScalars(); + + let data = { + type: "click", + tile_id: 42, + source: "newtab", + position: 0, + reporting_url: "https://test.reporting.net/", + }; + await instance.handleTopSitesSponsoredImpressionStats({ data }); + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "contextual.services.topsites.click", + "newtab_1", + 1 + ); + + sandbox.restore(); + } +); + +add_task( + async function test_handleTopSitesSponsoredImpressionStats_record_glean_impression() { + info( + "TelemetryFeed.handleTopSitesSponsoredImpressionStats should record a " + + "Glean topsites.impression event on an impression event" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + let data = { + type: "impression", + tile_id: 42, + source: "newtab", + position: 1, + reporting_url: "https://test.reporting.net/", + advertiser: "adnoid ads", + }; + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + await instance.handleTopSitesSponsoredImpressionStats({ data }); + let impressions = Glean.topsites.impression.testGetValue(); + Assert.equal(impressions.length, 1, "Should have recorded 1 impression"); + + Assert.deepEqual(impressions[0].extra, { + advertiser_name: "adnoid ads", + tile_id: data.tile_id, + newtab_visit_id: SESSION_ID, + is_sponsored: String(true), + position: String(1), + }); + + sandbox.restore(); + } +); + +add_task( + async function test_handleTopSitesSponsoredImpressionStats_record_glean_click() { + info( + "TelemetryFeed.handleTopSitesSponsoredImpressionStats should record " + + "a Glean topsites.click event on a click event" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + let data = { + type: "click", + advertiser: "test advertiser", + tile_id: 42, + source: "newtab", + position: 0, + reporting_url: "https://test.reporting.net/", + }; + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + await instance.handleTopSitesSponsoredImpressionStats({ data }); + let clicks = Glean.topsites.click.testGetValue(); + Assert.equal(clicks.length, 1, "Should have recorded 1 click"); + + Assert.deepEqual(clicks[0].extra, { + advertiser_name: "test advertiser", + tile_id: data.tile_id, + newtab_visit_id: SESSION_ID, + is_sponsored: String(true), + position: String(0), + }); + + sandbox.restore(); + } +); + +add_task( + async function test_handleTopSitesSponsoredImpressionStats_no_submit_unknown_pingType() { + info( + "TelemetryFeed.handleTopSitesSponsoredImpressionStats should not " + + "submit on unknown pingTypes" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + let data = { type: "unknown_type" }; + + await instance.handleTopSitesSponsoredImpressionStats({ data }); + let impressions = Glean.topsites.impression.testGetValue(); + Assert.ok(!impressions, "Should not have recorded any impressions"); + + sandbox.restore(); + } +); + +add_task( + async function test_handleTopSitesOrganicImpressionStats_record_glean_topsites_impression() { + info( + "TelemetryFeed.handleTopSitesOrganicImpressionStats should record a " + + "Glean topsites.impression event on an impression event" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + let data = { + type: "impression", + source: "newtab", + position: 0, + }; + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + await instance.handleTopSitesOrganicImpressionStats({ data }); + let impressions = Glean.topsites.impression.testGetValue(); + Assert.equal(impressions.length, 1, "Recorded 1 impression"); + + Assert.deepEqual(impressions[0].extra, { + newtab_visit_id: SESSION_ID, + is_sponsored: String(false), + position: String(0), + }); + + sandbox.restore(); + } +); + +add_task( + async function test_handleTopSitesOrganicImpressionStats_record_glean_topsites_click() { + info( + "TelemetryFeed.handleTopSitesOrganicImpressionStats should record a " + + "Glean topsites.click event on a click event" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + let data = { + type: "click", + source: "newtab", + position: 0, + }; + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + await instance.handleTopSitesOrganicImpressionStats({ data }); + let clicks = Glean.topsites.click.testGetValue(); + Assert.equal(clicks.length, 1, "Recorded 1 click"); + + Assert.deepEqual(clicks[0].extra, { + newtab_visit_id: SESSION_ID, + is_sponsored: String(false), + position: String(0), + }); + + sandbox.restore(); + } +); + +add_task( + async function test_handleTopSitesOrganicImpressionStats_no_recording() { + info( + "TelemetryFeed.handleTopSitesOrganicImpressionStats should not " + + "record events on an unknown session" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + sandbox.stub(instance.sessions, "get").returns(false); + + await instance.handleTopSitesOrganicImpressionStats({}); + Assert.ok(!Glean.topsites.click.testGetValue(), "Click was not recorded"); + Assert.ok( + !Glean.topsites.impression.testGetValue(), + "Impression was not recorded" + ); + + sandbox.restore(); + } +); + +add_task( + async function test_handleTopSitesOrganicImpressionStats_no_recording_with_session() { + info( + "TelemetryFeed.handleTopSitesOrganicImpressionStats should not record " + + "events on an unknown impressionStats action" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + await instance.handleTopSitesOrganicImpressionStats({ type: "unknown" }); + Assert.ok(!Glean.topsites.click.testGetValue(), "Click was not recorded"); + Assert.ok( + !Glean.topsites.impression.testGetValue(), + "Impression was not recorded" + ); + + sandbox.restore(); + } +); + +add_task( + async function test_handleDiscoveryStreamUserEvent_no_recording_with_session() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " + + "action with no `data`" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + let action = ac.DiscoveryStreamUserEvent(); + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleDiscoveryStreamUserEvent(action); + Assert.ok( + !Glean.pocket.topicClick.testGetValue(), + "Pocket topicClick was not recorded" + ); + Assert.ok( + !Glean.pocket.click.testGetValue(), + "Pocket click was not recorded" + ); + Assert.ok( + !Glean.pocket.save.testGetValue(), + "Pocket save was not recorded" + ); + + sandbox.restore(); + } +); + +add_task( + async function test_handleDiscoveryStreamUserEvent_click_with_no_value() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " + + "CLICK data with no value" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + let action = ac.DiscoveryStreamUserEvent({ + event: "CLICK", + source: "POPULAR_TOPICS", + }); + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleDiscoveryStreamUserEvent(action); + let topicClicks = Glean.pocket.topicClick.testGetValue(); + Assert.equal(topicClicks.length, 1, "Recorded 1 click"); + Assert.deepEqual(topicClicks[0].extra, { + newtab_visit_id: SESSION_ID, + }); + + sandbox.restore(); + } +); + +add_task( + async function test_handleDiscoveryStreamUserEvent_non_popular_click_with_no_value() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " + + "non-POPULAR_TOPICS CLICK data with no value" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + let action = ac.DiscoveryStreamUserEvent({ + event: "CLICK", + source: "not-POPULAR_TOPICS", + }); + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleDiscoveryStreamUserEvent(action); + Assert.ok( + !Glean.pocket.topicClick.testGetValue(), + "Pocket topicClick was not recorded" + ); + Assert.ok( + !Glean.pocket.click.testGetValue(), + "Pocket click was not recorded" + ); + Assert.ok( + !Glean.pocket.save.testGetValue(), + "Pocket save was not recorded" + ); + + sandbox.restore(); + } +); + +add_task( + async function test_handleDiscoveryStreamUserEvent_non_popular_click() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " + + "CLICK data with non-POPULAR_TOPICS source" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + const TOPIC = "atopic"; + let action = ac.DiscoveryStreamUserEvent({ + event: "CLICK", + source: "not-POPULAR_TOPICS", + value: { + card_type: "topics_widget", + topic: TOPIC, + }, + }); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleDiscoveryStreamUserEvent(action); + let topicClicks = Glean.pocket.topicClick.testGetValue(); + Assert.equal(topicClicks.length, 1, "Recorded 1 click"); + Assert.deepEqual(topicClicks[0].extra, { + newtab_visit_id: SESSION_ID, + topic: TOPIC, + }); + + sandbox.restore(); + } +); + +add_task( + async function test_handleDiscoveryStreamUserEvent_without_card_type() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent doesn't instrument " + + "a CLICK without a card_type" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + let action = ac.DiscoveryStreamUserEvent({ + event: "CLICK", + source: "not-POPULAR_TOPICS", + value: { + card_type: "not spoc, organic, or topics_widget", + }, + }); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleDiscoveryStreamUserEvent(action); + + Assert.ok( + !Glean.pocket.topicClick.testGetValue(), + "Pocket topicClick was not recorded" + ); + Assert.ok( + !Glean.pocket.click.testGetValue(), + "Pocket click was not recorded" + ); + Assert.ok( + !Glean.pocket.save.testGetValue(), + "Pocket save was not recorded" + ); + + sandbox.restore(); + } +); + +add_task(async function test_handleDiscoveryStreamUserEvent_popular_click() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a popular " + + "topic click" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + const TOPIC = "entertainment"; + let action = ac.DiscoveryStreamUserEvent({ + event: "CLICK", + source: "POPULAR_TOPICS", + value: { + card_type: "topics_widget", + topic: TOPIC, + }, + }); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleDiscoveryStreamUserEvent(action); + let topicClicks = Glean.pocket.topicClick.testGetValue(); + Assert.equal(topicClicks.length, 1, "Recorded 1 click"); + Assert.deepEqual(topicClicks[0].extra, { + newtab_visit_id: SESSION_ID, + topic: TOPIC, + }); + + sandbox.restore(); +}); + +add_task( + async function test_handleDiscoveryStreamUserEvent_organic_top_stories_click() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent instruments an organic " + + "top stories click" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + const ACTION_POSITION = 42; + let action = ac.DiscoveryStreamUserEvent({ + event: "CLICK", + action_position: ACTION_POSITION, + value: { + card_type: "organic", + recommendation_id: "decaf-c0ff33", + tile_id: 314623757745896, + }, + }); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleDiscoveryStreamUserEvent(action); + + let clicks = Glean.pocket.click.testGetValue(); + Assert.equal(clicks.length, 1, "Recorded 1 click"); + Assert.deepEqual(clicks[0].extra, { + newtab_visit_id: SESSION_ID, + is_sponsored: String(false), + position: ACTION_POSITION, + recommendation_id: "decaf-c0ff33", + tile_id: String(314623757745896), + }); + + Assert.ok( + !Glean.pocket.shim.testGetValue(), + "Pocket shim was not recorded" + ); + + sandbox.restore(); + } +); + +add_task( + async function test_handleDiscoveryStreamUserEvent_sponsored_top_stories_click() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a sponsored " + + "top stories click" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + const ACTION_POSITION = 42; + const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ="; + let action = ac.DiscoveryStreamUserEvent({ + event: "CLICK", + action_position: ACTION_POSITION, + value: { + card_type: "spoc", + recommendation_id: undefined, + tile_id: 448685088, + shim: SHIM, + }, + }); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + let pingSubmitted = new Promise(resolve => { + GleanPings.spoc.testBeforeNextSubmit(reason => { + Assert.equal(reason, "click"); + resolve(); + }); + }); + + instance.handleDiscoveryStreamUserEvent(action); + + let clicks = Glean.pocket.click.testGetValue(); + Assert.equal(clicks.length, 1, "Recorded 1 click"); + Assert.deepEqual(clicks[0].extra, { + newtab_visit_id: SESSION_ID, + is_sponsored: String(true), + position: ACTION_POSITION, + tile_id: String(448685088), + }); + + await pingSubmitted; + + sandbox.restore(); + } +); + +add_task( + async function test_handleDiscoveryStreamUserEvent_organic_top_stories_save() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a save of an " + + "organic top story" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + const ACTION_POSITION = 42; + let action = ac.DiscoveryStreamUserEvent({ + event: "SAVE_TO_POCKET", + action_position: ACTION_POSITION, + value: { + card_type: "organic", + recommendation_id: "decaf-c0ff33", + tile_id: 314623757745896, + }, + }); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleDiscoveryStreamUserEvent(action); + + let saves = Glean.pocket.save.testGetValue(); + Assert.equal(saves.length, 1, "Recorded 1 save"); + Assert.deepEqual(saves[0].extra, { + newtab_visit_id: SESSION_ID, + is_sponsored: String(false), + position: ACTION_POSITION, + recommendation_id: "decaf-c0ff33", + tile_id: String(314623757745896), + }); + Assert.ok( + !Glean.pocket.shim.testGetValue(), + "Pocket shim was not recorded" + ); + + sandbox.restore(); + } +); + +add_task( + async function test_handleDiscoveryStreamUserEvent_sponsored_top_stories_save() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a save of a " + + "sponsored top story" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + const ACTION_POSITION = 42; + const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ="; + let action = ac.DiscoveryStreamUserEvent({ + event: "SAVE_TO_POCKET", + action_position: ACTION_POSITION, + value: { + card_type: "spoc", + recommendation_id: undefined, + tile_id: 448685088, + shim: SHIM, + }, + }); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + let pingSubmitted = new Promise(resolve => { + GleanPings.spoc.testBeforeNextSubmit(reason => { + Assert.equal(reason, "save"); + Assert.equal( + Glean.pocket.shim.testGetValue(), + SHIM, + "Pocket shim was recorded" + ); + + resolve(); + }); + }); + + instance.handleDiscoveryStreamUserEvent(action); + + let saves = Glean.pocket.save.testGetValue(); + Assert.equal(saves.length, 1, "Recorded 1 save"); + Assert.deepEqual(saves[0].extra, { + newtab_visit_id: SESSION_ID, + is_sponsored: String(true), + position: ACTION_POSITION, + tile_id: String(448685088), + }); + + await pingSubmitted; + + sandbox.restore(); + } +); + +add_task( + async function test_handleDiscoveryStreamUserEvent_sponsored_top_stories_save_no_value() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a save of a " + + "sponsored top story, without `value`" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + const ACTION_POSITION = 42; + let action = ac.DiscoveryStreamUserEvent({ + event: "SAVE_TO_POCKET", + action_position: ACTION_POSITION, + }); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleDiscoveryStreamUserEvent(action); + + let saves = Glean.pocket.save.testGetValue(); + Assert.equal(saves.length, 1, "Recorded 1 save"); + Assert.deepEqual(saves[0].extra, { + newtab_visit_id: SESSION_ID, + is_sponsored: String(false), + position: ACTION_POSITION, + }); + Assert.ok( + !Glean.pocket.shim.testGetValue(), + "Pocket shim was not recorded" + ); + + sandbox.restore(); + } +); + +add_task( + async function test_handleAboutSponsoredTopSites_record_showPrivacyClick() { + info( + "TelemetryFeed.handleAboutSponsoredTopSites should record a Glean " + + "topsites.showPrivacyClick event on action" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + let data = { + position: 42, + advertiser_name: "mozilla", + tile_id: 4567, + }; + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleAboutSponsoredTopSites({ data }); + + let clicks = Glean.topsites.showPrivacyClick.testGetValue(); + Assert.equal(clicks.length, 1, "Recorded 1 click"); + Assert.deepEqual(clicks[0].extra, { + advertiser_name: data.advertiser_name, + tile_id: String(data.tile_id), + newtab_visit_id: SESSION_ID, + position: String(data.position), + }); + + sandbox.restore(); + } +); + +add_task( + async function test_handleAboutSponsoredTopSites_no_record_showPrivacyClick() { + info( + "TelemetryFeed.handleAboutSponsoredTopSites should not record a Glean " + + "topsites.showPrivacyClick event if there's no session" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + let data = { + position: 42, + advertiser_name: "mozilla", + tile_id: 4567, + }; + + sandbox.stub(instance.sessions, "get").returns(null); + + instance.handleAboutSponsoredTopSites({ data }); + + let clicks = Glean.topsites.showPrivacyClick.testGetValue(); + Assert.ok(!clicks, "Did not record any clicks"); + + sandbox.restore(); + } +); + +add_task(async function test_handleBlockUrl_no_record_dismisses() { + info( + "TelemetryFeed.handleBlockUrl shouldn't record events for pocket " + + "cards' dismisses" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + let data = [ + { + // Shouldn't record anything for this one + is_pocket_card: true, + position: 43, + tile_id: undefined, + }, + ]; + + await instance.handleBlockUrl({ data }); + + Assert.ok( + !Glean.topsites.dismiss.testGetValue(), + "Should not record a dismiss for Pocket cards" + ); + + sandbox.restore(); +}); + +add_task(async function test_handleBlockUrl_record_dismiss_on_action() { + info( + "TelemetryFeed.handleBlockUrl should record a topsites.dismiss event " + + "on action" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + let data = [ + { + is_pocket_card: false, + position: 42, + advertiser_name: "mozilla", + tile_id: 4567, + isSponsoredTopSite: 1, // for some reason this is an int. + }, + ]; + + await instance.handleBlockUrl({ data }); + + let dismisses = Glean.topsites.dismiss.testGetValue(); + Assert.equal(dismisses.length, 1, "Should have recorded 1 dismiss"); + Assert.deepEqual(dismisses[0].extra, { + advertiser_name: data[0].advertiser_name, + tile_id: String(data[0].tile_id), + newtab_visit_id: SESSION_ID, + is_sponsored: String(!!data[0].isSponsoredTopSite), + position: String(data[0].position), + }); + + sandbox.restore(); +}); + +add_task( + async function test_handleBlockUrl_record_dismiss_on_nonsponsored_action() { + info( + "TelemetryFeed.handleBlockUrl should record a Glean topsites.dismiss " + + "event on action on non-sponsored topsite" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + let data = [ + { + is_pocket_card: false, + position: 42, + tile_id: undefined, + }, + ]; + + await instance.handleBlockUrl({ data }); + + let dismisses = Glean.topsites.dismiss.testGetValue(); + Assert.equal(dismisses.length, 1, "Should have recorded 1 dismiss"); + Assert.deepEqual(dismisses[0].extra, { + newtab_visit_id: SESSION_ID, + is_sponsored: String(false), + position: String(data[0].position), + }); + + sandbox.restore(); + } +); + +add_task(async function test_handleBlockUrl_no_record_dismiss_on_no_session() { + info( + "TelemetryFeed.handleBlockUrl should not record a Glean " + + "topsites.dismiss event if there's no session" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + + sandbox.stub(instance.sessions, "get").returns(null); + + let data = {}; + + await instance.handleBlockUrl({ data }); + + Assert.ok( + !Glean.topsites.dismiss.testGetValue(), + "Should not have recorded a dismiss" + ); + + sandbox.restore(); +}); |