tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_TelemetryFeed.js (83046B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { updateAppInfo } = ChromeUtils.importESModule(
      7  "resource://testing-common/AppInfo.sys.mjs"
      8 );
      9 
     10 ChromeUtils.defineESModuleGetters(this, {
     11  AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
     12  ContextId: "moz-src:///browser/modules/ContextId.sys.mjs",
     13  actionCreators: "resource://newtab/common/Actions.mjs",
     14  actionTypes: "resource://newtab/common/Actions.mjs",
     15  ExtensionSettingsStore:
     16    "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
     17  HomePage: "resource:///modules/HomePage.sys.mjs",
     18  JsonSchemaValidator:
     19    "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
     20  NewTabContentPing: "resource://newtab/lib/NewTabContentPing.sys.mjs",
     21  sinon: "resource://testing-common/Sinon.sys.mjs",
     22  TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
     23  TelemetryFeed: "resource://newtab/lib/TelemetryFeed.sys.mjs",
     24  TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
     25  USER_PREFS_ENCODING: "resource://newtab/lib/TelemetryFeed.sys.mjs",
     26  UTEventReporting: "resource://newtab/lib/UTEventReporting.sys.mjs",
     27 });
     28 
     29 const FAKE_UUID = "{foo-123-foo}";
     30 const PREF_IMPRESSION_ID = "browser.newtabpage.activity-stream.impressionId";
     31 const PREF_TELEMETRY = "browser.newtabpage.activity-stream.telemetry";
     32 const PREF_PRIVATE_PING_ENABLED =
     33  "browser.newtabpage.activity-stream.telemetry.privatePing.enabled";
     34 const PREF_REDACT_NEWTAB_PING_ENABLED =
     35  "browser.newtabpage.activity-stream.telemetry.privatePing.redactNewtabPing.enabled";
     36 const PREF_EVENT_TELEMETRY =
     37  "browser.newtabpage.activity-stream.telemetry.ut.events";
     38 
     39 let BasePingSchemaPromise;
     40 let SessionPingSchemaPromise;
     41 let UserEventPingSchemaPromise;
     42 
     43 function assertPingMatchesSchema(pingKind, ping, schema) {
     44  // Unlike the validator from JsonSchema.sys.mjs, JsonSchemaValidator
     45  // lets us opt-in to having "undefined" properties, which are then
     46  // ignored. This is fine because the ping is sent as a JSON string
     47  // over an XHR, and undefined properties are culled as part of the
     48  // JSON encoding process.
     49  let result = JsonSchemaValidator.validate(ping, schema, {
     50    allowExplicitUndefinedProperties: true,
     51  });
     52 
     53  if (!result.valid) {
     54    info(`${pingKind} failed to validate against the schema: ${result.error}`);
     55  }
     56 
     57  Assert.ok(result.valid, `${pingKind} is valid against the schema.`);
     58 }
     59 
     60 async function assertSessionPingValid(ping) {
     61  let schema = await SessionPingSchemaPromise;
     62  assertPingMatchesSchema("SessionPing", ping, schema);
     63 }
     64 
     65 async function assertBasePingValid(ping) {
     66  let schema = await BasePingSchemaPromise;
     67  assertPingMatchesSchema("BasePing", ping, schema);
     68 }
     69 
     70 async function assertUserEventPingValid(ping) {
     71  let schema = await UserEventPingSchemaPromise;
     72  assertPingMatchesSchema("UserEventPing", ping, schema);
     73 }
     74 
     75 add_setup(async function setup() {
     76  BasePingSchemaPromise = IOUtils.readJSON(
     77    do_get_file("../schemas/base_ping.schema.json").path
     78  );
     79 
     80  SessionPingSchemaPromise = IOUtils.readJSON(
     81    do_get_file("../schemas/session_ping.schema.json").path
     82  );
     83 
     84  UserEventPingSchemaPromise = IOUtils.readJSON(
     85    do_get_file("../schemas/user_event_ping.schema.json").path
     86  );
     87 
     88  do_get_profile();
     89  // FOG needs to be initialized in order for data to flow.
     90  Services.fog.initializeFOG();
     91 
     92  await TelemetryController.testReset();
     93 
     94  updateAppInfo({
     95    name: "XPCShell",
     96    ID: "xpcshell@tests.mozilla.org",
     97    version: "122",
     98    platformVersion: "122",
     99  });
    100 
    101  let sandbox = sinon.createSandbox();
    102  sandbox.stub(ContextId, "requestSynchronously").returns(FAKE_UUID);
    103 
    104  registerCleanupFunction(() => {
    105    sandbox.restore();
    106  });
    107 });
    108 
    109 add_task(async function test_construction() {
    110  let testInstance = new TelemetryFeed();
    111  Assert.ok(
    112    testInstance,
    113    "Should have been able to create an instance of TelemetryFeed."
    114  );
    115  Assert.ok(
    116    testInstance.utEvents instanceof UTEventReporting,
    117    "Should add .utEvents, a UTEventReporting instance."
    118  );
    119  Assert.ok(
    120    testInstance._impressionId,
    121    "Should create impression id if none exists"
    122  );
    123 });
    124 
    125 add_task(async function test_load_impressionId() {
    126  info(
    127    "Constructing a TelemetryFeed should use a saved impression ID if one exists."
    128  );
    129  const FAKE_IMPRESSION_ID = "{some-fake-impression-ID}";
    130  const IMPRESSION_PREF = "browser.newtabpage.activity-stream.impressionId";
    131  Services.prefs.setCharPref(IMPRESSION_PREF, FAKE_IMPRESSION_ID);
    132  Assert.equal(new TelemetryFeed()._impressionId, FAKE_IMPRESSION_ID);
    133  Services.prefs.clearUserPref(IMPRESSION_PREF);
    134 });
    135 
    136 add_task(async function test_init() {
    137  info(
    138    "init should make this.browserOpenNewtabStart() observe browser-open-newtab-start"
    139  );
    140  let sandbox = sinon.createSandbox();
    141  sandbox
    142    .stub(TelemetryFeed.prototype, "getOrCreateImpressionId")
    143    .returns(FAKE_UUID);
    144 
    145  let instance = new TelemetryFeed();
    146  sandbox.stub(instance, "browserOpenNewtabStart");
    147  instance.init();
    148 
    149  Services.obs.notifyObservers(null, "browser-open-newtab-start");
    150  Assert.ok(
    151    instance.browserOpenNewtabStart.calledOnce,
    152    "browserOpenNewtabStart called once."
    153  );
    154 
    155  info("init should create impression id if none exists");
    156  Assert.equal(instance._impressionId, FAKE_UUID);
    157 
    158  instance.uninit();
    159  sandbox.restore();
    160 });
    161 
    162 add_task(async function test_saved_impression_id() {
    163  const FAKE_IMPRESSION_ID = "fakeImpressionId";
    164  Services.prefs.setCharPref(PREF_IMPRESSION_ID, FAKE_IMPRESSION_ID);
    165  Assert.equal(new TelemetryFeed()._impressionId, FAKE_IMPRESSION_ID);
    166  Services.prefs.clearUserPref(PREF_IMPRESSION_ID);
    167 });
    168 
    169 add_task(async function test_telemetry_prefs() {
    170  info("Telemetry pref changes from false to true");
    171  Services.prefs.setBoolPref(PREF_TELEMETRY, false);
    172  let instance = new TelemetryFeed();
    173  Assert.ok(!instance.telemetryEnabled, "Telemetry disabled");
    174 
    175  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
    176  Assert.ok(instance.telemetryEnabled, "Telemetry enabled");
    177 
    178  info("Event telemetry pref changes from false to true");
    179 
    180  Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, false);
    181  Assert.ok(!instance.eventTelemetryEnabled, "Event telemetry disabled");
    182 
    183  Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
    184  Assert.ok(instance.eventTelemetryEnabled, "Event telemetry enabled");
    185 
    186  Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
    187  Services.prefs.clearUserPref(PREF_TELEMETRY);
    188 });
    189 
    190 add_task(async function test_deletionRequest_scalars() {
    191  info("TelemetryFeed.init should set two scalars for deletion-request");
    192 
    193  Services.telemetry.clearScalars();
    194  let instance = new TelemetryFeed();
    195  instance.init();
    196 
    197  let snapshot = Services.telemetry.getSnapshotForScalars(
    198    "deletion-request",
    199    false
    200  ).parent;
    201  TelemetryTestUtils.assertScalar(
    202    snapshot,
    203    "deletion.request.impression_id",
    204    instance._impressionId
    205  );
    206 
    207  // We'll only set the context_id in the deletion request if rotation is
    208  // disabled.
    209  if (!ContextId.rotationEnabled) {
    210    TelemetryTestUtils.assertScalar(
    211      snapshot,
    212      "deletion.request.context_id",
    213      FAKE_UUID
    214    );
    215  }
    216 
    217  instance.uninit();
    218 });
    219 
    220 add_task(async function test_metrics_on_initialization() {
    221  info("TelemetryFeed.init should record initial metrics from newtab prefs");
    222  Services.fog.testResetFOG();
    223  const ENABLED_SETTING = true;
    224  const TOP_SITES_ROWS = 3;
    225  const BLOCKED_SPONSORS = ["mozilla"];
    226 
    227  Services.prefs.setBoolPref(
    228    "browser.newtabpage.activity-stream.feeds.topsites",
    229    ENABLED_SETTING
    230  );
    231  Services.prefs.setIntPref(
    232    "browser.newtabpage.activity-stream.topSitesRows",
    233    TOP_SITES_ROWS
    234  );
    235  Services.prefs.setCharPref(
    236    "browser.topsites.blockedSponsors",
    237    JSON.stringify(BLOCKED_SPONSORS)
    238  );
    239 
    240  let instance = new TelemetryFeed();
    241  instance.init();
    242 
    243  Assert.equal(Glean.topsites.enabled.testGetValue(), ENABLED_SETTING);
    244  Assert.equal(Glean.topsites.rows.testGetValue(), TOP_SITES_ROWS);
    245  Assert.deepEqual(
    246    Glean.newtab.blockedSponsors.testGetValue(),
    247    BLOCKED_SPONSORS
    248  );
    249 
    250  instance.uninit();
    251 
    252  Services.prefs.clearUserPref(
    253    "browser.newtabpage.activity-stream.feeds.topsites"
    254  );
    255  Services.prefs.clearUserPref(
    256    "browser.newtabpage.activity-stream.topSitesRows"
    257  );
    258  Services.prefs.clearUserPref("browser.topsites.blockedSponsors");
    259 });
    260 
    261 add_task(async function test_metrics_with_bad_json() {
    262  info(
    263    "TelemetryFeed.init should not record blocked sponsor metrics when " +
    264      "bad json string is passed"
    265  );
    266  Services.fog.testResetFOG();
    267  Services.prefs.setCharPref("browser.topsites.blockedSponsors", "BAD[JSON]");
    268 
    269  let instance = new TelemetryFeed();
    270  instance.init();
    271 
    272  Assert.equal(Glean.newtab.blockedSponsors.testGetValue(), null);
    273 
    274  instance.uninit();
    275 
    276  Services.prefs.clearUserPref("browser.topsites.blockedSponsors");
    277 });
    278 
    279 add_task(async function test_metrics_on_pref_changes() {
    280  info("TelemetryFeed.init should record new metrics for newtab pref changes");
    281  const INITIAL_TOP_SITES_ROWS = 3;
    282  const INITIAL_BLOCKED_SPONSORS = [];
    283  Services.fog.testResetFOG();
    284  Services.prefs.setIntPref(
    285    "browser.newtabpage.activity-stream.topSitesRows",
    286    INITIAL_TOP_SITES_ROWS
    287  );
    288  Services.prefs.setCharPref(
    289    "browser.topsites.blockedSponsors",
    290    JSON.stringify(INITIAL_BLOCKED_SPONSORS)
    291  );
    292 
    293  let instance = new TelemetryFeed();
    294  instance.init();
    295 
    296  Assert.equal(Glean.topsites.rows.testGetValue(), INITIAL_TOP_SITES_ROWS);
    297  Assert.deepEqual(
    298    Glean.newtab.blockedSponsors.testGetValue(),
    299    INITIAL_BLOCKED_SPONSORS
    300  );
    301 
    302  const NEXT_TOP_SITES_ROWS = 2;
    303  const NEXT_BLOCKED_SPONSORS = ["mozilla"];
    304 
    305  Services.prefs.setIntPref(
    306    "browser.newtabpage.activity-stream.topSitesRows",
    307    NEXT_TOP_SITES_ROWS
    308  );
    309 
    310  Services.prefs.setStringPref(
    311    "browser.topsites.blockedSponsors",
    312    JSON.stringify(NEXT_BLOCKED_SPONSORS)
    313  );
    314 
    315  Assert.equal(Glean.topsites.rows.testGetValue(), NEXT_TOP_SITES_ROWS);
    316  Assert.deepEqual(
    317    Glean.newtab.blockedSponsors.testGetValue(),
    318    NEXT_BLOCKED_SPONSORS
    319  );
    320 
    321  instance.uninit();
    322 
    323  Services.prefs.clearUserPref(
    324    "browser.newtabpage.activity-stream.topSitesRows"
    325  );
    326  Services.prefs.clearUserPref("browser.topsites.blockedSponsors");
    327 });
    328 
    329 add_task(async function test_events_on_pref_changes() {
    330  info("TelemetryFeed.init should record events for some newtab pref changes");
    331  // We only record events for browser.newtabpage.activity-stream.feeds.topsites and
    332  // browser.newtabpage.activity-stream.showSponsoredTopSites being changed.
    333  const INITIAL_TOPSITES_ENABLED = false;
    334  const INITIAL_SHOW_SPONSORED_TOP_SITES = true;
    335  Services.fog.testResetFOG();
    336  Services.prefs.setBoolPref(
    337    "browser.newtabpage.activity-stream.feeds.topsites",
    338    INITIAL_TOPSITES_ENABLED
    339  );
    340  Services.prefs.setBoolPref(
    341    "browser.newtabpage.activity-stream.showSponsoredTopSites",
    342    INITIAL_SHOW_SPONSORED_TOP_SITES
    343  );
    344 
    345  let instance = new TelemetryFeed();
    346  instance.init();
    347 
    348  const NEXT_TOPSITES_ENABLED = true;
    349  const NEXT_SHOW_SPONSORED_TOP_SITES = false;
    350 
    351  Services.prefs.setBoolPref(
    352    "browser.newtabpage.activity-stream.feeds.topsites",
    353    NEXT_TOPSITES_ENABLED
    354  );
    355  Services.prefs.setBoolPref(
    356    "browser.newtabpage.activity-stream.showSponsoredTopSites",
    357    NEXT_SHOW_SPONSORED_TOP_SITES
    358  );
    359 
    360  let prefChangeEvents = Glean.topsites.prefChanged.testGetValue();
    361  Assert.deepEqual(prefChangeEvents[0].extra, {
    362    pref_name: "browser.newtabpage.activity-stream.feeds.topsites",
    363    new_value: String(NEXT_TOPSITES_ENABLED),
    364  });
    365  Assert.deepEqual(prefChangeEvents[1].extra, {
    366    pref_name: "browser.newtabpage.activity-stream.showSponsoredTopSites",
    367    new_value: String(NEXT_SHOW_SPONSORED_TOP_SITES),
    368  });
    369 
    370  instance.uninit();
    371 
    372  Services.prefs.clearUserPref(
    373    "browser.newtabpage.activity-stream.feeds.topsites"
    374  );
    375  Services.prefs.clearUserPref(
    376    "browser.newtabpage.activity-stream.showSponsoredTopSites"
    377  );
    378 });
    379 
    380 add_task(async function test_browserOpenNewtabStart() {
    381  info(
    382    "TelemetryFeed.browserOpenNewtabStart should call " +
    383      "ChromeUtils.addProfilerMarker with browser-open-newtab-start"
    384  );
    385 
    386  let instance = new TelemetryFeed();
    387 
    388  let entries = 10000;
    389  let interval = 1;
    390  let threads = ["GeckoMain"];
    391  let features = [];
    392  await Services.profiler.StartProfiler(entries, interval, features, threads);
    393  instance.browserOpenNewtabStart();
    394 
    395  let profileArrayBuffer =
    396    await Services.profiler.getProfileDataAsArrayBuffer();
    397  await Services.profiler.StopProfiler();
    398 
    399  let profileUint8Array = new Uint8Array(profileArrayBuffer);
    400  let textDecoder = new TextDecoder("utf-8", { fatal: true });
    401  let profileString = textDecoder.decode(profileUint8Array);
    402  let profile = JSON.parse(profileString);
    403  Assert.ok(profile.threads);
    404  Assert.equal(profile.threads.length, 1);
    405 
    406  let foundMarker = profile.threads[0].markers.data.find(marker => {
    407    return marker[5]?.name === "browser-open-newtab-start";
    408  });
    409 
    410  Assert.ok(foundMarker, "Found the browser-open-newtab-start marker");
    411 });
    412 
    413 add_task(async function test_addSession_and_get_session() {
    414  info("TelemetryFeed.addSession should add a session and return it");
    415  let instance = new TelemetryFeed();
    416  let session = instance.addSession("foo");
    417 
    418  Assert.equal(instance.sessions.get("foo"), session);
    419 
    420  info("TelemetryFeed.addSession should set a session_id");
    421  Assert.ok(session.session_id, "Should have a session_id set");
    422 });
    423 
    424 add_task(async function test_addSession_url_param() {
    425  info("TelemetryFeed.addSession should set the page if a url param is given");
    426  let instance = new TelemetryFeed();
    427  let session = instance.addSession("foo", "about:monkeys");
    428  Assert.equal(session.page, "about:monkeys");
    429 
    430  info(
    431    "TelemetryFeed.assSession should set the page prop to 'unknown' " +
    432      "if no URL param given"
    433  );
    434  session = instance.addSession("test2");
    435  Assert.equal(session.page, "unknown");
    436 });
    437 
    438 add_task(async function test_addSession_perf_properties() {
    439  info(
    440    "TelemetryFeed.addSession should set the perf type when lacking " +
    441      "timestamp"
    442  );
    443  let instance = new TelemetryFeed();
    444  let session = instance.addSession("foo");
    445  Assert.equal(session.perf.load_trigger_type, "unexpected");
    446 
    447  info(
    448    "TelemetryFeed.addSession should set load_trigger_type to " +
    449      "first_window_opened on the first about:home seen"
    450  );
    451  session = instance.addSession("test2", "about:home");
    452  Assert.equal(session.perf.load_trigger_type, "first_window_opened");
    453 
    454  info(
    455    "TelemetryFeed.addSession should set load_trigger_ts to the " +
    456      "value of the process start timestamp"
    457  );
    458  Assert.equal(
    459    session.perf.load_trigger_ts,
    460    Services.startup.getStartupInfo().process.getTime(),
    461    "Should have set a timestamp to be the process start time"
    462  );
    463 
    464  info(
    465    "TelemetryFeed.addSession should NOT set load_trigger_type to " +
    466      "first_window_opened on the second about:home seen"
    467  );
    468  let session2 = instance.addSession("test2", "about:home");
    469  Assert.notEqual(session2.perf.load_trigger_type, "first_window_opened");
    470 });
    471 
    472 add_task(async function test_addSession_valid_ping_on_first_abouthome() {
    473  info(
    474    "TelemetryFeed.addSession should create a valid session ping " +
    475      "on the first about:home seen"
    476  );
    477  let instance = new TelemetryFeed();
    478  // Add a session
    479  const PORT_ID = "foo";
    480  let session = instance.addSession(PORT_ID, "about:home");
    481 
    482  // Create a ping referencing the session
    483  let ping = instance.createSessionEndEvent(session);
    484  await assertSessionPingValid(ping);
    485 });
    486 
    487 add_task(async function test_addSession_valid_ping_data_late_by_ms() {
    488  info(
    489    "TelemetryFeed.addSession should create a valid session ping " +
    490      "with the data_late_by_ms perf"
    491  );
    492  let instance = new TelemetryFeed();
    493  // Add a session
    494  const PORT_ID = "foo";
    495  let session = instance.addSession(PORT_ID, "about:home");
    496 
    497  const TOPSITES_LATE_BY_MS = 10;
    498  const HIGHLIGHTS_LATE_BY_MS = 20;
    499  instance.saveSessionPerfData("foo", {
    500    topsites_data_late_by_ms: TOPSITES_LATE_BY_MS,
    501  });
    502  instance.saveSessionPerfData("foo", {
    503    highlights_data_late_by_ms: HIGHLIGHTS_LATE_BY_MS,
    504  });
    505 
    506  // Create a ping referencing the session
    507  let ping = instance.createSessionEndEvent(session);
    508  await assertSessionPingValid(ping);
    509  Assert.equal(session.perf.topsites_data_late_by_ms, TOPSITES_LATE_BY_MS);
    510  Assert.equal(session.perf.highlights_data_late_by_ms, HIGHLIGHTS_LATE_BY_MS);
    511 });
    512 
    513 add_task(async function test_addSession_valid_ping_topsites_stats_perf() {
    514  info(
    515    "TelemetryFeed.addSession should create a valid session ping " +
    516      "with the topsites stats perf"
    517  );
    518  let instance = new TelemetryFeed();
    519  // Add a session
    520  const PORT_ID = "foo";
    521  let session = instance.addSession(PORT_ID, "about:home");
    522 
    523  const SCREENSHOT_WITH_ICON = 2;
    524  const TOPSITES_PINNED = 3;
    525  const TOPSITES_SEARCH_SHORTCUTS = 2;
    526 
    527  instance.saveSessionPerfData("foo", {
    528    topsites_icon_stats: {
    529      custom_screenshot: 0,
    530      screenshot_with_icon: SCREENSHOT_WITH_ICON,
    531      screenshot: 1,
    532      tippytop: 2,
    533      rich_icon: 1,
    534      no_image: 0,
    535    },
    536    topsites_pinned: TOPSITES_PINNED,
    537    topsites_search_shortcuts: TOPSITES_SEARCH_SHORTCUTS,
    538  });
    539 
    540  // Create a ping referencing the session
    541  let ping = instance.createSessionEndEvent(session);
    542  await assertSessionPingValid(ping);
    543  Assert.equal(
    544    instance.sessions.get("foo").perf.topsites_icon_stats.screenshot_with_icon,
    545    SCREENSHOT_WITH_ICON
    546  );
    547  Assert.equal(
    548    instance.sessions.get("foo").perf.topsites_pinned,
    549    TOPSITES_PINNED
    550  );
    551  Assert.equal(
    552    instance.sessions.get("foo").perf.topsites_search_shortcuts,
    553    TOPSITES_SEARCH_SHORTCUTS
    554  );
    555 });
    556 
    557 add_task(async function test_endSession_no_throw_on_bad_session() {
    558  info(
    559    "TelemetryFeed.endSession should not throw if there is no " +
    560      "session for a given port ID"
    561  );
    562  let instance = new TelemetryFeed();
    563  try {
    564    instance.endSession("doesn't exist");
    565    Assert.ok(true, "Did not throw.");
    566  } catch (e) {
    567    Assert.ok(false, "Should not have thrown.");
    568  }
    569 });
    570 
    571 add_task(async function test_endSession_session_duration() {
    572  info(
    573    "TelemetryFeed.endSession should add a session_duration integer " +
    574      "if there is a visibility_event_rcvd_ts"
    575  );
    576  let instance = new TelemetryFeed();
    577  let session = instance.addSession("foo");
    578  session.perf.visibility_event_rcvd_ts = 444.4732;
    579  instance.endSession("foo");
    580 
    581  Assert.ok(
    582    Number.isInteger(session.session_duration),
    583    "session_duration should be an integer"
    584  );
    585 });
    586 
    587 add_task(async function test_endSession_no_ping_on_no_visibility_event() {
    588  info(
    589    "TelemetryFeed.endSession shouldn't send session ping if there's " +
    590      "no visibility_event_rcvd_ts"
    591  );
    592  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
    593  Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
    594  let instance = new TelemetryFeed();
    595 
    596  let sandbox = sinon.createSandbox();
    597  sandbox.stub(instance, "configureContentPing");
    598 
    599  instance.addSession("foo");
    600 
    601  Services.telemetry.clearEvents();
    602  instance.endSession("foo");
    603  TelemetryTestUtils.assertNumberOfEvents(0);
    604 
    605  info("TelemetryFeed.endSession should remove the session from .sessions");
    606  Assert.ok(!instance.sessions.has("foo"));
    607 
    608  Services.prefs.clearUserPref(PREF_TELEMETRY);
    609  Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
    610 
    611  sandbox.restore();
    612 });
    613 
    614 add_task(async function test_endSession_send_ping() {
    615  info(
    616    "TelemetryFeed.endSession should call createSessionSendEvent with the " +
    617      "session if visibilty_event_rcvd_ts was set"
    618  );
    619  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
    620  Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
    621  let instance = new TelemetryFeed();
    622 
    623  let sandbox = sinon.createSandbox();
    624  sandbox.stub(instance, "createSessionEndEvent");
    625  sandbox.stub(instance.utEvents, "sendSessionEndEvent");
    626  sandbox.stub(instance, "configureContentPing");
    627 
    628  let session = instance.addSession("foo");
    629 
    630  session.perf.visibility_event_rcvd_ts = 444.4732;
    631  instance.endSession("foo");
    632 
    633  Assert.ok(instance.createSessionEndEvent.calledWith(session));
    634  let sessionEndEvent = instance.createSessionEndEvent.firstCall.returnValue;
    635  Assert.ok(instance.utEvents.sendSessionEndEvent.calledWith(sessionEndEvent));
    636 
    637  info("TelemetryFeed.endSession should remove the session from .sessions");
    638  Assert.ok(!instance.sessions.has("foo"));
    639 
    640  Services.prefs.clearUserPref(PREF_TELEMETRY);
    641  Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
    642 
    643  sandbox.restore();
    644 });
    645 
    646 add_task(async function test_createPing_valid_base_if_no_portID() {
    647  info(
    648    "TelemetryFeed.createPing should create a valid base ping " +
    649      "without a session if no portID is supplied"
    650  );
    651  let instance = new TelemetryFeed();
    652  let ping = await instance.createPing();
    653  await assertBasePingValid(ping);
    654  Assert.ok(!ping.session_id);
    655  Assert.ok(!ping.page);
    656 });
    657 
    658 add_task(async function test_createPing_valid_base_if_portID() {
    659  info(
    660    "TelemetryFeed.createPing should create a valid base ping " +
    661      "with session info if a portID is supplied"
    662  );
    663  // Add a session
    664  const PORT_ID = "foo";
    665  let instance = new TelemetryFeed();
    666  instance.addSession(PORT_ID, "about:home");
    667  let sessionID = instance.sessions.get(PORT_ID).session_id;
    668 
    669  // Create a ping referencing the session
    670  let ping = await instance.createPing(PORT_ID);
    671  await assertBasePingValid(ping);
    672 
    673  // Make sure we added the right session-related stuff to the ping
    674  Assert.equal(ping.session_id, sessionID);
    675  Assert.equal(ping.page, "about:home");
    676 });
    677 
    678 add_task(async function test_createPing_no_session_yet_portID() {
    679  info(
    680    "TelemetryFeed.createPing should create an 'unexpected' base ping " +
    681      "if no session yet portID is supplied"
    682  );
    683  let instance = new TelemetryFeed();
    684  let ping = await instance.createPing("foo");
    685  await assertBasePingValid(ping);
    686 
    687  Assert.equal(ping.page, "unknown");
    688  Assert.equal(
    689    instance.sessions.get("foo").perf.load_trigger_type,
    690    "unexpected"
    691  );
    692 });
    693 
    694 add_task(async function test_createPing_includes_userPrefs() {
    695  info("TelemetryFeed.createPing should create a base ping with user_prefs");
    696  let expectedUserPrefs = 0;
    697 
    698  for (let pref of Object.keys(USER_PREFS_ENCODING)) {
    699    Services.prefs.setBoolPref(
    700      `browser.newtabpage.activity-stream.${pref}`,
    701      true
    702    );
    703    expectedUserPrefs |= USER_PREFS_ENCODING[pref];
    704  }
    705 
    706  let instance = new TelemetryFeed();
    707  let ping = await instance.createPing("foo");
    708  await assertBasePingValid(ping);
    709  Assert.equal(ping.user_prefs, expectedUserPrefs);
    710 
    711  for (const pref of Object.keys(USER_PREFS_ENCODING)) {
    712    Services.prefs.clearUserPref(`browser.newtabpage.activity-stream.${pref}`);
    713  }
    714 });
    715 
    716 add_task(async function test_createUserEvent_is_valid() {
    717  info(
    718    "TelemetryFeed.createUserEvent should create a valid user event ping " +
    719      "with the right session_id"
    720  );
    721  const PORT_ID = "foo";
    722 
    723  let instance = new TelemetryFeed();
    724  let data = { source: "TOP_SITES", event: "CLICK" };
    725  let action = actionCreators.AlsoToMain(
    726    actionCreators.UserEvent(data),
    727    PORT_ID
    728  );
    729  let session = instance.addSession(PORT_ID);
    730 
    731  let ping = await instance.createUserEvent(action);
    732 
    733  // Is it valid?
    734  await assertUserEventPingValid(ping);
    735  // Does it have the right session_id?
    736  Assert.equal(ping.session_id, session.session_id);
    737 });
    738 
    739 add_task(async function test_createSessionEndEvent_is_valid() {
    740  info(
    741    "TelemetryFeed.createSessionEndEvent should create a valid session ping"
    742  );
    743  const FAKE_DURATION = 12345;
    744  let instance = new TelemetryFeed();
    745  let ping = await instance.createSessionEndEvent({
    746    session_id: FAKE_UUID,
    747    page: "about:newtab",
    748    session_duration: FAKE_DURATION,
    749    perf: {
    750      load_trigger_ts: 10,
    751      load_trigger_type: "menu_plus_or_keyboard",
    752      visibility_event_rcvd_ts: 20,
    753      is_preloaded: true,
    754    },
    755  });
    756 
    757  // Is it valid?
    758  await assertSessionPingValid(ping);
    759  Assert.equal(ping.session_id, FAKE_UUID);
    760  Assert.equal(ping.page, "about:newtab");
    761  Assert.equal(ping.session_duration, FAKE_DURATION);
    762 });
    763 
    764 add_task(async function test_createSessionEndEvent_with_unexpected_is_valid() {
    765  info(
    766    "TelemetryFeed.createSessionEndEvent should create a valid 'unexpected' " +
    767      "session ping"
    768  );
    769  const FAKE_DURATION = 12345;
    770  const FAKE_TRIGGER_TYPE = "unexpected";
    771 
    772  let instance = new TelemetryFeed();
    773  let ping = await instance.createSessionEndEvent({
    774    session_id: FAKE_UUID,
    775    page: "about:newtab",
    776    session_duration: FAKE_DURATION,
    777    perf: {
    778      load_trigger_type: FAKE_TRIGGER_TYPE,
    779      is_preloaded: true,
    780    },
    781  });
    782 
    783  // Is it valid?
    784  await assertSessionPingValid(ping);
    785  Assert.equal(ping.session_id, FAKE_UUID);
    786  Assert.equal(ping.page, "about:newtab");
    787  Assert.equal(ping.session_duration, FAKE_DURATION);
    788  Assert.equal(ping.perf.load_trigger_type, FAKE_TRIGGER_TYPE);
    789 });
    790 
    791 add_task(async function test_sendUTEvent_call_right_function() {
    792  info("TelemetryFeed.sendUTEvent should call the UT event function passed in");
    793  let sandbox = sinon.createSandbox();
    794 
    795  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
    796  Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
    797 
    798  let event = {};
    799  let instance = new TelemetryFeed();
    800  sandbox.stub(instance.utEvents, "sendUserEvent");
    801  instance.addSession("foo");
    802 
    803  await instance.sendUTEvent(event, instance.utEvents.sendUserEvent);
    804  Assert.ok(instance.utEvents.sendUserEvent.calledWith(event));
    805 
    806  Services.prefs.clearUserPref(PREF_TELEMETRY);
    807  Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
    808  sandbox.restore();
    809 });
    810 
    811 add_task(async function test_setLoadTriggerInfo() {
    812  info(
    813    "TelemetryFeed.setLoadTriggerInfo should call saveSessionPerfData " +
    814      "w/load_trigger_{ts,type} data"
    815  );
    816  let sandbox = sinon.createSandbox();
    817  let instance = new TelemetryFeed();
    818  sandbox.stub(instance, "saveSessionPerfData");
    819 
    820  instance.browserOpenNewtabStart();
    821  instance.addSession("port123");
    822  instance.setLoadTriggerInfo("port123");
    823 
    824  Assert.ok(
    825    instance.saveSessionPerfData.calledWith(
    826      "port123",
    827      sinon.match({
    828        load_trigger_type: "menu_plus_or_keyboard",
    829        load_trigger_ts: sinon.match.number,
    830      })
    831    ),
    832    "TelemetryFeed.saveSessionPerfData was called with the right arguments"
    833  );
    834 
    835  sandbox.restore();
    836 });
    837 
    838 add_task(async function test_setLoadTriggerInfo_no_saveSessionPerfData() {
    839  info(
    840    "TelemetryFeed.setLoadTriggerInfo should not call saveSessionPerfData " +
    841      "when getting mark throws"
    842  );
    843  let sandbox = sinon.createSandbox();
    844  let instance = new TelemetryFeed();
    845  sandbox.stub(instance, "saveSessionPerfData");
    846 
    847  instance.addSession("port123");
    848  instance.setLoadTriggerInfo("port123");
    849 
    850  Assert.ok(
    851    instance.saveSessionPerfData.notCalled,
    852    "TelemetryFeed.saveSessionPerfData was not called"
    853  );
    854 
    855  sandbox.restore();
    856 });
    857 
    858 add_task(async function test_saveSessionPerfData_updates_session_with_data() {
    859  info(
    860    "TelemetryFeed.saveSessionPerfData should update the given session " +
    861      "with the given data"
    862  );
    863  let sandbox = sinon.createSandbox();
    864  let instance = new TelemetryFeed();
    865 
    866  instance.addSession("port123");
    867  Assert.equal(instance.sessions.get("port123").fake_ts, undefined);
    868  let data = { fake_ts: 456, other_fake_ts: 789 };
    869  instance.saveSessionPerfData("port123", data);
    870 
    871  let sessionPerfData = instance.sessions.get("port123").perf;
    872  Assert.equal(sessionPerfData.fake_ts, 456);
    873  Assert.equal(sessionPerfData.other_fake_ts, 789);
    874 
    875  sandbox.restore();
    876 });
    877 
    878 add_task(async function test_saveSessionPerfData_calls_setLoadTriggerInfo() {
    879  info(
    880    "TelemetryFeed.saveSessionPerfData should call setLoadTriggerInfo if " +
    881      "data has visibility_event_rcvd_ts"
    882  );
    883  let sandbox = sinon.createSandbox();
    884  let instance = new TelemetryFeed();
    885 
    886  sandbox.stub(instance, "setLoadTriggerInfo");
    887  instance.addSession("port123");
    888  let data = { visibility_event_rcvd_ts: 444455 };
    889 
    890  instance.saveSessionPerfData("port123", data);
    891 
    892  Assert.ok(
    893    instance.setLoadTriggerInfo.calledOnce,
    894    "TelemetryFeed.setLoadTriggerInfo was called once"
    895  );
    896  Assert.ok(instance.setLoadTriggerInfo.calledWithExactly("port123"));
    897 
    898  Assert.equal(
    899    instance.sessions.get("port123").perf.visibility_event_rcvd_ts,
    900    444455
    901  );
    902 
    903  sandbox.restore();
    904 });
    905 
    906 add_task(
    907  async function test_saveSessionPerfData_does_not_call_setLoadTriggerInfo() {
    908    info(
    909      "TelemetryFeed.saveSessionPerfData shouldn't call setLoadTriggerInfo if " +
    910        "data has no visibility_event_rcvd_ts"
    911    );
    912    let sandbox = sinon.createSandbox();
    913    let instance = new TelemetryFeed();
    914 
    915    sandbox.stub(instance, "setLoadTriggerInfo");
    916    instance.addSession("port123");
    917    instance.saveSessionPerfData("port123", { monkeys_ts: 444455 });
    918 
    919    Assert.ok(
    920      instance.setLoadTriggerInfo.notCalled,
    921      "TelemetryFeed.setLoadTriggerInfo was not called"
    922    );
    923 
    924    sandbox.restore();
    925  }
    926 );
    927 
    928 add_task(
    929  async function test_saveSessionPerfData_does_not_call_setLoadTriggerInfo_about_home() {
    930    info(
    931      "TelemetryFeed.saveSessionPerfData should not call setLoadTriggerInfo when " +
    932        "url is about:home"
    933    );
    934    let sandbox = sinon.createSandbox();
    935    let instance = new TelemetryFeed();
    936 
    937    sandbox.stub(instance, "setLoadTriggerInfo");
    938    instance.addSession("port123", "about:home");
    939    let data = { visibility_event_rcvd_ts: 444455 };
    940    instance.saveSessionPerfData("port123", data);
    941 
    942    Assert.ok(
    943      instance.setLoadTriggerInfo.notCalled,
    944      "TelemetryFeed.setLoadTriggerInfo was not called"
    945    );
    946 
    947    sandbox.restore();
    948  }
    949 );
    950 
    951 add_task(
    952  async function test_saveSessionPerfData_calls_maybeRecordTopsitesPainted() {
    953    info(
    954      "TelemetryFeed.saveSessionPerfData should call maybeRecordTopsitesPainted " +
    955        "when url is about:home and topsites_first_painted_ts is given"
    956    );
    957    let sandbox = sinon.createSandbox();
    958    let instance = new TelemetryFeed();
    959 
    960    const TOPSITES_FIRST_PAINTED_TS = 44455;
    961    let data = { topsites_first_painted_ts: TOPSITES_FIRST_PAINTED_TS };
    962 
    963    sandbox.stub(AboutNewTab, "maybeRecordTopsitesPainted");
    964    instance.addSession("port123", "about:home");
    965    instance.saveSessionPerfData("port123", data);
    966 
    967    Assert.ok(
    968      AboutNewTab.maybeRecordTopsitesPainted.calledOnce,
    969      "AboutNewTab.maybeRecordTopsitesPainted called once"
    970    );
    971    Assert.ok(
    972      AboutNewTab.maybeRecordTopsitesPainted.calledWith(
    973        TOPSITES_FIRST_PAINTED_TS
    974      )
    975    );
    976    sandbox.restore();
    977  }
    978 );
    979 
    980 add_task(
    981  async function test_saveSessionPerfData_records_Glean_newtab_opened_event() {
    982    info(
    983      "TelemetryFeed.saveSessionPerfData should record a Glean newtab.opened event " +
    984        "with the correct visit_id when visibility event received"
    985    );
    986    let sandbox = sinon.createSandbox();
    987    let instance = new TelemetryFeed();
    988    Services.fog.testResetFOG();
    989 
    990    const SESSION_ID = "decafc0ffee";
    991    const PAGE = "about:newtab";
    992    let session = { page: PAGE, perf: {}, session_id: SESSION_ID };
    993    let data = { visibility_event_rcvd_ts: 444455 };
    994 
    995    sandbox.stub(instance.sessions, "get").returns(session);
    996    instance.saveSessionPerfData("port123", data);
    997 
    998    let newtabOpenedEvents = Glean.newtab.opened.testGetValue();
    999    Assert.deepEqual(newtabOpenedEvents[0].extra, {
   1000      newtab_visit_id: SESSION_ID,
   1001      source: PAGE,
   1002    });
   1003 
   1004    sandbox.restore();
   1005  }
   1006 );
   1007 
   1008 add_task(async function test_uninit_deregisters_observer() {
   1009  info(
   1010    "TelemetryFeed.uninit should make this.browserOpenNewtabStart() stop " +
   1011      "observing browser-open-newtab-start"
   1012  );
   1013  let sandbox = sinon.createSandbox();
   1014  let instance = new TelemetryFeed();
   1015  let countObservers = () => {
   1016    return [...Services.obs.enumerateObservers("browser-open-newtab-start")]
   1017      .length;
   1018  };
   1019 
   1020  const ORIGINAL_COUNT = countObservers();
   1021  instance.init();
   1022  Assert.equal(countObservers(), ORIGINAL_COUNT + 1, "Observer was added");
   1023 
   1024  instance.uninit();
   1025  Assert.equal(countObservers(), ORIGINAL_COUNT, "Observer was removed");
   1026 
   1027  sandbox.restore();
   1028 });
   1029 
   1030 add_task(async function test_onAction_basic_actions() {
   1031  let browser = Services.appShell
   1032    .createWindowlessBrowser(false)
   1033    .document.createElement("browser");
   1034 
   1035  let testOnAction = (setupFn, action, checkFn) => {
   1036    let sandbox = sinon.createSandbox();
   1037    let instance = new TelemetryFeed();
   1038    setupFn(sandbox, instance);
   1039 
   1040    instance.onAction(action);
   1041    checkFn(instance);
   1042    sandbox.restore();
   1043  };
   1044 
   1045  info("TelemetryFeed.onAction should call .init() on an INIT action");
   1046  testOnAction(
   1047    (sandbox, instance) => {
   1048      sandbox.stub(instance, "init");
   1049      sandbox.stub(instance, "sendPageTakeoverData");
   1050    },
   1051    { type: actionTypes.INIT },
   1052    instance => {
   1053      Assert.ok(instance.init.calledOnce, "TelemetryFeed.init called once");
   1054      Assert.ok(
   1055        instance.sendPageTakeoverData.calledOnce,
   1056        "TelemetryFeed.sendPageTakeoverData called once"
   1057      );
   1058    }
   1059  );
   1060 
   1061  info("TelemetryFeed.onAction should call .uninit() on an UNINIT action");
   1062  testOnAction(
   1063    (sandbox, instance) => {
   1064      sandbox.stub(instance, "uninit");
   1065    },
   1066    { type: actionTypes.UNINIT },
   1067    instance => {
   1068      Assert.ok(instance.uninit.calledOnce, "TelemetryFeed.uninit called once");
   1069    }
   1070  );
   1071 
   1072  info(
   1073    "TelemetryFeed.onAction should call .handleNewTabInit on a " +
   1074      "NEW_TAB_INIT action"
   1075  );
   1076  testOnAction(
   1077    (sandbox, instance) => {
   1078      sandbox.stub(instance, "handleNewTabInit");
   1079    },
   1080    actionCreators.AlsoToMain({
   1081      type: actionTypes.NEW_TAB_INIT,
   1082      data: { url: "about:newtab", browser },
   1083    }),
   1084    instance => {
   1085      Assert.ok(
   1086        instance.handleNewTabInit.calledOnce,
   1087        "TelemetryFeed.handleNewTabInit called once"
   1088      );
   1089    }
   1090  );
   1091 
   1092  info(
   1093    "TelemetryFeed.onAction should call .addSession() on a " +
   1094      "NEW_TAB_INIT action"
   1095  );
   1096  testOnAction(
   1097    (sandbox, instance) => {
   1098      sandbox.stub(instance, "addSession").returns({ perf: {} });
   1099      sandbox.stub(instance, "setLoadTriggerInfo");
   1100    },
   1101    actionCreators.AlsoToMain(
   1102      {
   1103        type: actionTypes.NEW_TAB_INIT,
   1104        data: { url: "about:monkeys", browser },
   1105      },
   1106      "port123"
   1107    ),
   1108    instance => {
   1109      Assert.ok(
   1110        instance.addSession.calledOnce,
   1111        "TelemetryFeed.addSession called once"
   1112      );
   1113      Assert.ok(instance.addSession.calledWith("port123", "about:monkeys"));
   1114    }
   1115  );
   1116 
   1117  info(
   1118    "TelemetryFeed.onAction should call .endSession() on a " +
   1119      "NEW_TAB_UNLOAD action"
   1120  );
   1121  testOnAction(
   1122    (sandbox, instance) => {
   1123      sandbox.stub(instance, "endSession");
   1124    },
   1125    actionCreators.AlsoToMain({ type: actionTypes.NEW_TAB_UNLOAD }, "port123"),
   1126    instance => {
   1127      Assert.ok(
   1128        instance.endSession.calledOnce,
   1129        "TelemetryFeed.endSession called once"
   1130      );
   1131      Assert.ok(instance.endSession.calledWith("port123"));
   1132    }
   1133  );
   1134 
   1135  info(
   1136    "TelemetryFeed.onAction should call .saveSessionPerfData " +
   1137      "on SAVE_SESSION_PERF_DATA"
   1138  );
   1139  testOnAction(
   1140    (sandbox, instance) => {
   1141      sandbox.stub(instance, "saveSessionPerfData");
   1142    },
   1143    actionCreators.AlsoToMain(
   1144      { type: actionTypes.SAVE_SESSION_PERF_DATA, data: { some_ts: 10 } },
   1145      "port123"
   1146    ),
   1147    instance => {
   1148      Assert.ok(
   1149        instance.saveSessionPerfData.calledOnce,
   1150        "TelemetryFeed.saveSessionPerfData called once"
   1151      );
   1152      Assert.ok(
   1153        instance.saveSessionPerfData.calledWith("port123", { some_ts: 10 })
   1154      );
   1155    }
   1156  );
   1157 
   1158  info(
   1159    "TelemetryFeed.onAction should send an event on a TELEMETRY_USER_EVENT " +
   1160      "action"
   1161  );
   1162  Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
   1163  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
   1164  testOnAction(
   1165    (sandbox, instance) => {
   1166      sandbox.stub(instance, "createUserEvent");
   1167      sandbox.stub(instance.utEvents, "sendUserEvent");
   1168    },
   1169    { type: actionTypes.TELEMETRY_USER_EVENT },
   1170    instance => {
   1171      Assert.ok(
   1172        instance.createUserEvent.calledOnce,
   1173        "TelemetryFeed.createUserEvent called once"
   1174      );
   1175      Assert.ok(
   1176        instance.createUserEvent.calledWith({
   1177          type: actionTypes.TELEMETRY_USER_EVENT,
   1178        })
   1179      );
   1180      Assert.ok(
   1181        instance.utEvents.sendUserEvent.calledOnce,
   1182        "TelemetryFeed.utEvents.sendUserEvent called once"
   1183      );
   1184      Assert.ok(
   1185        instance.utEvents.sendUserEvent.calledWith(
   1186          instance.createUserEvent.returnValue
   1187        )
   1188      );
   1189    }
   1190  );
   1191  Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
   1192  Services.prefs.clearUserPref(PREF_TELEMETRY);
   1193 
   1194  info(
   1195    "TelemetryFeed.onAction should send an event on a " +
   1196      "DISCOVERY_STREAM_USER_EVENT action"
   1197  );
   1198  Services.prefs.setBoolPref(PREF_EVENT_TELEMETRY, true);
   1199  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
   1200  testOnAction(
   1201    (sandbox, instance) => {
   1202      sandbox.stub(instance, "createUserEvent");
   1203      sandbox.stub(instance.utEvents, "sendUserEvent");
   1204    },
   1205    { type: actionTypes.DISCOVERY_STREAM_USER_EVENT },
   1206    instance => {
   1207      Assert.ok(
   1208        instance.createUserEvent.calledOnce,
   1209        "TelemetryFeed.createUserEvent called once"
   1210      );
   1211      Assert.ok(
   1212        instance.utEvents.sendUserEvent.calledOnce,
   1213        "TelemetryFeed.utEvents.sendUserEvent called once"
   1214      );
   1215      Assert.ok(
   1216        instance.utEvents.sendUserEvent.calledWith(
   1217          instance.createUserEvent.returnValue
   1218        )
   1219      );
   1220    }
   1221  );
   1222  Services.prefs.clearUserPref(PREF_EVENT_TELEMETRY);
   1223  Services.prefs.clearUserPref(PREF_TELEMETRY);
   1224 });
   1225 
   1226 add_task(
   1227  async function test_onAction_calls_handleDiscoveryStreamImpressionStats_ds() {
   1228    info(
   1229      "TelemetryFeed.onAction should call " +
   1230        ".handleDiscoveryStreamImpressionStats on a " +
   1231        "DISCOVERY_STREAM_IMPRESSION_STATS action"
   1232    );
   1233    let sandbox = sinon.createSandbox();
   1234    let instance = new TelemetryFeed();
   1235 
   1236    let session = {};
   1237    sandbox.stub(instance.sessions, "get").returns(session);
   1238    let data = { source: "foo", tiles: [{ id: 1 }] };
   1239    let action = { type: actionTypes.DISCOVERY_STREAM_IMPRESSION_STATS, data };
   1240    sandbox.spy(instance, "handleDiscoveryStreamImpressionStats");
   1241 
   1242    instance.onAction(actionCreators.AlsoToMain(action, "port123"));
   1243 
   1244    Assert.ok(
   1245      instance.handleDiscoveryStreamImpressionStats.calledWith("port123", data)
   1246    );
   1247 
   1248    sandbox.restore();
   1249  }
   1250 );
   1251 
   1252 add_task(
   1253  async function test_onAction_calls_handleTopSitesSponsoredImpressionStats() {
   1254    info(
   1255      "TelemetryFeed.onAction should call " +
   1256        ".handleTopSitesSponsoredImpressionStats on a " +
   1257        "TOP_SITES_SPONSORED_IMPRESSION_STATS action"
   1258    );
   1259    let sandbox = sinon.createSandbox();
   1260    let instance = new TelemetryFeed();
   1261 
   1262    let session = {};
   1263    sandbox.stub(instance.sessions, "get").returns(session);
   1264    let data = { type: "impression", tile_id: 42, position: 1 };
   1265    let action = {
   1266      type: actionTypes.TOP_SITES_SPONSORED_IMPRESSION_STATS,
   1267      data,
   1268    };
   1269    sandbox.spy(instance, "handleTopSitesSponsoredImpressionStats");
   1270 
   1271    instance.onAction(actionCreators.AlsoToMain(action));
   1272 
   1273    Assert.ok(
   1274      instance.handleTopSitesSponsoredImpressionStats.calledOnce,
   1275      "TelemetryFeed.handleTopSitesSponsoredImpressionStats called once"
   1276    );
   1277    Assert.deepEqual(
   1278      instance.handleTopSitesSponsoredImpressionStats.firstCall.args[0].data,
   1279      data
   1280    );
   1281 
   1282    sandbox.restore();
   1283  }
   1284 );
   1285 
   1286 add_task(async function test_onAction_calls_handleAboutSponsoredTopSites() {
   1287  info(
   1288    "TelemetryFeed.onAction should call " +
   1289      ".handleAboutSponsoredTopSites on a " +
   1290      "ABOUT_SPONSORED_TOP_SITES action"
   1291  );
   1292  let sandbox = sinon.createSandbox();
   1293  let instance = new TelemetryFeed();
   1294 
   1295  let data = { position: 0, advertiser_name: "moo", tile_id: 42 };
   1296  let action = { type: actionTypes.ABOUT_SPONSORED_TOP_SITES, data };
   1297  sandbox.spy(instance, "handleAboutSponsoredTopSites");
   1298 
   1299  instance.onAction(actionCreators.AlsoToMain(action));
   1300 
   1301  Assert.ok(
   1302    instance.handleAboutSponsoredTopSites.calledOnce,
   1303    "TelemetryFeed.handleAboutSponsoredTopSites called once"
   1304  );
   1305 
   1306  sandbox.restore();
   1307 });
   1308 
   1309 add_task(async function test_onAction_calls_handleBlockUrl() {
   1310  info(
   1311    "TelemetryFeed.onAction should call #handleBlockUrl on a BLOCK_URL action"
   1312  );
   1313  let sandbox = sinon.createSandbox();
   1314  let instance = new TelemetryFeed();
   1315 
   1316  let data = { position: 0, advertiser_name: "moo", tile_id: 42 };
   1317  let action = { type: actionTypes.BLOCK_URL, data };
   1318  sandbox.spy(instance, "handleBlockUrl");
   1319 
   1320  instance.onAction(actionCreators.AlsoToMain(action));
   1321 
   1322  Assert.ok(
   1323    instance.handleBlockUrl.calledOnce,
   1324    "TelemetryFeed.handleBlockUrl called once"
   1325  );
   1326 
   1327  sandbox.restore();
   1328 });
   1329 
   1330 add_task(
   1331  async function test_onAction_calls_handleTopSitesOrganicImpressionStats() {
   1332    info(
   1333      "TelemetryFeed.onAction should call .handleTopSitesOrganicImpressionStats " +
   1334        "on a TOP_SITES_ORGANIC_IMPRESSION_STATS action"
   1335    );
   1336    let sandbox = sinon.createSandbox();
   1337    let instance = new TelemetryFeed();
   1338 
   1339    let session = {};
   1340    sandbox.stub(instance.sessions, "get").returns(session);
   1341 
   1342    let data = { type: "impression", position: 1 };
   1343    let action = { type: actionTypes.TOP_SITES_ORGANIC_IMPRESSION_STATS, data };
   1344    sandbox.spy(instance, "handleTopSitesOrganicImpressionStats");
   1345 
   1346    instance.onAction(actionCreators.AlsoToMain(action));
   1347 
   1348    Assert.ok(
   1349      instance.handleTopSitesOrganicImpressionStats.calledOnce,
   1350      "TelemetryFeed.handleTopSitesOrganicImpressionStats called once"
   1351    );
   1352    Assert.deepEqual(
   1353      instance.handleTopSitesOrganicImpressionStats.firstCall.args[0].data,
   1354      data
   1355    );
   1356 
   1357    sandbox.restore();
   1358  }
   1359 );
   1360 
   1361 add_task(async function test_handleNewTabInit_sets_preloaded_session() {
   1362  info(
   1363    "TelemetryFeed.handleNewTabInit should set the session as preloaded " +
   1364      "if the browser is preloaded"
   1365  );
   1366  let sandbox = sinon.createSandbox();
   1367  let instance = new TelemetryFeed();
   1368 
   1369  let session = { perf: {} };
   1370  let preloadedBrowser = {
   1371    getAttribute() {
   1372      return "preloaded";
   1373    },
   1374  };
   1375  sandbox.stub(instance, "addSession").returns(session);
   1376 
   1377  instance.onAction(
   1378    actionCreators.AlsoToMain({
   1379      type: actionTypes.NEW_TAB_INIT,
   1380      data: { url: "about:newtab", browser: preloadedBrowser },
   1381    })
   1382  );
   1383 
   1384  Assert.ok(session.perf.is_preloaded, "is_preloaded property was set");
   1385 
   1386  sandbox.restore();
   1387 });
   1388 
   1389 add_task(async function test_handleNewTabInit_sets_nonpreloaded_session() {
   1390  info(
   1391    "TelemetryFeed.handleNewTabInit should set the session as non-preloaded " +
   1392      "if the browser is non-preloaded"
   1393  );
   1394  let sandbox = sinon.createSandbox();
   1395  let instance = new TelemetryFeed();
   1396 
   1397  let session = { perf: {} };
   1398  let preloadedBrowser = {
   1399    getAttribute() {
   1400      return "";
   1401    },
   1402  };
   1403  sandbox.stub(instance, "addSession").returns(session);
   1404 
   1405  instance.onAction(
   1406    actionCreators.AlsoToMain({
   1407      type: actionTypes.NEW_TAB_INIT,
   1408      data: { url: "about:newtab", browser: preloadedBrowser },
   1409    })
   1410  );
   1411 
   1412  Assert.ok(!session.perf.is_preloaded, "is_preloaded property is not true");
   1413 
   1414  sandbox.restore();
   1415 });
   1416 
   1417 add_task(async function test_sendPageTakeoverData_homepage_category() {
   1418  info(
   1419    "TelemetryFeed.sendPageTakeoverData should call " +
   1420      "handleASRouterUserEvent"
   1421  );
   1422  let sandbox = sinon.createSandbox();
   1423  let instance = new TelemetryFeed();
   1424  Services.fog.testResetFOG();
   1425 
   1426  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
   1427  sandbox.stub(HomePage, "get").returns("https://searchprovider.com");
   1428  sandbox.stub(instance, "configureContentPing");
   1429  instance._classifySite = () => Promise.resolve("other");
   1430 
   1431  await instance.sendPageTakeoverData();
   1432  Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "other");
   1433 
   1434  Services.prefs.clearUserPref(PREF_TELEMETRY);
   1435  sandbox.restore();
   1436 });
   1437 
   1438 add_task(async function test_sendPageTakeoverData_newtab_category_custom() {
   1439  info(
   1440    "TelemetryFeed.sendPageTakeoverData should send correct newtab " +
   1441      "category for about:newtab set to custom URL"
   1442  );
   1443  let sandbox = sinon.createSandbox();
   1444  let instance = new TelemetryFeed();
   1445  Services.fog.testResetFOG();
   1446 
   1447  sandbox.stub(AboutNewTab, "newTabURLOverridden").get(() => true);
   1448  sandbox
   1449    .stub(AboutNewTab, "newTabURL")
   1450    .get(() => "https://searchprovider.com");
   1451  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
   1452  sandbox.stub(instance, "configureContentPing");
   1453  instance._classifySite = () => Promise.resolve("other");
   1454 
   1455  await instance.sendPageTakeoverData();
   1456  Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "other");
   1457 
   1458  Services.prefs.clearUserPref(PREF_TELEMETRY);
   1459  sandbox.restore();
   1460 });
   1461 
   1462 add_task(async function test_sendPageTakeoverData_newtab_category_custom() {
   1463  info(
   1464    "TelemetryFeed.sendPageTakeoverData should not set home|newtab " +
   1465      "category if neither about:{home,newtab} are set to custom URL"
   1466  );
   1467  let sandbox = sinon.createSandbox();
   1468  let instance = new TelemetryFeed();
   1469  Services.fog.testResetFOG();
   1470 
   1471  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
   1472  sandbox.stub(instance, "configureContentPing");
   1473  instance._classifySite = () => Promise.resolve("other");
   1474 
   1475  await instance.sendPageTakeoverData();
   1476  Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "enabled");
   1477  Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "enabled");
   1478 
   1479  Services.prefs.clearUserPref(PREF_TELEMETRY);
   1480  sandbox.restore();
   1481 });
   1482 
   1483 add_task(async function test_sendPageTakeoverData_newtab_category_extension() {
   1484  info(
   1485    "TelemetryFeed.sendPageTakeoverData should set correct home|newtab " +
   1486      "category when changed by extension"
   1487  );
   1488  let sandbox = sinon.createSandbox();
   1489  let instance = new TelemetryFeed();
   1490  Services.fog.testResetFOG();
   1491 
   1492  const ID = "{abc-foo-bar}";
   1493  sandbox.stub(ExtensionSettingsStore, "getSetting").returns({ id: ID });
   1494 
   1495  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
   1496  sandbox.stub(instance, "configureContentPing");
   1497  instance._classifySite = () => Promise.resolve("other");
   1498 
   1499  await instance.sendPageTakeoverData();
   1500  Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "extension");
   1501  Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "extension");
   1502 
   1503  Services.prefs.clearUserPref(PREF_TELEMETRY);
   1504  sandbox.restore();
   1505 });
   1506 
   1507 add_task(async function test_sendPageTakeoverData_newtab_disabled() {
   1508  info(
   1509    "TelemetryFeed.sendPageTakeoverData instruments when newtab is disabled"
   1510  );
   1511  let sandbox = sinon.createSandbox();
   1512  let instance = new TelemetryFeed();
   1513  Services.fog.testResetFOG();
   1514 
   1515  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
   1516  Services.prefs.setBoolPref("browser.newtabpage.enabled", false);
   1517  sandbox.stub(instance, "configureContentPing");
   1518  instance._classifySite = () => Promise.resolve("other");
   1519 
   1520  await instance.sendPageTakeoverData();
   1521  Assert.equal(Glean.newtab.newtabCategory.testGetValue(), "disabled");
   1522 
   1523  Services.prefs.clearUserPref(PREF_TELEMETRY);
   1524  Services.prefs.clearUserPref("browser.newtabpage.enabled");
   1525  sandbox.restore();
   1526 });
   1527 
   1528 add_task(async function test_sendPageTakeoverData_homepage_disabled() {
   1529  info(
   1530    "TelemetryFeed.sendPageTakeoverData instruments when homepage is disabled"
   1531  );
   1532  let sandbox = sinon.createSandbox();
   1533  let instance = new TelemetryFeed();
   1534  Services.fog.testResetFOG();
   1535 
   1536  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
   1537  sandbox.stub(HomePage, "overridden").get(() => true);
   1538  sandbox.stub(instance, "configureContentPing");
   1539 
   1540  await instance.sendPageTakeoverData();
   1541  Assert.equal(Glean.newtab.homepageCategory.testGetValue(), "disabled");
   1542 
   1543  Services.prefs.clearUserPref(PREF_TELEMETRY);
   1544  sandbox.restore();
   1545 });
   1546 
   1547 add_task(async function test_sendPageTakeoverData_newtab_ping() {
   1548  info("TelemetryFeed.sendPageTakeoverData should send a 'newtab' ping");
   1549  let sandbox = sinon.createSandbox();
   1550  let instance = new TelemetryFeed();
   1551  Services.fog.testResetFOG();
   1552 
   1553  Services.prefs.setBoolPref(PREF_TELEMETRY, true);
   1554  sandbox.stub(instance, "configureContentPing");
   1555 
   1556  let pingSubmitted = new Promise(resolve => {
   1557    GleanPings.newtab.testBeforeNextSubmit(reason => {
   1558      Assert.equal(reason, "component_init");
   1559      resolve();
   1560    });
   1561  });
   1562 
   1563  await instance.sendPageTakeoverData();
   1564  await pingSubmitted;
   1565 
   1566  Services.prefs.clearUserPref(PREF_TELEMETRY);
   1567  sandbox.restore();
   1568 });
   1569 
   1570 add_task(
   1571  async function test_handleDiscoveryStreamImpressionStats_should_throw() {
   1572    info(
   1573      "TelemetryFeed.handleDiscoveryStreamImpressionStats should throw " +
   1574        "for a missing session"
   1575    );
   1576 
   1577    let instance = new TelemetryFeed();
   1578    try {
   1579      instance.handleDiscoveryStreamImpressionStats("a_missing_port", {});
   1580      Assert.ok(false, "Should not have reached here.");
   1581    } catch (e) {
   1582      Assert.ok(true, "Should have thrown for a missing session.");
   1583    }
   1584  }
   1585 );
   1586 
   1587 add_task(
   1588  async function test_handleDiscoveryStreamImpressionStats_instrument_pocket_impressions() {
   1589    info(
   1590      "TelemetryFeed.handleDiscoveryStreamImpressionStats should throw " +
   1591        "for a missing session"
   1592    );
   1593 
   1594    let sandbox = sinon.createSandbox();
   1595    let instance = new TelemetryFeed();
   1596    Services.fog.testResetFOG();
   1597 
   1598    const SESSION_ID = "1337cafe";
   1599    const POS_1 = 1;
   1600    const POS_2 = 4;
   1601    const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ=";
   1602    const FETCH_TIMESTAMP = new Date("March 22, 2024 10:15:20");
   1603    const NEWTAB_CREATION_TIMESTAMP = new Date("March 23, 2024 11:10:30");
   1604    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   1605 
   1606    let pingSubmitted = new Promise(resolve => {
   1607      GleanPings.spoc.testBeforeNextSubmit(reason => {
   1608        Assert.equal(reason, "impression");
   1609        let pocketImpressions = Glean.pocket.impression.testGetValue();
   1610        Assert.equal(pocketImpressions.length, 2);
   1611        Assert.deepEqual(pocketImpressions[0].extra, {
   1612          newtab_visit_id: SESSION_ID,
   1613          is_sponsored: String(false),
   1614          position: String(POS_1),
   1615          recommendation_id: "decaf-c0ff33",
   1616          content_redacted: String(true),
   1617        });
   1618        Assert.deepEqual(pocketImpressions[1].extra, {
   1619          newtab_visit_id: SESSION_ID,
   1620          is_sponsored: String(true),
   1621          position: String(POS_2),
   1622          tile_id: String(2),
   1623          content_redacted: String(true),
   1624        });
   1625        Assert.equal(Glean.pocket.shim.testGetValue(), SHIM);
   1626        Assert.deepEqual(
   1627          Glean.pocket.fetchTimestamp.testGetValue(),
   1628          FETCH_TIMESTAMP
   1629        );
   1630        Assert.deepEqual(
   1631          Glean.pocket.newtabCreationTimestamp.testGetValue(),
   1632          NEWTAB_CREATION_TIMESTAMP
   1633        );
   1634 
   1635        resolve();
   1636      });
   1637    });
   1638 
   1639    instance.handleDiscoveryStreamImpressionStats("_", {
   1640      source: "foo",
   1641      tiles: [
   1642        {
   1643          id: 1,
   1644          pos: POS_1,
   1645          type: "organic",
   1646          recommendation_id: "decaf-c0ff33",
   1647        },
   1648        {
   1649          id: 2,
   1650          pos: POS_2,
   1651          type: "spoc",
   1652          recommendation_id: undefined,
   1653          shim: SHIM,
   1654          fetchTimestamp: FETCH_TIMESTAMP.valueOf(),
   1655        },
   1656      ],
   1657      window_inner_width: 1000,
   1658      window_inner_height: 900,
   1659      firstVisibleTimestamp: NEWTAB_CREATION_TIMESTAMP.valueOf(),
   1660    });
   1661 
   1662    await pingSubmitted;
   1663 
   1664    sandbox.restore();
   1665  }
   1666 );
   1667 
   1668 add_task(
   1669  async function test_handleTopSitesSponsoredImpressionStats_add_keyed_scalar() {
   1670    info(
   1671      "TelemetryFeed.handleTopSitesSponsoredImpressionStats should add to " +
   1672        "keyed scalar on an impression event"
   1673    );
   1674 
   1675    let sandbox = sinon.createSandbox();
   1676    let instance = new TelemetryFeed();
   1677    Services.telemetry.clearScalars();
   1678 
   1679    let data = {
   1680      type: "impression",
   1681      tile_id: 42,
   1682      source: "newtab",
   1683      position: 0,
   1684      reporting_url: "https://test.reporting.net/",
   1685    };
   1686    await instance.handleTopSitesSponsoredImpressionStats({ data });
   1687    TelemetryTestUtils.assertKeyedScalar(
   1688      TelemetryTestUtils.getProcessScalars("parent", true, true),
   1689      "contextual.services.topsites.impression",
   1690      "newtab_1",
   1691      1
   1692    );
   1693 
   1694    sandbox.restore();
   1695  }
   1696 );
   1697 
   1698 add_task(
   1699  async function test_handleTopSitesSponsoredImpressionStats_add_keyed_scalar_click() {
   1700    info(
   1701      "TelemetryFeed.handleTopSitesSponsoredImpressionStats should add to " +
   1702        "keyed scalar on a click event"
   1703    );
   1704 
   1705    let sandbox = sinon.createSandbox();
   1706    let instance = new TelemetryFeed();
   1707    Services.telemetry.clearScalars();
   1708 
   1709    let data = {
   1710      type: "click",
   1711      tile_id: 42,
   1712      source: "newtab",
   1713      position: 0,
   1714      reporting_url: "https://test.reporting.net/",
   1715    };
   1716    await instance.handleTopSitesSponsoredImpressionStats({ data });
   1717    TelemetryTestUtils.assertKeyedScalar(
   1718      TelemetryTestUtils.getProcessScalars("parent", true, true),
   1719      "contextual.services.topsites.click",
   1720      "newtab_1",
   1721      1
   1722    );
   1723 
   1724    sandbox.restore();
   1725  }
   1726 );
   1727 
   1728 add_task(
   1729  async function test_handleTopSitesSponsoredImpressionStats_record_glean_impression() {
   1730    info(
   1731      "TelemetryFeed.handleTopSitesSponsoredImpressionStats should record a " +
   1732        "Glean topsites.impression event on an impression event"
   1733    );
   1734 
   1735    let sandbox = sinon.createSandbox();
   1736    let instance = new TelemetryFeed();
   1737    Services.fog.testResetFOG();
   1738 
   1739    let data = {
   1740      type: "impression",
   1741      tile_id: 42,
   1742      source: "newtab",
   1743      position: 1,
   1744      reporting_url: "https://test.reporting.net/",
   1745      advertiser: "adnoid ads",
   1746    };
   1747    const SESSION_ID = "decafc0ffee";
   1748    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   1749    await instance.handleTopSitesSponsoredImpressionStats({ data });
   1750    let impressions = Glean.topsites.impression.testGetValue();
   1751    Assert.equal(impressions.length, 1, "Should have recorded 1 impression");
   1752 
   1753    Assert.deepEqual(impressions[0].extra, {
   1754      advertiser_name: "adnoid ads",
   1755      tile_id: data.tile_id,
   1756      newtab_visit_id: SESSION_ID,
   1757      is_sponsored: String(true),
   1758      position: String(1),
   1759    });
   1760 
   1761    sandbox.restore();
   1762  }
   1763 );
   1764 
   1765 add_task(
   1766  async function test_handleTopSitesSponsoredImpressionStats_record_glean_click() {
   1767    info(
   1768      "TelemetryFeed.handleTopSitesSponsoredImpressionStats should record " +
   1769        "a Glean topsites.click event on a click event"
   1770    );
   1771 
   1772    let sandbox = sinon.createSandbox();
   1773    let instance = new TelemetryFeed();
   1774    Services.fog.testResetFOG();
   1775 
   1776    let data = {
   1777      type: "click",
   1778      advertiser: "test advertiser",
   1779      tile_id: 42,
   1780      source: "newtab",
   1781      position: 0,
   1782      reporting_url: "https://test.reporting.net/",
   1783    };
   1784    const SESSION_ID = "decafc0ffee";
   1785    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   1786    await instance.handleTopSitesSponsoredImpressionStats({ data });
   1787    let clicks = Glean.topsites.click.testGetValue();
   1788    Assert.equal(clicks.length, 1, "Should have recorded 1 click");
   1789 
   1790    Assert.deepEqual(clicks[0].extra, {
   1791      advertiser_name: "test advertiser",
   1792      tile_id: data.tile_id,
   1793      newtab_visit_id: SESSION_ID,
   1794      is_sponsored: String(true),
   1795      position: String(0),
   1796    });
   1797 
   1798    sandbox.restore();
   1799  }
   1800 );
   1801 
   1802 add_task(
   1803  async function test_handleTopSitesSponsoredImpressionStats_no_submit_unknown_pingType() {
   1804    info(
   1805      "TelemetryFeed.handleTopSitesSponsoredImpressionStats should not " +
   1806        "submit on unknown pingTypes"
   1807    );
   1808 
   1809    let sandbox = sinon.createSandbox();
   1810    let instance = new TelemetryFeed();
   1811    Services.fog.testResetFOG();
   1812 
   1813    let data = { type: "unknown_type" };
   1814 
   1815    await instance.handleTopSitesSponsoredImpressionStats({ data });
   1816    let impressions = Glean.topsites.impression.testGetValue();
   1817    Assert.ok(!impressions, "Should not have recorded any impressions");
   1818 
   1819    sandbox.restore();
   1820  }
   1821 );
   1822 
   1823 add_task(
   1824  async function test_handleTopSitesOrganicImpressionStats_record_glean_topsites_impression() {
   1825    info(
   1826      "TelemetryFeed.handleTopSitesOrganicImpressionStats should record a " +
   1827        "Glean topsites.impression event on an impression event"
   1828    );
   1829 
   1830    let sandbox = sinon.createSandbox();
   1831    let instance = new TelemetryFeed();
   1832    Services.fog.testResetFOG();
   1833 
   1834    let data = {
   1835      type: "impression",
   1836      source: "newtab",
   1837      position: 0,
   1838      isPinned: false,
   1839      smartScores: { moo: 1 },
   1840      smartWeights: { moo: 0 },
   1841    };
   1842    const SESSION_ID = "decafc0ffee";
   1843    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   1844 
   1845    await instance.handleTopSitesOrganicImpressionStats({ data });
   1846    let impressions = Glean.topsites.impression.testGetValue();
   1847    Assert.equal(impressions.length, 1, "Recorded 1 impression");
   1848 
   1849    Assert.deepEqual(impressions[0].extra, {
   1850      is_pinned: String(false),
   1851      newtab_visit_id: SESSION_ID,
   1852      is_sponsored: String(false),
   1853      position: String(0),
   1854      smart_scores: JSON.stringify({ moo: 1 }),
   1855      smart_weights: JSON.stringify({ moo: 0 }),
   1856    });
   1857 
   1858    sandbox.restore();
   1859  }
   1860 );
   1861 
   1862 add_task(
   1863  async function test_handleTopSitesOrganicImpressionStats_record_glean_topsites_click() {
   1864    info(
   1865      "TelemetryFeed.handleTopSitesOrganicImpressionStats should record a " +
   1866        "Glean topsites.click event on a click event"
   1867    );
   1868 
   1869    let sandbox = sinon.createSandbox();
   1870    let instance = new TelemetryFeed();
   1871    Services.fog.testResetFOG();
   1872 
   1873    let data = {
   1874      type: "click",
   1875      source: "newtab",
   1876      position: 0,
   1877      isPinned: false,
   1878      smartScores: { moo: 1 },
   1879      smartWeights: { moo: 0 },
   1880    };
   1881    const SESSION_ID = "decafc0ffee";
   1882    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   1883 
   1884    await instance.handleTopSitesOrganicImpressionStats({ data });
   1885    let clicks = Glean.topsites.click.testGetValue();
   1886    Assert.equal(clicks.length, 1, "Recorded 1 click");
   1887 
   1888    Assert.deepEqual(clicks[0].extra, {
   1889      newtab_visit_id: SESSION_ID,
   1890      is_sponsored: String(false),
   1891      position: String(0),
   1892      is_pinned: String(false),
   1893      smart_scores: JSON.stringify({ moo: 1 }),
   1894      smart_weights: JSON.stringify({ moo: 0 }),
   1895    });
   1896 
   1897    sandbox.restore();
   1898  }
   1899 );
   1900 
   1901 add_task(
   1902  async function test_handleTopSitesOrganicImpressionStats_no_recording() {
   1903    info(
   1904      "TelemetryFeed.handleTopSitesOrganicImpressionStats should not " +
   1905        "record events on an unknown session"
   1906    );
   1907 
   1908    let sandbox = sinon.createSandbox();
   1909    let instance = new TelemetryFeed();
   1910    Services.fog.testResetFOG();
   1911 
   1912    sandbox.stub(instance.sessions, "get").returns(false);
   1913 
   1914    await instance.handleTopSitesOrganicImpressionStats({});
   1915    Assert.ok(!Glean.topsites.click.testGetValue(), "Click was not recorded");
   1916    Assert.ok(
   1917      !Glean.topsites.impression.testGetValue(),
   1918      "Impression was not recorded"
   1919    );
   1920 
   1921    sandbox.restore();
   1922  }
   1923 );
   1924 
   1925 add_task(
   1926  async function test_handleTopSitesOrganicImpressionStats_no_recording_with_session() {
   1927    info(
   1928      "TelemetryFeed.handleTopSitesOrganicImpressionStats should not record " +
   1929        "events on an unknown impressionStats action"
   1930    );
   1931 
   1932    let sandbox = sinon.createSandbox();
   1933    let instance = new TelemetryFeed();
   1934    Services.fog.testResetFOG();
   1935 
   1936    const SESSION_ID = "decafc0ffee";
   1937    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   1938 
   1939    await instance.handleTopSitesOrganicImpressionStats({ type: "unknown" });
   1940    Assert.ok(!Glean.topsites.click.testGetValue(), "Click was not recorded");
   1941    Assert.ok(
   1942      !Glean.topsites.impression.testGetValue(),
   1943      "Impression was not recorded"
   1944    );
   1945 
   1946    sandbox.restore();
   1947  }
   1948 );
   1949 
   1950 add_task(
   1951  async function test_handleDiscoveryStreamUserEvent_no_recording_with_session() {
   1952    info(
   1953      "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " +
   1954        "action with no `data`"
   1955    );
   1956 
   1957    let sandbox = sinon.createSandbox();
   1958    let instance = new TelemetryFeed();
   1959    Services.fog.testResetFOG();
   1960 
   1961    let action = actionCreators.DiscoveryStreamUserEvent();
   1962    const SESSION_ID = "decafc0ffee";
   1963    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   1964 
   1965    instance.handleDiscoveryStreamUserEvent(action);
   1966    Assert.ok(
   1967      !Glean.pocket.topicClick.testGetValue(),
   1968      "Pocket topicClick was not recorded"
   1969    );
   1970    Assert.ok(
   1971      !Glean.pocket.click.testGetValue(),
   1972      "Pocket click was not recorded"
   1973    );
   1974    Assert.ok(
   1975      !Glean.pocket.save.testGetValue(),
   1976      "Pocket save was not recorded"
   1977    );
   1978 
   1979    sandbox.restore();
   1980  }
   1981 );
   1982 
   1983 add_task(
   1984  async function test_handleDiscoveryStreamUserEvent_click_with_no_value() {
   1985    info(
   1986      "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " +
   1987        "CLICK data with no value"
   1988    );
   1989 
   1990    let sandbox = sinon.createSandbox();
   1991    let instance = new TelemetryFeed();
   1992    Services.fog.testResetFOG();
   1993 
   1994    let action = actionCreators.DiscoveryStreamUserEvent({
   1995      event: "CLICK",
   1996      source: "POPULAR_TOPICS",
   1997    });
   1998    const SESSION_ID = "decafc0ffee";
   1999    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2000 
   2001    instance.handleDiscoveryStreamUserEvent(action);
   2002    let topicClicks = Glean.pocket.topicClick.testGetValue();
   2003    Assert.equal(topicClicks.length, 1, "Recorded 1 click");
   2004    Assert.deepEqual(topicClicks[0].extra, {
   2005      newtab_visit_id: SESSION_ID,
   2006    });
   2007 
   2008    sandbox.restore();
   2009  }
   2010 );
   2011 
   2012 add_task(
   2013  async function test_handleDiscoveryStreamUserEvent_non_popular_click_with_no_value() {
   2014    info(
   2015      "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " +
   2016        "non-POPULAR_TOPICS CLICK data with no value"
   2017    );
   2018 
   2019    let sandbox = sinon.createSandbox();
   2020    let instance = new TelemetryFeed();
   2021    Services.fog.testResetFOG();
   2022 
   2023    let action = actionCreators.DiscoveryStreamUserEvent({
   2024      event: "CLICK",
   2025      source: "not-POPULAR_TOPICS",
   2026    });
   2027    const SESSION_ID = "decafc0ffee";
   2028    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2029 
   2030    instance.handleDiscoveryStreamUserEvent(action);
   2031    Assert.ok(
   2032      !Glean.pocket.topicClick.testGetValue(),
   2033      "Pocket topicClick was not recorded"
   2034    );
   2035    Assert.ok(
   2036      !Glean.pocket.click.testGetValue(),
   2037      "Pocket click was not recorded"
   2038    );
   2039    Assert.ok(
   2040      !Glean.pocket.save.testGetValue(),
   2041      "Pocket save was not recorded"
   2042    );
   2043 
   2044    sandbox.restore();
   2045  }
   2046 );
   2047 
   2048 add_task(
   2049  async function test_handleDiscoveryStreamUserEvent_non_popular_click() {
   2050    info(
   2051      "TelemetryFeed.handleDiscoveryStreamUserEvent correctly handles " +
   2052        "CLICK data with non-POPULAR_TOPICS source"
   2053    );
   2054 
   2055    let sandbox = sinon.createSandbox();
   2056    let instance = new TelemetryFeed();
   2057    Services.fog.testResetFOG();
   2058    const TOPIC = "atopic";
   2059    let action = actionCreators.DiscoveryStreamUserEvent({
   2060      event: "CLICK",
   2061      source: "not-POPULAR_TOPICS",
   2062      value: {
   2063        card_type: "topics_widget",
   2064        topic: TOPIC,
   2065      },
   2066    });
   2067 
   2068    const SESSION_ID = "decafc0ffee";
   2069    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2070 
   2071    instance.handleDiscoveryStreamUserEvent(action);
   2072    let topicClicks = Glean.pocket.topicClick.testGetValue();
   2073    Assert.equal(topicClicks.length, 1, "Recorded 1 click");
   2074    Assert.deepEqual(topicClicks[0].extra, {
   2075      newtab_visit_id: SESSION_ID,
   2076      topic: TOPIC,
   2077    });
   2078 
   2079    sandbox.restore();
   2080  }
   2081 );
   2082 
   2083 add_task(
   2084  async function test_handleDiscoveryStreamUserEvent_without_card_type() {
   2085    info(
   2086      "TelemetryFeed.handleDiscoveryStreamUserEvent doesn't instrument " +
   2087        "a CLICK without a card_type"
   2088    );
   2089 
   2090    let sandbox = sinon.createSandbox();
   2091    let instance = new TelemetryFeed();
   2092    Services.fog.testResetFOG();
   2093    let action = actionCreators.DiscoveryStreamUserEvent({
   2094      event: "CLICK",
   2095      source: "not-POPULAR_TOPICS",
   2096      value: {
   2097        card_type: "not spoc, organic, or topics_widget",
   2098      },
   2099    });
   2100 
   2101    const SESSION_ID = "decafc0ffee";
   2102    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2103 
   2104    instance.handleDiscoveryStreamUserEvent(action);
   2105 
   2106    Assert.ok(
   2107      !Glean.pocket.topicClick.testGetValue(),
   2108      "Pocket topicClick was not recorded"
   2109    );
   2110    Assert.ok(
   2111      !Glean.pocket.click.testGetValue(),
   2112      "Pocket click was not recorded"
   2113    );
   2114    Assert.ok(
   2115      !Glean.pocket.save.testGetValue(),
   2116      "Pocket save was not recorded"
   2117    );
   2118 
   2119    sandbox.restore();
   2120  }
   2121 );
   2122 
   2123 add_task(async function test_handleDiscoveryStreamUserEvent_popular_click() {
   2124  info(
   2125    "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a popular " +
   2126      "topic click"
   2127  );
   2128 
   2129  let sandbox = sinon.createSandbox();
   2130  let instance = new TelemetryFeed();
   2131  Services.fog.testResetFOG();
   2132  const TOPIC = "entertainment";
   2133  let action = actionCreators.DiscoveryStreamUserEvent({
   2134    event: "CLICK",
   2135    source: "POPULAR_TOPICS",
   2136    value: {
   2137      card_type: "topics_widget",
   2138      topic: TOPIC,
   2139    },
   2140  });
   2141 
   2142  const SESSION_ID = "decafc0ffee";
   2143  sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2144 
   2145  instance.handleDiscoveryStreamUserEvent(action);
   2146  let topicClicks = Glean.pocket.topicClick.testGetValue();
   2147  Assert.equal(topicClicks.length, 1, "Recorded 1 click");
   2148  Assert.deepEqual(topicClicks[0].extra, {
   2149    newtab_visit_id: SESSION_ID,
   2150    topic: TOPIC,
   2151  });
   2152 
   2153  sandbox.restore();
   2154 });
   2155 
   2156 add_task(async function test_handleDiscoveryStreamUserEvent_tooltip_click() {
   2157  info(
   2158    "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a " +
   2159      "tooltip click"
   2160  );
   2161 
   2162  let sandbox = sinon.createSandbox();
   2163  let instance = new TelemetryFeed();
   2164  Services.fog.testResetFOG();
   2165  const feature = "FEATURE_HIGHLIGHT_DEFAULT";
   2166  let action = actionCreators.DiscoveryStreamUserEvent({
   2167    event: "CLICK",
   2168    source: "FEATURE_HIGHLIGHT",
   2169    value: {
   2170      feature,
   2171    },
   2172  });
   2173 
   2174  const SESSION_ID = "decafc0ffee";
   2175  sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2176 
   2177  instance.handleDiscoveryStreamUserEvent(action);
   2178  let tooltipClicks = Glean.newtab.tooltipClick.testGetValue();
   2179  Assert.equal(tooltipClicks.length, 1, "Recorded 1 click");
   2180  Assert.deepEqual(tooltipClicks[0].extra, {
   2181    newtab_visit_id: SESSION_ID,
   2182    feature,
   2183  });
   2184 
   2185  sandbox.restore();
   2186 });
   2187 
   2188 add_task(
   2189  async function test_handleDiscoveryStreamUserEvent_organic_top_stories_click() {
   2190    info(
   2191      "TelemetryFeed.handleDiscoveryStreamUserEvent instruments an organic " +
   2192        "top stories click"
   2193    );
   2194    Services.prefs.setBoolPref(PREF_PRIVATE_PING_ENABLED, false);
   2195    Services.prefs.setBoolPref(PREF_REDACT_NEWTAB_PING_ENABLED, false);
   2196 
   2197    let sandbox = sinon.createSandbox();
   2198    let instance = new TelemetryFeed();
   2199    Services.fog.testResetFOG();
   2200    const ACTION_POSITION = 42;
   2201    let action = actionCreators.DiscoveryStreamUserEvent({
   2202      event: "CLICK",
   2203      action_position: ACTION_POSITION,
   2204      value: {
   2205        card_type: "organic",
   2206        corpus_item_id: "decaf-beef",
   2207        scheduled_corpus_item_id: "dead-beef",
   2208        tile_id: 314623757745896,
   2209      },
   2210    });
   2211 
   2212    const SESSION_ID = "decafc0ffee";
   2213    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2214 
   2215    instance.handleDiscoveryStreamUserEvent(action);
   2216 
   2217    let clicks = Glean.pocket.click.testGetValue();
   2218    Assert.equal(clicks.length, 1, "Recorded 1 click");
   2219    Assert.deepEqual(clicks[0].extra, {
   2220      newtab_visit_id: SESSION_ID,
   2221      is_sponsored: String(false),
   2222      position: String(ACTION_POSITION),
   2223      corpus_item_id: "decaf-beef",
   2224      scheduled_corpus_item_id: "dead-beef",
   2225      tile_id: String(314623757745896),
   2226    });
   2227 
   2228    Assert.ok(
   2229      !Glean.pocket.shim.testGetValue(),
   2230      "Pocket shim was not recorded"
   2231    );
   2232 
   2233    sandbox.restore();
   2234    Services.prefs.clearUserPref(PREF_PRIVATE_PING_ENABLED);
   2235    Services.prefs.clearUserPref(PREF_REDACT_NEWTAB_PING_ENABLED);
   2236  }
   2237 );
   2238 
   2239 add_task(
   2240  async function test_handleDiscoveryStreamUserEvent_private_ping_without_redactions_organic_top_stories_click() {
   2241    info(
   2242      "TelemetryFeed.handleDiscoveryStreamUserEvent instruments an organic " +
   2243        "top stories click with private ping fully enabled"
   2244    );
   2245 
   2246    Services.prefs.setBoolPref(PREF_PRIVATE_PING_ENABLED, true);
   2247    Services.prefs.setBoolPref(PREF_REDACT_NEWTAB_PING_ENABLED, false);
   2248 
   2249    let sandbox = sinon.createSandbox();
   2250    let instance = new TelemetryFeed();
   2251    Services.fog.testResetFOG();
   2252    const ACTION_POSITION = 42;
   2253    let action = actionCreators.DiscoveryStreamUserEvent({
   2254      event: "CLICK",
   2255      action_position: ACTION_POSITION,
   2256      value: {
   2257        card_type: "organic",
   2258        corpus_item_id: "decaf-beef",
   2259        scheduled_corpus_item_id: "dead-beef",
   2260        tile_id: 314623757745896,
   2261      },
   2262    });
   2263 
   2264    const SESSION_ID = "decafc0ffee";
   2265    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2266    sandbox.spy(instance.newtabContentPing, "recordEvent");
   2267 
   2268    instance.handleDiscoveryStreamUserEvent(action);
   2269 
   2270    let clicks = Glean.pocket.click.testGetValue();
   2271 
   2272    Assert.equal(clicks.length, 1, "Recorded 1 content click");
   2273    Assert.equal(clicks.length, 1, "Recorded 1 private click");
   2274    Assert.deepEqual(clicks[0].extra, {
   2275      newtab_visit_id: SESSION_ID,
   2276      is_sponsored: String(false),
   2277      corpus_item_id: "decaf-beef",
   2278      scheduled_corpus_item_id: "dead-beef",
   2279      position: String(ACTION_POSITION),
   2280      tile_id: 314623757745896,
   2281    });
   2282 
   2283    Assert.ok(
   2284      instance.newtabContentPing.recordEvent.calledWith(
   2285        "click",
   2286        sinon.match({
   2287          newtab_visit_id: SESSION_ID,
   2288          is_sponsored: false,
   2289          position: ACTION_POSITION,
   2290          tile_id: 314623757745896,
   2291          corpus_item_id: "decaf-beef",
   2292          scheduled_corpus_item_id: "dead-beef",
   2293        })
   2294      ),
   2295      "NewTabContentPing passed the expected arguments."
   2296    );
   2297 
   2298    Assert.ok(
   2299      !Glean.pocket.shim.testGetValue(),
   2300      "Pocket shim was not recorded"
   2301    );
   2302 
   2303    sandbox.restore();
   2304    Services.prefs.clearUserPref(PREF_PRIVATE_PING_ENABLED);
   2305    Services.prefs.clearUserPref(PREF_REDACT_NEWTAB_PING_ENABLED);
   2306  }
   2307 );
   2308 
   2309 add_task(
   2310  async function test_handleDiscoveryStreamUserEvent_private_ping_with_redactions_organic_top_stories_click() {
   2311    info(
   2312      "TelemetryFeed.handleDiscoveryStreamUserEvent instruments an organic " +
   2313        "top stories click with private ping fully enabled"
   2314    );
   2315 
   2316    Services.prefs.setBoolPref(PREF_PRIVATE_PING_ENABLED, true);
   2317    Services.prefs.setBoolPref(PREF_REDACT_NEWTAB_PING_ENABLED, true);
   2318 
   2319    let sandbox = sinon.createSandbox();
   2320    let instance = new TelemetryFeed();
   2321    Services.fog.testResetFOG();
   2322    const ACTION_POSITION = 42;
   2323    let action = actionCreators.DiscoveryStreamUserEvent({
   2324      event: "CLICK",
   2325      action_position: ACTION_POSITION,
   2326      value: {
   2327        card_type: "organic",
   2328        corpus_item_id: "decaf-beef",
   2329        scheduled_corpus_item_id: "dead-beef",
   2330        tile_id: 314623757745896,
   2331        content_redacted: true,
   2332      },
   2333    });
   2334 
   2335    const SESSION_ID = "decafc0ffee";
   2336    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2337    sandbox.spy(instance.newtabContentPing, "recordEvent");
   2338 
   2339    instance.handleDiscoveryStreamUserEvent(action);
   2340 
   2341    let clicks = Glean.pocket.click.testGetValue();
   2342 
   2343    Assert.equal(clicks.length, 1, "Recorded 1 content click");
   2344    Assert.equal(clicks.length, 1, "Recorded 1 private click");
   2345    Assert.deepEqual(clicks[0].extra, {
   2346      content_redacted: String(true),
   2347      newtab_visit_id: SESSION_ID,
   2348      is_sponsored: String(false),
   2349      position: String(ACTION_POSITION),
   2350    });
   2351 
   2352    Assert.ok(
   2353      instance.newtabContentPing.recordEvent.calledWith(
   2354        "click",
   2355        sinon.match({
   2356          newtab_visit_id: SESSION_ID,
   2357          is_sponsored: false,
   2358          position: ACTION_POSITION,
   2359          corpus_item_id: "decaf-beef",
   2360          scheduled_corpus_item_id: "dead-beef",
   2361          tile_id: 314623757745896,
   2362        })
   2363      ),
   2364      "NewTabContentPing passed the expected arguments."
   2365    );
   2366 
   2367    Assert.ok(
   2368      !Glean.pocket.shim.testGetValue(),
   2369      "Pocket shim was not recorded"
   2370    );
   2371 
   2372    sandbox.restore();
   2373    Services.prefs.clearUserPref(PREF_PRIVATE_PING_ENABLED);
   2374    Services.prefs.clearUserPref(PREF_REDACT_NEWTAB_PING_ENABLED);
   2375  }
   2376 );
   2377 
   2378 add_task(
   2379  async function test_handleDiscoveryStreamUserEvent_sponsored_top_stories_click() {
   2380    info(
   2381      "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a sponsored " +
   2382        "top stories click"
   2383    );
   2384 
   2385    let sandbox = sinon.createSandbox();
   2386    let instance = new TelemetryFeed();
   2387    Services.fog.testResetFOG();
   2388    const ACTION_POSITION = 42;
   2389    const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ=";
   2390    const FETCH_TIMESTAMP = new Date("March 22, 2024 10:15:20");
   2391    const NEWTAB_CREATION_TIMESTAMP = new Date("March 23, 2024 11:10:30");
   2392    let action = actionCreators.DiscoveryStreamUserEvent({
   2393      event: "CLICK",
   2394      action_position: ACTION_POSITION,
   2395      value: {
   2396        card_type: "spoc",
   2397        recommendation_id: undefined,
   2398        tile_id: 448685088,
   2399        shim: SHIM,
   2400        fetchTimestamp: FETCH_TIMESTAMP.valueOf(),
   2401        firstVisibleTimestamp: NEWTAB_CREATION_TIMESTAMP.valueOf(),
   2402      },
   2403    });
   2404 
   2405    const SESSION_ID = "decafc0ffee";
   2406    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2407 
   2408    let pingSubmitted = new Promise(resolve => {
   2409      GleanPings.spoc.testBeforeNextSubmit(reason => {
   2410        Assert.equal(reason, "click");
   2411        Assert.deepEqual(
   2412          Glean.pocket.fetchTimestamp.testGetValue(),
   2413          FETCH_TIMESTAMP
   2414        );
   2415        Assert.deepEqual(
   2416          Glean.pocket.newtabCreationTimestamp.testGetValue(),
   2417          NEWTAB_CREATION_TIMESTAMP
   2418        );
   2419        resolve();
   2420      });
   2421    });
   2422 
   2423    instance.handleDiscoveryStreamUserEvent(action);
   2424 
   2425    let clicks = Glean.pocket.click.testGetValue();
   2426    Assert.equal(clicks.length, 1, "Recorded 1 click");
   2427    Assert.deepEqual(clicks[0].extra, {
   2428      newtab_visit_id: SESSION_ID,
   2429      is_sponsored: String(true),
   2430      position: String(ACTION_POSITION),
   2431      tile_id: String(448685088),
   2432      content_redacted: String(true),
   2433    });
   2434 
   2435    await pingSubmitted;
   2436 
   2437    sandbox.restore();
   2438  }
   2439 );
   2440 
   2441 add_task(
   2442  async function test_handleAboutSponsoredTopSites_record_showPrivacyClick() {
   2443    info(
   2444      "TelemetryFeed.handleAboutSponsoredTopSites should record a Glean " +
   2445        "topsites.showPrivacyClick event on action"
   2446    );
   2447 
   2448    let sandbox = sinon.createSandbox();
   2449    let instance = new TelemetryFeed();
   2450    Services.fog.testResetFOG();
   2451 
   2452    let data = {
   2453      position: 42,
   2454      advertiser_name: "mozilla",
   2455      tile_id: 4567,
   2456    };
   2457 
   2458    const SESSION_ID = "decafc0ffee";
   2459    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2460 
   2461    instance.handleAboutSponsoredTopSites({ data });
   2462 
   2463    let clicks = Glean.topsites.showPrivacyClick.testGetValue();
   2464    Assert.equal(clicks.length, 1, "Recorded 1 click");
   2465    Assert.deepEqual(clicks[0].extra, {
   2466      advertiser_name: data.advertiser_name,
   2467      tile_id: String(data.tile_id),
   2468      newtab_visit_id: SESSION_ID,
   2469      position: String(data.position),
   2470    });
   2471 
   2472    sandbox.restore();
   2473  }
   2474 );
   2475 
   2476 add_task(
   2477  async function test_handleAboutSponsoredTopSites_no_record_showPrivacyClick() {
   2478    info(
   2479      "TelemetryFeed.handleAboutSponsoredTopSites should not record a Glean " +
   2480        "topsites.showPrivacyClick event if there's no session"
   2481    );
   2482 
   2483    let sandbox = sinon.createSandbox();
   2484    let instance = new TelemetryFeed();
   2485    Services.fog.testResetFOG();
   2486 
   2487    let data = {
   2488      position: 42,
   2489      advertiser_name: "mozilla",
   2490      tile_id: 4567,
   2491    };
   2492 
   2493    sandbox.stub(instance.sessions, "get").returns(null);
   2494 
   2495    instance.handleAboutSponsoredTopSites({ data });
   2496 
   2497    let clicks = Glean.topsites.showPrivacyClick.testGetValue();
   2498    Assert.ok(!clicks, "Did not record any clicks");
   2499 
   2500    sandbox.restore();
   2501  }
   2502 );
   2503 
   2504 add_task(async function test_handleBlockUrl_no_record_dismisses() {
   2505  info(
   2506    "TelemetryFeed.handleBlockUrl shouldn't record events for pocket " +
   2507      "cards' dismisses"
   2508  );
   2509 
   2510  let sandbox = sinon.createSandbox();
   2511  let instance = new TelemetryFeed();
   2512  Services.fog.testResetFOG();
   2513 
   2514  const SESSION_ID = "decafc0ffee";
   2515  sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2516 
   2517  let data = [
   2518    {
   2519      // Shouldn't record anything for this one
   2520      is_pocket_card: true,
   2521      position: 43,
   2522      tile_id: undefined,
   2523    },
   2524  ];
   2525 
   2526  await instance.handleBlockUrl({ data });
   2527 
   2528  Assert.ok(
   2529    !Glean.topsites.dismiss.testGetValue(),
   2530    "Should not record a dismiss for Pocket cards"
   2531  );
   2532 
   2533  sandbox.restore();
   2534 });
   2535 
   2536 add_task(async function test_handleBlockUrl_record_dismiss_on_action() {
   2537  info(
   2538    "TelemetryFeed.handleBlockUrl should record a topsites.dismiss event " +
   2539      "on action"
   2540  );
   2541 
   2542  let sandbox = sinon.createSandbox();
   2543  let instance = new TelemetryFeed();
   2544  Services.fog.testResetFOG();
   2545 
   2546  const SESSION_ID = "decafc0ffee";
   2547  sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2548 
   2549  let data = [
   2550    {
   2551      is_pocket_card: false,
   2552      position: 42,
   2553      advertiser_name: "mozilla",
   2554      tile_id: 4567,
   2555      isSponsoredTopSite: 1, // for some reason this is an int.
   2556    },
   2557  ];
   2558 
   2559  await instance.handleBlockUrl({ data, source: "TOP_SITES" });
   2560 
   2561  let dismisses = Glean.topsites.dismiss.testGetValue();
   2562  Assert.equal(dismisses.length, 1, "Should have recorded 1 dismiss");
   2563  Assert.deepEqual(dismisses[0].extra, {
   2564    advertiser_name: data[0].advertiser_name,
   2565    tile_id: String(data[0].tile_id),
   2566    newtab_visit_id: SESSION_ID,
   2567    is_sponsored: String(!!data[0].isSponsoredTopSite),
   2568    position: String(data[0].position),
   2569  });
   2570 
   2571  sandbox.restore();
   2572 });
   2573 
   2574 add_task(
   2575  async function test_handleBlockUrl_record_dismiss_on_nonsponsored_action() {
   2576    info(
   2577      "TelemetryFeed.handleBlockUrl should record a Glean topsites.dismiss " +
   2578        "event on action on non-sponsored topsite"
   2579    );
   2580 
   2581    let sandbox = sinon.createSandbox();
   2582    let instance = new TelemetryFeed();
   2583    Services.fog.testResetFOG();
   2584 
   2585    const SESSION_ID = "decafc0ffee";
   2586    sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID });
   2587 
   2588    let data = [
   2589      {
   2590        is_pocket_card: false,
   2591        position: 42,
   2592        tile_id: undefined,
   2593      },
   2594    ];
   2595 
   2596    await instance.handleBlockUrl({ data, source: "TOP_SITES" });
   2597 
   2598    let dismisses = Glean.topsites.dismiss.testGetValue();
   2599    Assert.equal(dismisses.length, 1, "Should have recorded 1 dismiss");
   2600    Assert.deepEqual(dismisses[0].extra, {
   2601      newtab_visit_id: SESSION_ID,
   2602      is_sponsored: String(false),
   2603      position: String(data[0].position),
   2604    });
   2605 
   2606    sandbox.restore();
   2607  }
   2608 );
   2609 
   2610 add_task(async function test_handleBlockUrl_no_record_dismiss_on_no_session() {
   2611  info(
   2612    "TelemetryFeed.handleBlockUrl should not record a Glean " +
   2613      "topsites.dismiss event if there's no session"
   2614  );
   2615 
   2616  let sandbox = sinon.createSandbox();
   2617  let instance = new TelemetryFeed();
   2618  Services.fog.testResetFOG();
   2619 
   2620  sandbox.stub(instance.sessions, "get").returns(null);
   2621 
   2622  let data = {};
   2623 
   2624  await instance.handleBlockUrl({ data });
   2625 
   2626  Assert.ok(
   2627    !Glean.topsites.dismiss.testGetValue(),
   2628    "Should not have recorded a dismiss"
   2629  );
   2630 
   2631  sandbox.restore();
   2632 });
   2633 
   2634 add_task(function test_randomizeOrganicContentEvent() {
   2635  info(
   2636    "TelemetryFeed._randomizeOrganicContentEvent should return true or false" +
   2637      " based on the given probability"
   2638  );
   2639  let sandbox = sinon.createSandbox();
   2640  let instance = new TelemetryFeed();
   2641 
   2642  const computeRec = id => ({
   2643    corpus_item_id: `item-${id}`,
   2644    topic: "a",
   2645    is_sponsored: false,
   2646    section_id: "section",
   2647    section_position: 3,
   2648  });
   2649  const allRecs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(computeRec);
   2650  sandbox.stub(instance, "getRecommendationCount").returns(allRecs.length);
   2651  sandbox.stub(instance, "getAllRecommendations").returns(allRecs);
   2652  instance._privateRandomContentTelemetryProbablityValues = { epsilon: 30 };
   2653  let decideStub = sandbox.stub(NewTabContentPing, "decideWithProbability");
   2654  decideStub.returns(true);
   2655  let result = instance.randomizeOrganicContentEvent(allRecs[0]);
   2656  Assert.equal(result, allRecs[0]);
   2657  Assert.ok(decideStub.calledOnce, "decideWithProbability was called once");
   2658  const [probUsed] = decideStub.firstCall.args;
   2659  Assert.greater(probUsed, 0.9); // Epsilon 30 is very high probability
   2660  Assert.less(probUsed, 1.01);
   2661 
   2662  // Run again - randomization kicks in
   2663  decideStub.returns(false);
   2664  sandbox.stub(NewTabContentPing, "secureRandIntInRange").returns(3);
   2665  result = instance.randomizeOrganicContentEvent(allRecs[0]);
   2666  Assert.equal(probUsed, decideStub.lastCall.args[0]);
   2667  Assert.deepEqual(result, allRecs[3]);
   2668 
   2669  sandbox.restore();
   2670 });
   2671 
   2672 add_task(async function test_handleSpocPlaceholderDuration_records_metric() {
   2673  info(
   2674    "TelemetryFeed.handleSpocPlaceholderDuration should record the " +
   2675      "spoc_placeholder_duration metric"
   2676  );
   2677 
   2678  let instance = new TelemetryFeed();
   2679  Services.fog.testResetFOG();
   2680 
   2681  const DURATION_MS = 150;
   2682  let action = {
   2683    type: actionTypes.DISCOVERY_STREAM_SPOC_PLACEHOLDER_DURATION,
   2684    data: { duration: DURATION_MS },
   2685  };
   2686 
   2687  instance.handleSpocPlaceholderDuration(action);
   2688 
   2689  let recordedDuration = Glean.pocket.spocPlaceholderDuration.testGetValue();
   2690  Assert.ok(recordedDuration, "Metric should be recorded");
   2691  Assert.equal(recordedDuration.count, 1, "Should have 1 sample");
   2692  Assert.greaterOrEqual(
   2693    recordedDuration.sum,
   2694    DURATION_MS,
   2695    "Duration should be at least the input value"
   2696  );
   2697 });
   2698 
   2699 add_task(async function test_handleSpocPlaceholderDuration_ignores_negative() {
   2700  info(
   2701    "TelemetryFeed.handleSpocPlaceholderDuration should ignore negative durations"
   2702  );
   2703 
   2704  let instance = new TelemetryFeed();
   2705  Services.fog.testResetFOG();
   2706 
   2707  let action = {
   2708    type: actionTypes.DISCOVERY_STREAM_SPOC_PLACEHOLDER_DURATION,
   2709    data: { duration: -1 },
   2710  };
   2711 
   2712  instance.handleSpocPlaceholderDuration(action);
   2713 
   2714  let recordedDuration = Glean.pocket.spocPlaceholderDuration.testGetValue();
   2715  Assert.equal(
   2716    recordedDuration,
   2717    null,
   2718    "Metric should not be recorded for negative duration"
   2719  );
   2720 });
   2721 
   2722 add_task(async function test_handleSpocPlaceholderDuration_ignores_undefined() {
   2723  info(
   2724    "TelemetryFeed.handleSpocPlaceholderDuration should ignore undefined durations"
   2725  );
   2726 
   2727  let instance = new TelemetryFeed();
   2728  Services.fog.testResetFOG();
   2729 
   2730  let action = {
   2731    type: actionTypes.DISCOVERY_STREAM_SPOC_PLACEHOLDER_DURATION,
   2732    data: {},
   2733  };
   2734 
   2735  instance.handleSpocPlaceholderDuration(action);
   2736 
   2737  let recordedDuration = Glean.pocket.spocPlaceholderDuration.testGetValue();
   2738  Assert.equal(
   2739    recordedDuration,
   2740    null,
   2741    "Metric should not be recorded for undefined duration"
   2742  );
   2743 });