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/. */
8 var EXPORTED_SYMBOLS = [
9 "BrowserUsageTelemetry",
10 "getUniqueDomainsVisitedInPast24Hours",
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",
35 // This pref is in seconds!
36 XPCOMUtils.defineLazyPreferenceGetter(
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";
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 = [
81 const KNOWN_ONEOFF_SOURCES = [
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 = [
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 = [
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) {
146 let pos = KNOWN_ADDONS.indexOf(id);
148 pos = KNOWN_ADDONS.length;
149 KNOWN_ADDONS.push(id);
151 return `addon${pos}`;
154 if (widgetId.endsWith("-browser-action")) {
156 widgetId.substring(0, widgetId.length - "-browser-action".length)
158 } else if (widgetId.startsWith("pageAction-")) {
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);
167 let action = PageActions.actionForID(actionId);
168 widgetId = action?._isMozillaAction ? actionId : addonId(actionId);
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")) {
179 widgetId.substring(0, widgetId.length - "-sidebar-action".length)
184 return widgetId.replace(/_/g, "-");
187 function getOpenTabsAndWinsCounts() {
188 let loadedTabCount = 0;
192 for (let win of Services.wm.getEnumerator("navigator:browser")) {
194 tabCount += win.gBrowser.tabs.length;
195 for (const tab of win.gBrowser.tabs) {
196 if (tab.getAttribute("pending") !== "true") {
202 return { loadedTabCount, tabCount, winCount };
205 function getPinnedTabsCount() {
208 for (let win of Services.wm.getEnumerator("navigator:browser")) {
209 pinnedTabs += [...win.ownerGlobal.gBrowser.tabs].filter(t => t.pinned)
216 function shouldRecordSearchCount(tabbrowser) {
218 !PrivateBrowsingUtils.isWindowPrivate(tabbrowser.ownerGlobal) ||
219 !Services.prefs.getBoolPref("browser.engagement.search_counts.pbm", false)
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(),
234 // Only consider http(s) schemas.
235 return uri.schemeIs("http") || uri.schemeIs("https");
238 addRestoredURI(browser, uri) {
239 if (!this.isHttpURI(uri)) {
243 this._restoredURIsMap.set(browser, uri.spec);
246 onStateChange(browser, webProgress, request, stateFlags, status) {
248 !webProgress.isTopLevel ||
249 !(stateFlags & Ci.nsIWebProgressListener.STATE_STOP) ||
250 !(stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
255 if (!(request instanceof Ci.nsIChannel) || !this.isHttpURI(request.URI)) {
259 BrowserUsageTelemetry._recordSiteOriginsPerLoadedTabs();
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);
268 // Don't count this URI if it's an error page.
269 if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
273 // We only care about top level loads.
274 if (!webProgress.isTopLevel) {
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.
285 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
290 // Don't include URI and domain counts when in private mode.
292 !PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal) ||
293 Services.prefs.getBoolPref(
294 "browser.engagement.total_uri_count.pbm",
298 // Track URI loads, even if they're not http(s).
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);
311 // Don't count about:blank and similar pages, as they would artificially
312 // inflate the counts.
313 if (browser.ownerGlobal.gInitialPages.includes(uriSpec)) {
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);
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"
328 if (shouldCountURI) {
329 Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
332 if (!this.isHttpURI(uri)) {
337 shouldRecordSearchCount(browser.getTabBrowser()) &&
338 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
340 SearchTelemetry.updateTrackingStatus(browser, uriSpec);
343 if (!shouldCountURI) {
347 // Update the URI counts.
348 Services.telemetry.scalarAdd(TOTAL_URI_COUNT_SCALAR_NAME, 1);
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.
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);
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,
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);
385 * Reset the counts. This should be called when breaking a session in Telemetry.
388 this._domainSet.clear();
392 * Returns the number of unique domains visited in this session during the
395 get uniqueDomainsVisitedInPast24Hours() {
396 return this._domain24hrSet.size;
400 * Resets the number of unique domains visited in this session.
402 resetUniqueDomainsVisitedInPast24Hours() {
403 this._timeouts.forEach(timeoutId => clearTimeout(timeoutId));
404 this._timeouts.clear();
405 this._domain24hrSet.clear();
408 QueryInterface: ChromeUtils.generateQI([
409 "nsIWebProgressListener",
410 "nsISupportsWeakReference",
414 let BrowserUsageTelemetry = {
416 * This is a policy object used to override behavior for testing.
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),
430 this._lastRecordTabCount = 0;
431 this._lastRecordLoadedTabCount = 0;
432 this._lastRecordSiteOriginsPerLoadedTabs = 0;
433 this._setupAfterRestore();
436 Services.prefs.addObserver("browser.tabs.extraDragSpace", this);
437 Services.prefs.addObserver("browser.tabs.drawInTitlebar", this);
439 this._recordUITelemetry();
443 * Resets the masked add-on identifiers. Only for use in tests.
446 KNOWN_ADDONS.length = 0;
450 * Handle subsession splits in the parent process.
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
456 const counts = getOpenTabsAndWinsCounts();
457 Services.telemetry.scalarSetMaximum(
458 MAX_TAB_COUNT_SCALAR_NAME,
461 Services.telemetry.scalarSetMaximum(
462 MAX_WINDOW_COUNT_SCALAR_NAME,
466 // Reset the URI counter.
467 URICountListener.reset();
470 QueryInterface: ChromeUtils.generateQI([
472 "nsISupportsWeakReference",
479 Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC);
480 Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC);
483 observe(subject, topic, data) {
485 case DOMWINDOW_OPENED_TOPIC:
486 this._onWindowOpen(subject);
488 case TELEMETRY_SUBSESSIONSPLIT_TOPIC:
489 this.afterSubsessionSplit();
491 case "nsPref:changed":
493 case "browser.tabs.extraDragSpace":
494 this._recordWidgetChange(
496 Services.prefs.getBoolPref("browser.tabs.extraDragSpace")
502 case "browser.tabs.drawInTitlebar":
503 this._recordWidgetChange(
505 Services.prefs.getBoolPref("browser.tabs.drawInTitlebar")
517 switch (event.type) {
519 this._onTabOpen(getOpenTabsAndWinsCounts());
525 this._unregisterWindow(event.target);
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 });
541 * The main entry point for recording search related Telemetry. This includes
542 * search counts and engagement measurements.
544 * Telemetry records only search counts per engine and action origin, but
545 * nothing pertaining to the search contents themselves.
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
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.
567 recordSearch(tabbrowser, engine, source, details = {}) {
568 if (!shouldRecordSearchCount(tabbrowser)) {
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
583 if (["urlbar", "searchbar"].includes(source)) {
584 histogram.add(countIdSource);
585 PartnerLinkAttribution.makeSearchEngineRequest(
588 ).catch(Cu.reportError);
591 throw new Error("Unknown source for one-off search: " + source);
594 if (!KNOWN_SEARCH_SOURCES.includes(source)) {
595 throw new Error("Unknown source for search: " + source);
599 engine.isAppProvided &&
600 engine.aliases.includes(details.alias)
602 // This is a keyword search using an AppProvided engine.
603 // Record the source as "alias", not "urlbar".
604 histogram.add(countIdPrefix + "alias");
606 histogram.add(countIdSource);
610 // Dispatch the search signal to other handlers.
611 this._handleSearchAction(engine, source, details);
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(
626 let scalarKey = action ? "search_" + action : "search";
627 Services.telemetry.keyedScalarAdd(
628 "browser.engagement.navigation." + source,
632 Services.telemetry.recordEvent("navigation", "search", source, action, {
633 engine: engine.telemetryId,
638 * Records entry into the Urlbar's search mode.
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
646 recordSearchMode(searchMode) {
647 // Search mode preview is not search mode. Recording it would just create
649 if (searchMode.isPreview) {
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
658 if (!engine.isAppProvided) {
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";
667 scalarKey = searchMode.engineName;
669 } else if (searchMode.source) {
670 scalarKey = UrlbarUtils.getResultSourceName(searchMode.source) || "other";
673 Services.telemetry.keyedScalarAdd(
674 "urlbar.searchmode." + searchMode.entry,
680 _handleSearchAction(engine, source, details) {
683 case "oneoff-urlbar":
685 case "oneoff-searchbar":
686 case "unknown": // Edge case: this is the searchbar (see bug 1195733 comment 7).
687 this._handleSearchAndUrlbar(engine, source, details);
689 case "urlbar-searchmode":
690 this._handleSearchAndUrlbar(engine, "urlbar_searchmode", details);
693 this._recordSearch(engine, details.url, "about_home", "enter");
696 this._recordSearch(engine, details.url, "about_newtab", "enter");
701 this._recordSearch(engine, details.url, source);
707 * This function handles the "urlbar", "urlbar-oneoff", "searchbar" and
708 * "searchbar-oneoff" sources.
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.
717 source === "unknown" ? "searchbar" : source.replace("oneoff-", "");
719 const isOneOff = !!details.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)) {
731 // If that's a legit one-off search signal, record it using the relative key.
732 this._recordSearch(engine, details.url, sourceName, "oneoff");
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");
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");
745 } else if (details.alias) {
746 // This one came from a search that used an alias.
747 this._recordSearch(engine, details.url, sourceName, "alias");
751 // The search signal was generated by typing something and pressing enter.
752 this._recordSearch(engine, details.url, sourceName, "enter");
756 * Records the method by which the user selected a result from the urlbar.
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
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".
767 recordUrlbarSelectedResultMethod(
770 userSelectionBehavior = "none"
772 this._recordUrlOrSearchbarSelectedResultMethod(
775 "FX_URLBAR_SELECTED_RESULT_METHOD",
776 userSelectionBehavior
781 * Records the method by which the user selected a searchbar result.
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
789 recordSearchbarSelectedResultMethod(event, highlightedIndex) {
790 this._recordUrlOrSearchbarSelectedResultMethod(
793 "FX_SEARCHBAR_SELECTED_RESULT_METHOD",
798 _recordUrlOrSearchbarSelectedResultMethod(
802 userSelectionBehavior
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.
813 (ChromeUtils.getClassName(event) == "MouseEvent" ||
814 event.type == "command");
818 } else if (highlightedIndex >= 0) {
819 switch (userSelectionBehavior) {
821 category = "tabEnterSelection";
824 category = "arrowEnterSelection";
827 // Selected by right mouse button.
828 category = "rightClickEnter";
831 category = "enterSelection";
836 histogram.add(category);
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
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);
854 // Get the initial tab and windows max counts.
855 const counts = getOpenTabsAndWinsCounts();
856 Services.telemetry.scalarSetMaximum(
857 MAX_TAB_COUNT_SCALAR_NAME,
860 Services.telemetry.scalarSetMaximum(
861 MAX_WINDOW_COUNT_SCALAR_NAME,
866 _buildWidgetPositions() {
867 let widgetMap = new Map();
869 const toolbarState = nodeId => {
870 let value = Services.xulStore.getValue(
871 AppConstants.BROWSER_CHROME_URL,
876 return value == "true" ? "off" : "on";
882 BROWSER_UI_CONTAINER_IDS.PersonalToolbar,
883 toolbarState("PersonalToolbar")
887 Services.xulStore.getValue(
888 AppConstants.BROWSER_CHROME_URL,
893 widgetMap.set("menu-toolbar", menuBarHidden ? "off" : "on");
897 Services.prefs.getBoolPref("browser.tabs.extraDragSpace") ? "on" : "off"
900 // Drawing in the titlebar means not showing the titlebar, hence the negation.
903 Services.prefs.getBoolPref("browser.tabs.drawInTitlebar", true)
908 for (let area of CustomizableUI.areas) {
909 if (!(area in BROWSER_UI_CONTAINER_IDS)) {
913 let position = BROWSER_UI_CONTAINER_IDS[area];
914 if (area == "nav-bar") {
915 position = `${BROWSER_UI_CONTAINER_IDS[area]}-start`;
918 let widgets = CustomizableUI.getWidgetsInArea(area);
920 for (let widget of widgets) {
925 if (widget.id.startsWith("customizableui-special-")) {
929 if (area == "nav-bar" && widget.id == "urlbar-container") {
930 position = `${BROWSER_UI_CONTAINER_IDS[area]}-end`;
934 widgetMap.set(widget.id, position);
938 let actions = PageActions.actions;
939 for (let action of actions) {
940 if (action.pinnedToUrlbar) {
941 widgetMap.set(action.id, "pageaction-urlbar");
949 // We want to find a sensible ID for this element.
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)) {
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"
973 if (node.closest(`#${CSS.escape(widget)}`)) {
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)) {
993 // One of these will at least let us know what the widget is for.
994 let possibleAttributes = [
1001 // The key attribute on key elements is the actual key to listen for.
1002 if (node.localName != "key") {
1003 possibleAttributes.unshift("key");
1006 for (let idAttribute of possibleAttributes) {
1007 if (node.hasAttribute(idAttribute)) {
1008 return node.getAttribute(idAttribute);
1012 return this._getWidgetID(node.parentElement);
1015 _getWidgetContainer(node) {
1016 if (node.localName == "key") {
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];
1028 } else if (node.ownerDocument.URL.startsWith("about:preferences")) {
1029 // Find the element's category.
1030 let container = node.closest("[data-category]");
1035 let pane = container.getAttribute("data-category");
1037 if (!PREFERENCES_PANES.includes(pane)) {
1038 pane = "paneUnknown";
1041 return `preferences_${pane}`;
1047 lastClickTarget: null,
1049 ignoreEvent(event) {
1050 IGNORABLE_EVENTS.set(event, true);
1053 _recordCommand(event) {
1054 if (IGNORABLE_EVENTS.get(event)) {
1058 let types = [event.type];
1059 let sourceEvent = event;
1060 while (sourceEvent.sourceEvent) {
1061 sourceEvent = sourceEvent.sourceEvent;
1062 types.push(sourceEvent.type);
1065 let lastTarget = this.lastClickTarget?.get();
1068 sourceEvent.type == "command" &&
1069 sourceEvent.target.contains(lastTarget)
1071 // Ignore a command event triggered by a click.
1072 this.lastClickTarget = null;
1076 this.lastClickTarget = null;
1078 if (sourceEvent.type == "click") {
1079 // Only care about main button clicks.
1080 if (sourceEvent.button != 0) {
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);
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")) {
1096 // This is what events targetted at content will actually look like.
1097 if (sourceEvent.target.localName == "browser") {
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;
1106 // A click on a space or label or something we're not interested in.
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);
1121 * Listens for UI interactions in the window.
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);
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
1135 recordWidgetChange(widgetId, newPos, reason) {
1138 newPos = BROWSER_UI_CONTAINER_IDS[newPos];
1141 if (newPos == "nav-bar") {
1142 let { position } = CustomizableUI.getPlacementOfWidget(widgetId);
1143 let { position: urlPosition } = CustomizableUI.getPlacementOfWidget(
1146 newPos = newPos + (urlPosition > position ? "-start" : "-end");
1149 this._recordWidgetChange(widgetId, newPos, reason);
1155 recordToolbarVisibility(toolbarId, newState, reason) {
1156 this._recordWidgetChange(
1157 BROWSER_UI_CONTAINER_IDS[toolbarId],
1158 newState ? "on" : "off",
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) {
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) {
1182 if (widget.id.startsWith("customizableui-special-")) {
1186 if (widget.id == "urlbar-container") {
1187 position = "nav-bar-end";
1191 // This will do nothing if the position hasn't changed.
1192 this._recordWidgetChange(widget.id, position, reason);
1198 let oldPos = this.widgetMap.get(widgetId);
1199 if (oldPos == newPos) {
1203 let action = "move";
1207 } else if (!newPos) {
1211 let key = `${telemetryId(widgetId, false)}_${action}_${oldPos ??
1212 "na"}_${newPos ?? "na"}_${reason}`;
1213 Services.telemetry.keyedScalarAdd("browser.ui.customized_widgets", key, 1);
1216 this.widgetMap.set(widgetId, newPos);
1218 this.widgetMap.delete(widgetId);
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",
1236 * Adds listeners to a single chrome window.
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);
1250 * Removes listeners from a single chrome window.
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,
1261 win.defaultView.gBrowser.removeTabsProgressListener(URICountListener);
1265 * Updates the tab counts.
1266 * @param {Object} [counts] The counts returned by `getOpenTabsAndWindowCounts`.
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 });
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,
1288 * Tracks the window count and registers the listeners for the tab count.
1289 * @param{Object} win The window object.
1291 _onWindowOpen(win) {
1292 // Make sure to have a |nsIDOMWindow|.
1293 if (!(win instanceof Ci.nsIDOMWindow)) {
1297 let onLoad = () => {
1298 win.removeEventListener("load", onLoad);
1300 // Ignore non browser windows.
1302 win.document.documentElement.getAttribute("windowtype") !=
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,
1317 // We won't receive the "TabOpen" event for the first tab within a new window.
1318 // Account for that.
1319 this._onTabOpen(counts);
1321 win.addEventListener("load", onLoad);
1325 * Record telemetry about the given tab counts.
1327 * Telemetry for each count will only be recorded if the value isn't
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.
1335 _recordTabCounts({ tabCount, loadedTabCount }) {
1336 let currentTime = Date.now();
1338 tabCount !== undefined &&
1339 currentTime > this._lastRecordTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1341 Services.telemetry.getHistogramById("TAB_COUNT").add(tabCount);
1342 this._lastRecordTabCount = currentTime;
1346 loadedTabCount !== undefined &&
1348 this._lastRecordLoadedTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1351 .getHistogramById("LOADED_TAB_COUNT")
1352 .add(loadedTabCount);
1353 this._lastRecordLoadedTabCount = currentTime;
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");
1362 if (!Array.isArray(fileData.profileTelemetryIds)) {
1364 "Schema Mismatch Error: Bad type for 'profileTelemetryIds' field"
1367 for (let profileTelemetryId of fileData.profileTelemetryIds) {
1368 if (typeof profileTelemetryId != "string") {
1370 "Schema Mismatch Error: Bad type for an element of 'profileTelemetryIds'"
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.
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
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;
1407 let json = await BrowserUsageTelemetry.Policy.readProfileCountFile(
1408 profileCountFile.path
1410 fileData = JSON.parse(json);
1411 BrowserUsageTelemetry._checkProfileCountFileSchema(fileData);
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
1416 fileData = { version: "1", profileTelemetryIds: [] };
1417 if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
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
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.
1434 !fileData.profileTelemetryIds.includes(currentTelemetryId) &&
1435 fileData.profileTelemetryIds.length < Math.max(...buckets)
1437 fileData.profileTelemetryIds.push(currentTelemetryId);
1439 await BrowserUsageTelemetry.Policy.writeProfileCountFile(
1440 profileCountFile.path,
1441 JSON.stringify(fileData)
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;
1458 if (readError || writeError) {
1459 // We convey errors via a profile count of 0.
1463 Services.telemetry.scalarSet(
1464 "browser.engagement.profile_count",
1470 * Record telemetry about the ratio of number of site origins per number of
1473 * This will only record the telemetry if it has been five minutes since the
1476 _recordSiteOriginsPerLoadedTabs() {
1477 const currentTime = Date.now();
1480 this._lastRecordSiteOriginsPerLoadedTabs + MINIMUM_TAB_COUNT_INTERVAL_MS
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) {
1489 const { loadedTabCount } = getOpenTabsAndWinsCounts();
1490 const siteOrigins = BrowserUtils.computeSiteOriginCount(
1491 Services.wm.getEnumerator("navigator:browser"),
1494 const histogramId = this._getSiteOriginHistogram(loadedTabCount);
1495 // Telemetry doesn't support float values.
1497 .getHistogramById(histogramId)
1498 .add(Math.trunc((100 * siteOrigins) / loadedTabCount));
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"],
1517 * Return the appropriate histogram ID for the given loaded tab count.
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.
1522 * @param {number} [loadedTabCount] The number of loaded tabs.
1524 _getSiteOriginHistogram(loadedTabCount) {
1525 for (const [min, max, histogramId] of this._siteOriginHistogramIds) {
1526 if (min <= loadedTabCount && loadedTabCount <= max) {
1530 return "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_LOADED_TABS_50_PLUS";
1534 // Used by nsIBrowserUsage
1535 function getUniqueDomainsVisitedInPast24Hours() {
1536 return URICountListener.uniqueDomainsVisitedInPast24Hours;