tor-browser

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

BrowserUsageTelemetry.sys.mjs (66125B)


      1 /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      8 
      9 const lazy = {};
     10 
     11 ChromeUtils.defineESModuleGetters(lazy, {
     12  ClientID: "resource://gre/modules/ClientID.sys.mjs",
     13  CustomizableUI:
     14    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
     15  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
     16  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     17  PageActions: "resource:///modules/PageActions.sys.mjs",
     18  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     19  SearchSERPTelemetry:
     20    "moz-src:///browser/components/search/SearchSERPTelemetry.sys.mjs",
     21  SearchSERPTelemetryUtils:
     22    "moz-src:///browser/components/search/SearchSERPTelemetry.sys.mjs",
     23  SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
     24  TabMetrics: "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs",
     25  WindowsInstallsInfo:
     26    "resource://gre/modules/components-utils/WindowsInstallsInfo.sys.mjs",
     27 
     28  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
     29  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     30 });
     31 
     32 // This pref is in seconds!
     33 XPCOMUtils.defineLazyPreferenceGetter(
     34  lazy,
     35  "gRecentVisitedOriginsExpiry",
     36  "browser.engagement.recent_visited_origins.expiry"
     37 );
     38 XPCOMUtils.defineLazyPreferenceGetter(
     39  lazy,
     40  "sidebarVerticalTabs",
     41  "sidebar.verticalTabs",
     42  false,
     43  (_aPreference, _previousValue, isVertical) => {
     44    let tabCount = getOpenTabsAndWinsCounts().tabCount;
     45    BrowserUsageTelemetry.maxTabCount = tabCount;
     46    let pinnedTabCount = getPinnedTabsCount();
     47    BrowserUsageTelemetry.maxTabPinnedCount = pinnedTabCount;
     48    if (isVertical) {
     49      Glean.browserEngagement.maxConcurrentVerticalTabCount.set(tabCount);
     50      Glean.browserEngagement.maxConcurrentVerticalTabPinnedCount.set(
     51        pinnedTabCount
     52      );
     53    } else {
     54      Glean.browserEngagement.maxConcurrentTabCount.set(tabCount);
     55      Glean.browserEngagement.maxConcurrentTabPinnedCount.set(pinnedTabCount);
     56    }
     57    BrowserUsageTelemetry.recordPinnedTabsCount(pinnedTabCount);
     58  }
     59 );
     60 
     61 // The upper bound for the count of the visited unique domain names.
     62 const MAX_UNIQUE_VISITED_DOMAINS = 100;
     63 
     64 // Observed topic names.
     65 const TAB_RESTORING_TOPIC = "SSTabRestoring";
     66 const TELEMETRY_SUBSESSIONSPLIT_TOPIC =
     67  "internal-telemetry-after-subsession-split";
     68 const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
     69 const SESSION_STORE_SAVED_TAB_GROUPS_TOPIC =
     70  "sessionstore-saved-tab-groups-changed";
     71 
     72 export const MINIMUM_TAB_COUNT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes, in ms
     73 
     74 // The elements we consider to be interactive.
     75 const UI_TARGET_CHANGE_ELEMENTS = new Set([
     76  "moz-checkbox",
     77  "moz-select",
     78  "moz-radio",
     79  "moz-toggle",
     80  "moz-input-email",
     81  "moz-input-folder",
     82  "moz-input-number",
     83  "moz-input-password",
     84  "moz-input-search",
     85  "moz-input-tel",
     86  "moz-input-text",
     87  "moz-input-url",
     88  "moz-visual-picker-item",
     89  "sync-device-name",
     90 ]);
     91 const UI_TARGET_COMMAND_ELEMENTS = new Set([
     92  "menuitem",
     93  "toolbarbutton",
     94  "key",
     95  "command",
     96  "checkbox",
     97  "input",
     98  "button",
     99  "image",
    100  "radio",
    101  "richlistitem",
    102  "moz-button",
    103  "moz-box-button",
    104  "moz-box-link",
    105  "dialog-button",
    106 ]);
    107 const UI_TARGET_ELEMENTS = new Map([
    108  ["change", UI_TARGET_CHANGE_ELEMENTS],
    109  ["click", UI_TARGET_COMMAND_ELEMENTS],
    110  ["command", UI_TARGET_COMMAND_ELEMENTS],
    111 ]);
    112 
    113 // The containers of interactive elements that we care about and their pretty
    114 // names. These should be listed in order of most-specific to least-specific,
    115 // when iterating JavaScript will guarantee that ordering and so we will find
    116 // the most specific area first.
    117 const BROWSER_UI_CONTAINER_IDS = {
    118  "toolbar-menubar": "menu-bar",
    119  TabsToolbar: "tabs-bar",
    120  "vertical-tabs": "vertical-tabs-container",
    121  PersonalToolbar: "bookmarks-bar",
    122  "appMenu-popup": "app-menu",
    123  tabContextMenu: "tabs-context",
    124  contentAreaContextMenu: "content-context",
    125  "widget-overflow-list": "overflow-menu",
    126  "widget-overflow-fixed-list": "pinned-overflow-menu",
    127  "page-action-buttons": "pageaction-urlbar",
    128  pageActionPanel: "pageaction-panel",
    129  "unified-extensions-area": "unified-extensions-area",
    130  "allTabsMenu-allTabsView": "alltabs-menu",
    131  // Historically, panels opened from a button on any toolbar have been
    132  // considered part of the nav-bar. Due to a technical change these panels
    133  // are no longer descendants of the nav-bar; this entry just preserves
    134  // continuity for telemetry.
    135  "customizationui-widget-panel": "nav-bar",
    136 
    137  // This should appear last as some of the above are inside the nav bar.
    138  "nav-bar": "nav-bar",
    139 };
    140 
    141 const ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS = {
    142  [BROWSER_UI_CONTAINER_IDS.tabContextMenu]: "tabs-context-entrypoint",
    143 };
    144 
    145 // A list of the expected panes in about:preferences
    146 const PREFERENCES_PANES = [
    147  "paneHome",
    148  "paneGeneral",
    149  "panePrivacy",
    150  "paneSearch",
    151  "paneSearchResults",
    152  "paneSync",
    153  "paneContainers",
    154  "paneExperimental",
    155  "paneMoreFromMozilla",
    156  "paneAiFeatures",
    157 ];
    158 
    159 const IGNORABLE_EVENTS = new WeakMap();
    160 
    161 const KNOWN_ADDONS = [];
    162 
    163 // Buttons that, when clicked, set a preference to true. The convention
    164 // is that the preference is named:
    165 //
    166 // browser.engagement.<button id>.has-used
    167 //
    168 // and is defaulted to false.
    169 const SET_USAGE_PREF_BUTTONS = [
    170  "downloads-button",
    171  "fxa-toolbar-menu-button",
    172  "home-button",
    173  "sidebar-button",
    174  "library-button",
    175 ];
    176 
    177 // Buttons that, when clicked, increase a counter. The convention
    178 // is that the preference is named:
    179 //
    180 // browser.engagement.<button id>.used-count
    181 //
    182 // and doesn't have a default value.
    183 const SET_USAGECOUNT_PREF_BUTTONS = [
    184  "pageAction-panel-copyURL",
    185  "pageAction-panel-emailLink",
    186  "pageAction-panel-pinTab",
    187  "pageAction-panel-screenshots_mozilla_org",
    188  "pageAction-panel-shareURL",
    189 ];
    190 
    191 // Places context menu IDs.
    192 const PLACES_CONTEXT_MENU_ID = "placesContext";
    193 const PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID =
    194  "placesContext_open:newcontainertab";
    195 
    196 // Commands used to open history or bookmark links from places context menu.
    197 const PLACES_OPEN_COMMANDS = [
    198  "placesCmd_open",
    199  "placesCmd_open:window",
    200  "placesCmd_open:privatewindow",
    201  "placesCmd_open:tab",
    202 ];
    203 
    204 // How long of a delay between events means the start of a new flow?
    205 // Used by Browser UI Interaction event instrumentation.
    206 // Default: 5min.
    207 const FLOW_IDLE_TIME = 5 * 60 * 1000;
    208 
    209 const externalTabMovementRegistry = {
    210  internallyOpenedTabs: new WeakSet(),
    211  externallyOpenedTabsNextToActiveTab: new WeakSet(),
    212  externallyOpenedTabsAtEndOfTabStrip: new WeakSet(),
    213 };
    214 
    215 function telemetryId(widgetId, obscureAddons = true) {
    216  // Add-on IDs need to be obscured.
    217  function addonId(id) {
    218    if (!obscureAddons) {
    219      return id;
    220    }
    221 
    222    let pos = KNOWN_ADDONS.indexOf(id);
    223    if (pos < 0) {
    224      pos = KNOWN_ADDONS.length;
    225      KNOWN_ADDONS.push(id);
    226    }
    227    return `addon${pos}`;
    228  }
    229 
    230  if (widgetId.endsWith("-browser-action")) {
    231    widgetId = addonId(
    232      widgetId.substring(0, widgetId.length - "-browser-action".length)
    233    );
    234  } else if (widgetId.startsWith("pageAction-")) {
    235    let actionId;
    236    if (widgetId.startsWith("pageAction-urlbar-")) {
    237      actionId = widgetId.substring("pageAction-urlbar-".length);
    238    } else if (widgetId.startsWith("pageAction-panel-")) {
    239      actionId = widgetId.substring("pageAction-panel-".length);
    240    }
    241 
    242    if (actionId) {
    243      let action = lazy.PageActions.actionForID(actionId);
    244      widgetId = action?._isMozillaAction ? actionId : addonId(actionId);
    245    }
    246  } else if (widgetId.startsWith("ext-keyset-id-")) {
    247    // Webextension command shortcuts don't have an id on their key element so
    248    // we see the id from the keyset that contains them.
    249    widgetId = addonId(widgetId.substring("ext-keyset-id-".length));
    250  } else if (widgetId.startsWith("ext-key-id-")) {
    251    // The command for a webextension sidebar action is an exception to the above rule.
    252    widgetId = widgetId.substring("ext-key-id-".length);
    253    if (widgetId.endsWith("-sidebar-action")) {
    254      widgetId = addonId(
    255        widgetId.substring(0, widgetId.length - "-sidebar-action".length)
    256      );
    257    }
    258  }
    259 
    260  return widgetId.replace(/_/g, "-");
    261 }
    262 
    263 function getOpenTabsAndWinsCounts() {
    264  let loadedTabCount = 0;
    265  let tabCount = 0;
    266  let tabsInGroupsCount = 0;
    267  let winCount = 0;
    268 
    269  for (let win of Services.wm.getEnumerator("navigator:browser")) {
    270    winCount++;
    271    tabCount += win.gBrowser.tabs.length;
    272    for (const tab of win.gBrowser.tabs) {
    273      if (tab.getAttribute("pending") !== "true") {
    274        loadedTabCount += 1;
    275      }
    276 
    277      if (tab.getAttribute("group")) {
    278        tabsInGroupsCount += 1;
    279      }
    280    }
    281  }
    282 
    283  let tabsNotInGroupsCount = tabCount - tabsInGroupsCount;
    284 
    285  return {
    286    loadedTabCount,
    287    tabCount,
    288    winCount,
    289    tabsInGroupsCount,
    290    tabsNotInGroupsCount,
    291  };
    292 }
    293 
    294 function getPinnedTabsCount() {
    295  let pinnedTabs = 0;
    296 
    297  for (let win of Services.wm.getEnumerator("navigator:browser")) {
    298    pinnedTabs += [...win.ownerGlobal.gBrowser.tabs].filter(
    299      t => t.pinned
    300    ).length;
    301  }
    302 
    303  return pinnedTabs;
    304 }
    305 
    306 export let URICountListener = {
    307  // A set containing the visited domains, see bug 1271310.
    308  _domainSet: new Set(),
    309  // A map containing the visited origins during the last 24 hours (similar
    310  // to domains, but not quite the same), mapping to a timeoutId or 0.
    311  _domain24hrSet: new Map(),
    312  // A map to keep track of the URIs loaded from the restored tabs.
    313  _restoredURIsMap: new WeakMap(),
    314 
    315  isHttpURI(uri) {
    316    // Only consider http(s) schemas.
    317    return uri.schemeIs("http") || uri.schemeIs("https");
    318  },
    319 
    320  addRestoredURI(browser, uri) {
    321    if (!this.isHttpURI(uri)) {
    322      return;
    323    }
    324 
    325    this._restoredURIsMap.set(browser, uri.spec);
    326  },
    327 
    328  onLocationChange(browser, webProgress, request, uri, flags) {
    329    if (
    330      !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) &&
    331      webProgress.isTopLevel
    332    ) {
    333      // By default, assume we no longer need to track this tab.
    334      lazy.SearchSERPTelemetry.stopTrackingBrowser(
    335        browser,
    336        lazy.SearchSERPTelemetryUtils.ABANDONMENTS.NAVIGATION
    337      );
    338    }
    339 
    340    // Don't count this URI if it's an error page.
    341    if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
    342      return;
    343    }
    344 
    345    // We only care about top level loads.
    346    if (!webProgress.isTopLevel) {
    347      return;
    348    }
    349 
    350    // The SessionStore sets the URI of a tab first, firing onLocationChange the
    351    // first time, then manages content loading using its scheduler. Once content
    352    // loads, we will hit onLocationChange again.
    353    // We can catch the first case by checking for null requests: be advised that
    354    // this can also happen when navigating page fragments, so account for it.
    355    if (
    356      !request &&
    357      !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
    358    ) {
    359      return;
    360    }
    361 
    362    // Don't include URI and domain counts when in private mode.
    363    let shouldCountURI =
    364      !lazy.PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal) ||
    365      Services.prefs.getBoolPref(
    366        "browser.engagement.total_uri_count.pbm",
    367        false
    368      );
    369 
    370    // Track URI loads, even if they're not http(s).
    371    let uriSpec = null;
    372    try {
    373      uriSpec = uri.spec;
    374    } catch (e) {
    375      // If we have troubles parsing the spec, still count this as
    376      // an unfiltered URI.
    377      if (shouldCountURI) {
    378        Glean.browserEngagement.unfilteredUriCount.add(1);
    379      }
    380      return;
    381    }
    382 
    383    // Don't count about:blank and similar pages, as they would artificially
    384    // inflate the counts.
    385    if (browser.ownerGlobal.gInitialPages.includes(uriSpec)) {
    386      return;
    387    }
    388 
    389    // If the URI we're loading is in the _restoredURIsMap, then it comes from a
    390    // restored tab. If so, let's skip it and remove it from the map as we want to
    391    // count page refreshes.
    392    if (this._restoredURIsMap.get(browser) === uriSpec) {
    393      this._restoredURIsMap.delete(browser);
    394      return;
    395    }
    396 
    397    // The URI wasn't from a restored tab. Count it among the unfiltered URIs.
    398    // If this is an http(s) URI, this also gets counted by the "total_uri_count"
    399    // probe.
    400    if (shouldCountURI) {
    401      Glean.browserEngagement.unfilteredUriCount.add(1);
    402    }
    403 
    404    if (!this.isHttpURI(uri)) {
    405      return;
    406    }
    407 
    408    if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
    409      lazy.SearchSERPTelemetry.updateTrackingStatus(
    410        browser,
    411        uriSpec,
    412        webProgress.loadType
    413      );
    414    } else {
    415      lazy.SearchSERPTelemetry.updateTrackingSinglePageApp(
    416        browser,
    417        uriSpec,
    418        webProgress.loadType,
    419        flags
    420      );
    421    }
    422 
    423    // Update total URI count, including when in private mode.
    424    Glean.browserEngagement.uriCount.add(1);
    425 
    426    if (!shouldCountURI) {
    427      return;
    428    }
    429 
    430    // Update the URI counts.
    431    Glean.browserEngagement.uriCountNormalMode.add(1);
    432 
    433    // Update tab count
    434    BrowserUsageTelemetry._recordTabCounts(getOpenTabsAndWinsCounts());
    435 
    436    // Unique domains should be aggregated by (eTLD + 1): x.test.com and y.test.com
    437    // are counted once as test.com.
    438    let baseDomain;
    439    try {
    440      // Even if only considering http(s) URIs, |getBaseDomain| could still throw
    441      // due to the URI containing invalid characters or the domain actually being
    442      // an ipv4 or ipv6 address.
    443      baseDomain = Services.eTLD.getBaseDomain(uri);
    444    } catch (e) {
    445      return;
    446    }
    447 
    448    // We only want to count the unique domains up to MAX_UNIQUE_VISITED_DOMAINS.
    449    if (this._domainSet.size < MAX_UNIQUE_VISITED_DOMAINS) {
    450      this._domainSet.add(baseDomain);
    451      Glean.browserEngagement.uniqueDomainsCount.set(this._domainSet.size);
    452    }
    453 
    454    // Clear and re-add the expiration timeout for this base domain, if any.
    455    let timeoutId = this._domain24hrSet.get(baseDomain);
    456    if (timeoutId) {
    457      lazy.clearTimeout(timeoutId);
    458    }
    459    if (lazy.gRecentVisitedOriginsExpiry) {
    460      timeoutId = lazy.setTimeout(() => {
    461        this._domain24hrSet.delete(baseDomain);
    462      }, lazy.gRecentVisitedOriginsExpiry * 1000);
    463    } else {
    464      timeoutId = 0;
    465    }
    466    this._domain24hrSet.set(baseDomain, timeoutId);
    467  },
    468 
    469  /**
    470   * Reset the counts. This should be called when breaking a session in Telemetry.
    471   */
    472  reset() {
    473    this._domainSet.clear();
    474  },
    475 
    476  /**
    477   * Returns the number of unique domains visited in this session during the
    478   * last 24 hours.
    479   */
    480  get uniqueDomainsVisitedInPast24Hours() {
    481    return this._domain24hrSet.size;
    482  },
    483 
    484  /**
    485   * Resets the number of unique domains visited in this session.
    486   */
    487  resetUniqueDomainsVisitedInPast24Hours() {
    488    this._domain24hrSet.forEach(value => lazy.clearTimeout(value));
    489    this._domain24hrSet.clear();
    490  },
    491 
    492  QueryInterface: ChromeUtils.generateQI([
    493    "nsIWebProgressListener",
    494    "nsISupportsWeakReference",
    495  ]),
    496 };
    497 
    498 let gInstallationTelemetryPromise = null;
    499 
    500 export let BrowserUsageTelemetry = {
    501  /**
    502   * This is a policy object used to override behavior for testing.
    503   */
    504  Policy: {
    505    getTelemetryClientId: async () => lazy.ClientID.getClientID(),
    506    getUpdateDirectory: () => Services.dirsvc.get("UpdRootD", Ci.nsIFile),
    507    readProfileCountFile: async path => IOUtils.readUTF8(path),
    508    writeProfileCountFile: async (path, data) => IOUtils.writeUTF8(path, data),
    509  },
    510 
    511  _inited: false,
    512 
    513  /**
    514   * @typedef {object} TabMovementsRecord
    515   * @property {DeferredTask} deferredTask
    516   *   The `DeferredTask` that will report this record's metrics once all
    517   *   tab movement events with the same `telemetrySource` have been received
    518   *   in the current event loop.
    519   * @property {number} numberAddedToTabGroup
    520   *   The number of tabs from `tabs` which started out as ungrouped tabs but
    521   *   moved into a tab group during the tab movement operation.
    522   */
    523 
    524  /** @type {Map<string, TabMovementsRecord>} */
    525  _tabMovementsBySegment: new Map(),
    526 
    527  init() {
    528    this._lastRecordTabCount = 0;
    529    this._lastRecordLoadedTabCount = 0;
    530    this._setupAfterRestore();
    531    this._inited = true;
    532 
    533    Services.prefs.addObserver("browser.tabs.inTitlebar", this);
    534    Services.prefs.addObserver("idle-daily", this);
    535 
    536    this._recordUITelemetry();
    537    this._recordInitialPrefValues();
    538    this.recordPinnedTabsCount();
    539 
    540    this._onTabsOpenedTask = new lazy.DeferredTask(
    541      () => this._onTabsOpened(),
    542      0
    543    );
    544 
    545    this._onTabGroupChangeTask = new lazy.DeferredTask(
    546      () => this._doOnTabGroupChange(),
    547      0
    548    );
    549 
    550    this._onTabGroupExpandOrCollapseTask = new lazy.DeferredTask(
    551      () => this._doOnTabGroupExpandOrCollapse(),
    552      0
    553    );
    554 
    555    this._onSavedTabGroupsChangedTask = new lazy.DeferredTask(
    556      () => this._doOnSavedTabGroupsChange(),
    557      0
    558    );
    559    this._onSavedTabGroupsChangedTask.arm();
    560  },
    561 
    562  maxWindowCount: 0,
    563  maxTabCount: 0,
    564  get maxTabCountGleanQuantity() {
    565    return lazy.sidebarVerticalTabs
    566      ? Glean.browserEngagement.maxConcurrentVerticalTabCount
    567      : Glean.browserEngagement.maxConcurrentTabCount;
    568  },
    569 
    570  maxTabPinnedCount: 0,
    571  updateMaxTabPinnedCount(pinnedTabs) {
    572    if (pinnedTabs > this.maxTabPinnedCount) {
    573      this.maxTabPinnedCount = pinnedTabs;
    574      if (lazy.sidebarVerticalTabs) {
    575        Glean.browserEngagement.maxConcurrentVerticalTabPinnedCount.set(
    576          pinnedTabs
    577        );
    578      } else {
    579        Glean.browserEngagement.maxConcurrentTabPinnedCount.set(pinnedTabs);
    580      }
    581    }
    582  },
    583 
    584  recordPinnedTabsCount(count = getPinnedTabsCount()) {
    585    if (lazy.sidebarVerticalTabs) {
    586      Glean.pinnedTabs.count.sidebar.set(count);
    587    } else {
    588      Glean.pinnedTabs.count.horizontalBar.set(count);
    589    }
    590  },
    591 
    592  /**
    593   * Resets the masked add-on identifiers. Only for use in tests.
    594   */
    595  _resetAddonIds() {
    596    KNOWN_ADDONS.length = 0;
    597  },
    598 
    599  /**
    600   * Handle subsession splits in the parent process.
    601   */
    602  afterSubsessionSplit() {
    603    // Scalars just got cleared due to a subsession split. We need to set the maximum
    604    // concurrent tab and window counts so that they reflect the correct value for the
    605    // new subsession.
    606    this._initMaxTabAndWindowCounts();
    607 
    608    // Reset the URI counter.
    609    URICountListener.reset();
    610  },
    611 
    612  QueryInterface: ChromeUtils.generateQI([
    613    "nsIObserver",
    614    "nsISupportsWeakReference",
    615  ]),
    616 
    617  uninit() {
    618    if (!this._inited) {
    619      return;
    620    }
    621    Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC);
    622    Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC);
    623    Services.obs.removeObserver(this, SESSION_STORE_SAVED_TAB_GROUPS_TOPIC);
    624  },
    625 
    626  observe(subject, topic, data) {
    627    switch (topic) {
    628      case DOMWINDOW_OPENED_TOPIC:
    629        this._onWindowOpen(subject);
    630        break;
    631      case TELEMETRY_SUBSESSIONSPLIT_TOPIC:
    632        this.afterSubsessionSplit();
    633        break;
    634      case SESSION_STORE_SAVED_TAB_GROUPS_TOPIC:
    635        this._onSavedTabGroupsChange();
    636        break;
    637      case "nsPref:changed":
    638        switch (data) {
    639          case "browser.tabs.inTitlebar":
    640            this._recordWidgetChange(
    641              "titlebar",
    642              Services.appinfo.drawInTitlebar ? "off" : "on",
    643              "pref"
    644            );
    645            break;
    646          case "idle-daily":
    647            this._recordInitialPrefValues();
    648            break;
    649        }
    650        break;
    651    }
    652  },
    653 
    654  handleEvent(event) {
    655    switch (event.type) {
    656      case "TabOpen":
    657        this._onTabOpen(event);
    658        break;
    659      case "TabClose":
    660        this._onTabClosed(event);
    661        break;
    662      case "TabPinned":
    663        this._onTabPinned(event);
    664        break;
    665      case "TabUnpinned":
    666        this._onTabUnpinned();
    667        break;
    668      case "TabGroupCreateByUser":
    669        this._onTabGroupCreateByUser(event);
    670        break;
    671      case "TabGrouped":
    672      case "TabUngrouped":
    673        this._onTabGroupChange();
    674        break;
    675      case "TabGroupCollapse":
    676      case "TabGroupExpand":
    677        this._onTabGroupExpandOrCollapse();
    678        break;
    679      case "TabMove":
    680        this._onTabMove(event);
    681        break;
    682      case "TabSelect":
    683        this._onTabSelect(event);
    684        break;
    685      case "TabGroupRemoveRequested":
    686        this._onTabGroupRemoveRequested(event);
    687        break;
    688      case "TabGroupSaved":
    689        this._onTabGroupSave(event);
    690        break;
    691      case "TabGroupUngroup":
    692        this._onTabGroupUngroup(event);
    693        break;
    694 
    695      case "unload":
    696        this._unregisterWindow(event.target);
    697        break;
    698      case TAB_RESTORING_TOPIC: {
    699        // We're restoring a new tab from a previous or crashed session.
    700        // We don't want to track the URIs from these tabs, so let
    701        // |URICountListener| know about them.
    702        let browser = event.target.linkedBrowser;
    703        URICountListener.addRestoredURI(browser, browser.currentURI);
    704 
    705        const { loadedTabCount } = getOpenTabsAndWinsCounts();
    706        this._recordTabCounts({ loadedTabCount });
    707        break;
    708      }
    709    }
    710  },
    711 
    712  _initMaxTabAndWindowCounts() {
    713    const counts = getOpenTabsAndWinsCounts();
    714    this.maxTabCount = counts.tabCount;
    715    this.maxTabCountGleanQuantity.set(counts.tabCount);
    716    this.maxWindowCount = counts.winCount;
    717    Glean.browserEngagement.maxConcurrentWindowCount.set(counts.winCount);
    718  },
    719 
    720  /**
    721   * This gets called shortly after the SessionStore has finished restoring
    722   * windows and tabs. It counts the open tabs and adds listeners to all the
    723   * windows.
    724   */
    725  _setupAfterRestore() {
    726    // Make sure to catch new chrome windows and subsession splits.
    727    Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, true);
    728    Services.obs.addObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, true);
    729    Services.obs.addObserver(this, SESSION_STORE_SAVED_TAB_GROUPS_TOPIC, true);
    730 
    731    // Attach the tabopen handlers to the existing Windows.
    732    for (let win of Services.wm.getEnumerator("navigator:browser")) {
    733      this._registerWindow(win);
    734    }
    735 
    736    // Get the initial tab and windows max counts.
    737    this._initMaxTabAndWindowCounts();
    738  },
    739 
    740  _buildWidgetPositions() {
    741    let widgetMap = new Map();
    742 
    743    const toolbarState = nodeId => {
    744      let value;
    745      if (nodeId == "PersonalToolbar") {
    746        value = Services.prefs.getCharPref(
    747          "browser.toolbars.bookmarks.visibility",
    748          "newtab"
    749        );
    750        if (value != "newtab") {
    751          return value == "never" ? "off" : "on";
    752        }
    753        return value;
    754      }
    755      value = Services.xulStore.getValue(
    756        AppConstants.BROWSER_CHROME_URL,
    757        nodeId,
    758        "collapsed"
    759      );
    760 
    761      if (value) {
    762        return value == "true" ? "off" : "on";
    763      }
    764      return "off";
    765    };
    766 
    767    widgetMap.set(
    768      BROWSER_UI_CONTAINER_IDS.PersonalToolbar,
    769      toolbarState("PersonalToolbar")
    770    );
    771 
    772    let menuBarHidden =
    773      Services.xulStore.getValue(
    774        AppConstants.BROWSER_CHROME_URL,
    775        "toolbar-menubar",
    776        "autohide"
    777      ) != "false";
    778 
    779    widgetMap.set("menu-toolbar", menuBarHidden ? "off" : "on");
    780 
    781    // Drawing in the titlebar means not showing the titlebar, hence the negation.
    782    widgetMap.set("titlebar", Services.appinfo.drawInTitlebar ? "off" : "on");
    783 
    784    for (let area of lazy.CustomizableUI.areas) {
    785      if (!(area in BROWSER_UI_CONTAINER_IDS)) {
    786        continue;
    787      }
    788 
    789      let position = BROWSER_UI_CONTAINER_IDS[area];
    790      if (area == "nav-bar") {
    791        position = `${BROWSER_UI_CONTAINER_IDS[area]}-start`;
    792      }
    793 
    794      let widgets = lazy.CustomizableUI.getWidgetsInArea(area);
    795 
    796      for (let widget of widgets) {
    797        if (!widget) {
    798          continue;
    799        }
    800 
    801        if (widget.id.startsWith("customizableui-special-")) {
    802          continue;
    803        }
    804 
    805        if (area == "nav-bar" && widget.id == "urlbar-container") {
    806          position = `${BROWSER_UI_CONTAINER_IDS[area]}-end`;
    807          continue;
    808        }
    809 
    810        widgetMap.set(widget.id, position);
    811      }
    812    }
    813 
    814    let actions = lazy.PageActions.actions;
    815    for (let action of actions) {
    816      if (action.pinnedToUrlbar) {
    817        widgetMap.set(action.id, "pageaction-urlbar");
    818      }
    819    }
    820 
    821    return widgetMap;
    822  },
    823 
    824  _getWidgetID(node) {
    825    // We want to find a sensible ID for this element.
    826    if (!node) {
    827      return null;
    828    }
    829 
    830    // See if this is a customizable widget.
    831    if (node.ownerDocument.URL == AppConstants.BROWSER_CHROME_URL) {
    832      // First find if it is inside one of the customizable areas.
    833      for (let area of lazy.CustomizableUI.areas) {
    834        if (node.closest(`#${CSS.escape(area)}`)) {
    835          for (let widget of lazy.CustomizableUI.getWidgetIdsInArea(area)) {
    836            if (
    837              // We care about the buttons on the tabs themselves.
    838              widget == "tabbrowser-tabs" ||
    839              // We care about the page action and other buttons in here.
    840              widget == "urlbar-container" ||
    841              // We care about the actual menu items.
    842              widget == "menubar-items" ||
    843              // We care about individual bookmarks here.
    844              widget == "personal-bookmarks"
    845            ) {
    846              continue;
    847            }
    848 
    849            if (node.closest(`#${CSS.escape(widget)}`)) {
    850              return widget;
    851            }
    852          }
    853          break;
    854        }
    855      }
    856    }
    857 
    858    if (node.id) {
    859      return node.id;
    860    }
    861 
    862    // A couple of special cases in the tabs.
    863    for (let cls of ["bookmark-item", "tab-icon-sound", "tab-close-button"]) {
    864      if (!node.classList.contains(cls)) {
    865        continue;
    866      }
    867      if (cls == "bookmark-item" && node.parentElement.id.includes("history")) {
    868        return "history-item";
    869      }
    870      return cls;
    871    }
    872 
    873    // One of these will at least let us know what the widget is for.
    874    let possibleAttributes = [
    875      "preference",
    876      "command",
    877      "observes",
    878      "data-l10n-id",
    879    ];
    880 
    881    // The key attribute on key elements is the actual key to listen for.
    882    if (node.localName != "key") {
    883      possibleAttributes.unshift("key");
    884    }
    885 
    886    for (let idAttribute of possibleAttributes) {
    887      if (node.hasAttribute(idAttribute)) {
    888        return node.getAttribute(idAttribute);
    889      }
    890    }
    891 
    892    return this._getWidgetID(node.parentElement);
    893  },
    894 
    895  _getBrowserWidgetContainer(node) {
    896    // Find the container holding this element.
    897    for (let containerId of Object.keys(BROWSER_UI_CONTAINER_IDS)) {
    898      let container = node.ownerDocument.getElementById(containerId);
    899      if (container && container.contains(node)) {
    900        return BROWSER_UI_CONTAINER_IDS[containerId];
    901      }
    902    }
    903    // Treat toolbar context menu items that relate to tabs as the tab menu:
    904    if (
    905      node.closest("#toolbar-context-menu") &&
    906      node.getAttribute("contexttype") == "tabbar"
    907    ) {
    908      return BROWSER_UI_CONTAINER_IDS.tabContextMenu;
    909    }
    910    return null;
    911  },
    912 
    913  _getWidgetContainer(node) {
    914    if (node.localName == "key") {
    915      return "keyboard";
    916    }
    917 
    918    const { URL: url } = node.ownerDocument;
    919    if (url == AppConstants.BROWSER_CHROME_URL) {
    920      return this._getBrowserWidgetContainer(node);
    921    }
    922    if (
    923      url.startsWith("about:preferences") ||
    924      url.startsWith("about:settings")
    925    ) {
    926      // Find the element's category.
    927      let container = node.closest("[data-category]");
    928      if (!container) {
    929        return null;
    930      }
    931 
    932      let pane = container.getAttribute("data-category");
    933 
    934      if (!PREFERENCES_PANES.includes(pane)) {
    935        pane = "paneUnknown";
    936      }
    937 
    938      return `preferences_${pane}`;
    939    }
    940 
    941    return null;
    942  },
    943 
    944  lastClickTarget: null,
    945 
    946  ignoreEvent(event) {
    947    IGNORABLE_EVENTS.set(event, true);
    948  },
    949 
    950  _recordCommand(event) {
    951    if (IGNORABLE_EVENTS.get(event)) {
    952      return;
    953    }
    954 
    955    let sourceEvent = event;
    956    while (sourceEvent.sourceEvent) {
    957      sourceEvent = sourceEvent.sourceEvent;
    958    }
    959 
    960    let lastTarget = this.lastClickTarget?.get();
    961    if (
    962      lastTarget &&
    963      sourceEvent.type == "command" &&
    964      sourceEvent.target.contains(lastTarget)
    965    ) {
    966      // Ignore a command event triggered by a click.
    967      this.lastClickTarget = null;
    968      return;
    969    }
    970 
    971    this.lastClickTarget = null;
    972 
    973    if (sourceEvent.type == "click") {
    974      // Only care about main button clicks.
    975      if (sourceEvent.button != 0) {
    976        return;
    977      }
    978 
    979      // This click may trigger a command event so retain the target to be able
    980      // to dedupe that event.
    981      this.lastClickTarget = Cu.getWeakReference(sourceEvent.target);
    982    }
    983 
    984    // We should never see events from web content as they are fired in a
    985    // content process, but let's be safe.
    986    let url = sourceEvent.target.ownerDocument.documentURIObject;
    987    if (!url.schemeIs("chrome") && !url.schemeIs("about")) {
    988      return;
    989    }
    990 
    991    // This is what events targetted  at content will actually look like.
    992    if (sourceEvent.target.localName == "browser") {
    993      return;
    994    }
    995 
    996    // Find the actual element we're interested in.
    997    let node = sourceEvent.target;
    998    const isAboutPreferences =
    999      node.ownerDocument.URL.startsWith("about:preferences") ||
   1000      node.ownerDocument.URL.startsWith("about:settings");
   1001    let targetElements = UI_TARGET_ELEMENTS.get(event.type);
   1002 
   1003    while (
   1004      !targetElements.has(node.localName) &&
   1005      !node.classList?.contains("wants-telemetry") &&
   1006      // We are interested in links on about:preferences as well.
   1007      !(
   1008        isAboutPreferences &&
   1009        (node.getAttribute("is") === "text-link" || node.localName === "a")
   1010      )
   1011    ) {
   1012      node = node.parentNode;
   1013      if (!node?.parentNode) {
   1014        // A click on a space or label or top-level document or something we're
   1015        // not interested in.
   1016        return;
   1017      }
   1018    }
   1019 
   1020    if (sourceEvent.type === "command") {
   1021      const { command, ownerDocument, parentNode } = node;
   1022      // Check if this command is for a history or bookmark link being opened
   1023      // from the context menu. In this case, we are interested in the DOM node
   1024      // for the link, not the menu item itself.
   1025      if (
   1026        PLACES_OPEN_COMMANDS.includes(command) ||
   1027        parentNode?.parentNode?.id === PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID
   1028      ) {
   1029        node = ownerDocument.getElementById(PLACES_CONTEXT_MENU_ID).triggerNode;
   1030      }
   1031    }
   1032 
   1033    let item = this._getWidgetID(node);
   1034    let source = this._getWidgetContainer(node);
   1035 
   1036    if (item && source) {
   1037      this.recordInteractionEvent(item, source);
   1038      let name = source
   1039        .replace(/-/g, "_")
   1040        .replace(/_([a-z])/g, (m, p) => p.toUpperCase());
   1041      Glean.browserUiInteraction[name]?.[telemetryId(item)].add(1);
   1042      if (SET_USAGECOUNT_PREF_BUTTONS.includes(item)) {
   1043        let pref = `browser.engagement.${item}.used-count`;
   1044        Services.prefs.setIntPref(pref, Services.prefs.getIntPref(pref, 0) + 1);
   1045      }
   1046      if (SET_USAGE_PREF_BUTTONS.includes(item)) {
   1047        Services.prefs.setBoolPref(`browser.engagement.${item}.has-used`, true);
   1048      }
   1049    }
   1050 
   1051    if (ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source]) {
   1052      let contextMenu = ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source];
   1053      let triggerContainer = this._getWidgetContainer(
   1054        node.closest("menupopup")?.triggerNode
   1055      );
   1056      if (triggerContainer) {
   1057        this.recordInteractionEvent(item, contextMenu);
   1058        let name = contextMenu
   1059          .replace(/-/g, "_")
   1060          .replace(/_([a-z])/g, (m, p) => p.toUpperCase());
   1061        Glean.browserUiInteraction[name]?.[telemetryId(triggerContainer)].add(
   1062          1
   1063        );
   1064      }
   1065    }
   1066  },
   1067 
   1068  _flowId: null,
   1069  _flowIdTS: 0,
   1070 
   1071  recordInteractionEvent(widgetId, source) {
   1072    // A note on clocks. ChromeUtils.now() is monotonic, but its behaviour across
   1073    // computer sleeps is different per platform.
   1074    // We're okay with this for flows because we're looking at idle times
   1075    // on the order of minutes and within the same machine, so the weirdest
   1076    // thing we may expect is a flow that accidentally continues across a
   1077    // sleep. Until we have evidence that this is common, we're in the clear.
   1078    if (!this._flowId || this._flowIdTS + FLOW_IDLE_TIME < ChromeUtils.now()) {
   1079      // We submit the ping full o' events on every new flow,
   1080      // including at startup.
   1081      GleanPings.prototypeNoCodeEvents.submit();
   1082      // We use a GUID here because we need to identify events in a flow
   1083      // out of all events from all flows across all clients.
   1084      this._flowId = Services.uuid.generateUUID();
   1085    }
   1086    this._flowIdTS = ChromeUtils.now();
   1087 
   1088    const extra = {
   1089      source,
   1090      widget_id: telemetryId(widgetId),
   1091      flow_id: this._flowId,
   1092    };
   1093    Glean.browserUsage.interaction.record(extra);
   1094  },
   1095 
   1096  /**
   1097   * Listens for UI interactions in the window.
   1098   */
   1099  _addUsageListeners(win) {
   1100    // Listen for events that UI_TARGET_ELEMENTS expect from the UI.
   1101    UI_TARGET_ELEMENTS.keys().forEach(type =>
   1102      win.addEventListener(type, event => this._recordCommand(event), true)
   1103    );
   1104  },
   1105 
   1106  /**
   1107   * A public version of the private method to take care of the `nav-bar-start`,
   1108   * `nav-bar-end` thing that callers shouldn't have to care about. It also
   1109   * accepts the DOM ids for the areas rather than the cleaner ones we report
   1110   * to telemetry.
   1111   */
   1112  recordWidgetChange(widgetId, newPos, reason) {
   1113    try {
   1114      if (newPos) {
   1115        newPos = BROWSER_UI_CONTAINER_IDS[newPos];
   1116      }
   1117 
   1118      if (newPos == "nav-bar") {
   1119        let { position } = lazy.CustomizableUI.getPlacementOfWidget(widgetId);
   1120        let { position: urlPosition } =
   1121          lazy.CustomizableUI.getPlacementOfWidget("urlbar-container");
   1122        newPos = newPos + (urlPosition > position ? "-start" : "-end");
   1123      }
   1124 
   1125      this._recordWidgetChange(widgetId, newPos, reason);
   1126    } catch (e) {
   1127      console.error(e);
   1128    }
   1129  },
   1130 
   1131  recordToolbarVisibility(toolbarId, newState, reason) {
   1132    if (typeof newState != "string") {
   1133      newState = newState ? "on" : "off";
   1134    }
   1135    this._recordWidgetChange(
   1136      BROWSER_UI_CONTAINER_IDS[toolbarId],
   1137      newState,
   1138      reason
   1139    );
   1140  },
   1141 
   1142  _recordWidgetChange(widgetId, newPos, reason) {
   1143    // In some cases (like when add-ons are detected during startup) this gets
   1144    // called before we've reported the initial positions. Ignore such cases.
   1145    if (!this.widgetMap) {
   1146      return;
   1147    }
   1148 
   1149    if (widgetId == "urlbar-container") {
   1150      // We don't report the position of the url bar, it is after nav-bar-start
   1151      // and before nav-bar-end. But moving it means the widgets around it have
   1152      // effectively moved so update those.
   1153      let position = "nav-bar-start";
   1154      let widgets = lazy.CustomizableUI.getWidgetsInArea("nav-bar");
   1155 
   1156      for (let widget of widgets) {
   1157        if (!widget) {
   1158          continue;
   1159        }
   1160 
   1161        if (widget.id.startsWith("customizableui-special-")) {
   1162          continue;
   1163        }
   1164 
   1165        if (widget.id == "urlbar-container") {
   1166          position = "nav-bar-end";
   1167          continue;
   1168        }
   1169 
   1170        // This will do nothing if the position hasn't changed.
   1171        this._recordWidgetChange(widget.id, position, reason);
   1172      }
   1173 
   1174      return;
   1175    }
   1176 
   1177    let oldPos = this.widgetMap.get(widgetId);
   1178    if (oldPos == newPos) {
   1179      return;
   1180    }
   1181 
   1182    let action = "move";
   1183 
   1184    if (!oldPos) {
   1185      action = "add";
   1186    } else if (!newPos) {
   1187      action = "remove";
   1188    }
   1189 
   1190    let key = `${telemetryId(widgetId, false)}_${action}_${oldPos ?? "na"}_${
   1191      newPos ?? "na"
   1192    }_${reason}`;
   1193    Glean.browserUi.customizedWidgets[key].add(1);
   1194 
   1195    if (newPos) {
   1196      this.widgetMap.set(widgetId, newPos);
   1197    } else {
   1198      this.widgetMap.delete(widgetId);
   1199    }
   1200  },
   1201 
   1202  _recordUITelemetry() {
   1203    this.widgetMap = this._buildWidgetPositions();
   1204 
   1205    // FIXME(bug 1883857): object metric type not available in artefact builds.
   1206    if ("toolbarWidgets" in Glean.browserUi) {
   1207      Glean.browserUi.toolbarWidgets.set(
   1208        this.widgetMap
   1209          .entries()
   1210          .map(([widgetId, position]) => {
   1211            return { widgetId: telemetryId(widgetId, false), position };
   1212          })
   1213          .toArray()
   1214      );
   1215    }
   1216 
   1217    for (let [widgetId, position] of this.widgetMap.entries()) {
   1218      let key = `${telemetryId(widgetId, false)}_pinned_${position}`;
   1219      Glean.browserUi.mirrorForToolbarWidgets[key].set(true);
   1220    }
   1221  },
   1222 
   1223  /**
   1224   * Records the startup values of prefs that govern important browser behavior
   1225   * options.
   1226   */
   1227  _recordInitialPrefValues() {
   1228    this._recordOpenNextToActiveTabSettingValue();
   1229  },
   1230 
   1231  /**
   1232   * @returns {boolean}
   1233   */
   1234  _isOpenNextToActiveTabSettingEnabled() {
   1235    /** @type {number} proxy for `browser.link.open_newwindow.override.external` */
   1236    const externalLinkOpeningBehavior =
   1237      lazy.NimbusFeatures.externalLinkHandling.getVariable("openBehavior");
   1238    return (
   1239      externalLinkOpeningBehavior ==
   1240      Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT
   1241    );
   1242  },
   1243 
   1244  _recordOpenNextToActiveTabSettingValue() {
   1245    Glean.linkHandling.openNextToActiveTabSettingsEnabled.set(
   1246      this._isOpenNextToActiveTabSettingEnabled()
   1247    );
   1248  },
   1249 
   1250  /**
   1251   * Adds listeners to a single chrome window.
   1252   *
   1253   * @param {Window} win
   1254   */
   1255  _registerWindow(win) {
   1256    this._addUsageListeners(win);
   1257 
   1258    win.addEventListener("unload", this);
   1259    win.addEventListener("TabMove", this);
   1260    win.addEventListener("TabOpen", this, true);
   1261    win.addEventListener("TabClose", this, true);
   1262    win.addEventListener("TabPinned", this, true);
   1263    win.addEventListener("TabUnpinned", this, true);
   1264    win.addEventListener("TabSelect", this);
   1265    win.addEventListener("TabGroupCreateByUser", this);
   1266    win.addEventListener("TabGroupRemoveRequested", this);
   1267    win.addEventListener("TabGrouped", this);
   1268    win.addEventListener("TabUngrouped", this);
   1269    win.addEventListener("TabGroupCollapse", this);
   1270    win.addEventListener("TabGroupExpand", this);
   1271    win.addEventListener("TabGroupSaved", this);
   1272    win.addEventListener("TabGroupUngroup", this);
   1273 
   1274    win.gBrowser.tabContainer.addEventListener(TAB_RESTORING_TOPIC, this);
   1275    win.gBrowser.addTabsProgressListener(URICountListener);
   1276  },
   1277 
   1278  /**
   1279   * Removes listeners from a single chrome window.
   1280   */
   1281  _unregisterWindow(win) {
   1282    win.removeEventListener("unload", this);
   1283    win.removeEventListener("TabMove", this);
   1284    win.removeEventListener("TabOpen", this, true);
   1285    win.removeEventListener("TabClose", this, true);
   1286    win.removeEventListener("TabPinned", this, true);
   1287    win.removeEventListener("TabUnpinned", this, true);
   1288    win.removeEventListener("TabSelect", this);
   1289    win.removeEventListener("TabGroupCreateByUser", this);
   1290    win.removeEventListener("TabGroupRemoveRequested", this);
   1291    win.removeEventListener("TabGrouped", this);
   1292    win.removeEventListener("TabUngrouped", this);
   1293    win.removeEventListener("TabGroupCollapse", this);
   1294    win.removeEventListener("TabGroupExpand", this);
   1295    win.removeEventListener("TabGroupSaved", this);
   1296    win.removeEventListener("TabGroupUngroup", this);
   1297 
   1298    win.defaultView.gBrowser.tabContainer.removeEventListener(
   1299      TAB_RESTORING_TOPIC,
   1300      this
   1301    );
   1302    win.defaultView.gBrowser.removeTabsProgressListener(URICountListener);
   1303  },
   1304 
   1305  /**
   1306   * Updates the tab counts.
   1307   *
   1308   * @param {CustomEvent} [event]
   1309   *   `TabOpen` event
   1310   */
   1311  _onTabOpen(event) {
   1312    // Update the "tab opened" count and its maximum.
   1313    if (lazy.sidebarVerticalTabs) {
   1314      Glean.browserEngagement.verticalTabOpenEventCount.add(1);
   1315    } else {
   1316      Glean.browserEngagement.tabOpenEventCount.add(1);
   1317    }
   1318 
   1319    if (event?.target?.group) {
   1320      Glean.tabgroup.tabInteractions.new.add();
   1321    }
   1322 
   1323    if (event) {
   1324      if (event.detail?.fromExternal) {
   1325        const wasOpenedNextToActiveTab =
   1326          this._isOpenNextToActiveTabSettingEnabled();
   1327 
   1328        Glean.linkHandling.openFromExternalApp.record({
   1329          next_to_active_tab: wasOpenedNextToActiveTab,
   1330        });
   1331 
   1332        if (wasOpenedNextToActiveTab) {
   1333          externalTabMovementRegistry.externallyOpenedTabsNextToActiveTab.add(
   1334            event.target
   1335          );
   1336        } else {
   1337          externalTabMovementRegistry.externallyOpenedTabsAtEndOfTabStrip.add(
   1338            event.target
   1339          );
   1340        }
   1341      } else {
   1342        externalTabMovementRegistry.internallyOpenedTabs.add(event.target);
   1343      }
   1344    }
   1345 
   1346    const userContextId = event?.target?.getAttribute("usercontextid");
   1347    if (userContextId) {
   1348      Glean.containers.containerTabOpened.record({
   1349        container_id: String(userContextId),
   1350      });
   1351    }
   1352 
   1353    // In the case of opening multiple tabs at once, avoid enumerating all open
   1354    // tabs and windows each time a tab opens.
   1355    this._onTabsOpenedTask.disarm();
   1356    this._onTabsOpenedTask.arm();
   1357  },
   1358 
   1359  /**
   1360   * Update tab counts after opening multiple tabs.
   1361   */
   1362  _onTabsOpened() {
   1363    const { tabCount, loadedTabCount } = getOpenTabsAndWinsCounts();
   1364    if (tabCount > this.maxTabCount) {
   1365      this.maxTabCount = tabCount;
   1366      this.maxTabCountGleanQuantity.set(tabCount);
   1367    }
   1368 
   1369    this._recordTabCounts({ tabCount, loadedTabCount });
   1370  },
   1371 
   1372  /**
   1373   *
   1374   * @param {CustomEvent} event
   1375   *   TabClose event.
   1376   */
   1377  _onTabClosed(event) {
   1378    const group = event.target?.group;
   1379    const isUserTriggered = event.detail?.isUserTriggered;
   1380    const source = event.detail?.telemetrySource;
   1381 
   1382    if (group && isUserTriggered) {
   1383      if (source == lazy.TabMetrics.METRIC_SOURCE.TAB_STRIP) {
   1384        Glean.tabgroup.tabInteractions.close_tabstrip.add();
   1385      } else if (source == lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU) {
   1386        Glean.tabgroup.tabInteractions.close_tabmenu.add();
   1387      } else {
   1388        Glean.tabgroup.tabInteractions.close_tab_other.add();
   1389      }
   1390    }
   1391 
   1392    const userContextId = event?.target?.getAttribute("usercontextid");
   1393    if (userContextId) {
   1394      Glean.containers.containerTabClosed.record({
   1395        container_id: String(userContextId),
   1396      });
   1397    }
   1398 
   1399    if (event.target?.pinned) {
   1400      const pinnedTabs = getPinnedTabsCount();
   1401      this.recordPinnedTabsCount(pinnedTabs - 1);
   1402      Glean.pinnedTabs.close.record({
   1403        layout: lazy.sidebarVerticalTabs ? "vertical" : "horizontal",
   1404      });
   1405    }
   1406 
   1407    if (event.target) {
   1408      // Stop tracking any tabs that have been tracked since their `TabOpen` events.
   1409      Object.values(externalTabMovementRegistry).forEach(set => {
   1410        set.delete(event.target);
   1411      });
   1412    }
   1413  },
   1414 
   1415  _onTabPinned(event) {
   1416    const pinnedTabs = getPinnedTabsCount();
   1417 
   1418    // Update the "tab pinned" count and its maximum.
   1419    if (lazy.sidebarVerticalTabs) {
   1420      Glean.browserEngagement.verticalTabPinnedEventCount.add(1);
   1421    } else {
   1422      Glean.browserEngagement.tabPinnedEventCount.add(1);
   1423    }
   1424    this.updateMaxTabPinnedCount(pinnedTabs);
   1425    this.recordPinnedTabsCount(pinnedTabs);
   1426    Glean.pinnedTabs.pin.record({
   1427      layout: lazy.sidebarVerticalTabs ? "vertical" : "horizontal",
   1428      source: event.detail?.telemetrySource,
   1429    });
   1430  },
   1431 
   1432  _onTabUnpinned() {
   1433    this.recordPinnedTabsCount();
   1434  },
   1435 
   1436  _onTabGroupCreateByUser(event) {
   1437    Glean.tabgroup.createGroup.record({
   1438      id: event.target.id,
   1439      layout: lazy.sidebarVerticalTabs
   1440        ? lazy.TabMetrics.METRIC_TABS_LAYOUT.VERTICAL
   1441        : lazy.TabMetrics.METRIC_TABS_LAYOUT.HORIZONTAL,
   1442      source: event.detail.telemetryUserCreateSource,
   1443      tabs: event.target.tabs.length,
   1444    });
   1445 
   1446    this._onTabGroupChange();
   1447  },
   1448 
   1449  _onTabGroupSave(event) {
   1450    const { isUserTriggered } = event.detail;
   1451 
   1452    Glean.tabgroup.save.record({
   1453      user_triggered: isUserTriggered,
   1454      id: event.target.id,
   1455    });
   1456 
   1457    if (isUserTriggered) {
   1458      Glean.tabgroup.groupInteractions.save.add(1);
   1459    }
   1460 
   1461    this._onTabGroupChange();
   1462  },
   1463 
   1464  _onTabGroupChange() {
   1465    this._onTabGroupChangeTask.disarm();
   1466    this._onTabGroupChangeTask.arm();
   1467  },
   1468 
   1469  /**
   1470   * @param {CustomEvent} event `TabGroupUngroup` event
   1471   */
   1472  _onTabGroupUngroup(event) {
   1473    const { isUserTriggered, telemetrySource } = event.detail;
   1474    if (isUserTriggered) {
   1475      Glean.tabgroup.ungroup.record({ source: telemetrySource });
   1476      // Only count explicit user actions (i.e. "Ungroup tabs" in the tab group
   1477      // context menu) toward the total number of tab group ungroup interations.
   1478      // This excludes implicit user actions, e.g. canceling tab group creation.
   1479      if (telemetrySource == lazy.TabMetrics.METRIC_SOURCE.TAB_GROUP_MENU) {
   1480        Glean.tabgroup.groupInteractions.ungroup.add(1);
   1481      }
   1482    }
   1483  },
   1484 
   1485  /**
   1486   * Returns summary statistics of a set of numbers.
   1487   *
   1488   * @param {number[]} data
   1489   * @returns {{max: number, min: number, median: number, average: number}}
   1490   */
   1491  _getSummaryStats(data) {
   1492    let count = data.length;
   1493    data.sort((a, b) => a - b);
   1494    let middleIndex = Math.floor(count / 2);
   1495 
   1496    return {
   1497      max: data.at(-1),
   1498      min: data.at(0),
   1499      median:
   1500        count % 2 == 0
   1501          ? (data[middleIndex - 1] + data[middleIndex]) / 2
   1502          : data[middleIndex],
   1503      average: data.reduce((a, b) => a + b, 0) / count,
   1504    };
   1505  },
   1506 
   1507  _doOnTabGroupChange() {
   1508    let totalTabs = 0;
   1509    let totalTabsInGroups = 0;
   1510 
   1511    // Used for calculation of average and median
   1512    let tabGroupLengths = [];
   1513 
   1514    for (let win of Services.wm.getEnumerator("navigator:browser")) {
   1515      totalTabs += win.gBrowser.tabs.length;
   1516      for (let group of win.gBrowser.tabGroups) {
   1517        totalTabsInGroups += group.tabs.length;
   1518        tabGroupLengths.push(group.tabs.length);
   1519      }
   1520    }
   1521 
   1522    let { max, min, median, average } = this._getSummaryStats(tabGroupLengths);
   1523 
   1524    Glean.tabgroup.tabCountInGroups.inside.set(totalTabsInGroups);
   1525    Glean.tabgroup.tabCountInGroups.outside.set(totalTabs - totalTabsInGroups);
   1526 
   1527    Glean.tabgroup.tabsPerActiveGroup.median.set(median);
   1528    Glean.tabgroup.tabsPerActiveGroup.average.set(average);
   1529    Glean.tabgroup.tabsPerActiveGroup.max.set(max);
   1530    Glean.tabgroup.tabsPerActiveGroup.min.set(min);
   1531  },
   1532 
   1533  _onSavedTabGroupsChange() {
   1534    this._onSavedTabGroupsChangedTask.disarm();
   1535    this._onSavedTabGroupsChangedTask.arm();
   1536  },
   1537 
   1538  _doOnSavedTabGroupsChange() {
   1539    let savedGroups = lazy.SessionStore.getSavedTabGroups();
   1540    let tabCounts = savedGroups.map(group => group.tabs.length);
   1541    let { max, min, median, average } = this._getSummaryStats(tabCounts);
   1542 
   1543    Glean.tabgroup.savedGroups.set(savedGroups.length);
   1544 
   1545    Glean.tabgroup.tabsPerSavedGroup.median.set(median);
   1546    Glean.tabgroup.tabsPerSavedGroup.average.set(average);
   1547    Glean.tabgroup.tabsPerSavedGroup.max.set(max);
   1548    Glean.tabgroup.tabsPerSavedGroup.min.set(min);
   1549  },
   1550 
   1551  _onTabGroupExpandOrCollapse() {
   1552    this._onTabGroupExpandOrCollapseTask.disarm();
   1553    this._onTabGroupExpandOrCollapseTask.arm();
   1554  },
   1555 
   1556  _doOnTabGroupExpandOrCollapse() {
   1557    let expanded = 0,
   1558      collapsed = 0;
   1559 
   1560    for (let win of Services.wm.getEnumerator("navigator:browser")) {
   1561      for (let group of win.gBrowser.tabGroups) {
   1562        if (group.collapsed) {
   1563          collapsed += 1;
   1564        } else {
   1565          expanded += 1;
   1566        }
   1567      }
   1568    }
   1569 
   1570    Glean.tabgroup.activeGroups.collapsed.set(collapsed);
   1571    Glean.tabgroup.activeGroups.expanded.set(expanded);
   1572  },
   1573 
   1574  /**
   1575   * @param {CustomEvent} event
   1576   */
   1577  _onTabGroupRemoveRequested(event) {
   1578    let {
   1579      isUserTriggered = false,
   1580      telemetrySource = lazy.TabMetrics.METRIC_SOURCE.UNKNOWN,
   1581    } = event.detail;
   1582 
   1583    if (isUserTriggered) {
   1584      Glean.tabgroup.delete.record({
   1585        id: event.target.id,
   1586        source: telemetrySource,
   1587      });
   1588      Glean.tabgroup.groupInteractions.delete.add(1);
   1589    }
   1590  },
   1591 
   1592  /**
   1593   * Accumulates `TabMove` events in order to record 1 metrics event per frame
   1594   * per telemetry source.
   1595   *
   1596   * For example, dragging and dropping 4 tabs should listen for 4 `TabMove`
   1597   * events but result in 1 metrics event being recorded with a source of
   1598   * `drag` and a tab count of 4.
   1599   *
   1600   * @param {CustomEvent} event
   1601   */
   1602  _onTabMove(event) {
   1603    let { isUserTriggered, telemetrySource } = event.detail;
   1604 
   1605    if (!isUserTriggered) {
   1606      return;
   1607    }
   1608 
   1609    let groupType = "";
   1610    if (event.target.group) {
   1611      groupType = event.target.group.collapsed
   1612        ? lazy.TabMetrics.METRIC_GROUP_TYPE.COLLAPSED
   1613        : lazy.TabMetrics.METRIC_GROUP_TYPE.EXPANDED;
   1614    }
   1615 
   1616    let segmentKey = [telemetrySource, groupType].join(",");
   1617 
   1618    let tabMovementsRecord = this._tabMovementsBySegment.get(segmentKey);
   1619    if (!tabMovementsRecord) {
   1620      let deferredTask = new lazy.DeferredTask(() => {
   1621        if (tabMovementsRecord.numberAddedToTabGroup) {
   1622          Glean.tabgroup.addTab.record({
   1623            source: telemetrySource,
   1624            tabs: tabMovementsRecord.numberAddedToTabGroup,
   1625            layout: lazy.sidebarVerticalTabs
   1626              ? lazy.TabMetrics.METRIC_TABS_LAYOUT.VERTICAL
   1627              : lazy.TabMetrics.METRIC_TABS_LAYOUT.HORIZONTAL,
   1628            group_type: groupType,
   1629          });
   1630        }
   1631        this._tabMovementsBySegment.delete(segmentKey);
   1632      }, 0);
   1633      tabMovementsRecord = {
   1634        deferredTask,
   1635        numberAddedToTabGroup: 0,
   1636      };
   1637      this._tabMovementsBySegment.set(segmentKey, tabMovementsRecord);
   1638      this._updateTabMovementsRecord(tabMovementsRecord, event);
   1639      deferredTask.arm();
   1640    } else {
   1641      tabMovementsRecord.deferredTask.disarm();
   1642      this._updateTabMovementsRecord(tabMovementsRecord, event);
   1643      tabMovementsRecord.deferredTask.arm();
   1644    }
   1645 
   1646    this._recordExternalTabMovement(event);
   1647  },
   1648 
   1649  /**
   1650   * @param {TabMovementsRecord} record
   1651   * @param {CustomEvent} event
   1652   */
   1653  _updateTabMovementsRecord(record, event) {
   1654    let { previousTabState, currentTabState } = event.detail;
   1655 
   1656    if (!previousTabState.tabGroupId && currentTabState.tabGroupId) {
   1657      Glean.tabgroup.tabInteractions.add.add();
   1658      record.numberAddedToTabGroup += 1;
   1659    }
   1660 
   1661    if (
   1662      previousTabState.tabGroupId &&
   1663      previousTabState.tabGroupId == currentTabState.tabGroupId &&
   1664      previousTabState.tabIndex != currentTabState.tabIndex
   1665    ) {
   1666      Glean.tabgroup.tabInteractions.reorder.add();
   1667    }
   1668 
   1669    if (previousTabState.tabGroupId && !currentTabState.tabGroupId) {
   1670      Glean.tabgroup.tabInteractions.remove_same_window.add();
   1671    }
   1672  },
   1673 
   1674  /**
   1675   * @param {CustomEvent} event
   1676   *   TabMove event
   1677   */
   1678  _recordExternalTabMovement(event) {
   1679    if (externalTabMovementRegistry.internallyOpenedTabs.has(event.target)) {
   1680      Glean.browserUiInteraction.tabMovement.not_from_external_app.add();
   1681    } else if (
   1682      externalTabMovementRegistry.externallyOpenedTabsNextToActiveTab.has(
   1683        event.target
   1684      )
   1685    ) {
   1686      Glean.browserUiInteraction.tabMovement.from_external_app_next_to_active_tab.add();
   1687    } else if (
   1688      externalTabMovementRegistry.externallyOpenedTabsAtEndOfTabStrip.has(
   1689        event.target
   1690      )
   1691    ) {
   1692      Glean.browserUiInteraction.tabMovement.from_external_app_tab_strip_end.add();
   1693    }
   1694  },
   1695 
   1696  _onTabSelect(event) {
   1697    if (event.target.group) {
   1698      let interaction = event.target.group.collapsed
   1699        ? Glean.tabgroup.tabInteractions.activate_collapsed
   1700        : Glean.tabgroup.tabInteractions.activate_expanded;
   1701      interaction.add();
   1702    }
   1703    if (event.target.pinned) {
   1704      const counter = lazy.sidebarVerticalTabs
   1705        ? Glean.pinnedTabs.activations.sidebar
   1706        : Glean.pinnedTabs.activations.horizontalBar;
   1707      counter.add();
   1708    }
   1709  },
   1710 
   1711  /**
   1712   * Tracks the window count and registers the listeners for the tab count.
   1713   *
   1714   * @param {object} win The window object.
   1715   */
   1716  _onWindowOpen(win) {
   1717    // Make sure to have a |nsIDOMWindow|.
   1718    if (!(win instanceof Ci.nsIDOMWindow)) {
   1719      return;
   1720    }
   1721 
   1722    let onLoad = () => {
   1723      win.removeEventListener("load", onLoad);
   1724 
   1725      // Ignore non browser windows.
   1726      if (
   1727        win.document.documentElement.getAttribute("windowtype") !=
   1728        "navigator:browser"
   1729      ) {
   1730        return;
   1731      }
   1732 
   1733      this._registerWindow(win);
   1734      // Track the window open event and check the maximum.
   1735      const counts = getOpenTabsAndWinsCounts();
   1736      Glean.browserEngagement.windowOpenEventCount.add(1);
   1737 
   1738      if (counts.winCount > this.maxWindowCount) {
   1739        this.maxWindowCount = counts.winCount;
   1740        Glean.browserEngagement.maxConcurrentWindowCount.set(counts.winCount);
   1741      }
   1742 
   1743      // We won't receive the "TabOpen" event for the first tab within a new window.
   1744      // Account for that.
   1745      this._onTabOpen();
   1746    };
   1747    win.addEventListener("load", onLoad);
   1748  },
   1749 
   1750  /**
   1751   * Record telemetry about the given tab counts.
   1752   *
   1753   * Telemetry for each count will only be recorded if the value isn't
   1754   * `undefined`.
   1755   *
   1756   * @param {object} [counts] The tab counts to register with telemetry.
   1757   * @param {number} [counts.tabCount] The number of tabs in all browsers.
   1758   * @param {number} [counts.loadedTabCount] The number of loaded (i.e., not
   1759   *                                         pending) tabs in all browsers.
   1760   */
   1761  _recordTabCounts({ tabCount, loadedTabCount }) {
   1762    let currentTime = Date.now();
   1763    if (
   1764      tabCount !== undefined &&
   1765      currentTime > this._lastRecordTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
   1766    ) {
   1767      Glean.browserEngagement.tabCount.accumulateSingleSample(tabCount);
   1768      this._lastRecordTabCount = currentTime;
   1769    }
   1770 
   1771    if (
   1772      loadedTabCount !== undefined &&
   1773      currentTime >
   1774        this._lastRecordLoadedTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
   1775    ) {
   1776      Glean.browserEngagement.loadedTabCount.accumulateSingleSample(
   1777        loadedTabCount
   1778      );
   1779      this._lastRecordLoadedTabCount = currentTime;
   1780    }
   1781  },
   1782 
   1783  _checkProfileCountFileSchema(fileData) {
   1784    // Verifies that the schema of the file is the expected schema
   1785    if (typeof fileData.version != "string") {
   1786      throw new Error("Schema Mismatch Error: Bad type for 'version' field");
   1787    }
   1788    if (!Array.isArray(fileData.profileTelemetryIds)) {
   1789      throw new Error(
   1790        "Schema Mismatch Error: Bad type for 'profileTelemetryIds' field"
   1791      );
   1792    }
   1793    for (let profileTelemetryId of fileData.profileTelemetryIds) {
   1794      if (typeof profileTelemetryId != "string") {
   1795        throw new Error(
   1796          "Schema Mismatch Error: Bad type for an element of 'profileTelemetryIds'"
   1797        );
   1798      }
   1799    }
   1800  },
   1801 
   1802  // Reports the number of Firefox profiles on this machine to telemetry.
   1803  async reportProfileCount() {
   1804    // Note: this is currently a windows-only feature.
   1805 
   1806    // To report only as much data as we need, we will bucket our values.
   1807    // Rather than the raw value, we will report the greatest value in the list
   1808    // below that is no larger than the raw value.
   1809    const buckets = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000, 10000];
   1810 
   1811    // We need both the C:\ProgramData\Mozilla directory and the install
   1812    // directory hash to create the profile count file path. We can easily
   1813    // reassemble this from the update directory, which looks like:
   1814    // C:\ProgramData\Mozilla\updates\hash
   1815    // Retrieving the directory this way also ensures that the "Mozilla"
   1816    // directory is created with the correct permissions.
   1817    // The ProgramData directory, by default, grants write permissions only to
   1818    // file creators. The directory service calls GetCommonUpdateDirectory,
   1819    // which makes sure the the directory is created with user-writable
   1820    // permissions.
   1821    const updateDirectory = BrowserUsageTelemetry.Policy.getUpdateDirectory();
   1822    const hash = updateDirectory.leafName;
   1823    const profileCountFilename = "profile_count_" + hash + ".json";
   1824    let profileCountFile = updateDirectory.parent.parent;
   1825    profileCountFile.append(profileCountFilename);
   1826 
   1827    let readError = false;
   1828    let fileData;
   1829    try {
   1830      let json = await BrowserUsageTelemetry.Policy.readProfileCountFile(
   1831        profileCountFile.path
   1832      );
   1833      fileData = JSON.parse(json);
   1834      BrowserUsageTelemetry._checkProfileCountFileSchema(fileData);
   1835    } catch (ex) {
   1836      // Note that since this also catches the "no such file" error, this is
   1837      // always the template that we use when writing to the file for the first
   1838      // time.
   1839      fileData = { version: "1", profileTelemetryIds: [] };
   1840      if (!(ex.name == "NotFoundError")) {
   1841        console.error(ex);
   1842        // Don't just return here on a read error. We need to send the error
   1843        // value to telemetry and we want to attempt to fix the file.
   1844        // However, we will still report an error for this ping, even if we
   1845        // fix the file. This is to prevent always sending a profile count of 1
   1846        // if, for some reason, we always get a read error but never a write
   1847        // error.
   1848        readError = true;
   1849      }
   1850    }
   1851 
   1852    let writeError = false;
   1853    let currentTelemetryId =
   1854      await BrowserUsageTelemetry.Policy.getTelemetryClientId();
   1855    // Don't add our telemetry ID to the file if we've already reached the
   1856    // largest bucket. This prevents the file size from growing forever.
   1857    if (
   1858      !fileData.profileTelemetryIds.includes(currentTelemetryId) &&
   1859      fileData.profileTelemetryIds.length < Math.max(...buckets)
   1860    ) {
   1861      fileData.profileTelemetryIds.push(currentTelemetryId);
   1862      try {
   1863        await BrowserUsageTelemetry.Policy.writeProfileCountFile(
   1864          profileCountFile.path,
   1865          JSON.stringify(fileData)
   1866        );
   1867      } catch (ex) {
   1868        console.error(ex);
   1869        writeError = true;
   1870      }
   1871    }
   1872 
   1873    // Determine the bucketed value to report
   1874    let rawProfileCount = fileData.profileTelemetryIds.length;
   1875    let valueToReport = 0;
   1876    for (let bucket of buckets) {
   1877      if (bucket <= rawProfileCount && bucket > valueToReport) {
   1878        valueToReport = bucket;
   1879      }
   1880    }
   1881 
   1882    if (readError || writeError) {
   1883      // We convey errors via a profile count of 0.
   1884      valueToReport = 0;
   1885    }
   1886 
   1887    Glean.browserEngagement.profileCount.set(valueToReport);
   1888  },
   1889 
   1890  /**
   1891   * Check if this is the first run of this profile since installation,
   1892   * if so then collect installation telemetry.
   1893   *
   1894   * @param {nsIFile} [dataPathOverride] Optional, full data file path, for tests.
   1895   * @param {Array<string>} [msixPackagePrefixes] Optional, list of prefixes to
   1896            consider "existing" installs when looking at installed MSIX packages.
   1897            Defaults to prefixes for builds produced in Firefox automation.
   1898   * @returns {Promise<object>}
   1899   *   Resolves to a JSON object containing install telemetry when the event has
   1900   *   been recorded, or if the data file was not found.
   1901   * @rejects JavaScript exception on any failure.
   1902   */
   1903  async collectInstallationTelemetry(
   1904    dataPathOverride,
   1905    msixPackagePrefixes = ["Mozilla.Firefox", "Mozilla.MozillaFirefox"]
   1906  ) {
   1907    if (AppConstants.platform != "win") {
   1908      // This is a windows-only feature.
   1909      return {};
   1910    }
   1911 
   1912    const TIMESTAMP_PREF = "app.installation.timestamp";
   1913    const lastInstallTime = Services.prefs.getStringPref(TIMESTAMP_PREF, null);
   1914    const wpm = Cc["@mozilla.org/windows-package-manager;1"].createInstance(
   1915      Ci.nsIWindowsPackageManager
   1916    );
   1917    let installer_type = "";
   1918    let pfn;
   1919    try {
   1920      pfn = Services.sysinfo.getProperty("winPackageFamilyName");
   1921    } catch (e) {}
   1922 
   1923    function getInstallData() {
   1924      // We only care about where _any_ other install existed - no
   1925      // need to count more than 1.
   1926      const installPaths = lazy.WindowsInstallsInfo.getInstallPaths(
   1927        1,
   1928        new Set([Services.dirsvc.get("GreBinD", Ci.nsIFile).path])
   1929      );
   1930      const msixInstalls = new Set();
   1931      // We're just going to eat all errors here -- we don't want the event
   1932      // to go unsent if we were unable to look for MSIX installs.
   1933      try {
   1934        wpm
   1935          .findUserInstalledPackages(msixPackagePrefixes)
   1936          .forEach(i => msixInstalls.add(i));
   1937        if (pfn) {
   1938          msixInstalls.delete(pfn);
   1939        }
   1940      } catch (ex) {}
   1941      return {
   1942        installPaths,
   1943        msixInstalls,
   1944      };
   1945    }
   1946 
   1947    let extra = {};
   1948 
   1949    if (pfn) {
   1950      if (lastInstallTime != null) {
   1951        // We've already seen this install
   1952        return {};
   1953      }
   1954 
   1955      // First time seeing this install, record the timestamp.
   1956      Services.prefs.setStringPref(TIMESTAMP_PREF, wpm.getInstalledDate());
   1957      let install_data = getInstallData();
   1958 
   1959      installer_type = "msix";
   1960 
   1961      // Build the extra event data
   1962      extra.version = AppConstants.MOZ_APP_VERSION;
   1963      extra.build_id = AppConstants.MOZ_BUILDID;
   1964      // The next few keys are static for the reasons described
   1965      // No way to detect whether or not we were installed by an admin
   1966      extra.admin_user = "false";
   1967      // Always false at the moment, because we create a new profile
   1968      // on first launch
   1969      extra.profdir_existed = "false";
   1970      // Obviously false for MSIX installs
   1971      extra.from_msi = "false";
   1972      // We have no way of knowing whether we were installed via the GUI,
   1973      // through the command line, or some Enterprise management tool.
   1974      extra.silent = "false";
   1975      // There's no way to change the install path for an MSIX package
   1976      extra.default_path = "true";
   1977      extra.install_existed = install_data.msixInstalls.has(pfn).toString();
   1978      install_data.msixInstalls.delete(pfn);
   1979      extra.other_inst = (!!install_data.installPaths.size).toString();
   1980      extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
   1981    } else {
   1982      let dataPath = dataPathOverride;
   1983      if (!dataPath) {
   1984        dataPath = Services.dirsvc.get("GreD", Ci.nsIFile);
   1985        dataPath.append("installation_telemetry.json");
   1986      }
   1987 
   1988      let dataBytes;
   1989      try {
   1990        dataBytes = await IOUtils.read(dataPath.path);
   1991      } catch (ex) {
   1992        if (ex.name == "NotFoundError") {
   1993          // Many systems will not have the data file, return silently if not found as
   1994          // there is nothing to record.
   1995          return {};
   1996        }
   1997        throw ex;
   1998      }
   1999      const dataString = new TextDecoder("utf-16").decode(dataBytes);
   2000      const data = JSON.parse(dataString);
   2001 
   2002      if (lastInstallTime && data.install_timestamp == lastInstallTime) {
   2003        // We've already seen this install
   2004        return {};
   2005      }
   2006 
   2007      // First time seeing this install, record the timestamp.
   2008      Services.prefs.setStringPref(TIMESTAMP_PREF, data.install_timestamp);
   2009      let install_data = getInstallData();
   2010 
   2011      installer_type = data.installer_type;
   2012 
   2013      // Installation timestamp is not intended to be sent with telemetry,
   2014      // remove it to emphasize this point.
   2015      delete data.install_timestamp;
   2016 
   2017      // Build the extra event data
   2018      extra.version = data.version;
   2019      extra.build_id = data.build_id;
   2020      extra.admin_user = data.admin_user.toString();
   2021      extra.install_existed = data.install_existed.toString();
   2022      extra.profdir_existed = data.profdir_existed.toString();
   2023      extra.other_inst = (!!install_data.installPaths.size).toString();
   2024      extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
   2025 
   2026      if (data.installer_type == "full") {
   2027        extra.silent = data.silent.toString();
   2028        extra.from_msi = data.from_msi.toString();
   2029        extra.default_path = data.default_path.toString();
   2030      }
   2031    }
   2032    return { installer_type, extra };
   2033  },
   2034 
   2035  async reportInstallationTelemetry(
   2036    dataPathOverride,
   2037    msixPackagePrefixes = ["Mozilla.Firefox", "Mozilla.MozillaFirefox"]
   2038  ) {
   2039    // The optional dataPathOverride is only used for testing purposes.
   2040    // Use this as a proxy for whether we're in a testing environment.
   2041    // If we're in a testing environment we don't want to return the
   2042    // same data even if we call this function multiple times in the
   2043    // same instance.
   2044    if (gInstallationTelemetryPromise && !dataPathOverride) {
   2045      return gInstallationTelemetryPromise;
   2046    }
   2047 
   2048    gInstallationTelemetryPromise = (async () => {
   2049      let data = await BrowserUsageTelemetry.collectInstallationTelemetry(
   2050        dataPathOverride,
   2051        msixPackagePrefixes
   2052      );
   2053 
   2054      if (data?.installer_type) {
   2055        let { installer_type, extra } = data;
   2056 
   2057        // Record the event (mirrored to legacy telemetry using GIFFT)
   2058        if (installer_type == "full") {
   2059          Glean.installation.firstSeenFull.record(extra);
   2060        } else if (installer_type == "stub") {
   2061          Glean.installation.firstSeenStub.record(extra);
   2062        } else if (installer_type == "msix") {
   2063          Glean.installation.firstSeenMsix.record(extra);
   2064        }
   2065 
   2066        // Scalars for the new-profile ping. We don't need to collect the build version
   2067        // These are mirrored to legacy telemetry using GIFFT
   2068        Glean.installationFirstSeen.installerType.set(installer_type);
   2069        Glean.installationFirstSeen.version.set(extra.version);
   2070        // Convert "true" or "false" strings back into booleans
   2071        Glean.installationFirstSeen.adminUser.set(extra.admin_user === "true");
   2072        Glean.installationFirstSeen.installExisted.set(
   2073          extra.install_existed === "true"
   2074        );
   2075        Glean.installationFirstSeen.profdirExisted.set(
   2076          extra.profdir_existed === "true"
   2077        );
   2078        Glean.installationFirstSeen.otherInst.set(extra.other_inst === "true");
   2079        Glean.installationFirstSeen.otherMsixInst.set(
   2080          extra.other_msix_inst === "true"
   2081        );
   2082        if (installer_type == "full") {
   2083          Glean.installationFirstSeen.silent.set(extra.silent === "true");
   2084          Glean.installationFirstSeen.fromMsi.set(extra.from_msi === "true");
   2085          Glean.installationFirstSeen.defaultPath.set(
   2086            extra.default_path === "true"
   2087          );
   2088        }
   2089      }
   2090      return data;
   2091    })();
   2092 
   2093    return gInstallationTelemetryPromise;
   2094  },
   2095 };
   2096 
   2097 // Used by nsIBrowserUsage
   2098 export function getUniqueDomainsVisitedInPast24Hours() {
   2099  return URICountListener.uniqueDomainsVisitedInPast24Hours;
   2100 }