Bug 1667155 [wpt PR 25777] - [AspectRatio] Fix bug in flex-aspect-ratio-024 test...
[gecko.git] / browser / modules / BrowserUsageTelemetry.jsm
blob5ea77ae08656e3c31c1760120e330b9760a659e4
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 "use strict";
8 var EXPORTED_SYMBOLS = [
9   "BrowserUsageTelemetry",
10   "getUniqueDomainsVisitedInPast24Hours",
11   "URICountListener",
12   "MINIMUM_TAB_COUNT_INTERVAL_MS",
15 const { XPCOMUtils } = ChromeUtils.import(
16   "resource://gre/modules/XPCOMUtils.jsm"
19 XPCOMUtils.defineLazyModuleGetters(this, {
20   AppConstants: "resource://gre/modules/AppConstants.jsm",
21   ClientID: "resource://gre/modules/ClientID.jsm",
22   BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
23   CustomizableUI: "resource:///modules/CustomizableUI.jsm",
24   OS: "resource://gre/modules/osfile.jsm",
25   PageActions: "resource:///modules/PageActions.jsm",
26   PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.jsm",
27   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
28   SearchTelemetry: "resource:///modules/SearchTelemetry.jsm",
29   Services: "resource://gre/modules/Services.jsm",
30   setTimeout: "resource://gre/modules/Timer.jsm",
31   clearTimeout: "resource://gre/modules/Timer.jsm",
32   UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
33 });
35 // This pref is in seconds!
36 XPCOMUtils.defineLazyPreferenceGetter(
37   this,
38   "gRecentVisitedOriginsExpiry",
39   "browser.engagement.recent_visited_origins.expiry"
42 // The upper bound for the count of the visited unique domain names.
43 const MAX_UNIQUE_VISITED_DOMAINS = 100;
45 // Observed topic names.
46 const TAB_RESTORING_TOPIC = "SSTabRestoring";
47 const TELEMETRY_SUBSESSIONSPLIT_TOPIC =
48   "internal-telemetry-after-subsession-split";
49 const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
51 // Probe names.
52 const MAX_TAB_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_tab_count";
53 const MAX_WINDOW_COUNT_SCALAR_NAME =
54   "browser.engagement.max_concurrent_window_count";
55 const TAB_OPEN_EVENT_COUNT_SCALAR_NAME =
56   "browser.engagement.tab_open_event_count";
57 const MAX_TAB_PINNED_COUNT_SCALAR_NAME =
58   "browser.engagement.max_concurrent_tab_pinned_count";
59 const TAB_PINNED_EVENT_COUNT_SCALAR_NAME =
60   "browser.engagement.tab_pinned_event_count";
61 const WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME =
62   "browser.engagement.window_open_event_count";
63 const UNIQUE_DOMAINS_COUNT_SCALAR_NAME =
64   "browser.engagement.unique_domains_count";
65 const TOTAL_URI_COUNT_SCALAR_NAME = "browser.engagement.total_uri_count";
66 const UNFILTERED_URI_COUNT_SCALAR_NAME =
67   "browser.engagement.unfiltered_uri_count";
69 // A list of known search origins.
70 const KNOWN_SEARCH_SOURCES = [
71   "abouthome",
72   "contextmenu",
73   "newtab",
74   "searchbar",
75   "system",
76   "urlbar",
77   "urlbar-searchmode",
78   "webextension",
81 const KNOWN_ONEOFF_SOURCES = [
82   "oneoff-urlbar",
83   "oneoff-searchbar",
84   "unknown", // Edge case: this is the searchbar (see bug 1195733 comment 7).
87 const MINIMUM_TAB_COUNT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes, in ms
89 // The elements we consider to be interactive.
90 const UI_TARGET_ELEMENTS = [
91   "menuitem",
92   "toolbarbutton",
93   "key",
94   "command",
95   "checkbox",
96   "input",
97   "button",
98   "image",
99   "radio",
100   "richlistitem",
103 // The containers of interactive elements that we care about and their pretty
104 // names. These should be listed in order of most-specific to least-specific,
105 // when iterating JavaScript will guarantee that ordering and so we will find
106 // the most specific area first.
107 const BROWSER_UI_CONTAINER_IDS = {
108   "toolbar-menubar": "menu-bar",
109   TabsToolbar: "tabs-bar",
110   PersonalToolbar: "bookmarks-bar",
111   "appMenu-popup": "app-menu",
112   tabContextMenu: "tabs-context",
113   contentAreaContextMenu: "content-context",
114   "widget-overflow-list": "overflow-menu",
115   "widget-overflow-fixed-list": "pinned-overflow-menu",
116   "page-action-buttons": "pageaction-urlbar",
117   pageActionPanel: "pageaction-panel",
119   // This should appear last as some of the above are inside the nav bar.
120   "nav-bar": "nav-bar",
123 // A list of the expected panes in about:preferences
124 const PREFERENCES_PANES = [
125   "paneHome",
126   "paneGeneral",
127   "panePrivacy",
128   "paneSearch",
129   "paneSearchResults",
130   "paneSync",
131   "paneContainers",
132   "paneExperimental",
135 const IGNORABLE_EVENTS = new WeakMap();
137 const KNOWN_ADDONS = [];
139 function telemetryId(widgetId, obscureAddons = true) {
140   // Add-on IDs need to be obscured.
141   function addonId(id) {
142     if (!obscureAddons) {
143       return id;
144     }
146     let pos = KNOWN_ADDONS.indexOf(id);
147     if (pos < 0) {
148       pos = KNOWN_ADDONS.length;
149       KNOWN_ADDONS.push(id);
150     }
151     return `addon${pos}`;
152   }
154   if (widgetId.endsWith("-browser-action")) {
155     widgetId = addonId(
156       widgetId.substring(0, widgetId.length - "-browser-action".length)
157     );
158   } else if (widgetId.startsWith("pageAction-")) {
159     let actionId;
160     if (widgetId.startsWith("pageAction-urlbar-")) {
161       actionId = widgetId.substring("pageAction-urlbar-".length);
162     } else if (widgetId.startsWith("pageAction-panel-")) {
163       actionId = widgetId.substring("pageAction-panel-".length);
164     }
166     if (actionId) {
167       let action = PageActions.actionForID(actionId);
168       widgetId = action?._isMozillaAction ? actionId : addonId(actionId);
169     }
170   } else if (widgetId.startsWith("ext-keyset-id-")) {
171     // Webextension command shortcuts don't have an id on their key element so
172     // we see the id from the keyset that contains them.
173     widgetId = addonId(widgetId.substring("ext-keyset-id-".length));
174   } else if (widgetId.startsWith("ext-key-id-")) {
175     // The command for a webextension sidebar action is an exception to the above rule.
176     widgetId = widgetId.substring("ext-key-id-".length);
177     if (widgetId.endsWith("-sidebar-action")) {
178       widgetId = addonId(
179         widgetId.substring(0, widgetId.length - "-sidebar-action".length)
180       );
181     }
182   }
184   return widgetId.replace(/_/g, "-");
187 function getOpenTabsAndWinsCounts() {
188   let loadedTabCount = 0;
189   let tabCount = 0;
190   let winCount = 0;
192   for (let win of Services.wm.getEnumerator("navigator:browser")) {
193     winCount++;
194     tabCount += win.gBrowser.tabs.length;
195     for (const tab of win.gBrowser.tabs) {
196       if (tab.getAttribute("pending") !== "true") {
197         loadedTabCount += 1;
198       }
199     }
200   }
202   return { loadedTabCount, tabCount, winCount };
205 function getPinnedTabsCount() {
206   let pinnedTabs = 0;
208   for (let win of Services.wm.getEnumerator("navigator:browser")) {
209     pinnedTabs += [...win.ownerGlobal.gBrowser.tabs].filter(t => t.pinned)
210       .length;
211   }
213   return pinnedTabs;
216 function shouldRecordSearchCount(tabbrowser) {
217   return (
218     !PrivateBrowsingUtils.isWindowPrivate(tabbrowser.ownerGlobal) ||
219     !Services.prefs.getBoolPref("browser.engagement.search_counts.pbm", false)
220   );
223 let URICountListener = {
224   // A set containing the visited domains, see bug 1271310.
225   _domainSet: new Set(),
226   // A set containing the visited origins during the last 24 hours (similar to domains, but not quite the same)
227   _domain24hrSet: new Set(),
228   // A map to keep track of the URIs loaded from the restored tabs.
229   _restoredURIsMap: new WeakMap(),
230   // Ongoing expiration timeouts.
231   _timeouts: new Set(),
233   isHttpURI(uri) {
234     // Only consider http(s) schemas.
235     return uri.schemeIs("http") || uri.schemeIs("https");
236   },
238   addRestoredURI(browser, uri) {
239     if (!this.isHttpURI(uri)) {
240       return;
241     }
243     this._restoredURIsMap.set(browser, uri.spec);
244   },
246   onStateChange(browser, webProgress, request, stateFlags, status) {
247     if (
248       !webProgress.isTopLevel ||
249       !(stateFlags & Ci.nsIWebProgressListener.STATE_STOP) ||
250       !(stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
251     ) {
252       return;
253     }
255     if (!(request instanceof Ci.nsIChannel) || !this.isHttpURI(request.URI)) {
256       return;
257     }
259     BrowserUsageTelemetry._recordSiteOriginsPerLoadedTabs();
260   },
262   onLocationChange(browser, webProgress, request, uri, flags) {
263     if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
264       // By default, assume we no longer need to track this tab.
265       SearchTelemetry.stopTrackingBrowser(browser);
266     }
268     // Don't count this URI if it's an error page.
269     if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
270       return;
271     }
273     // We only care about top level loads.
274     if (!webProgress.isTopLevel) {
275       return;
276     }
278     // The SessionStore sets the URI of a tab first, firing onLocationChange the
279     // first time, then manages content loading using its scheduler. Once content
280     // loads, we will hit onLocationChange again.
281     // We can catch the first case by checking for null requests: be advised that
282     // this can also happen when navigating page fragments, so account for it.
283     if (
284       !request &&
285       !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
286     ) {
287       return;
288     }
290     // Don't include URI and domain counts when in private mode.
291     let shouldCountURI =
292       !PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal) ||
293       Services.prefs.getBoolPref(
294         "browser.engagement.total_uri_count.pbm",
295         false
296       );
298     // Track URI loads, even if they're not http(s).
299     let uriSpec = null;
300     try {
301       uriSpec = uri.spec;
302     } catch (e) {
303       // If we have troubles parsing the spec, still count this as
304       // an unfiltered URI.
305       if (shouldCountURI) {
306         Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
307       }
308       return;
309     }
311     // Don't count about:blank and similar pages, as they would artificially
312     // inflate the counts.
313     if (browser.ownerGlobal.gInitialPages.includes(uriSpec)) {
314       return;
315     }
317     // If the URI we're loading is in the _restoredURIsMap, then it comes from a
318     // restored tab. If so, let's skip it and remove it from the map as we want to
319     // count page refreshes.
320     if (this._restoredURIsMap.get(browser) === uriSpec) {
321       this._restoredURIsMap.delete(browser);
322       return;
323     }
325     // The URI wasn't from a restored tab. Count it among the unfiltered URIs.
326     // If this is an http(s) URI, this also gets counted by the "total_uri_count"
327     // probe.
328     if (shouldCountURI) {
329       Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
330     }
332     if (!this.isHttpURI(uri)) {
333       return;
334     }
336     if (
337       shouldRecordSearchCount(browser.getTabBrowser()) &&
338       !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
339     ) {
340       SearchTelemetry.updateTrackingStatus(browser, uriSpec);
341     }
343     if (!shouldCountURI) {
344       return;
345     }
347     // Update the URI counts.
348     Services.telemetry.scalarAdd(TOTAL_URI_COUNT_SCALAR_NAME, 1);
350     // Update tab count
351     BrowserUsageTelemetry._recordTabCounts(getOpenTabsAndWinsCounts());
353     // Unique domains should be aggregated by (eTLD + 1): x.test.com and y.test.com
354     // are counted once as test.com.
355     let baseDomain;
356     try {
357       // Even if only considering http(s) URIs, |getBaseDomain| could still throw
358       // due to the URI containing invalid characters or the domain actually being
359       // an ipv4 or ipv6 address.
360       baseDomain = Services.eTLD.getBaseDomain(uri);
361     } catch (e) {
362       return;
363     }
365     // We only want to count the unique domains up to MAX_UNIQUE_VISITED_DOMAINS.
366     if (this._domainSet.size < MAX_UNIQUE_VISITED_DOMAINS) {
367       this._domainSet.add(baseDomain);
368       Services.telemetry.scalarSet(
369         UNIQUE_DOMAINS_COUNT_SCALAR_NAME,
370         this._domainSet.size
371       );
372     }
374     this._domain24hrSet.add(baseDomain);
375     if (gRecentVisitedOriginsExpiry) {
376       let timeoutId = setTimeout(() => {
377         this._domain24hrSet.delete(baseDomain);
378         this._timeouts.delete(timeoutId);
379       }, gRecentVisitedOriginsExpiry * 1000);
380       this._timeouts.add(timeoutId);
381     }
382   },
384   /**
385    * Reset the counts. This should be called when breaking a session in Telemetry.
386    */
387   reset() {
388     this._domainSet.clear();
389   },
391   /**
392    * Returns the number of unique domains visited in this session during the
393    * last 24 hours.
394    */
395   get uniqueDomainsVisitedInPast24Hours() {
396     return this._domain24hrSet.size;
397   },
399   /**
400    * Resets the number of unique domains visited in this session.
401    */
402   resetUniqueDomainsVisitedInPast24Hours() {
403     this._timeouts.forEach(timeoutId => clearTimeout(timeoutId));
404     this._timeouts.clear();
405     this._domain24hrSet.clear();
406   },
408   QueryInterface: ChromeUtils.generateQI([
409     "nsIWebProgressListener",
410     "nsISupportsWeakReference",
411   ]),
414 let BrowserUsageTelemetry = {
415   /**
416    * This is a policy object used to override behavior for testing.
417    */
418   Policy: {
419     getTelemetryClientId: async () => ClientID.getClientID(),
420     getUpdateDirectory: () => Services.dirsvc.get("UpdRootD", Ci.nsIFile),
421     readProfileCountFile: async path =>
422       OS.File.read(path, { encoding: "UTF-8" }),
423     writeProfileCountFile: async (path, data) =>
424       OS.File.writeAtomic(path, data),
425   },
427   _inited: false,
429   init() {
430     this._lastRecordTabCount = 0;
431     this._lastRecordLoadedTabCount = 0;
432     this._lastRecordSiteOriginsPerLoadedTabs = 0;
433     this._setupAfterRestore();
434     this._inited = true;
436     Services.prefs.addObserver("browser.tabs.extraDragSpace", this);
437     Services.prefs.addObserver("browser.tabs.drawInTitlebar", this);
439     this._recordUITelemetry();
440   },
442   /**
443    * Resets the masked add-on identifiers. Only for use in tests.
444    */
445   _resetAddonIds() {
446     KNOWN_ADDONS.length = 0;
447   },
449   /**
450    * Handle subsession splits in the parent process.
451    */
452   afterSubsessionSplit() {
453     // Scalars just got cleared due to a subsession split. We need to set the maximum
454     // concurrent tab and window counts so that they reflect the correct value for the
455     // new subsession.
456     const counts = getOpenTabsAndWinsCounts();
457     Services.telemetry.scalarSetMaximum(
458       MAX_TAB_COUNT_SCALAR_NAME,
459       counts.tabCount
460     );
461     Services.telemetry.scalarSetMaximum(
462       MAX_WINDOW_COUNT_SCALAR_NAME,
463       counts.winCount
464     );
466     // Reset the URI counter.
467     URICountListener.reset();
468   },
470   QueryInterface: ChromeUtils.generateQI([
471     "nsIObserver",
472     "nsISupportsWeakReference",
473   ]),
475   uninit() {
476     if (!this._inited) {
477       return;
478     }
479     Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC);
480     Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC);
481   },
483   observe(subject, topic, data) {
484     switch (topic) {
485       case DOMWINDOW_OPENED_TOPIC:
486         this._onWindowOpen(subject);
487         break;
488       case TELEMETRY_SUBSESSIONSPLIT_TOPIC:
489         this.afterSubsessionSplit();
490         break;
491       case "nsPref:changed":
492         switch (data) {
493           case "browser.tabs.extraDragSpace":
494             this._recordWidgetChange(
495               "drag-space",
496               Services.prefs.getBoolPref("browser.tabs.extraDragSpace")
497                 ? "on"
498                 : "off",
499               "pref"
500             );
501             break;
502           case "browser.tabs.drawInTitlebar":
503             this._recordWidgetChange(
504               "titlebar",
505               Services.prefs.getBoolPref("browser.tabs.drawInTitlebar")
506                 ? "off"
507                 : "on",
508               "pref"
509             );
510             break;
511         }
512         break;
513     }
514   },
516   handleEvent(event) {
517     switch (event.type) {
518       case "TabOpen":
519         this._onTabOpen(getOpenTabsAndWinsCounts());
520         break;
521       case "TabPinned":
522         this._onTabPinned();
523         break;
524       case "unload":
525         this._unregisterWindow(event.target);
526         break;
527       case TAB_RESTORING_TOPIC:
528         // We're restoring a new tab from a previous or crashed session.
529         // We don't want to track the URIs from these tabs, so let
530         // |URICountListener| know about them.
531         let browser = event.target.linkedBrowser;
532         URICountListener.addRestoredURI(browser, browser.currentURI);
534         const { loadedTabCount } = getOpenTabsAndWinsCounts();
535         this._recordTabCounts({ loadedTabCount });
536         break;
537     }
538   },
540   /**
541    * The main entry point for recording search related Telemetry. This includes
542    * search counts and engagement measurements.
543    *
544    * Telemetry records only search counts per engine and action origin, but
545    * nothing pertaining to the search contents themselves.
546    *
547    * @param {tabbrowser} tabbrowser
548    *        The tabbrowser where the search was loaded.
549    * @param {nsISearchEngine} engine
550    *        The engine handling the search.
551    * @param {String} source
552    *        Where the search originated from. See KNOWN_SEARCH_SOURCES for allowed
553    *        values.
554    * @param {Object} [details] Options object.
555    * @param {Boolean} [details.isOneOff=false]
556    *        true if this event was generated by a one-off search.
557    * @param {Boolean} [details.isSuggestion=false]
558    *        true if this event was generated by a suggested search.
559    * @param {Boolean} [details.isFormHistory=false]
560    *        true if this event was generated by a form history result.
561    * @param {String} [details.alias=null]
562    *        The search engine alias used in the search, if any.
563    * @param {Object} [details.type=null]
564    *        The object describing the event that triggered the search.
565    * @throws if source is not in the known sources list.
566    */
567   recordSearch(tabbrowser, engine, source, details = {}) {
568     if (!shouldRecordSearchCount(tabbrowser)) {
569       return;
570     }
572     const countIdPrefix = `${engine.telemetryId}.`;
573     const countIdSource = countIdPrefix + source;
574     let histogram = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
576     if (details.isOneOff) {
577       if (!KNOWN_ONEOFF_SOURCES.includes(source)) {
578         // Silently drop the error if this bogus call
579         // came from 'urlbar' or 'searchbar'. They're
580         // calling |recordSearch| twice from two different
581         // code paths because they want to record the search
582         // in SEARCH_COUNTS.
583         if (["urlbar", "searchbar"].includes(source)) {
584           histogram.add(countIdSource);
585           PartnerLinkAttribution.makeSearchEngineRequest(
586             engine,
587             details.url
588           ).catch(Cu.reportError);
589           return;
590         }
591         throw new Error("Unknown source for one-off search: " + source);
592       }
593     } else {
594       if (!KNOWN_SEARCH_SOURCES.includes(source)) {
595         throw new Error("Unknown source for search: " + source);
596       }
597       if (
598         details.alias &&
599         engine.isAppProvided &&
600         engine.aliases.includes(details.alias)
601       ) {
602         // This is a keyword search using an AppProvided engine.
603         // Record the source as "alias", not "urlbar".
604         histogram.add(countIdPrefix + "alias");
605       } else {
606         histogram.add(countIdSource);
607       }
608     }
610     // Dispatch the search signal to other handlers.
611     this._handleSearchAction(engine, source, details);
612   },
614   _recordSearch(engine, url, source, action = null) {
615     // The one-off buttons are logged in two places, if we hit here with the
616     // action as oneoff and no url, then we are hitting the attribution case
617     // in `recordSearch` above. Really this needs re-architecturing so we
618     // do not have two distinct calls to `recordSearch` for one-offs
619     // (see bug 1662553).
620     if (!(action == "oneoff" && !url)) {
621       PartnerLinkAttribution.makeSearchEngineRequest(engine, url).catch(
622         Cu.reportError
623       );
624     }
626     let scalarKey = action ? "search_" + action : "search";
627     Services.telemetry.keyedScalarAdd(
628       "browser.engagement.navigation." + source,
629       scalarKey,
630       1
631     );
632     Services.telemetry.recordEvent("navigation", "search", source, action, {
633       engine: engine.telemetryId,
634     });
635   },
637   /**
638    * Records entry into the Urlbar's search mode.
639    *
640    * Telemetry records only which search mode is entered and how it was entered.
641    * It does not record anything pertaining to searches made within search mode.
642    * @param {object} searchMode
643    *   A search mode object. See UrlbarInput.setSearchMode documentation for
644    *   details.
645    */
646   recordSearchMode(searchMode) {
647     // Search mode preview is not search mode. Recording it would just create
648     // noise.
649     if (searchMode.isPreview) {
650       return;
651     }
652     let scalarKey;
653     if (searchMode.engineName) {
654       let engine = Services.search.getEngineByName(searchMode.engineName);
655       let resultDomain = engine.getResultDomain();
656       // For built-in engines, sanitize the data in a few special cases to make
657       // analysis easier.
658       if (!engine.isAppProvided) {
659         scalarKey = "other";
660       } else if (resultDomain.includes("amazon.")) {
661         // Group all the localized Amazon sites together.
662         scalarKey = "Amazon";
663       } else if (resultDomain.endsWith("wikipedia.org")) {
664         // Group all the localized Wikipedia sites together.
665         scalarKey = "Wikipedia";
666       } else {
667         scalarKey = searchMode.engineName;
668       }
669     } else if (searchMode.source) {
670       scalarKey = UrlbarUtils.getResultSourceName(searchMode.source) || "other";
671     }
673     Services.telemetry.keyedScalarAdd(
674       "urlbar.searchmode." + searchMode.entry,
675       scalarKey,
676       1
677     );
678   },
680   _handleSearchAction(engine, source, details) {
681     switch (source) {
682       case "urlbar":
683       case "oneoff-urlbar":
684       case "searchbar":
685       case "oneoff-searchbar":
686       case "unknown": // Edge case: this is the searchbar (see bug 1195733 comment 7).
687         this._handleSearchAndUrlbar(engine, source, details);
688         break;
689       case "urlbar-searchmode":
690         this._handleSearchAndUrlbar(engine, "urlbar_searchmode", details);
691         break;
692       case "abouthome":
693         this._recordSearch(engine, details.url, "about_home", "enter");
694         break;
695       case "newtab":
696         this._recordSearch(engine, details.url, "about_newtab", "enter");
697         break;
698       case "contextmenu":
699       case "system":
700       case "webextension":
701         this._recordSearch(engine, details.url, source);
702         break;
703     }
704   },
706   /**
707    * This function handles the "urlbar", "urlbar-oneoff", "searchbar" and
708    * "searchbar-oneoff" sources.
709    */
710   _handleSearchAndUrlbar(engine, source, details) {
711     // We want "urlbar" and "urlbar-oneoff" (and similar cases) to go in the same
712     // scalar, but in a different key.
714     // When using one-offs in the searchbar we get an "unknown" source. See bug
715     // 1195733 comment 7 for the context. Fix-up the label here.
716     const sourceName =
717       source === "unknown" ? "searchbar" : source.replace("oneoff-", "");
719     const isOneOff = !!details.isOneOff;
720     if (isOneOff) {
721       // We will receive a signal from the "urlbar"/"searchbar" even when the
722       // search came from "oneoff-urlbar". That's because both signals
723       // are propagated from search.xml. Skip it if that's the case.
724       // Moreover, we skip the "unknown" source that comes from the searchbar
725       // when performing searches from the default search engine. See bug 1195733
726       // comment 7 for context.
727       if (["urlbar", "searchbar", "unknown"].includes(source)) {
728         return;
729       }
731       // If that's a legit one-off search signal, record it using the relative key.
732       this._recordSearch(engine, details.url, sourceName, "oneoff");
733       return;
734     }
736     // The search was not a one-off. It was a search with the default search engine.
737     if (details.isFormHistory) {
738       // It came from a form history result.
739       this._recordSearch(engine, details.url, sourceName, "formhistory");
740       return;
741     } else if (details.isSuggestion) {
742       // It came from a suggested search, so count it as such.
743       this._recordSearch(engine, details.url, sourceName, "suggestion");
744       return;
745     } else if (details.alias) {
746       // This one came from a search that used an alias.
747       this._recordSearch(engine, details.url, sourceName, "alias");
748       return;
749     }
751     // The search signal was generated by typing something and pressing enter.
752     this._recordSearch(engine, details.url, sourceName, "enter");
753   },
755   /**
756    * Records the method by which the user selected a result from the urlbar.
757    *
758    * @param {Event} event
759    *        The event that triggered the selection.
760    * @param {number} index
761    *        The index that the user chose in the popup, or -1 if there wasn't a
762    *        selection.
763    * @param {string} userSelectionBehavior
764    *        How the user cycled through results before picking the current match.
765    *        Could be one of "tab", "arrow" or "none".
766    */
767   recordUrlbarSelectedResultMethod(
768     event,
769     index,
770     userSelectionBehavior = "none"
771   ) {
772     this._recordUrlOrSearchbarSelectedResultMethod(
773       event,
774       index,
775       "FX_URLBAR_SELECTED_RESULT_METHOD",
776       userSelectionBehavior
777     );
778   },
780   /**
781    * Records the method by which the user selected a searchbar result.
782    *
783    * @param {Event} event
784    *        The event that triggered the selection.
785    * @param {number} highlightedIndex
786    *        The index that the user chose in the popup, or -1 if there wasn't a
787    *        selection.
788    */
789   recordSearchbarSelectedResultMethod(event, highlightedIndex) {
790     this._recordUrlOrSearchbarSelectedResultMethod(
791       event,
792       highlightedIndex,
793       "FX_SEARCHBAR_SELECTED_RESULT_METHOD",
794       "none"
795     );
796   },
798   _recordUrlOrSearchbarSelectedResultMethod(
799     event,
800     highlightedIndex,
801     histogramID,
802     userSelectionBehavior
803   ) {
804     // If the contents of the histogram are changed then
805     // `UrlbarTestUtils.SELECTED_RESULT_METHODS` should also be updated.
807     let histogram = Services.telemetry.getHistogramById(histogramID);
808     // command events are from the one-off context menu.  Treat them as clicks.
809     // Note that we don't care about MouseEvent subclasses here, since
810     // those are not clicks.
811     let isClick =
812       event &&
813       (ChromeUtils.getClassName(event) == "MouseEvent" ||
814         event.type == "command");
815     let category;
816     if (isClick) {
817       category = "click";
818     } else if (highlightedIndex >= 0) {
819       switch (userSelectionBehavior) {
820         case "tab":
821           category = "tabEnterSelection";
822           break;
823         case "arrow":
824           category = "arrowEnterSelection";
825           break;
826         case "rightClick":
827           // Selected by right mouse button.
828           category = "rightClickEnter";
829           break;
830         default:
831           category = "enterSelection";
832       }
833     } else {
834       category = "enter";
835     }
836     histogram.add(category);
837   },
839   /**
840    * This gets called shortly after the SessionStore has finished restoring
841    * windows and tabs. It counts the open tabs and adds listeners to all the
842    * windows.
843    */
844   _setupAfterRestore() {
845     // Make sure to catch new chrome windows and subsession splits.
846     Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, true);
847     Services.obs.addObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, true);
849     // Attach the tabopen handlers to the existing Windows.
850     for (let win of Services.wm.getEnumerator("navigator:browser")) {
851       this._registerWindow(win);
852     }
854     // Get the initial tab and windows max counts.
855     const counts = getOpenTabsAndWinsCounts();
856     Services.telemetry.scalarSetMaximum(
857       MAX_TAB_COUNT_SCALAR_NAME,
858       counts.tabCount
859     );
860     Services.telemetry.scalarSetMaximum(
861       MAX_WINDOW_COUNT_SCALAR_NAME,
862       counts.winCount
863     );
864   },
866   _buildWidgetPositions() {
867     let widgetMap = new Map();
869     const toolbarState = nodeId => {
870       let value = Services.xulStore.getValue(
871         AppConstants.BROWSER_CHROME_URL,
872         nodeId,
873         "collapsed"
874       );
875       if (value) {
876         return value == "true" ? "off" : "on";
877       }
878       return "off";
879     };
881     widgetMap.set(
882       BROWSER_UI_CONTAINER_IDS.PersonalToolbar,
883       toolbarState("PersonalToolbar")
884     );
886     let menuBarHidden =
887       Services.xulStore.getValue(
888         AppConstants.BROWSER_CHROME_URL,
889         "toolbar-menubar",
890         "autohide"
891       ) != "false";
893     widgetMap.set("menu-toolbar", menuBarHidden ? "off" : "on");
895     widgetMap.set(
896       "drag-space",
897       Services.prefs.getBoolPref("browser.tabs.extraDragSpace") ? "on" : "off"
898     );
900     // Drawing in the titlebar means not showing the titlebar, hence the negation.
901     widgetMap.set(
902       "titlebar",
903       Services.prefs.getBoolPref("browser.tabs.drawInTitlebar", true)
904         ? "off"
905         : "on"
906     );
908     for (let area of CustomizableUI.areas) {
909       if (!(area in BROWSER_UI_CONTAINER_IDS)) {
910         continue;
911       }
913       let position = BROWSER_UI_CONTAINER_IDS[area];
914       if (area == "nav-bar") {
915         position = `${BROWSER_UI_CONTAINER_IDS[area]}-start`;
916       }
918       let widgets = CustomizableUI.getWidgetsInArea(area);
920       for (let widget of widgets) {
921         if (!widget) {
922           continue;
923         }
925         if (widget.id.startsWith("customizableui-special-")) {
926           continue;
927         }
929         if (area == "nav-bar" && widget.id == "urlbar-container") {
930           position = `${BROWSER_UI_CONTAINER_IDS[area]}-end`;
931           continue;
932         }
934         widgetMap.set(widget.id, position);
935       }
936     }
938     let actions = PageActions.actions;
939     for (let action of actions) {
940       if (action.pinnedToUrlbar) {
941         widgetMap.set(action.id, "pageaction-urlbar");
942       }
943     }
945     return widgetMap;
946   },
948   _getWidgetID(node) {
949     // We want to find a sensible ID for this element.
950     if (!node) {
951       return null;
952     }
954     // See if this is a customizable widget.
955     if (node.ownerDocument.URL == AppConstants.BROWSER_CHROME_URL) {
956       // First find if it is inside one of the customizable areas.
957       for (let area of CustomizableUI.areas) {
958         if (node.closest(`#${CSS.escape(area)}`)) {
959           for (let widget of CustomizableUI.getWidgetIdsInArea(area)) {
960             if (
961               // We care about the buttons on the tabs themselves.
962               widget == "tabbrowser-tabs" ||
963               // We care about the page action and other buttons in here.
964               widget == "urlbar-container" ||
965               // We care about the actual menu items.
966               widget == "menubar-items" ||
967               // We care about individual bookmarks here.
968               widget == "personal-bookmarks"
969             ) {
970               continue;
971             }
973             if (node.closest(`#${CSS.escape(widget)}`)) {
974               return widget;
975             }
976           }
977           break;
978         }
979       }
980     }
982     if (node.id) {
983       return node.id;
984     }
986     // A couple of special cases in the tabs.
987     for (let cls of ["bookmark-item", "tab-icon-sound", "tab-close-button"]) {
988       if (node.classList.contains(cls)) {
989         return cls;
990       }
991     }
993     // One of these will at least let us know what the widget is for.
994     let possibleAttributes = [
995       "preference",
996       "command",
997       "observes",
998       "data-l10n-id",
999     ];
1001     // The key attribute on key elements is the actual key to listen for.
1002     if (node.localName != "key") {
1003       possibleAttributes.unshift("key");
1004     }
1006     for (let idAttribute of possibleAttributes) {
1007       if (node.hasAttribute(idAttribute)) {
1008         return node.getAttribute(idAttribute);
1009       }
1010     }
1012     return this._getWidgetID(node.parentElement);
1013   },
1015   _getWidgetContainer(node) {
1016     if (node.localName == "key") {
1017       return "keyboard";
1018     }
1020     if (node.ownerDocument.URL == AppConstants.BROWSER_CHROME_URL) {
1021       // Find the container holding this element.
1022       for (let containerId of Object.keys(BROWSER_UI_CONTAINER_IDS)) {
1023         let container = node.ownerDocument.getElementById(containerId);
1024         if (container && container.contains(node)) {
1025           return BROWSER_UI_CONTAINER_IDS[containerId];
1026         }
1027       }
1028     } else if (node.ownerDocument.URL.startsWith("about:preferences")) {
1029       // Find the element's category.
1030       let container = node.closest("[data-category]");
1031       if (!container) {
1032         return null;
1033       }
1035       let pane = container.getAttribute("data-category");
1037       if (!PREFERENCES_PANES.includes(pane)) {
1038         pane = "paneUnknown";
1039       }
1041       return `preferences_${pane}`;
1042     }
1044     return null;
1045   },
1047   lastClickTarget: null,
1049   ignoreEvent(event) {
1050     IGNORABLE_EVENTS.set(event, true);
1051   },
1053   _recordCommand(event) {
1054     if (IGNORABLE_EVENTS.get(event)) {
1055       return;
1056     }
1058     let types = [event.type];
1059     let sourceEvent = event;
1060     while (sourceEvent.sourceEvent) {
1061       sourceEvent = sourceEvent.sourceEvent;
1062       types.push(sourceEvent.type);
1063     }
1065     let lastTarget = this.lastClickTarget?.get();
1066     if (
1067       lastTarget &&
1068       sourceEvent.type == "command" &&
1069       sourceEvent.target.contains(lastTarget)
1070     ) {
1071       // Ignore a command event triggered by a click.
1072       this.lastClickTarget = null;
1073       return;
1074     }
1076     this.lastClickTarget = null;
1078     if (sourceEvent.type == "click") {
1079       // Only care about main button clicks.
1080       if (sourceEvent.button != 0) {
1081         return;
1082       }
1084       // This click may trigger a command event so retain the target to be able
1085       // to dedupe that event.
1086       this.lastClickTarget = Cu.getWeakReference(sourceEvent.target);
1087     }
1089     // We should never see events from web content as they are fired in a
1090     // content process, but let's be safe.
1091     let url = sourceEvent.target.ownerDocument.documentURIObject;
1092     if (!url.schemeIs("chrome") && !url.schemeIs("about")) {
1093       return;
1094     }
1096     // This is what events targetted  at content will actually look like.
1097     if (sourceEvent.target.localName == "browser") {
1098       return;
1099     }
1101     // Find the actual element we're interested in.
1102     let node = sourceEvent.target;
1103     while (!UI_TARGET_ELEMENTS.includes(node.localName)) {
1104       node = node.parentNode;
1105       if (!node) {
1106         // A click on a space or label or something we're not interested in.
1107         return;
1108       }
1109     }
1111     let item = this._getWidgetID(node);
1112     let source = this._getWidgetContainer(node);
1114     if (item && source) {
1115       let scalar = `browser.ui.interaction.${source.replace("-", "_")}`;
1116       Services.telemetry.keyedScalarAdd(scalar, telemetryId(item), 1);
1117     }
1118   },
1120   /**
1121    * Listens for UI interactions in the window.
1122    */
1123   _addUsageListeners(win) {
1124     // Listen for command events from the UI.
1125     win.addEventListener("command", event => this._recordCommand(event), true);
1126     win.addEventListener("click", event => this._recordCommand(event), true);
1127   },
1129   /**
1130    * A public version of the private method to take care of the `nav-bar-start`,
1131    * `nav-bar-end` thing that callers shouldn't have to care about. It also
1132    * accepts the DOM ids for the areas rather than the cleaner ones we report
1133    * to telemetry.
1134    */
1135   recordWidgetChange(widgetId, newPos, reason) {
1136     try {
1137       if (newPos) {
1138         newPos = BROWSER_UI_CONTAINER_IDS[newPos];
1139       }
1141       if (newPos == "nav-bar") {
1142         let { position } = CustomizableUI.getPlacementOfWidget(widgetId);
1143         let { position: urlPosition } = CustomizableUI.getPlacementOfWidget(
1144           "urlbar-container"
1145         );
1146         newPos = newPos + (urlPosition > position ? "-start" : "-end");
1147       }
1149       this._recordWidgetChange(widgetId, newPos, reason);
1150     } catch (e) {
1151       console.error(e);
1152     }
1153   },
1155   recordToolbarVisibility(toolbarId, newState, reason) {
1156     this._recordWidgetChange(
1157       BROWSER_UI_CONTAINER_IDS[toolbarId],
1158       newState ? "on" : "off",
1159       reason
1160     );
1161   },
1163   _recordWidgetChange(widgetId, newPos, reason) {
1164     // In some cases (like when add-ons are detected during startup) this gets
1165     // called before we've reported the initial positions. Ignore such cases.
1166     if (!this.widgetMap) {
1167       return;
1168     }
1170     if (widgetId == "urlbar-container") {
1171       // We don't report the position of the url bar, it is after nav-bar-start
1172       // and before nav-bar-end. But moving it means the widgets around it have
1173       // effectively moved so update those.
1174       let position = "nav-bar-start";
1175       let widgets = CustomizableUI.getWidgetsInArea("nav-bar");
1177       for (let widget of widgets) {
1178         if (!widget) {
1179           continue;
1180         }
1182         if (widget.id.startsWith("customizableui-special-")) {
1183           continue;
1184         }
1186         if (widget.id == "urlbar-container") {
1187           position = "nav-bar-end";
1188           continue;
1189         }
1191         // This will do nothing if the position hasn't changed.
1192         this._recordWidgetChange(widget.id, position, reason);
1193       }
1195       return;
1196     }
1198     let oldPos = this.widgetMap.get(widgetId);
1199     if (oldPos == newPos) {
1200       return;
1201     }
1203     let action = "move";
1205     if (!oldPos) {
1206       action = "add";
1207     } else if (!newPos) {
1208       action = "remove";
1209     }
1211     let key = `${telemetryId(widgetId, false)}_${action}_${oldPos ??
1212       "na"}_${newPos ?? "na"}_${reason}`;
1213     Services.telemetry.keyedScalarAdd("browser.ui.customized_widgets", key, 1);
1215     if (newPos) {
1216       this.widgetMap.set(widgetId, newPos);
1217     } else {
1218       this.widgetMap.delete(widgetId);
1219     }
1220   },
1222   _recordUITelemetry() {
1223     this.widgetMap = this._buildWidgetPositions();
1225     for (let [widgetId, position] of this.widgetMap.entries()) {
1226       let key = `${telemetryId(widgetId, false)}_pinned_${position}`;
1227       Services.telemetry.keyedScalarSet(
1228         "browser.ui.toolbar_widgets",
1229         key,
1230         true
1231       );
1232     }
1233   },
1235   /**
1236    * Adds listeners to a single chrome window.
1237    */
1238   _registerWindow(win) {
1239     this._addUsageListeners(win);
1241     win.addEventListener("unload", this);
1242     win.addEventListener("TabOpen", this, true);
1243     win.addEventListener("TabPinned", this, true);
1245     win.gBrowser.tabContainer.addEventListener(TAB_RESTORING_TOPIC, this);
1246     win.gBrowser.addTabsProgressListener(URICountListener);
1247   },
1249   /**
1250    * Removes listeners from a single chrome window.
1251    */
1252   _unregisterWindow(win) {
1253     win.removeEventListener("unload", this);
1254     win.removeEventListener("TabOpen", this, true);
1255     win.removeEventListener("TabPinned", this, true);
1257     win.defaultView.gBrowser.tabContainer.removeEventListener(
1258       TAB_RESTORING_TOPIC,
1259       this
1260     );
1261     win.defaultView.gBrowser.removeTabsProgressListener(URICountListener);
1262   },
1264   /**
1265    * Updates the tab counts.
1266    * @param {Object} [counts] The counts returned by `getOpenTabsAndWindowCounts`.
1267    */
1268   _onTabOpen({ tabCount, loadedTabCount }) {
1269     // Update the "tab opened" count and its maximum.
1270     Services.telemetry.scalarAdd(TAB_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
1271     Services.telemetry.scalarSetMaximum(MAX_TAB_COUNT_SCALAR_NAME, tabCount);
1273     this._recordTabCounts({ tabCount, loadedTabCount });
1274   },
1276   _onTabPinned(target) {
1277     const pinnedTabs = getPinnedTabsCount();
1279     // Update the "tab pinned" count and its maximum.
1280     Services.telemetry.scalarAdd(TAB_PINNED_EVENT_COUNT_SCALAR_NAME, 1);
1281     Services.telemetry.scalarSetMaximum(
1282       MAX_TAB_PINNED_COUNT_SCALAR_NAME,
1283       pinnedTabs
1284     );
1285   },
1287   /**
1288    * Tracks the window count and registers the listeners for the tab count.
1289    * @param{Object} win The window object.
1290    */
1291   _onWindowOpen(win) {
1292     // Make sure to have a |nsIDOMWindow|.
1293     if (!(win instanceof Ci.nsIDOMWindow)) {
1294       return;
1295     }
1297     let onLoad = () => {
1298       win.removeEventListener("load", onLoad);
1300       // Ignore non browser windows.
1301       if (
1302         win.document.documentElement.getAttribute("windowtype") !=
1303         "navigator:browser"
1304       ) {
1305         return;
1306       }
1308       this._registerWindow(win);
1309       // Track the window open event and check the maximum.
1310       const counts = getOpenTabsAndWinsCounts();
1311       Services.telemetry.scalarAdd(WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
1312       Services.telemetry.scalarSetMaximum(
1313         MAX_WINDOW_COUNT_SCALAR_NAME,
1314         counts.winCount
1315       );
1317       // We won't receive the "TabOpen" event for the first tab within a new window.
1318       // Account for that.
1319       this._onTabOpen(counts);
1320     };
1321     win.addEventListener("load", onLoad);
1322   },
1324   /**
1325    * Record telemetry about the given tab counts.
1326    *
1327    * Telemetry for each count will only be recorded if the value isn't
1328    * `undefined`.
1329    *
1330    * @param {object} [counts] The tab counts to register with telemetry.
1331    * @param {number} [counts.tabCount] The number of tabs in all browsers.
1332    * @param {number} [counts.loadedTabCount] The number of loaded (i.e., not
1333    *                                         pending) tabs in all browsers.
1334    */
1335   _recordTabCounts({ tabCount, loadedTabCount }) {
1336     let currentTime = Date.now();
1337     if (
1338       tabCount !== undefined &&
1339       currentTime > this._lastRecordTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1340     ) {
1341       Services.telemetry.getHistogramById("TAB_COUNT").add(tabCount);
1342       this._lastRecordTabCount = currentTime;
1343     }
1345     if (
1346       loadedTabCount !== undefined &&
1347       currentTime >
1348         this._lastRecordLoadedTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1349     ) {
1350       Services.telemetry
1351         .getHistogramById("LOADED_TAB_COUNT")
1352         .add(loadedTabCount);
1353       this._lastRecordLoadedTabCount = currentTime;
1354     }
1355   },
1357   _checkProfileCountFileSchema(fileData) {
1358     // Verifies that the schema of the file is the expected schema
1359     if (typeof fileData.version != "string") {
1360       throw new Error("Schema Mismatch Error: Bad type for 'version' field");
1361     }
1362     if (!Array.isArray(fileData.profileTelemetryIds)) {
1363       throw new Error(
1364         "Schema Mismatch Error: Bad type for 'profileTelemetryIds' field"
1365       );
1366     }
1367     for (let profileTelemetryId of fileData.profileTelemetryIds) {
1368       if (typeof profileTelemetryId != "string") {
1369         throw new Error(
1370           "Schema Mismatch Error: Bad type for an element of 'profileTelemetryIds'"
1371         );
1372       }
1373     }
1374   },
1376   // Reports the number of Firefox profiles on this machine to telemetry.
1377   async reportProfileCount() {
1378     if (AppConstants.platform != "win") {
1379       // This is currently a windows-only feature.
1380       return;
1381     }
1383     // To report only as much data as we need, we will bucket our values.
1384     // Rather than the raw value, we will report the greatest value in the list
1385     // below that is no larger than the raw value.
1386     const buckets = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000, 10000];
1388     // We need both the C:\ProgramData\Mozilla directory and the install
1389     // directory hash to create the profile count file path. We can easily
1390     // reassemble this from the update directory, which looks like:
1391     // C:\ProgramData\Mozilla\updates\hash
1392     // Retrieving the directory this way also ensures that the "Mozilla"
1393     // directory is created with the correct permissions.
1394     // The ProgramData directory, by default, grants write permissions only to
1395     // file creators. The directory service calls GetCommonUpdateDirectory,
1396     // which makes sure the the directory is created with user-writable
1397     // permissions.
1398     const updateDirectory = BrowserUsageTelemetry.Policy.getUpdateDirectory();
1399     const hash = updateDirectory.leafName;
1400     const profileCountFilename = "profile_count_" + hash + ".json";
1401     let profileCountFile = updateDirectory.parent.parent;
1402     profileCountFile.append(profileCountFilename);
1404     let readError = false;
1405     let fileData;
1406     try {
1407       let json = await BrowserUsageTelemetry.Policy.readProfileCountFile(
1408         profileCountFile.path
1409       );
1410       fileData = JSON.parse(json);
1411       BrowserUsageTelemetry._checkProfileCountFileSchema(fileData);
1412     } catch (ex) {
1413       // Note that since this also catches the "no such file" error, this is
1414       // always the template that we use when writing to the file for the first
1415       // time.
1416       fileData = { version: "1", profileTelemetryIds: [] };
1417       if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
1418         Cu.reportError(ex);
1419         // Don't just return here on a read error. We need to send the error
1420         // value to telemetry and we want to attempt to fix the file.
1421         // However, we will still report an error for this ping, even if we
1422         // fix the file. This is to prevent always sending a profile count of 1
1423         // if, for some reason, we always get a read error but never a write
1424         // error.
1425         readError = true;
1426       }
1427     }
1429     let writeError = false;
1430     let currentTelemetryId = await BrowserUsageTelemetry.Policy.getTelemetryClientId();
1431     // Don't add our telemetry ID to the file if we've already reached the
1432     // largest bucket. This prevents the file size from growing forever.
1433     if (
1434       !fileData.profileTelemetryIds.includes(currentTelemetryId) &&
1435       fileData.profileTelemetryIds.length < Math.max(...buckets)
1436     ) {
1437       fileData.profileTelemetryIds.push(currentTelemetryId);
1438       try {
1439         await BrowserUsageTelemetry.Policy.writeProfileCountFile(
1440           profileCountFile.path,
1441           JSON.stringify(fileData)
1442         );
1443       } catch (ex) {
1444         Cu.reportError(ex);
1445         writeError = true;
1446       }
1447     }
1449     // Determine the bucketed value to report
1450     let rawProfileCount = fileData.profileTelemetryIds.length;
1451     let valueToReport = 0;
1452     for (let bucket of buckets) {
1453       if (bucket <= rawProfileCount && bucket > valueToReport) {
1454         valueToReport = bucket;
1455       }
1456     }
1458     if (readError || writeError) {
1459       // We convey errors via a profile count of 0.
1460       valueToReport = 0;
1461     }
1463     Services.telemetry.scalarSet(
1464       "browser.engagement.profile_count",
1465       valueToReport
1466     );
1467   },
1469   /**
1470    * Record telemetry about the ratio of number of site origins per number of
1471    * loaded tabs.
1472    *
1473    * This will only record the telemetry if it has been five minutes since the
1474    * last recording.
1475    */
1476   _recordSiteOriginsPerLoadedTabs() {
1477     const currentTime = Date.now();
1478     if (
1479       currentTime >
1480       this._lastRecordSiteOriginsPerLoadedTabs + MINIMUM_TAB_COUNT_INTERVAL_MS
1481     ) {
1482       this._lastRecordSiteOriginsPerLoadedTabs = currentTime;
1483       // If this is the first load, we discard it because it is likely just the
1484       // browser opening for the first time.
1485       if (this._lastRecordSiteOriginsPerLoadedTabs === 0) {
1486         return;
1487       }
1489       const { loadedTabCount } = getOpenTabsAndWinsCounts();
1490       const siteOrigins = BrowserUtils.computeSiteOriginCount(
1491         Services.wm.getEnumerator("navigator:browser"),
1492         false
1493       );
1494       const histogramId = this._getSiteOriginHistogram(loadedTabCount);
1495       // Telemetry doesn't support float values.
1496       Services.telemetry
1497         .getHistogramById(histogramId)
1498         .add(Math.trunc((100 * siteOrigins) / loadedTabCount));
1499     }
1500   },
1502   _siteOriginHistogramIds: [
1503     [1, 1, "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_1"],
1504     [2, 4, "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_2_4"],
1505     [5, 9, "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_5_9"],
1506     [10, 14, "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_10_14"],
1507     [15, 19, "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_15_19"],
1508     [20, 24, "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_20_24"],
1509     [25, 29, "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_25_29"],
1510     [31, 34, "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_30_34"],
1511     [35, 39, "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_35_39"],
1512     [40, 44, "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_40_44"],
1513     [45, 49, "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_45_49"],
1514   ],
1516   /**
1517    * Return the appropriate histogram ID for the given loaded tab count.
1518    *
1519    * Unique site origin telemetry is split across several histograms so that it
1520    * can approximate a unique site origin vs loaded tab count curve.
1521    *
1522    * @param {number} [loadedTabCount] The number of loaded tabs.
1523    */
1524   _getSiteOriginHistogram(loadedTabCount) {
1525     for (const [min, max, histogramId] of this._siteOriginHistogramIds) {
1526       if (min <= loadedTabCount && loadedTabCount <= max) {
1527         return histogramId;
1528       }
1529     }
1530     return "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_50_PLUS";
1531   },
1534 // Used by nsIBrowserUsage
1535 function getUniqueDomainsVisitedInPast24Hours() {
1536   return URICountListener.uniqueDomainsVisitedInPast24Hours;