Bug 1925561 - Use a quicker radius calculation for ArcParams. r=aosmond
[gecko.git] / browser / modules / BrowserUsageTelemetry.sys.mjs
blob3919453b8bb4c51aad7c1b39978c9e41d2cbdf9b
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/. */
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
9 const lazy = {};
11 ChromeUtils.defineESModuleGetters(lazy, {
12   ClientID: "resource://gre/modules/ClientID.sys.mjs",
13   CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
14   DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
15   PageActions: "resource:///modules/PageActions.sys.mjs",
16   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
17   SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs",
18   SearchSERPTelemetryUtils: "resource:///modules/SearchSERPTelemetry.sys.mjs",
20   WindowsInstallsInfo:
21     "resource://gre/modules/components-utils/WindowsInstallsInfo.sys.mjs",
23   clearTimeout: "resource://gre/modules/Timer.sys.mjs",
24   setTimeout: "resource://gre/modules/Timer.sys.mjs",
25 });
27 // This pref is in seconds!
28 XPCOMUtils.defineLazyPreferenceGetter(
29   lazy,
30   "gRecentVisitedOriginsExpiry",
31   "browser.engagement.recent_visited_origins.expiry"
33 XPCOMUtils.defineLazyPreferenceGetter(
34   lazy,
35   "sidebarVerticalTabs",
36   "sidebar.verticalTabs",
37   false,
38   (_aPreference, _previousValue, isVertical) => {
39     // Copy max tab counts into the "new" scalars.
40     Services.telemetry.scalarSetMaximum(
41       isVertical
42         ? VERTICAL_MAX_TAB_COUNT_SCALAR_NAME
43         : MAX_TAB_COUNT_SCALAR_NAME,
44       getOpenTabsAndWinsCounts().tabCount
45     );
46     Services.telemetry.scalarSetMaximum(
47       isVertical
48         ? VERTICAL_MAX_TAB_PINNED_COUNT_SCALAR_NAME
49         : MAX_TAB_PINNED_COUNT_SCALAR_NAME,
50       getPinnedTabsCount()
51     );
52   }
55 // The upper bound for the count of the visited unique domain names.
56 const MAX_UNIQUE_VISITED_DOMAINS = 100;
58 // Observed topic names.
59 const TAB_RESTORING_TOPIC = "SSTabRestoring";
60 const TELEMETRY_SUBSESSIONSPLIT_TOPIC =
61   "internal-telemetry-after-subsession-split";
62 const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
64 // Probe names.
65 const MAX_TAB_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_tab_count";
66 const VERTICAL_MAX_TAB_COUNT_SCALAR_NAME =
67   "browser.engagement.max_concurrent_vertical_tab_count";
68 const MAX_WINDOW_COUNT_SCALAR_NAME =
69   "browser.engagement.max_concurrent_window_count";
70 const TAB_OPEN_EVENT_COUNT_SCALAR_NAME =
71   "browser.engagement.tab_open_event_count";
72 const VERTICAL_TAB_OPEN_EVENT_COUNT_SCALAR_NAME =
73   "browser.engagement.vertical_tab_open_event_count";
74 const MAX_TAB_PINNED_COUNT_SCALAR_NAME =
75   "browser.engagement.max_concurrent_tab_pinned_count";
76 const VERTICAL_MAX_TAB_PINNED_COUNT_SCALAR_NAME =
77   "browser.engagement.max_concurrent_vertical_tab_pinned_count";
78 const TAB_PINNED_EVENT_COUNT_SCALAR_NAME =
79   "browser.engagement.tab_pinned_event_count";
80 const VERTICAL_TAB_PINNED_EVENT_COUNT_SCALAR_NAME =
81   "browser.engagement.vertical_tab_pinned_event_count";
82 const WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME =
83   "browser.engagement.window_open_event_count";
84 const UNIQUE_DOMAINS_COUNT_SCALAR_NAME =
85   "browser.engagement.unique_domains_count";
86 const TOTAL_URI_COUNT_SCALAR_NAME = "browser.engagement.total_uri_count";
87 const UNFILTERED_URI_COUNT_SCALAR_NAME =
88   "browser.engagement.unfiltered_uri_count";
89 const TOTAL_URI_COUNT_NORMAL_AND_PRIVATE_MODE_SCALAR_NAME =
90   "browser.engagement.total_uri_count_normal_and_private_mode";
92 export const MINIMUM_TAB_COUNT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes, in ms
94 // The elements we consider to be interactive.
95 const UI_TARGET_ELEMENTS = [
96   "menuitem",
97   "toolbarbutton",
98   "key",
99   "command",
100   "checkbox",
101   "input",
102   "button",
103   "image",
104   "radio",
105   "richlistitem",
106   "moz-checkbox",
108 const UI_TARGET_COMPOSED_ELEMENTS_MAP = new Map([["moz-checkbox", "input"]]);
110 // The containers of interactive elements that we care about and their pretty
111 // names. These should be listed in order of most-specific to least-specific,
112 // when iterating JavaScript will guarantee that ordering and so we will find
113 // the most specific area first.
114 const BROWSER_UI_CONTAINER_IDS = {
115   "toolbar-menubar": "menu-bar",
116   TabsToolbar: "tabs-bar",
117   "vertical-tabs": "vertical-tabs-container",
118   PersonalToolbar: "bookmarks-bar",
119   "appMenu-popup": "app-menu",
120   tabContextMenu: "tabs-context",
121   contentAreaContextMenu: "content-context",
122   "widget-overflow-list": "overflow-menu",
123   "widget-overflow-fixed-list": "pinned-overflow-menu",
124   "page-action-buttons": "pageaction-urlbar",
125   pageActionPanel: "pageaction-panel",
126   "unified-extensions-area": "unified-extensions-area",
127   "allTabsMenu-allTabsView": "alltabs-menu",
129   // This should appear last as some of the above are inside the nav bar.
130   "nav-bar": "nav-bar",
133 const ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS = {
134   [BROWSER_UI_CONTAINER_IDS.tabContextMenu]: "tabs-context-entrypoint",
137 // A list of the expected panes in about:preferences
138 const PREFERENCES_PANES = [
139   "paneHome",
140   "paneGeneral",
141   "panePrivacy",
142   "paneSearch",
143   "paneSearchResults",
144   "paneSync",
145   "paneContainers",
146   "paneExperimental",
147   "paneMoreFromMozilla",
150 const IGNORABLE_EVENTS = new WeakMap();
152 const KNOWN_ADDONS = [];
154 // Buttons that, when clicked, set a preference to true. The convention
155 // is that the preference is named:
157 // browser.engagement.<button id>.has-used
159 // and is defaulted to false.
160 const SET_USAGE_PREF_BUTTONS = [
161   "downloads-button",
162   "fxa-toolbar-menu-button",
163   "home-button",
164   "sidebar-button",
165   "library-button",
168 // Buttons that, when clicked, increase a counter. The convention
169 // is that the preference is named:
171 // browser.engagement.<button id>.used-count
173 // and doesn't have a default value.
174 const SET_USAGECOUNT_PREF_BUTTONS = [
175   "pageAction-panel-copyURL",
176   "pageAction-panel-emailLink",
177   "pageAction-panel-pinTab",
178   "pageAction-panel-screenshots_mozilla_org",
179   "pageAction-panel-shareURL",
182 // Places context menu IDs.
183 const PLACES_CONTEXT_MENU_ID = "placesContext";
184 const PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID =
185   "placesContext_open:newcontainertab";
187 // Commands used to open history or bookmark links from places context menu.
188 const PLACES_OPEN_COMMANDS = [
189   "placesCmd_open",
190   "placesCmd_open:window",
191   "placesCmd_open:privatewindow",
192   "placesCmd_open:tab",
195 // How long of a delay between events means the start of a new flow?
196 // Used by Browser UI Interaction event instrumentation.
197 // Default: 5min.
198 const FLOW_IDLE_TIME = 5 * 60 * 1000;
200 function telemetryId(widgetId, obscureAddons = true) {
201   // Add-on IDs need to be obscured.
202   function addonId(id) {
203     if (!obscureAddons) {
204       return id;
205     }
207     let pos = KNOWN_ADDONS.indexOf(id);
208     if (pos < 0) {
209       pos = KNOWN_ADDONS.length;
210       KNOWN_ADDONS.push(id);
211     }
212     return `addon${pos}`;
213   }
215   if (widgetId.endsWith("-browser-action")) {
216     widgetId = addonId(
217       widgetId.substring(0, widgetId.length - "-browser-action".length)
218     );
219   } else if (widgetId.startsWith("pageAction-")) {
220     let actionId;
221     if (widgetId.startsWith("pageAction-urlbar-")) {
222       actionId = widgetId.substring("pageAction-urlbar-".length);
223     } else if (widgetId.startsWith("pageAction-panel-")) {
224       actionId = widgetId.substring("pageAction-panel-".length);
225     }
227     if (actionId) {
228       let action = lazy.PageActions.actionForID(actionId);
229       widgetId = action?._isMozillaAction ? actionId : addonId(actionId);
230     }
231   } else if (widgetId.startsWith("ext-keyset-id-")) {
232     // Webextension command shortcuts don't have an id on their key element so
233     // we see the id from the keyset that contains them.
234     widgetId = addonId(widgetId.substring("ext-keyset-id-".length));
235   } else if (widgetId.startsWith("ext-key-id-")) {
236     // The command for a webextension sidebar action is an exception to the above rule.
237     widgetId = widgetId.substring("ext-key-id-".length);
238     if (widgetId.endsWith("-sidebar-action")) {
239       widgetId = addonId(
240         widgetId.substring(0, widgetId.length - "-sidebar-action".length)
241       );
242     }
243   }
245   return widgetId.replace(/_/g, "-");
248 function getOpenTabsAndWinsCounts() {
249   let loadedTabCount = 0;
250   let tabCount = 0;
251   let winCount = 0;
253   for (let win of Services.wm.getEnumerator("navigator:browser")) {
254     winCount++;
255     tabCount += win.gBrowser.tabs.length;
256     for (const tab of win.gBrowser.tabs) {
257       if (tab.getAttribute("pending") !== "true") {
258         loadedTabCount += 1;
259       }
260     }
261   }
263   return { loadedTabCount, tabCount, winCount };
266 function getPinnedTabsCount() {
267   let pinnedTabs = 0;
269   for (let win of Services.wm.getEnumerator("navigator:browser")) {
270     pinnedTabs += [...win.ownerGlobal.gBrowser.tabs].filter(
271       t => t.pinned
272     ).length;
273   }
275   return pinnedTabs;
278 export let URICountListener = {
279   // A set containing the visited domains, see bug 1271310.
280   _domainSet: new Set(),
281   // A set containing the visited origins during the last 24 hours (similar to domains, but not quite the same)
282   _domain24hrSet: new Set(),
283   // A map to keep track of the URIs loaded from the restored tabs.
284   _restoredURIsMap: new WeakMap(),
285   // Ongoing expiration timeouts.
286   _timeouts: new Set(),
288   isHttpURI(uri) {
289     // Only consider http(s) schemas.
290     return uri.schemeIs("http") || uri.schemeIs("https");
291   },
293   addRestoredURI(browser, uri) {
294     if (!this.isHttpURI(uri)) {
295       return;
296     }
298     this._restoredURIsMap.set(browser, uri.spec);
299   },
301   onLocationChange(browser, webProgress, request, uri, flags) {
302     if (
303       !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) &&
304       webProgress.isTopLevel
305     ) {
306       // By default, assume we no longer need to track this tab.
307       lazy.SearchSERPTelemetry.stopTrackingBrowser(
308         browser,
309         lazy.SearchSERPTelemetryUtils.ABANDONMENTS.NAVIGATION
310       );
311     }
313     // Don't count this URI if it's an error page.
314     if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
315       return;
316     }
318     // We only care about top level loads.
319     if (!webProgress.isTopLevel) {
320       return;
321     }
323     // The SessionStore sets the URI of a tab first, firing onLocationChange the
324     // first time, then manages content loading using its scheduler. Once content
325     // loads, we will hit onLocationChange again.
326     // We can catch the first case by checking for null requests: be advised that
327     // this can also happen when navigating page fragments, so account for it.
328     if (
329       !request &&
330       !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
331     ) {
332       return;
333     }
335     // Don't include URI and domain counts when in private mode.
336     let shouldCountURI =
337       !lazy.PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal) ||
338       Services.prefs.getBoolPref(
339         "browser.engagement.total_uri_count.pbm",
340         false
341       );
343     // Track URI loads, even if they're not http(s).
344     let uriSpec = null;
345     try {
346       uriSpec = uri.spec;
347     } catch (e) {
348       // If we have troubles parsing the spec, still count this as
349       // an unfiltered URI.
350       if (shouldCountURI) {
351         Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
352       }
353       return;
354     }
356     // Don't count about:blank and similar pages, as they would artificially
357     // inflate the counts.
358     if (browser.ownerGlobal.gInitialPages.includes(uriSpec)) {
359       return;
360     }
362     // If the URI we're loading is in the _restoredURIsMap, then it comes from a
363     // restored tab. If so, let's skip it and remove it from the map as we want to
364     // count page refreshes.
365     if (this._restoredURIsMap.get(browser) === uriSpec) {
366       this._restoredURIsMap.delete(browser);
367       return;
368     }
370     // The URI wasn't from a restored tab. Count it among the unfiltered URIs.
371     // If this is an http(s) URI, this also gets counted by the "total_uri_count"
372     // probe.
373     if (shouldCountURI) {
374       Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
375     }
377     if (!this.isHttpURI(uri)) {
378       return;
379     }
381     if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
382       lazy.SearchSERPTelemetry.updateTrackingStatus(
383         browser,
384         uriSpec,
385         webProgress.loadType
386       );
387     } else {
388       lazy.SearchSERPTelemetry.updateTrackingSinglePageApp(
389         browser,
390         uriSpec,
391         webProgress.loadType,
392         flags
393       );
394     }
396     // Update total URI count, including when in private mode.
397     Services.telemetry.scalarAdd(
398       TOTAL_URI_COUNT_NORMAL_AND_PRIVATE_MODE_SCALAR_NAME,
399       1
400     );
401     Glean.browserEngagement.uriCount.add(1);
403     if (!shouldCountURI) {
404       return;
405     }
407     // Update the URI counts.
408     Services.telemetry.scalarAdd(TOTAL_URI_COUNT_SCALAR_NAME, 1);
410     // Update tab count
411     BrowserUsageTelemetry._recordTabCounts(getOpenTabsAndWinsCounts());
413     // Unique domains should be aggregated by (eTLD + 1): x.test.com and y.test.com
414     // are counted once as test.com.
415     let baseDomain;
416     try {
417       // Even if only considering http(s) URIs, |getBaseDomain| could still throw
418       // due to the URI containing invalid characters or the domain actually being
419       // an ipv4 or ipv6 address.
420       baseDomain = Services.eTLD.getBaseDomain(uri);
421     } catch (e) {
422       return;
423     }
425     // We only want to count the unique domains up to MAX_UNIQUE_VISITED_DOMAINS.
426     if (this._domainSet.size < MAX_UNIQUE_VISITED_DOMAINS) {
427       this._domainSet.add(baseDomain);
428       Services.telemetry.scalarSet(
429         UNIQUE_DOMAINS_COUNT_SCALAR_NAME,
430         this._domainSet.size
431       );
432     }
434     this._domain24hrSet.add(baseDomain);
435     if (lazy.gRecentVisitedOriginsExpiry) {
436       let timeoutId = lazy.setTimeout(() => {
437         this._domain24hrSet.delete(baseDomain);
438         this._timeouts.delete(timeoutId);
439       }, lazy.gRecentVisitedOriginsExpiry * 1000);
440       this._timeouts.add(timeoutId);
441     }
442   },
444   /**
445    * Reset the counts. This should be called when breaking a session in Telemetry.
446    */
447   reset() {
448     this._domainSet.clear();
449   },
451   /**
452    * Returns the number of unique domains visited in this session during the
453    * last 24 hours.
454    */
455   get uniqueDomainsVisitedInPast24Hours() {
456     return this._domain24hrSet.size;
457   },
459   /**
460    * Resets the number of unique domains visited in this session.
461    */
462   resetUniqueDomainsVisitedInPast24Hours() {
463     this._timeouts.forEach(timeoutId => lazy.clearTimeout(timeoutId));
464     this._timeouts.clear();
465     this._domain24hrSet.clear();
466   },
468   QueryInterface: ChromeUtils.generateQI([
469     "nsIWebProgressListener",
470     "nsISupportsWeakReference",
471   ]),
474 let gInstallationTelemetryPromise = null;
476 export let BrowserUsageTelemetry = {
477   /**
478    * This is a policy object used to override behavior for testing.
479    */
480   Policy: {
481     getTelemetryClientId: async () => lazy.ClientID.getClientID(),
482     getUpdateDirectory: () => Services.dirsvc.get("UpdRootD", Ci.nsIFile),
483     readProfileCountFile: async path => IOUtils.readUTF8(path),
484     writeProfileCountFile: async (path, data) => IOUtils.writeUTF8(path, data),
485   },
487   _inited: false,
489   init() {
490     this._lastRecordTabCount = 0;
491     this._lastRecordLoadedTabCount = 0;
492     this._setupAfterRestore();
493     this._inited = true;
495     Services.prefs.addObserver("browser.tabs.inTitlebar", this);
496     Services.prefs.addObserver(
497       "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled",
498       this
499     );
501     this._recordUITelemetry();
503     this._onTabsOpenedTask = new lazy.DeferredTask(
504       () => this._onTabsOpened(),
505       0
506     );
507   },
509   get maxTabCountScalarName() {
510     return lazy.sidebarVerticalTabs
511       ? VERTICAL_MAX_TAB_COUNT_SCALAR_NAME
512       : MAX_TAB_COUNT_SCALAR_NAME;
513   },
515   get tabOpenEventCountScalarName() {
516     return lazy.sidebarVerticalTabs
517       ? VERTICAL_TAB_OPEN_EVENT_COUNT_SCALAR_NAME
518       : TAB_OPEN_EVENT_COUNT_SCALAR_NAME;
519   },
521   get maxTabPinnedCountScalarName() {
522     return lazy.sidebarVerticalTabs
523       ? VERTICAL_MAX_TAB_PINNED_COUNT_SCALAR_NAME
524       : MAX_TAB_PINNED_COUNT_SCALAR_NAME;
525   },
527   get tabPinnedEventCountScalarName() {
528     return lazy.sidebarVerticalTabs
529       ? VERTICAL_TAB_PINNED_EVENT_COUNT_SCALAR_NAME
530       : TAB_PINNED_EVENT_COUNT_SCALAR_NAME;
531   },
533   /**
534    * Resets the masked add-on identifiers. Only for use in tests.
535    */
536   _resetAddonIds() {
537     KNOWN_ADDONS.length = 0;
538   },
540   /**
541    * Handle subsession splits in the parent process.
542    */
543   afterSubsessionSplit() {
544     // Scalars just got cleared due to a subsession split. We need to set the maximum
545     // concurrent tab and window counts so that they reflect the correct value for the
546     // new subsession.
547     const counts = getOpenTabsAndWinsCounts();
548     Services.telemetry.scalarSetMaximum(
549       this.maxTabCountScalarName,
550       counts.tabCount
551     );
552     Services.telemetry.scalarSetMaximum(
553       MAX_WINDOW_COUNT_SCALAR_NAME,
554       counts.winCount
555     );
557     // Reset the URI counter.
558     URICountListener.reset();
559   },
561   QueryInterface: ChromeUtils.generateQI([
562     "nsIObserver",
563     "nsISupportsWeakReference",
564   ]),
566   uninit() {
567     if (!this._inited) {
568       return;
569     }
570     Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC);
571     Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC);
572   },
574   observe(subject, topic, data) {
575     switch (topic) {
576       case DOMWINDOW_OPENED_TOPIC:
577         this._onWindowOpen(subject);
578         break;
579       case TELEMETRY_SUBSESSIONSPLIT_TOPIC:
580         this.afterSubsessionSplit();
581         break;
582       case "nsPref:changed":
583         switch (data) {
584           case "browser.tabs.inTitlebar":
585             this._recordWidgetChange(
586               "titlebar",
587               Services.appinfo.drawInTitlebar ? "off" : "on",
588               "pref"
589             );
590             break;
591           case "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled":
592             if (Services.prefs.getBoolPref(data)) {
593               Glean.pictureinpictureSettings.enableAutotriggerSettings.record();
594             }
595             break;
596         }
597         break;
598     }
599   },
601   handleEvent(event) {
602     switch (event.type) {
603       case "TabOpen":
604         this._onTabOpen();
605         break;
606       case "TabPinned":
607         this._onTabPinned();
608         break;
609       case "unload":
610         this._unregisterWindow(event.target);
611         break;
612       case TAB_RESTORING_TOPIC:
613         // We're restoring a new tab from a previous or crashed session.
614         // We don't want to track the URIs from these tabs, so let
615         // |URICountListener| know about them.
616         let browser = event.target.linkedBrowser;
617         URICountListener.addRestoredURI(browser, browser.currentURI);
619         const { loadedTabCount } = getOpenTabsAndWinsCounts();
620         this._recordTabCounts({ loadedTabCount });
621         break;
622     }
623   },
625   /**
626    * This gets called shortly after the SessionStore has finished restoring
627    * windows and tabs. It counts the open tabs and adds listeners to all the
628    * windows.
629    */
630   _setupAfterRestore() {
631     // Make sure to catch new chrome windows and subsession splits.
632     Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, true);
633     Services.obs.addObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, true);
635     // Attach the tabopen handlers to the existing Windows.
636     for (let win of Services.wm.getEnumerator("navigator:browser")) {
637       this._registerWindow(win);
638     }
640     // Get the initial tab and windows max counts.
641     const counts = getOpenTabsAndWinsCounts();
642     Services.telemetry.scalarSetMaximum(
643       this.maxTabCountScalarName,
644       counts.tabCount
645     );
646     Services.telemetry.scalarSetMaximum(
647       MAX_WINDOW_COUNT_SCALAR_NAME,
648       counts.winCount
649     );
650   },
652   _buildWidgetPositions() {
653     let widgetMap = new Map();
655     const toolbarState = nodeId => {
656       let value;
657       if (nodeId == "PersonalToolbar") {
658         value = Services.prefs.getCharPref(
659           "browser.toolbars.bookmarks.visibility",
660           "newtab"
661         );
662         if (value != "newtab") {
663           return value == "never" ? "off" : "on";
664         }
665         return value;
666       }
667       value = Services.xulStore.getValue(
668         AppConstants.BROWSER_CHROME_URL,
669         nodeId,
670         "collapsed"
671       );
673       if (value) {
674         return value == "true" ? "off" : "on";
675       }
676       return "off";
677     };
679     widgetMap.set(
680       BROWSER_UI_CONTAINER_IDS.PersonalToolbar,
681       toolbarState("PersonalToolbar")
682     );
684     let menuBarHidden =
685       Services.xulStore.getValue(
686         AppConstants.BROWSER_CHROME_URL,
687         "toolbar-menubar",
688         "autohide"
689       ) != "false";
691     widgetMap.set("menu-toolbar", menuBarHidden ? "off" : "on");
693     // Drawing in the titlebar means not showing the titlebar, hence the negation.
694     widgetMap.set("titlebar", Services.appinfo.drawInTitlebar ? "off" : "on");
696     for (let area of lazy.CustomizableUI.areas) {
697       if (!(area in BROWSER_UI_CONTAINER_IDS)) {
698         continue;
699       }
701       let position = BROWSER_UI_CONTAINER_IDS[area];
702       if (area == "nav-bar") {
703         position = `${BROWSER_UI_CONTAINER_IDS[area]}-start`;
704       }
706       let widgets = lazy.CustomizableUI.getWidgetsInArea(area);
708       for (let widget of widgets) {
709         if (!widget) {
710           continue;
711         }
713         if (widget.id.startsWith("customizableui-special-")) {
714           continue;
715         }
717         if (area == "nav-bar" && widget.id == "urlbar-container") {
718           position = `${BROWSER_UI_CONTAINER_IDS[area]}-end`;
719           continue;
720         }
722         widgetMap.set(widget.id, position);
723       }
724     }
726     let actions = lazy.PageActions.actions;
727     for (let action of actions) {
728       if (action.pinnedToUrlbar) {
729         widgetMap.set(action.id, "pageaction-urlbar");
730       }
731     }
733     return widgetMap;
734   },
736   _getWidgetID(node) {
737     // We want to find a sensible ID for this element.
738     if (!node) {
739       return null;
740     }
742     // See if this is a customizable widget.
743     if (node.ownerDocument.URL == AppConstants.BROWSER_CHROME_URL) {
744       // First find if it is inside one of the customizable areas.
745       for (let area of lazy.CustomizableUI.areas) {
746         if (node.closest(`#${CSS.escape(area)}`)) {
747           for (let widget of lazy.CustomizableUI.getWidgetIdsInArea(area)) {
748             if (
749               // We care about the buttons on the tabs themselves.
750               widget == "tabbrowser-tabs" ||
751               // We care about the page action and other buttons in here.
752               widget == "urlbar-container" ||
753               // We care about the actual menu items.
754               widget == "menubar-items" ||
755               // We care about individual bookmarks here.
756               widget == "personal-bookmarks"
757             ) {
758               continue;
759             }
761             if (node.closest(`#${CSS.escape(widget)}`)) {
762               return widget;
763             }
764           }
765           break;
766         }
767       }
768     }
770     if (node.id) {
771       return node.id;
772     }
774     // A couple of special cases in the tabs.
775     for (let cls of ["bookmark-item", "tab-icon-sound", "tab-close-button"]) {
776       if (!node.classList.contains(cls)) {
777         continue;
778       }
779       if (cls == "bookmark-item" && node.parentElement.id.includes("history")) {
780         return "history-item";
781       }
782       return cls;
783     }
785     // One of these will at least let us know what the widget is for.
786     let possibleAttributes = [
787       "preference",
788       "command",
789       "observes",
790       "data-l10n-id",
791     ];
793     // The key attribute on key elements is the actual key to listen for.
794     if (node.localName != "key") {
795       possibleAttributes.unshift("key");
796     }
798     for (let idAttribute of possibleAttributes) {
799       if (node.hasAttribute(idAttribute)) {
800         return node.getAttribute(idAttribute);
801       }
802     }
804     return this._getWidgetID(node.parentElement);
805   },
807   _getBrowserWidgetContainer(node) {
808     // Find the container holding this element.
809     for (let containerId of Object.keys(BROWSER_UI_CONTAINER_IDS)) {
810       let container = node.ownerDocument.getElementById(containerId);
811       if (container && container.contains(node)) {
812         return BROWSER_UI_CONTAINER_IDS[containerId];
813       }
814     }
815     // Treat toolbar context menu items that relate to tabs as the tab menu:
816     if (
817       node.closest("#toolbar-context-menu") &&
818       node.getAttribute("contexttype") == "tabbar"
819     ) {
820       return BROWSER_UI_CONTAINER_IDS.tabContextMenu;
821     }
822     return null;
823   },
825   _getWidgetContainer(node) {
826     if (node.localName == "key") {
827       return "keyboard";
828     }
830     const { URL: url } = node.ownerDocument;
831     if (url == AppConstants.BROWSER_CHROME_URL) {
832       return this._getBrowserWidgetContainer(node);
833     }
834     if (
835       url.startsWith("about:preferences") ||
836       url.startsWith("about:settings")
837     ) {
838       // Find the element's category.
839       let container = node.closest("[data-category]");
840       if (!container) {
841         return null;
842       }
844       let pane = container.getAttribute("data-category");
846       if (!PREFERENCES_PANES.includes(pane)) {
847         pane = "paneUnknown";
848       }
850       return `preferences_${pane}`;
851     }
853     return null;
854   },
856   lastClickTarget: null,
858   ignoreEvent(event) {
859     IGNORABLE_EVENTS.set(event, true);
860   },
862   _recordCommand(event) {
863     if (IGNORABLE_EVENTS.get(event)) {
864       return;
865     }
867     let sourceEvent = event;
868     while (sourceEvent.sourceEvent) {
869       sourceEvent = sourceEvent.sourceEvent;
870     }
872     let lastTarget = this.lastClickTarget?.get();
873     if (
874       lastTarget &&
875       sourceEvent.type == "command" &&
876       sourceEvent.target.contains(lastTarget)
877     ) {
878       // Ignore a command event triggered by a click.
879       this.lastClickTarget = null;
880       return;
881     }
883     this.lastClickTarget = null;
885     if (sourceEvent.type == "click") {
886       // Only care about main button clicks.
887       if (sourceEvent.button != 0) {
888         return;
889       }
891       // This click may trigger a command event so retain the target to be able
892       // to dedupe that event.
893       this.lastClickTarget = Cu.getWeakReference(sourceEvent.target);
894     }
896     // We should never see events from web content as they are fired in a
897     // content process, but let's be safe.
898     let url = sourceEvent.target.ownerDocument.documentURIObject;
899     if (!url.schemeIs("chrome") && !url.schemeIs("about")) {
900       return;
901     }
903     // This is what events targetted  at content will actually look like.
904     if (sourceEvent.target.localName == "browser") {
905       return;
906     }
908     // Find the actual element we're interested in.
909     let node = sourceEvent.target;
910     const isAboutPreferences =
911       node.ownerDocument.URL.startsWith("about:preferences") ||
912       node.ownerDocument.URL.startsWith("about:settings");
913     while (
914       !UI_TARGET_ELEMENTS.includes(node.localName) &&
915       !node.classList?.contains("wants-telemetry") &&
916       // We are interested in links on about:preferences as well.
917       !(
918         isAboutPreferences &&
919         (node.getAttribute("is") === "text-link" || node.localName === "a")
920       )
921     ) {
922       node = node.parentNode;
923       if (!node?.parentNode) {
924         // A click on a space or label or top-level document or something we're
925         // not interested in.
926         return;
927       }
928     }
930     // When the expected target is a Custom Element with a Shadow Root, there
931     // may be a specific part of the component that click events correspond to
932     // changes. Ignore any other events if requested.
933     let expectedEventTarget = UI_TARGET_COMPOSED_ELEMENTS_MAP.get(
934       node.localName
935     );
936     if (
937       event.type == "click" &&
938       expectedEventTarget &&
939       expectedEventTarget != event.composedTarget?.localName
940     ) {
941       return;
942     }
944     if (sourceEvent.type === "command") {
945       const { command, ownerDocument, parentNode } = node;
946       // Check if this command is for a history or bookmark link being opened
947       // from the context menu. In this case, we are interested in the DOM node
948       // for the link, not the menu item itself.
949       if (
950         PLACES_OPEN_COMMANDS.includes(command) ||
951         parentNode?.parentNode?.id === PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID
952       ) {
953         node = ownerDocument.getElementById(PLACES_CONTEXT_MENU_ID).triggerNode;
954       }
955     }
957     let item = this._getWidgetID(node);
958     let source = this._getWidgetContainer(node);
960     if (item && source) {
961       this.recordInteractionEvent(item, source);
962       let scalar = `browser.ui.interaction.${source.replace(/-/g, "_")}`;
963       Services.telemetry.keyedScalarAdd(scalar, telemetryId(item), 1);
964       if (SET_USAGECOUNT_PREF_BUTTONS.includes(item)) {
965         let pref = `browser.engagement.${item}.used-count`;
966         Services.prefs.setIntPref(pref, Services.prefs.getIntPref(pref, 0) + 1);
967       }
968       if (SET_USAGE_PREF_BUTTONS.includes(item)) {
969         Services.prefs.setBoolPref(`browser.engagement.${item}.has-used`, true);
970       }
971     }
973     if (ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source]) {
974       let contextMenu = ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source];
975       let triggerContainer = this._getWidgetContainer(
976         node.closest("menupopup")?.triggerNode
977       );
978       if (triggerContainer) {
979         this.recordInteractionEvent(item, contextMenu);
980         let scalar = `browser.ui.interaction.${contextMenu.replace(/-/g, "_")}`;
981         Services.telemetry.keyedScalarAdd(
982           scalar,
983           telemetryId(triggerContainer),
984           1
985         );
986       }
987     }
988   },
990   _flowId: null,
991   _flowIdTS: 0,
993   recordInteractionEvent(widgetId, source) {
994     // A note on clocks. Cu.now() is monotonic, but its behaviour across
995     // computer sleeps is different per platform.
996     // We're okay with this for flows because we're looking at idle times
997     // on the order of minutes and within the same machine, so the weirdest
998     // thing we may expect is a flow that accidentally continues across a
999     // sleep. Until we have evidence that this is common, we're in the clear.
1000     if (!this._flowId || this._flowIdTS + FLOW_IDLE_TIME < Cu.now()) {
1001       // We submit the ping full o' events on every new flow,
1002       // including at startup.
1003       GleanPings.prototypeNoCodeEvents.submit();
1004       // We use a GUID here because we need to identify events in a flow
1005       // out of all events from all flows across all clients.
1006       this._flowId = Services.uuid.generateUUID();
1007     }
1008     this._flowIdTS = Cu.now();
1010     const extra = {
1011       source,
1012       widget_id: telemetryId(widgetId),
1013       flow_id: this._flowId,
1014     };
1015     Glean.browserUsage.interaction.record(extra);
1016   },
1018   /**
1019    * Listens for UI interactions in the window.
1020    */
1021   _addUsageListeners(win) {
1022     // Listen for command events from the UI.
1023     win.addEventListener("command", event => this._recordCommand(event), true);
1024     win.addEventListener("click", event => this._recordCommand(event), true);
1025   },
1027   /**
1028    * A public version of the private method to take care of the `nav-bar-start`,
1029    * `nav-bar-end` thing that callers shouldn't have to care about. It also
1030    * accepts the DOM ids for the areas rather than the cleaner ones we report
1031    * to telemetry.
1032    */
1033   recordWidgetChange(widgetId, newPos, reason) {
1034     try {
1035       if (newPos) {
1036         newPos = BROWSER_UI_CONTAINER_IDS[newPos];
1037       }
1039       if (newPos == "nav-bar") {
1040         let { position } = lazy.CustomizableUI.getPlacementOfWidget(widgetId);
1041         let { position: urlPosition } =
1042           lazy.CustomizableUI.getPlacementOfWidget("urlbar-container");
1043         newPos = newPos + (urlPosition > position ? "-start" : "-end");
1044       }
1046       this._recordWidgetChange(widgetId, newPos, reason);
1047     } catch (e) {
1048       console.error(e);
1049     }
1050   },
1052   recordToolbarVisibility(toolbarId, newState, reason) {
1053     if (typeof newState != "string") {
1054       newState = newState ? "on" : "off";
1055     }
1056     this._recordWidgetChange(
1057       BROWSER_UI_CONTAINER_IDS[toolbarId],
1058       newState,
1059       reason
1060     );
1061   },
1063   _recordWidgetChange(widgetId, newPos, reason) {
1064     // In some cases (like when add-ons are detected during startup) this gets
1065     // called before we've reported the initial positions. Ignore such cases.
1066     if (!this.widgetMap) {
1067       return;
1068     }
1070     if (widgetId == "urlbar-container") {
1071       // We don't report the position of the url bar, it is after nav-bar-start
1072       // and before nav-bar-end. But moving it means the widgets around it have
1073       // effectively moved so update those.
1074       let position = "nav-bar-start";
1075       let widgets = lazy.CustomizableUI.getWidgetsInArea("nav-bar");
1077       for (let widget of widgets) {
1078         if (!widget) {
1079           continue;
1080         }
1082         if (widget.id.startsWith("customizableui-special-")) {
1083           continue;
1084         }
1086         if (widget.id == "urlbar-container") {
1087           position = "nav-bar-end";
1088           continue;
1089         }
1091         // This will do nothing if the position hasn't changed.
1092         this._recordWidgetChange(widget.id, position, reason);
1093       }
1095       return;
1096     }
1098     let oldPos = this.widgetMap.get(widgetId);
1099     if (oldPos == newPos) {
1100       return;
1101     }
1103     let action = "move";
1105     if (!oldPos) {
1106       action = "add";
1107     } else if (!newPos) {
1108       action = "remove";
1109     }
1111     let key = `${telemetryId(widgetId, false)}_${action}_${oldPos ?? "na"}_${
1112       newPos ?? "na"
1113     }_${reason}`;
1114     Services.telemetry.keyedScalarAdd("browser.ui.customized_widgets", key, 1);
1116     if (newPos) {
1117       this.widgetMap.set(widgetId, newPos);
1118     } else {
1119       this.widgetMap.delete(widgetId);
1120     }
1121   },
1123   _recordUITelemetry() {
1124     this.widgetMap = this._buildWidgetPositions();
1126     // FIXME(bug 1883857): object metric type not available in artefact builds.
1127     if ("toolbarWidgets" in Glean.browserUi) {
1128       Glean.browserUi.toolbarWidgets.set(
1129         this.widgetMap
1130           .entries()
1131           .map(([widgetId, position]) => {
1132             return { widgetId: telemetryId(widgetId, false), position };
1133           })
1134           .toArray()
1135       );
1136     }
1138     for (let [widgetId, position] of this.widgetMap.entries()) {
1139       let key = `${telemetryId(widgetId, false)}_pinned_${position}`;
1140       Services.telemetry.keyedScalarSet(
1141         "browser.ui.toolbar_widgets",
1142         key,
1143         true
1144       );
1145     }
1146   },
1148   /**
1149    * Adds listeners to a single chrome window.
1150    */
1151   _registerWindow(win) {
1152     this._addUsageListeners(win);
1154     win.addEventListener("unload", this);
1155     win.addEventListener("TabOpen", this, true);
1156     win.addEventListener("TabPinned", this, true);
1158     win.gBrowser.tabContainer.addEventListener(TAB_RESTORING_TOPIC, this);
1159     win.gBrowser.addTabsProgressListener(URICountListener);
1160   },
1162   /**
1163    * Removes listeners from a single chrome window.
1164    */
1165   _unregisterWindow(win) {
1166     win.removeEventListener("unload", this);
1167     win.removeEventListener("TabOpen", this, true);
1168     win.removeEventListener("TabPinned", this, true);
1170     win.defaultView.gBrowser.tabContainer.removeEventListener(
1171       TAB_RESTORING_TOPIC,
1172       this
1173     );
1174     win.defaultView.gBrowser.removeTabsProgressListener(URICountListener);
1175   },
1177   /**
1178    * Updates the tab counts.
1179    */
1180   _onTabOpen() {
1181     // Update the "tab opened" count and its maximum.
1182     Services.telemetry.scalarAdd(this.tabOpenEventCountScalarName, 1);
1184     // In the case of opening multiple tabs at once, avoid enumerating all open
1185     // tabs and windows each time a tab opens.
1186     this._onTabsOpenedTask.disarm();
1187     this._onTabsOpenedTask.arm();
1188   },
1190   /**
1191    * Update tab counts after opening multiple tabs.
1192    */
1193   _onTabsOpened() {
1194     const { tabCount, loadedTabCount } = getOpenTabsAndWinsCounts();
1195     Services.telemetry.scalarSetMaximum(this.maxTabCountScalarName, tabCount);
1197     this._recordTabCounts({ tabCount, loadedTabCount });
1198   },
1200   _onTabPinned() {
1201     const pinnedTabs = getPinnedTabsCount();
1203     // Update the "tab pinned" count and its maximum.
1204     Services.telemetry.scalarAdd(this.tabPinnedEventCountScalarName, 1);
1205     Services.telemetry.scalarSetMaximum(
1206       this.maxTabPinnedCountScalarName,
1207       pinnedTabs
1208     );
1209   },
1211   /**
1212    * Tracks the window count and registers the listeners for the tab count.
1213    * @param{Object} win The window object.
1214    */
1215   _onWindowOpen(win) {
1216     // Make sure to have a |nsIDOMWindow|.
1217     if (!(win instanceof Ci.nsIDOMWindow)) {
1218       return;
1219     }
1221     let onLoad = () => {
1222       win.removeEventListener("load", onLoad);
1224       // Ignore non browser windows.
1225       if (
1226         win.document.documentElement.getAttribute("windowtype") !=
1227         "navigator:browser"
1228       ) {
1229         return;
1230       }
1232       this._registerWindow(win);
1233       // Track the window open event and check the maximum.
1234       const counts = getOpenTabsAndWinsCounts();
1235       Services.telemetry.scalarAdd(WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
1236       Services.telemetry.scalarSetMaximum(
1237         MAX_WINDOW_COUNT_SCALAR_NAME,
1238         counts.winCount
1239       );
1241       // We won't receive the "TabOpen" event for the first tab within a new window.
1242       // Account for that.
1243       this._onTabOpen(counts);
1244     };
1245     win.addEventListener("load", onLoad);
1246   },
1248   /**
1249    * Record telemetry about the given tab counts.
1250    *
1251    * Telemetry for each count will only be recorded if the value isn't
1252    * `undefined`.
1253    *
1254    * @param {object} [counts] The tab counts to register with telemetry.
1255    * @param {number} [counts.tabCount] The number of tabs in all browsers.
1256    * @param {number} [counts.loadedTabCount] The number of loaded (i.e., not
1257    *                                         pending) tabs in all browsers.
1258    */
1259   _recordTabCounts({ tabCount, loadedTabCount }) {
1260     let currentTime = Date.now();
1261     if (
1262       tabCount !== undefined &&
1263       currentTime > this._lastRecordTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1264     ) {
1265       Services.telemetry.getHistogramById("TAB_COUNT").add(tabCount);
1266       this._lastRecordTabCount = currentTime;
1267     }
1269     if (
1270       loadedTabCount !== undefined &&
1271       currentTime >
1272         this._lastRecordLoadedTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1273     ) {
1274       Services.telemetry
1275         .getHistogramById("LOADED_TAB_COUNT")
1276         .add(loadedTabCount);
1277       this._lastRecordLoadedTabCount = currentTime;
1278     }
1279   },
1281   _checkProfileCountFileSchema(fileData) {
1282     // Verifies that the schema of the file is the expected schema
1283     if (typeof fileData.version != "string") {
1284       throw new Error("Schema Mismatch Error: Bad type for 'version' field");
1285     }
1286     if (!Array.isArray(fileData.profileTelemetryIds)) {
1287       throw new Error(
1288         "Schema Mismatch Error: Bad type for 'profileTelemetryIds' field"
1289       );
1290     }
1291     for (let profileTelemetryId of fileData.profileTelemetryIds) {
1292       if (typeof profileTelemetryId != "string") {
1293         throw new Error(
1294           "Schema Mismatch Error: Bad type for an element of 'profileTelemetryIds'"
1295         );
1296       }
1297     }
1298   },
1300   // Reports the number of Firefox profiles on this machine to telemetry.
1301   async reportProfileCount() {
1302     if (
1303       AppConstants.platform != "win" ||
1304       !AppConstants.MOZ_TELEMETRY_REPORTING
1305     ) {
1306       // This is currently a windows-only feature.
1307       // Also, this function writes directly to disk, without using the usual
1308       // telemetry recording functions. So we excplicitly check if telemetry
1309       // reporting was disabled at compile time, and we do not do anything in
1310       // case.
1311       return;
1312     }
1314     // To report only as much data as we need, we will bucket our values.
1315     // Rather than the raw value, we will report the greatest value in the list
1316     // below that is no larger than the raw value.
1317     const buckets = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000, 10000];
1319     // We need both the C:\ProgramData\Mozilla directory and the install
1320     // directory hash to create the profile count file path. We can easily
1321     // reassemble this from the update directory, which looks like:
1322     // C:\ProgramData\Mozilla\updates\hash
1323     // Retrieving the directory this way also ensures that the "Mozilla"
1324     // directory is created with the correct permissions.
1325     // The ProgramData directory, by default, grants write permissions only to
1326     // file creators. The directory service calls GetCommonUpdateDirectory,
1327     // which makes sure the the directory is created with user-writable
1328     // permissions.
1329     const updateDirectory = BrowserUsageTelemetry.Policy.getUpdateDirectory();
1330     const hash = updateDirectory.leafName;
1331     const profileCountFilename = "profile_count_" + hash + ".json";
1332     let profileCountFile = updateDirectory.parent.parent;
1333     profileCountFile.append(profileCountFilename);
1335     let readError = false;
1336     let fileData;
1337     try {
1338       let json = await BrowserUsageTelemetry.Policy.readProfileCountFile(
1339         profileCountFile.path
1340       );
1341       fileData = JSON.parse(json);
1342       BrowserUsageTelemetry._checkProfileCountFileSchema(fileData);
1343     } catch (ex) {
1344       // Note that since this also catches the "no such file" error, this is
1345       // always the template that we use when writing to the file for the first
1346       // time.
1347       fileData = { version: "1", profileTelemetryIds: [] };
1348       if (!(ex.name == "NotFoundError")) {
1349         console.error(ex);
1350         // Don't just return here on a read error. We need to send the error
1351         // value to telemetry and we want to attempt to fix the file.
1352         // However, we will still report an error for this ping, even if we
1353         // fix the file. This is to prevent always sending a profile count of 1
1354         // if, for some reason, we always get a read error but never a write
1355         // error.
1356         readError = true;
1357       }
1358     }
1360     let writeError = false;
1361     let currentTelemetryId =
1362       await BrowserUsageTelemetry.Policy.getTelemetryClientId();
1363     // Don't add our telemetry ID to the file if we've already reached the
1364     // largest bucket. This prevents the file size from growing forever.
1365     if (
1366       !fileData.profileTelemetryIds.includes(currentTelemetryId) &&
1367       fileData.profileTelemetryIds.length < Math.max(...buckets)
1368     ) {
1369       fileData.profileTelemetryIds.push(currentTelemetryId);
1370       try {
1371         await BrowserUsageTelemetry.Policy.writeProfileCountFile(
1372           profileCountFile.path,
1373           JSON.stringify(fileData)
1374         );
1375       } catch (ex) {
1376         console.error(ex);
1377         writeError = true;
1378       }
1379     }
1381     // Determine the bucketed value to report
1382     let rawProfileCount = fileData.profileTelemetryIds.length;
1383     let valueToReport = 0;
1384     for (let bucket of buckets) {
1385       if (bucket <= rawProfileCount && bucket > valueToReport) {
1386         valueToReport = bucket;
1387       }
1388     }
1390     if (readError || writeError) {
1391       // We convey errors via a profile count of 0.
1392       valueToReport = 0;
1393     }
1395     Services.telemetry.scalarSet(
1396       "browser.engagement.profile_count",
1397       valueToReport
1398     );
1399     // Manually mirror to Glean
1400     Glean.browserEngagement.profileCount.set(valueToReport);
1401   },
1403   /**
1404    * Check if this is the first run of this profile since installation,
1405    * if so then collect installation telemetry.
1406    *
1407    * @param {nsIFile} [dataPathOverride] Optional, full data file path, for tests.
1408    * @param {Array<string>} [msixPackagePrefixes] Optional, list of prefixes to
1409             consider "existing" installs when looking at installed MSIX packages.
1410             Defaults to prefixes for builds produced in Firefox automation.
1411    * @return {Promise<Object>} A JSON object containing install telemetry.
1412    * @resolves When the event has been recorded, or if the data file was not found.
1413    * @rejects JavaScript exception on any failure.
1414    */
1415   async collectInstallationTelemetry(
1416     dataPathOverride,
1417     msixPackagePrefixes = ["Mozilla.Firefox", "Mozilla.MozillaFirefox"]
1418   ) {
1419     if (AppConstants.platform != "win") {
1420       // This is a windows-only feature.
1421       return {};
1422     }
1424     const TIMESTAMP_PREF = "app.installation.timestamp";
1425     const lastInstallTime = Services.prefs.getStringPref(TIMESTAMP_PREF, null);
1426     const wpm = Cc["@mozilla.org/windows-package-manager;1"].createInstance(
1427       Ci.nsIWindowsPackageManager
1428     );
1429     let installer_type = "";
1430     let pfn;
1431     try {
1432       pfn = Services.sysinfo.getProperty("winPackageFamilyName");
1433     } catch (e) {}
1435     function getInstallData() {
1436       // We only care about where _any_ other install existed - no
1437       // need to count more than 1.
1438       const installPaths = lazy.WindowsInstallsInfo.getInstallPaths(
1439         1,
1440         new Set([Services.dirsvc.get("GreBinD", Ci.nsIFile).path])
1441       );
1442       const msixInstalls = new Set();
1443       // We're just going to eat all errors here -- we don't want the event
1444       // to go unsent if we were unable to look for MSIX installs.
1445       try {
1446         wpm
1447           .findUserInstalledPackages(msixPackagePrefixes)
1448           .forEach(i => msixInstalls.add(i));
1449         if (pfn) {
1450           msixInstalls.delete(pfn);
1451         }
1452       } catch (ex) {}
1453       return {
1454         installPaths,
1455         msixInstalls,
1456       };
1457     }
1459     let extra = {};
1461     if (pfn) {
1462       if (lastInstallTime != null) {
1463         // We've already seen this install
1464         return {};
1465       }
1467       // First time seeing this install, record the timestamp.
1468       Services.prefs.setStringPref(TIMESTAMP_PREF, wpm.getInstalledDate());
1469       let install_data = getInstallData();
1471       installer_type = "msix";
1473       // Build the extra event data
1474       extra.version = AppConstants.MOZ_APP_VERSION;
1475       extra.build_id = AppConstants.MOZ_BUILDID;
1476       // The next few keys are static for the reasons described
1477       // No way to detect whether or not we were installed by an admin
1478       extra.admin_user = "false";
1479       // Always false at the moment, because we create a new profile
1480       // on first launch
1481       extra.profdir_existed = "false";
1482       // Obviously false for MSIX installs
1483       extra.from_msi = "false";
1484       // We have no way of knowing whether we were installed via the GUI,
1485       // through the command line, or some Enterprise management tool.
1486       extra.silent = "false";
1487       // There's no way to change the install path for an MSIX package
1488       extra.default_path = "true";
1489       extra.install_existed = install_data.msixInstalls.has(pfn).toString();
1490       install_data.msixInstalls.delete(pfn);
1491       extra.other_inst = (!!install_data.installPaths.size).toString();
1492       extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
1493     } else {
1494       let dataPath = dataPathOverride;
1495       if (!dataPath) {
1496         dataPath = Services.dirsvc.get("GreD", Ci.nsIFile);
1497         dataPath.append("installation_telemetry.json");
1498       }
1500       let dataBytes;
1501       try {
1502         dataBytes = await IOUtils.read(dataPath.path);
1503       } catch (ex) {
1504         if (ex.name == "NotFoundError") {
1505           // Many systems will not have the data file, return silently if not found as
1506           // there is nothing to record.
1507           return {};
1508         }
1509         throw ex;
1510       }
1511       const dataString = new TextDecoder("utf-16").decode(dataBytes);
1512       const data = JSON.parse(dataString);
1514       if (lastInstallTime && data.install_timestamp == lastInstallTime) {
1515         // We've already seen this install
1516         return {};
1517       }
1519       // First time seeing this install, record the timestamp.
1520       Services.prefs.setStringPref(TIMESTAMP_PREF, data.install_timestamp);
1521       let install_data = getInstallData();
1523       installer_type = data.installer_type;
1525       // Installation timestamp is not intended to be sent with telemetry,
1526       // remove it to emphasize this point.
1527       delete data.install_timestamp;
1529       // Build the extra event data
1530       extra.version = data.version;
1531       extra.build_id = data.build_id;
1532       extra.admin_user = data.admin_user.toString();
1533       extra.install_existed = data.install_existed.toString();
1534       extra.profdir_existed = data.profdir_existed.toString();
1535       extra.other_inst = (!!install_data.installPaths.size).toString();
1536       extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
1538       if (data.installer_type == "full") {
1539         extra.silent = data.silent.toString();
1540         extra.from_msi = data.from_msi.toString();
1541         extra.default_path = data.default_path.toString();
1542       }
1543     }
1544     return { installer_type, extra };
1545   },
1547   async reportInstallationTelemetry(
1548     dataPathOverride,
1549     msixPackagePrefixes = ["Mozilla.Firefox", "Mozilla.MozillaFirefox"]
1550   ) {
1551     // The optional dataPathOverride is only used for testing purposes.
1552     // Use this as a proxy for whether we're in a testing environment.
1553     // If we're in a testing environment we don't want to return the
1554     // same data even if we call this function multiple times in the
1555     // same instance.
1556     if (gInstallationTelemetryPromise && !dataPathOverride) {
1557       return gInstallationTelemetryPromise;
1558     }
1560     gInstallationTelemetryPromise = (async () => {
1561       let data = await BrowserUsageTelemetry.collectInstallationTelemetry(
1562         dataPathOverride,
1563         msixPackagePrefixes
1564       );
1566       if (data?.installer_type) {
1567         let { installer_type, extra } = data;
1569         // Record the event (mirrored to legacy telemetry using GIFFT)
1570         if (installer_type == "full") {
1571           Glean.installation.firstSeenFull.record(extra);
1572         } else if (installer_type == "stub") {
1573           Glean.installation.firstSeenStub.record(extra);
1574         } else if (installer_type == "msix") {
1575           Glean.installation.firstSeenMsix.record(extra);
1576         }
1578         // Scalars for the new-profile ping. We don't need to collect the build version
1579         // These are mirrored to legacy telemetry using GIFFT
1580         Glean.installationFirstSeen.installerType.set(installer_type);
1581         Glean.installationFirstSeen.version.set(extra.version);
1582         // Convert "true" or "false" strings back into booleans
1583         Glean.installationFirstSeen.adminUser.set(extra.admin_user === "true");
1584         Glean.installationFirstSeen.installExisted.set(
1585           extra.install_existed === "true"
1586         );
1587         Glean.installationFirstSeen.profdirExisted.set(
1588           extra.profdir_existed === "true"
1589         );
1590         Glean.installationFirstSeen.otherInst.set(extra.other_inst === "true");
1591         Glean.installationFirstSeen.otherMsixInst.set(
1592           extra.other_msix_inst === "true"
1593         );
1594         if (installer_type == "full") {
1595           Glean.installationFirstSeen.silent.set(extra.silent === "true");
1596           Glean.installationFirstSeen.fromMsi.set(extra.from_msi === "true");
1597           Glean.installationFirstSeen.defaultPath.set(
1598             extra.default_path === "true"
1599           );
1600         }
1601       }
1602       return data;
1603     })();
1605     return gInstallationTelemetryPromise;
1606   },
1609 // Used by nsIBrowserUsage
1610 export function getUniqueDomainsVisitedInPast24Hours() {
1611   return URICountListener.uniqueDomainsVisitedInPast24Hours;