Backed out changeset 496886cb30a5 (bug 1867152) for bc failures on browser_user_input...
[gecko.git] / browser / modules / BrowserUsageTelemetry.sys.mjs
blob1c508945d6919d31c03f6cf0cc843eeb649151a2
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"
34 // The upper bound for the count of the visited unique domain names.
35 const MAX_UNIQUE_VISITED_DOMAINS = 100;
37 // Observed topic names.
38 const TAB_RESTORING_TOPIC = "SSTabRestoring";
39 const TELEMETRY_SUBSESSIONSPLIT_TOPIC =
40   "internal-telemetry-after-subsession-split";
41 const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
43 // Probe names.
44 const MAX_TAB_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_tab_count";
45 const MAX_WINDOW_COUNT_SCALAR_NAME =
46   "browser.engagement.max_concurrent_window_count";
47 const TAB_OPEN_EVENT_COUNT_SCALAR_NAME =
48   "browser.engagement.tab_open_event_count";
49 const MAX_TAB_PINNED_COUNT_SCALAR_NAME =
50   "browser.engagement.max_concurrent_tab_pinned_count";
51 const TAB_PINNED_EVENT_COUNT_SCALAR_NAME =
52   "browser.engagement.tab_pinned_event_count";
53 const WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME =
54   "browser.engagement.window_open_event_count";
55 const UNIQUE_DOMAINS_COUNT_SCALAR_NAME =
56   "browser.engagement.unique_domains_count";
57 const TOTAL_URI_COUNT_SCALAR_NAME = "browser.engagement.total_uri_count";
58 const UNFILTERED_URI_COUNT_SCALAR_NAME =
59   "browser.engagement.unfiltered_uri_count";
60 const TOTAL_URI_COUNT_NORMAL_AND_PRIVATE_MODE_SCALAR_NAME =
61   "browser.engagement.total_uri_count_normal_and_private_mode";
63 export const MINIMUM_TAB_COUNT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes, in ms
65 // The elements we consider to be interactive.
66 const UI_TARGET_ELEMENTS = [
67   "menuitem",
68   "toolbarbutton",
69   "key",
70   "command",
71   "checkbox",
72   "input",
73   "button",
74   "image",
75   "radio",
76   "richlistitem",
79 // The containers of interactive elements that we care about and their pretty
80 // names. These should be listed in order of most-specific to least-specific,
81 // when iterating JavaScript will guarantee that ordering and so we will find
82 // the most specific area first.
83 const BROWSER_UI_CONTAINER_IDS = {
84   "toolbar-menubar": "menu-bar",
85   TabsToolbar: "tabs-bar",
86   PersonalToolbar: "bookmarks-bar",
87   "appMenu-popup": "app-menu",
88   tabContextMenu: "tabs-context",
89   contentAreaContextMenu: "content-context",
90   "widget-overflow-list": "overflow-menu",
91   "widget-overflow-fixed-list": "pinned-overflow-menu",
92   "page-action-buttons": "pageaction-urlbar",
93   pageActionPanel: "pageaction-panel",
94   "unified-extensions-area": "unified-extensions-area",
95   "allTabsMenu-allTabsView": "alltabs-menu",
97   // This should appear last as some of the above are inside the nav bar.
98   "nav-bar": "nav-bar",
101 const ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS = {
102   [BROWSER_UI_CONTAINER_IDS.tabContextMenu]: "tabs-context-entrypoint",
105 // A list of the expected panes in about:preferences
106 const PREFERENCES_PANES = [
107   "paneHome",
108   "paneGeneral",
109   "panePrivacy",
110   "paneSearch",
111   "paneSearchResults",
112   "paneSync",
113   "paneContainers",
114   "paneExperimental",
115   "paneMoreFromMozilla",
118 const IGNORABLE_EVENTS = new WeakMap();
120 const KNOWN_ADDONS = [];
122 // Buttons that, when clicked, set a preference to true. The convention
123 // is that the preference is named:
125 // browser.engagement.<button id>.has-used
127 // and is defaulted to false.
128 const SET_USAGE_PREF_BUTTONS = [
129   "downloads-button",
130   "fxa-toolbar-menu-button",
131   "home-button",
132   "sidebar-button",
133   "library-button",
136 // Buttons that, when clicked, increase a counter. The convention
137 // is that the preference is named:
139 // browser.engagement.<button id>.used-count
141 // and doesn't have a default value.
142 const SET_USAGECOUNT_PREF_BUTTONS = [
143   "pageAction-panel-copyURL",
144   "pageAction-panel-emailLink",
145   "pageAction-panel-pinTab",
146   "pageAction-panel-screenshots_mozilla_org",
147   "pageAction-panel-shareURL",
150 // Places context menu IDs.
151 const PLACES_CONTEXT_MENU_ID = "placesContext";
152 const PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID =
153   "placesContext_open:newcontainertab";
155 // Commands used to open history or bookmark links from places context menu.
156 const PLACES_OPEN_COMMANDS = [
157   "placesCmd_open",
158   "placesCmd_open:window",
159   "placesCmd_open:privatewindow",
160   "placesCmd_open:tab",
163 function telemetryId(widgetId, obscureAddons = true) {
164   // Add-on IDs need to be obscured.
165   function addonId(id) {
166     if (!obscureAddons) {
167       return id;
168     }
170     let pos = KNOWN_ADDONS.indexOf(id);
171     if (pos < 0) {
172       pos = KNOWN_ADDONS.length;
173       KNOWN_ADDONS.push(id);
174     }
175     return `addon${pos}`;
176   }
178   if (widgetId.endsWith("-browser-action")) {
179     widgetId = addonId(
180       widgetId.substring(0, widgetId.length - "-browser-action".length)
181     );
182   } else if (widgetId.startsWith("pageAction-")) {
183     let actionId;
184     if (widgetId.startsWith("pageAction-urlbar-")) {
185       actionId = widgetId.substring("pageAction-urlbar-".length);
186     } else if (widgetId.startsWith("pageAction-panel-")) {
187       actionId = widgetId.substring("pageAction-panel-".length);
188     }
190     if (actionId) {
191       let action = lazy.PageActions.actionForID(actionId);
192       widgetId = action?._isMozillaAction ? actionId : addonId(actionId);
193     }
194   } else if (widgetId.startsWith("ext-keyset-id-")) {
195     // Webextension command shortcuts don't have an id on their key element so
196     // we see the id from the keyset that contains them.
197     widgetId = addonId(widgetId.substring("ext-keyset-id-".length));
198   } else if (widgetId.startsWith("ext-key-id-")) {
199     // The command for a webextension sidebar action is an exception to the above rule.
200     widgetId = widgetId.substring("ext-key-id-".length);
201     if (widgetId.endsWith("-sidebar-action")) {
202       widgetId = addonId(
203         widgetId.substring(0, widgetId.length - "-sidebar-action".length)
204       );
205     }
206   }
208   return widgetId.replace(/_/g, "-");
211 function getOpenTabsAndWinsCounts() {
212   let loadedTabCount = 0;
213   let tabCount = 0;
214   let winCount = 0;
216   for (let win of Services.wm.getEnumerator("navigator:browser")) {
217     winCount++;
218     tabCount += win.gBrowser.tabs.length;
219     for (const tab of win.gBrowser.tabs) {
220       if (tab.getAttribute("pending") !== "true") {
221         loadedTabCount += 1;
222       }
223     }
224   }
226   return { loadedTabCount, tabCount, winCount };
229 function getPinnedTabsCount() {
230   let pinnedTabs = 0;
232   for (let win of Services.wm.getEnumerator("navigator:browser")) {
233     pinnedTabs += [...win.ownerGlobal.gBrowser.tabs].filter(
234       t => t.pinned
235     ).length;
236   }
238   return pinnedTabs;
241 export let URICountListener = {
242   // A set containing the visited domains, see bug 1271310.
243   _domainSet: new Set(),
244   // A set containing the visited origins during the last 24 hours (similar to domains, but not quite the same)
245   _domain24hrSet: new Set(),
246   // A map to keep track of the URIs loaded from the restored tabs.
247   _restoredURIsMap: new WeakMap(),
248   // Ongoing expiration timeouts.
249   _timeouts: new Set(),
251   isHttpURI(uri) {
252     // Only consider http(s) schemas.
253     return uri.schemeIs("http") || uri.schemeIs("https");
254   },
256   addRestoredURI(browser, uri) {
257     if (!this.isHttpURI(uri)) {
258       return;
259     }
261     this._restoredURIsMap.set(browser, uri.spec);
262   },
264   onLocationChange(browser, webProgress, request, uri, flags) {
265     if (
266       !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) &&
267       webProgress.isTopLevel
268     ) {
269       // By default, assume we no longer need to track this tab.
270       lazy.SearchSERPTelemetry.stopTrackingBrowser(
271         browser,
272         lazy.SearchSERPTelemetryUtils.ABANDONMENTS.NAVIGATION
273       );
274     }
276     // Don't count this URI if it's an error page.
277     if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
278       return;
279     }
281     // We only care about top level loads.
282     if (!webProgress.isTopLevel) {
283       return;
284     }
286     // The SessionStore sets the URI of a tab first, firing onLocationChange the
287     // first time, then manages content loading using its scheduler. Once content
288     // loads, we will hit onLocationChange again.
289     // We can catch the first case by checking for null requests: be advised that
290     // this can also happen when navigating page fragments, so account for it.
291     if (
292       !request &&
293       !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
294     ) {
295       return;
296     }
298     // Don't include URI and domain counts when in private mode.
299     let shouldCountURI =
300       !lazy.PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal) ||
301       Services.prefs.getBoolPref(
302         "browser.engagement.total_uri_count.pbm",
303         false
304       );
306     // Track URI loads, even if they're not http(s).
307     let uriSpec = null;
308     try {
309       uriSpec = uri.spec;
310     } catch (e) {
311       // If we have troubles parsing the spec, still count this as
312       // an unfiltered URI.
313       if (shouldCountURI) {
314         Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
315       }
316       return;
317     }
319     // Don't count about:blank and similar pages, as they would artificially
320     // inflate the counts.
321     if (browser.ownerGlobal.gInitialPages.includes(uriSpec)) {
322       return;
323     }
325     // If the URI we're loading is in the _restoredURIsMap, then it comes from a
326     // restored tab. If so, let's skip it and remove it from the map as we want to
327     // count page refreshes.
328     if (this._restoredURIsMap.get(browser) === uriSpec) {
329       this._restoredURIsMap.delete(browser);
330       return;
331     }
333     // The URI wasn't from a restored tab. Count it among the unfiltered URIs.
334     // If this is an http(s) URI, this also gets counted by the "total_uri_count"
335     // probe.
336     if (shouldCountURI) {
337       Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
338     }
340     if (!this.isHttpURI(uri)) {
341       return;
342     }
344     if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
345       lazy.SearchSERPTelemetry.updateTrackingStatus(
346         browser,
347         uriSpec,
348         webProgress.loadType
349       );
350     }
352     // Update total URI count, including when in private mode.
353     Services.telemetry.scalarAdd(
354       TOTAL_URI_COUNT_NORMAL_AND_PRIVATE_MODE_SCALAR_NAME,
355       1
356     );
357     Glean.browserEngagement.uriCount.add(1);
359     if (!shouldCountURI) {
360       return;
361     }
363     // Update the URI counts.
364     Services.telemetry.scalarAdd(TOTAL_URI_COUNT_SCALAR_NAME, 1);
366     // Update tab count
367     BrowserUsageTelemetry._recordTabCounts(getOpenTabsAndWinsCounts());
369     // Unique domains should be aggregated by (eTLD + 1): x.test.com and y.test.com
370     // are counted once as test.com.
371     let baseDomain;
372     try {
373       // Even if only considering http(s) URIs, |getBaseDomain| could still throw
374       // due to the URI containing invalid characters or the domain actually being
375       // an ipv4 or ipv6 address.
376       baseDomain = Services.eTLD.getBaseDomain(uri);
377     } catch (e) {
378       return;
379     }
381     // We only want to count the unique domains up to MAX_UNIQUE_VISITED_DOMAINS.
382     if (this._domainSet.size < MAX_UNIQUE_VISITED_DOMAINS) {
383       this._domainSet.add(baseDomain);
384       Services.telemetry.scalarSet(
385         UNIQUE_DOMAINS_COUNT_SCALAR_NAME,
386         this._domainSet.size
387       );
388     }
390     this._domain24hrSet.add(baseDomain);
391     if (lazy.gRecentVisitedOriginsExpiry) {
392       let timeoutId = lazy.setTimeout(() => {
393         this._domain24hrSet.delete(baseDomain);
394         this._timeouts.delete(timeoutId);
395       }, lazy.gRecentVisitedOriginsExpiry * 1000);
396       this._timeouts.add(timeoutId);
397     }
398   },
400   /**
401    * Reset the counts. This should be called when breaking a session in Telemetry.
402    */
403   reset() {
404     this._domainSet.clear();
405   },
407   /**
408    * Returns the number of unique domains visited in this session during the
409    * last 24 hours.
410    */
411   get uniqueDomainsVisitedInPast24Hours() {
412     return this._domain24hrSet.size;
413   },
415   /**
416    * Resets the number of unique domains visited in this session.
417    */
418   resetUniqueDomainsVisitedInPast24Hours() {
419     this._timeouts.forEach(timeoutId => lazy.clearTimeout(timeoutId));
420     this._timeouts.clear();
421     this._domain24hrSet.clear();
422   },
424   QueryInterface: ChromeUtils.generateQI([
425     "nsIWebProgressListener",
426     "nsISupportsWeakReference",
427   ]),
430 export let BrowserUsageTelemetry = {
431   /**
432    * This is a policy object used to override behavior for testing.
433    */
434   Policy: {
435     getTelemetryClientId: async () => lazy.ClientID.getClientID(),
436     getUpdateDirectory: () => Services.dirsvc.get("UpdRootD", Ci.nsIFile),
437     readProfileCountFile: async path => IOUtils.readUTF8(path),
438     writeProfileCountFile: async (path, data) => IOUtils.writeUTF8(path, data),
439   },
441   _inited: false,
443   init() {
444     this._lastRecordTabCount = 0;
445     this._lastRecordLoadedTabCount = 0;
446     this._setupAfterRestore();
447     this._inited = true;
449     Services.prefs.addObserver("browser.tabs.inTitlebar", this);
451     this._recordUITelemetry();
453     this._onTabsOpenedTask = new lazy.DeferredTask(
454       () => this._onTabsOpened(),
455       0
456     );
457   },
459   /**
460    * Resets the masked add-on identifiers. Only for use in tests.
461    */
462   _resetAddonIds() {
463     KNOWN_ADDONS.length = 0;
464   },
466   /**
467    * Handle subsession splits in the parent process.
468    */
469   afterSubsessionSplit() {
470     // Scalars just got cleared due to a subsession split. We need to set the maximum
471     // concurrent tab and window counts so that they reflect the correct value for the
472     // new subsession.
473     const counts = getOpenTabsAndWinsCounts();
474     Services.telemetry.scalarSetMaximum(
475       MAX_TAB_COUNT_SCALAR_NAME,
476       counts.tabCount
477     );
478     Services.telemetry.scalarSetMaximum(
479       MAX_WINDOW_COUNT_SCALAR_NAME,
480       counts.winCount
481     );
483     // Reset the URI counter.
484     URICountListener.reset();
485   },
487   QueryInterface: ChromeUtils.generateQI([
488     "nsIObserver",
489     "nsISupportsWeakReference",
490   ]),
492   uninit() {
493     if (!this._inited) {
494       return;
495     }
496     Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC);
497     Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC);
498   },
500   observe(subject, topic, data) {
501     switch (topic) {
502       case DOMWINDOW_OPENED_TOPIC:
503         this._onWindowOpen(subject);
504         break;
505       case TELEMETRY_SUBSESSIONSPLIT_TOPIC:
506         this.afterSubsessionSplit();
507         break;
508       case "nsPref:changed":
509         switch (data) {
510           case "browser.tabs.inTitlebar":
511             this._recordWidgetChange(
512               "titlebar",
513               Services.appinfo.drawInTitlebar ? "off" : "on",
514               "pref"
515             );
516             break;
517         }
518         break;
519     }
520   },
522   handleEvent(event) {
523     switch (event.type) {
524       case "TabOpen":
525         this._onTabOpen();
526         break;
527       case "TabPinned":
528         this._onTabPinned();
529         break;
530       case "unload":
531         this._unregisterWindow(event.target);
532         break;
533       case TAB_RESTORING_TOPIC:
534         // We're restoring a new tab from a previous or crashed session.
535         // We don't want to track the URIs from these tabs, so let
536         // |URICountListener| know about them.
537         let browser = event.target.linkedBrowser;
538         URICountListener.addRestoredURI(browser, browser.currentURI);
540         const { loadedTabCount } = getOpenTabsAndWinsCounts();
541         this._recordTabCounts({ loadedTabCount });
542         break;
543     }
544   },
546   /**
547    * This gets called shortly after the SessionStore has finished restoring
548    * windows and tabs. It counts the open tabs and adds listeners to all the
549    * windows.
550    */
551   _setupAfterRestore() {
552     // Make sure to catch new chrome windows and subsession splits.
553     Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, true);
554     Services.obs.addObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, true);
556     // Attach the tabopen handlers to the existing Windows.
557     for (let win of Services.wm.getEnumerator("navigator:browser")) {
558       this._registerWindow(win);
559     }
561     // Get the initial tab and windows max counts.
562     const counts = getOpenTabsAndWinsCounts();
563     Services.telemetry.scalarSetMaximum(
564       MAX_TAB_COUNT_SCALAR_NAME,
565       counts.tabCount
566     );
567     Services.telemetry.scalarSetMaximum(
568       MAX_WINDOW_COUNT_SCALAR_NAME,
569       counts.winCount
570     );
571   },
573   _buildWidgetPositions() {
574     let widgetMap = new Map();
576     const toolbarState = nodeId => {
577       let value;
578       if (nodeId == "PersonalToolbar") {
579         value = Services.prefs.getCharPref(
580           "browser.toolbars.bookmarks.visibility",
581           "newtab"
582         );
583         if (value != "newtab") {
584           return value == "never" ? "off" : "on";
585         }
586         return value;
587       }
588       value = Services.xulStore.getValue(
589         AppConstants.BROWSER_CHROME_URL,
590         nodeId,
591         "collapsed"
592       );
594       if (value) {
595         return value == "true" ? "off" : "on";
596       }
597       return "off";
598     };
600     widgetMap.set(
601       BROWSER_UI_CONTAINER_IDS.PersonalToolbar,
602       toolbarState("PersonalToolbar")
603     );
605     let menuBarHidden =
606       Services.xulStore.getValue(
607         AppConstants.BROWSER_CHROME_URL,
608         "toolbar-menubar",
609         "autohide"
610       ) != "false";
612     widgetMap.set("menu-toolbar", menuBarHidden ? "off" : "on");
614     // Drawing in the titlebar means not showing the titlebar, hence the negation.
615     widgetMap.set("titlebar", Services.appinfo.drawInTitlebar ? "off" : "on");
617     for (let area of lazy.CustomizableUI.areas) {
618       if (!(area in BROWSER_UI_CONTAINER_IDS)) {
619         continue;
620       }
622       let position = BROWSER_UI_CONTAINER_IDS[area];
623       if (area == "nav-bar") {
624         position = `${BROWSER_UI_CONTAINER_IDS[area]}-start`;
625       }
627       let widgets = lazy.CustomizableUI.getWidgetsInArea(area);
629       for (let widget of widgets) {
630         if (!widget) {
631           continue;
632         }
634         if (widget.id.startsWith("customizableui-special-")) {
635           continue;
636         }
638         if (area == "nav-bar" && widget.id == "urlbar-container") {
639           position = `${BROWSER_UI_CONTAINER_IDS[area]}-end`;
640           continue;
641         }
643         widgetMap.set(widget.id, position);
644       }
645     }
647     let actions = lazy.PageActions.actions;
648     for (let action of actions) {
649       if (action.pinnedToUrlbar) {
650         widgetMap.set(action.id, "pageaction-urlbar");
651       }
652     }
654     return widgetMap;
655   },
657   _getWidgetID(node) {
658     // We want to find a sensible ID for this element.
659     if (!node) {
660       return null;
661     }
663     // See if this is a customizable widget.
664     if (node.ownerDocument.URL == AppConstants.BROWSER_CHROME_URL) {
665       // First find if it is inside one of the customizable areas.
666       for (let area of lazy.CustomizableUI.areas) {
667         if (node.closest(`#${CSS.escape(area)}`)) {
668           for (let widget of lazy.CustomizableUI.getWidgetIdsInArea(area)) {
669             if (
670               // We care about the buttons on the tabs themselves.
671               widget == "tabbrowser-tabs" ||
672               // We care about the page action and other buttons in here.
673               widget == "urlbar-container" ||
674               // We care about the actual menu items.
675               widget == "menubar-items" ||
676               // We care about individual bookmarks here.
677               widget == "personal-bookmarks"
678             ) {
679               continue;
680             }
682             if (node.closest(`#${CSS.escape(widget)}`)) {
683               return widget;
684             }
685           }
686           break;
687         }
688       }
689     }
691     if (node.id) {
692       return node.id;
693     }
695     // A couple of special cases in the tabs.
696     for (let cls of ["bookmark-item", "tab-icon-sound", "tab-close-button"]) {
697       if (!node.classList.contains(cls)) {
698         continue;
699       }
700       if (cls == "bookmark-item" && node.parentElement.id.includes("history")) {
701         return "history-item";
702       }
703       return cls;
704     }
706     // One of these will at least let us know what the widget is for.
707     let possibleAttributes = [
708       "preference",
709       "command",
710       "observes",
711       "data-l10n-id",
712     ];
714     // The key attribute on key elements is the actual key to listen for.
715     if (node.localName != "key") {
716       possibleAttributes.unshift("key");
717     }
719     for (let idAttribute of possibleAttributes) {
720       if (node.hasAttribute(idAttribute)) {
721         return node.getAttribute(idAttribute);
722       }
723     }
725     return this._getWidgetID(node.parentElement);
726   },
728   _getBrowserWidgetContainer(node) {
729     // Find the container holding this element.
730     for (let containerId of Object.keys(BROWSER_UI_CONTAINER_IDS)) {
731       let container = node.ownerDocument.getElementById(containerId);
732       if (container && container.contains(node)) {
733         return BROWSER_UI_CONTAINER_IDS[containerId];
734       }
735     }
736     // Treat toolbar context menu items that relate to tabs as the tab menu:
737     if (
738       node.closest("#toolbar-context-menu") &&
739       node.getAttribute("contexttype") == "tabbar"
740     ) {
741       return BROWSER_UI_CONTAINER_IDS.tabContextMenu;
742     }
743     return null;
744   },
746   _getWidgetContainer(node) {
747     if (node.localName == "key") {
748       return "keyboard";
749     }
751     const { URL } = node.ownerDocument;
752     if (URL == AppConstants.BROWSER_CHROME_URL) {
753       return this._getBrowserWidgetContainer(node);
754     }
755     if (URL.startsWith("about:preferences")) {
756       // Find the element's category.
757       let container = node.closest("[data-category]");
758       if (!container) {
759         return null;
760       }
762       let pane = container.getAttribute("data-category");
764       if (!PREFERENCES_PANES.includes(pane)) {
765         pane = "paneUnknown";
766       }
768       return `preferences_${pane}`;
769     }
771     return null;
772   },
774   lastClickTarget: null,
776   ignoreEvent(event) {
777     IGNORABLE_EVENTS.set(event, true);
778   },
780   _recordCommand(event) {
781     if (IGNORABLE_EVENTS.get(event)) {
782       return;
783     }
785     let sourceEvent = event;
786     while (sourceEvent.sourceEvent) {
787       sourceEvent = sourceEvent.sourceEvent;
788     }
790     let lastTarget = this.lastClickTarget?.get();
791     if (
792       lastTarget &&
793       sourceEvent.type == "command" &&
794       sourceEvent.target.contains(lastTarget)
795     ) {
796       // Ignore a command event triggered by a click.
797       this.lastClickTarget = null;
798       return;
799     }
801     this.lastClickTarget = null;
803     if (sourceEvent.type == "click") {
804       // Only care about main button clicks.
805       if (sourceEvent.button != 0) {
806         return;
807       }
809       // This click may trigger a command event so retain the target to be able
810       // to dedupe that event.
811       this.lastClickTarget = Cu.getWeakReference(sourceEvent.target);
812     }
814     // We should never see events from web content as they are fired in a
815     // content process, but let's be safe.
816     let url = sourceEvent.target.ownerDocument.documentURIObject;
817     if (!url.schemeIs("chrome") && !url.schemeIs("about")) {
818       return;
819     }
821     // This is what events targetted  at content will actually look like.
822     if (sourceEvent.target.localName == "browser") {
823       return;
824     }
826     // Find the actual element we're interested in.
827     let node = sourceEvent.target;
828     const isAboutPreferences =
829       node.ownerDocument.URL.startsWith("about:preferences");
830     while (
831       !UI_TARGET_ELEMENTS.includes(node.localName) &&
832       !node.classList?.contains("wants-telemetry") &&
833       // We are interested in links on about:preferences as well.
834       !(
835         isAboutPreferences &&
836         (node.getAttribute("is") === "text-link" || node.localName === "a")
837       )
838     ) {
839       node = node.parentNode;
840       if (!node?.parentNode) {
841         // A click on a space or label or top-level document or something we're
842         // not interested in.
843         return;
844       }
845     }
847     if (sourceEvent.type === "command") {
848       const { command, ownerDocument, parentNode } = node;
849       // Check if this command is for a history or bookmark link being opened
850       // from the context menu. In this case, we are interested in the DOM node
851       // for the link, not the menu item itself.
852       if (
853         PLACES_OPEN_COMMANDS.includes(command) ||
854         parentNode?.parentNode?.id === PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID
855       ) {
856         node = ownerDocument.getElementById(PLACES_CONTEXT_MENU_ID).triggerNode;
857       }
858     }
860     let item = this._getWidgetID(node);
861     let source = this._getWidgetContainer(node);
863     if (item && source) {
864       let scalar = `browser.ui.interaction.${source.replace(/-/g, "_")}`;
865       Services.telemetry.keyedScalarAdd(scalar, telemetryId(item), 1);
866       if (SET_USAGECOUNT_PREF_BUTTONS.includes(item)) {
867         let pref = `browser.engagement.${item}.used-count`;
868         Services.prefs.setIntPref(pref, Services.prefs.getIntPref(pref, 0) + 1);
869       }
870       if (SET_USAGE_PREF_BUTTONS.includes(item)) {
871         Services.prefs.setBoolPref(`browser.engagement.${item}.has-used`, true);
872       }
873     }
875     if (ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source]) {
876       let contextMenu = ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source];
877       let triggerContainer = this._getWidgetContainer(
878         node.closest("menupopup")?.triggerNode
879       );
880       if (triggerContainer) {
881         let scalar = `browser.ui.interaction.${contextMenu.replace(/-/g, "_")}`;
882         Services.telemetry.keyedScalarAdd(
883           scalar,
884           telemetryId(triggerContainer),
885           1
886         );
887       }
888     }
889   },
891   /**
892    * Listens for UI interactions in the window.
893    */
894   _addUsageListeners(win) {
895     // Listen for command events from the UI.
896     win.addEventListener("command", event => this._recordCommand(event), true);
897     win.addEventListener("click", event => this._recordCommand(event), true);
898   },
900   /**
901    * A public version of the private method to take care of the `nav-bar-start`,
902    * `nav-bar-end` thing that callers shouldn't have to care about. It also
903    * accepts the DOM ids for the areas rather than the cleaner ones we report
904    * to telemetry.
905    */
906   recordWidgetChange(widgetId, newPos, reason) {
907     try {
908       if (newPos) {
909         newPos = BROWSER_UI_CONTAINER_IDS[newPos];
910       }
912       if (newPos == "nav-bar") {
913         let { position } = lazy.CustomizableUI.getPlacementOfWidget(widgetId);
914         let { position: urlPosition } =
915           lazy.CustomizableUI.getPlacementOfWidget("urlbar-container");
916         newPos = newPos + (urlPosition > position ? "-start" : "-end");
917       }
919       this._recordWidgetChange(widgetId, newPos, reason);
920     } catch (e) {
921       console.error(e);
922     }
923   },
925   recordToolbarVisibility(toolbarId, newState, reason) {
926     if (typeof newState != "string") {
927       newState = newState ? "on" : "off";
928     }
929     this._recordWidgetChange(
930       BROWSER_UI_CONTAINER_IDS[toolbarId],
931       newState,
932       reason
933     );
934   },
936   _recordWidgetChange(widgetId, newPos, reason) {
937     // In some cases (like when add-ons are detected during startup) this gets
938     // called before we've reported the initial positions. Ignore such cases.
939     if (!this.widgetMap) {
940       return;
941     }
943     if (widgetId == "urlbar-container") {
944       // We don't report the position of the url bar, it is after nav-bar-start
945       // and before nav-bar-end. But moving it means the widgets around it have
946       // effectively moved so update those.
947       let position = "nav-bar-start";
948       let widgets = lazy.CustomizableUI.getWidgetsInArea("nav-bar");
950       for (let widget of widgets) {
951         if (!widget) {
952           continue;
953         }
955         if (widget.id.startsWith("customizableui-special-")) {
956           continue;
957         }
959         if (widget.id == "urlbar-container") {
960           position = "nav-bar-end";
961           continue;
962         }
964         // This will do nothing if the position hasn't changed.
965         this._recordWidgetChange(widget.id, position, reason);
966       }
968       return;
969     }
971     let oldPos = this.widgetMap.get(widgetId);
972     if (oldPos == newPos) {
973       return;
974     }
976     let action = "move";
978     if (!oldPos) {
979       action = "add";
980     } else if (!newPos) {
981       action = "remove";
982     }
984     let key = `${telemetryId(widgetId, false)}_${action}_${oldPos ?? "na"}_${
985       newPos ?? "na"
986     }_${reason}`;
987     Services.telemetry.keyedScalarAdd("browser.ui.customized_widgets", key, 1);
989     if (newPos) {
990       this.widgetMap.set(widgetId, newPos);
991     } else {
992       this.widgetMap.delete(widgetId);
993     }
994   },
996   _recordUITelemetry() {
997     this.widgetMap = this._buildWidgetPositions();
999     for (let [widgetId, position] of this.widgetMap.entries()) {
1000       let key = `${telemetryId(widgetId, false)}_pinned_${position}`;
1001       Services.telemetry.keyedScalarSet(
1002         "browser.ui.toolbar_widgets",
1003         key,
1004         true
1005       );
1006     }
1007   },
1009   /**
1010    * Adds listeners to a single chrome window.
1011    */
1012   _registerWindow(win) {
1013     this._addUsageListeners(win);
1015     win.addEventListener("unload", this);
1016     win.addEventListener("TabOpen", this, true);
1017     win.addEventListener("TabPinned", this, true);
1019     win.gBrowser.tabContainer.addEventListener(TAB_RESTORING_TOPIC, this);
1020     win.gBrowser.addTabsProgressListener(URICountListener);
1021   },
1023   /**
1024    * Removes listeners from a single chrome window.
1025    */
1026   _unregisterWindow(win) {
1027     win.removeEventListener("unload", this);
1028     win.removeEventListener("TabOpen", this, true);
1029     win.removeEventListener("TabPinned", this, true);
1031     win.defaultView.gBrowser.tabContainer.removeEventListener(
1032       TAB_RESTORING_TOPIC,
1033       this
1034     );
1035     win.defaultView.gBrowser.removeTabsProgressListener(URICountListener);
1036   },
1038   /**
1039    * Updates the tab counts.
1040    */
1041   _onTabOpen() {
1042     // Update the "tab opened" count and its maximum.
1043     Services.telemetry.scalarAdd(TAB_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
1045     // In the case of opening multiple tabs at once, avoid enumerating all open
1046     // tabs and windows each time a tab opens.
1047     this._onTabsOpenedTask.disarm();
1048     this._onTabsOpenedTask.arm();
1049   },
1051   /**
1052    * Update tab counts after opening multiple tabs.
1053    */
1054   _onTabsOpened() {
1055     const { tabCount, loadedTabCount } = getOpenTabsAndWinsCounts();
1056     Services.telemetry.scalarSetMaximum(MAX_TAB_COUNT_SCALAR_NAME, tabCount);
1058     this._recordTabCounts({ tabCount, loadedTabCount });
1059   },
1061   _onTabPinned(target) {
1062     const pinnedTabs = getPinnedTabsCount();
1064     // Update the "tab pinned" count and its maximum.
1065     Services.telemetry.scalarAdd(TAB_PINNED_EVENT_COUNT_SCALAR_NAME, 1);
1066     Services.telemetry.scalarSetMaximum(
1067       MAX_TAB_PINNED_COUNT_SCALAR_NAME,
1068       pinnedTabs
1069     );
1070   },
1072   /**
1073    * Tracks the window count and registers the listeners for the tab count.
1074    * @param{Object} win The window object.
1075    */
1076   _onWindowOpen(win) {
1077     // Make sure to have a |nsIDOMWindow|.
1078     if (!(win instanceof Ci.nsIDOMWindow)) {
1079       return;
1080     }
1082     let onLoad = () => {
1083       win.removeEventListener("load", onLoad);
1085       // Ignore non browser windows.
1086       if (
1087         win.document.documentElement.getAttribute("windowtype") !=
1088         "navigator:browser"
1089       ) {
1090         return;
1091       }
1093       this._registerWindow(win);
1094       // Track the window open event and check the maximum.
1095       const counts = getOpenTabsAndWinsCounts();
1096       Services.telemetry.scalarAdd(WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
1097       Services.telemetry.scalarSetMaximum(
1098         MAX_WINDOW_COUNT_SCALAR_NAME,
1099         counts.winCount
1100       );
1102       // We won't receive the "TabOpen" event for the first tab within a new window.
1103       // Account for that.
1104       this._onTabOpen(counts);
1105     };
1106     win.addEventListener("load", onLoad);
1107   },
1109   /**
1110    * Record telemetry about the given tab counts.
1111    *
1112    * Telemetry for each count will only be recorded if the value isn't
1113    * `undefined`.
1114    *
1115    * @param {object} [counts] The tab counts to register with telemetry.
1116    * @param {number} [counts.tabCount] The number of tabs in all browsers.
1117    * @param {number} [counts.loadedTabCount] The number of loaded (i.e., not
1118    *                                         pending) tabs in all browsers.
1119    */
1120   _recordTabCounts({ tabCount, loadedTabCount }) {
1121     let currentTime = Date.now();
1122     if (
1123       tabCount !== undefined &&
1124       currentTime > this._lastRecordTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1125     ) {
1126       Services.telemetry.getHistogramById("TAB_COUNT").add(tabCount);
1127       this._lastRecordTabCount = currentTime;
1128     }
1130     if (
1131       loadedTabCount !== undefined &&
1132       currentTime >
1133         this._lastRecordLoadedTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1134     ) {
1135       Services.telemetry
1136         .getHistogramById("LOADED_TAB_COUNT")
1137         .add(loadedTabCount);
1138       this._lastRecordLoadedTabCount = currentTime;
1139     }
1140   },
1142   _checkProfileCountFileSchema(fileData) {
1143     // Verifies that the schema of the file is the expected schema
1144     if (typeof fileData.version != "string") {
1145       throw new Error("Schema Mismatch Error: Bad type for 'version' field");
1146     }
1147     if (!Array.isArray(fileData.profileTelemetryIds)) {
1148       throw new Error(
1149         "Schema Mismatch Error: Bad type for 'profileTelemetryIds' field"
1150       );
1151     }
1152     for (let profileTelemetryId of fileData.profileTelemetryIds) {
1153       if (typeof profileTelemetryId != "string") {
1154         throw new Error(
1155           "Schema Mismatch Error: Bad type for an element of 'profileTelemetryIds'"
1156         );
1157       }
1158     }
1159   },
1161   // Reports the number of Firefox profiles on this machine to telemetry.
1162   async reportProfileCount() {
1163     if (
1164       AppConstants.platform != "win" ||
1165       !AppConstants.MOZ_TELEMETRY_REPORTING
1166     ) {
1167       // This is currently a windows-only feature.
1168       // Also, this function writes directly to disk, without using the usual
1169       // telemetry recording functions. So we excplicitly check if telemetry
1170       // reporting was disabled at compile time, and we do not do anything in
1171       // case.
1172       return;
1173     }
1175     // To report only as much data as we need, we will bucket our values.
1176     // Rather than the raw value, we will report the greatest value in the list
1177     // below that is no larger than the raw value.
1178     const buckets = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000, 10000];
1180     // We need both the C:\ProgramData\Mozilla directory and the install
1181     // directory hash to create the profile count file path. We can easily
1182     // reassemble this from the update directory, which looks like:
1183     // C:\ProgramData\Mozilla\updates\hash
1184     // Retrieving the directory this way also ensures that the "Mozilla"
1185     // directory is created with the correct permissions.
1186     // The ProgramData directory, by default, grants write permissions only to
1187     // file creators. The directory service calls GetCommonUpdateDirectory,
1188     // which makes sure the the directory is created with user-writable
1189     // permissions.
1190     const updateDirectory = BrowserUsageTelemetry.Policy.getUpdateDirectory();
1191     const hash = updateDirectory.leafName;
1192     const profileCountFilename = "profile_count_" + hash + ".json";
1193     let profileCountFile = updateDirectory.parent.parent;
1194     profileCountFile.append(profileCountFilename);
1196     let readError = false;
1197     let fileData;
1198     try {
1199       let json = await BrowserUsageTelemetry.Policy.readProfileCountFile(
1200         profileCountFile.path
1201       );
1202       fileData = JSON.parse(json);
1203       BrowserUsageTelemetry._checkProfileCountFileSchema(fileData);
1204     } catch (ex) {
1205       // Note that since this also catches the "no such file" error, this is
1206       // always the template that we use when writing to the file for the first
1207       // time.
1208       fileData = { version: "1", profileTelemetryIds: [] };
1209       if (!(ex.name == "NotFoundError")) {
1210         console.error(ex);
1211         // Don't just return here on a read error. We need to send the error
1212         // value to telemetry and we want to attempt to fix the file.
1213         // However, we will still report an error for this ping, even if we
1214         // fix the file. This is to prevent always sending a profile count of 1
1215         // if, for some reason, we always get a read error but never a write
1216         // error.
1217         readError = true;
1218       }
1219     }
1221     let writeError = false;
1222     let currentTelemetryId =
1223       await BrowserUsageTelemetry.Policy.getTelemetryClientId();
1224     // Don't add our telemetry ID to the file if we've already reached the
1225     // largest bucket. This prevents the file size from growing forever.
1226     if (
1227       !fileData.profileTelemetryIds.includes(currentTelemetryId) &&
1228       fileData.profileTelemetryIds.length < Math.max(...buckets)
1229     ) {
1230       fileData.profileTelemetryIds.push(currentTelemetryId);
1231       try {
1232         await BrowserUsageTelemetry.Policy.writeProfileCountFile(
1233           profileCountFile.path,
1234           JSON.stringify(fileData)
1235         );
1236       } catch (ex) {
1237         console.error(ex);
1238         writeError = true;
1239       }
1240     }
1242     // Determine the bucketed value to report
1243     let rawProfileCount = fileData.profileTelemetryIds.length;
1244     let valueToReport = 0;
1245     for (let bucket of buckets) {
1246       if (bucket <= rawProfileCount && bucket > valueToReport) {
1247         valueToReport = bucket;
1248       }
1249     }
1251     if (readError || writeError) {
1252       // We convey errors via a profile count of 0.
1253       valueToReport = 0;
1254     }
1256     Services.telemetry.scalarSet(
1257       "browser.engagement.profile_count",
1258       valueToReport
1259     );
1260     // Manually mirror to Glean
1261     Glean.browserEngagement.profileCount.set(valueToReport);
1262   },
1264   /**
1265    * Check if this is the first run of this profile since installation,
1266    * if so then send installation telemetry.
1267    *
1268    * @param {nsIFile} [dataPathOverride] Optional, full data file path, for tests.
1269    * @param {Array<string>} [msixPackagePrefixes] Optional, list of prefixes to
1270             consider "existing" installs when looking at installed MSIX packages.
1271             Defaults to prefixes for builds produced in Firefox automation.
1272    * @return {Promise}
1273    * @resolves When the event has been recorded, or if the data file was not found.
1274    * @rejects JavaScript exception on any failure.
1275    */
1276   async reportInstallationTelemetry(
1277     dataPathOverride,
1278     msixPackagePrefixes = ["Mozilla.Firefox", "Mozilla.MozillaFirefox"]
1279   ) {
1280     if (AppConstants.platform != "win") {
1281       // This is a windows-only feature.
1282       return;
1283     }
1285     const TIMESTAMP_PREF = "app.installation.timestamp";
1286     const lastInstallTime = Services.prefs.getStringPref(TIMESTAMP_PREF, null);
1287     const wpm = Cc["@mozilla.org/windows-package-manager;1"].createInstance(
1288       Ci.nsIWindowsPackageManager
1289     );
1290     let installer_type = "";
1291     let pfn;
1292     try {
1293       pfn = Services.sysinfo.getProperty("winPackageFamilyName");
1294     } catch (e) {}
1296     function getInstallData() {
1297       // We only care about where _any_ other install existed - no
1298       // need to count more than 1.
1299       const installPaths = lazy.WindowsInstallsInfo.getInstallPaths(
1300         1,
1301         new Set([Services.dirsvc.get("GreBinD", Ci.nsIFile).path])
1302       );
1303       const msixInstalls = new Set();
1304       // We're just going to eat all errors here -- we don't want the event
1305       // to go unsent if we were unable to look for MSIX installs.
1306       try {
1307         wpm
1308           .findUserInstalledPackages(msixPackagePrefixes)
1309           .forEach(i => msixInstalls.add(i));
1310         if (pfn) {
1311           msixInstalls.delete(pfn);
1312         }
1313       } catch (ex) {}
1314       return {
1315         installPaths,
1316         msixInstalls,
1317       };
1318     }
1320     let extra = {};
1322     if (pfn) {
1323       if (lastInstallTime != null) {
1324         // We've already seen this install
1325         return;
1326       }
1328       // First time seeing this install, record the timestamp.
1329       Services.prefs.setStringPref(TIMESTAMP_PREF, wpm.getInstalledDate());
1330       let install_data = getInstallData();
1332       installer_type = "msix";
1334       // Build the extra event data
1335       extra.version = AppConstants.MOZ_APP_VERSION;
1336       extra.build_id = AppConstants.MOZ_BUILDID;
1337       // The next few keys are static for the reasons described
1338       // No way to detect whether or not we were installed by an admin
1339       extra.admin_user = "false";
1340       // Always false at the moment, because we create a new profile
1341       // on first launch
1342       extra.profdir_existed = "false";
1343       // Obviously false for MSIX installs
1344       extra.from_msi = "false";
1345       // We have no way of knowing whether we were installed via the GUI,
1346       // through the command line, or some Enterprise management tool.
1347       extra.silent = "false";
1348       // There's no way to change the install path for an MSIX package
1349       extra.default_path = "true";
1350       extra.install_existed = install_data.msixInstalls.has(pfn).toString();
1351       install_data.msixInstalls.delete(pfn);
1352       extra.other_inst = (!!install_data.installPaths.size).toString();
1353       extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
1354     } else {
1355       let dataPath = dataPathOverride;
1356       if (!dataPath) {
1357         dataPath = Services.dirsvc.get("GreD", Ci.nsIFile);
1358         dataPath.append("installation_telemetry.json");
1359       }
1361       let dataBytes;
1362       try {
1363         dataBytes = await IOUtils.read(dataPath.path);
1364       } catch (ex) {
1365         if (ex.name == "NotFoundError") {
1366           // Many systems will not have the data file, return silently if not found as
1367           // there is nothing to record.
1368           return;
1369         }
1370         throw ex;
1371       }
1372       const dataString = new TextDecoder("utf-16").decode(dataBytes);
1373       const data = JSON.parse(dataString);
1375       if (lastInstallTime && data.install_timestamp == lastInstallTime) {
1376         // We've already seen this install
1377         return;
1378       }
1380       // First time seeing this install, record the timestamp.
1381       Services.prefs.setStringPref(TIMESTAMP_PREF, data.install_timestamp);
1382       let install_data = getInstallData();
1384       installer_type = data.installer_type;
1386       // Installation timestamp is not intended to be sent with telemetry,
1387       // remove it to emphasize this point.
1388       delete data.install_timestamp;
1390       // Build the extra event data
1391       extra.version = data.version;
1392       extra.build_id = data.build_id;
1393       extra.admin_user = data.admin_user.toString();
1394       extra.install_existed = data.install_existed.toString();
1395       extra.profdir_existed = data.profdir_existed.toString();
1396       extra.other_inst = (!!install_data.installPaths.size).toString();
1397       extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
1399       if (data.installer_type == "full") {
1400         extra.silent = data.silent.toString();
1401         extra.from_msi = data.from_msi.toString();
1402         extra.default_path = data.default_path.toString();
1403       }
1404     }
1405     // Record the event
1406     Services.telemetry.setEventRecordingEnabled("installation", true);
1407     Services.telemetry.recordEvent(
1408       "installation",
1409       "first_seen",
1410       installer_type,
1411       null,
1412       extra
1413     );
1414   },
1417 // Used by nsIBrowserUsage
1418 export function getUniqueDomainsVisitedInPast24Hours() {
1419   return URICountListener.uniqueDomainsVisitedInPast24Hours;