1 /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
11 ChromeUtils.defineESModuleGetters(lazy, {
12 ClientID: "resource://gre/modules/ClientID.sys.mjs",
13 CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
14 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
15 PageActions: "resource:///modules/PageActions.sys.mjs",
16 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
17 SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs",
18 SearchSERPTelemetryUtils: "resource:///modules/SearchSERPTelemetry.sys.mjs",
21 "resource://gre/modules/components-utils/WindowsInstallsInfo.sys.mjs",
23 clearTimeout: "resource://gre/modules/Timer.sys.mjs",
24 setTimeout: "resource://gre/modules/Timer.sys.mjs",
27 // This pref is in seconds!
28 XPCOMUtils.defineLazyPreferenceGetter(
30 "gRecentVisitedOriginsExpiry",
31 "browser.engagement.recent_visited_origins.expiry"
33 XPCOMUtils.defineLazyPreferenceGetter(
35 "sidebarVerticalTabs",
36 "sidebar.verticalTabs",
38 (_aPreference, _previousValue, isVertical) => {
39 // Copy max tab counts into the "new" scalars.
40 Services.telemetry.scalarSetMaximum(
42 ? VERTICAL_MAX_TAB_COUNT_SCALAR_NAME
43 : MAX_TAB_COUNT_SCALAR_NAME,
44 getOpenTabsAndWinsCounts().tabCount
46 Services.telemetry.scalarSetMaximum(
48 ? VERTICAL_MAX_TAB_PINNED_COUNT_SCALAR_NAME
49 : MAX_TAB_PINNED_COUNT_SCALAR_NAME,
55 // The upper bound for the count of the visited unique domain names.
56 const MAX_UNIQUE_VISITED_DOMAINS = 100;
58 // Observed topic names.
59 const TAB_RESTORING_TOPIC = "SSTabRestoring";
60 const TELEMETRY_SUBSESSIONSPLIT_TOPIC =
61 "internal-telemetry-after-subsession-split";
62 const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
65 const MAX_TAB_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_tab_count";
66 const VERTICAL_MAX_TAB_COUNT_SCALAR_NAME =
67 "browser.engagement.max_concurrent_vertical_tab_count";
68 const MAX_WINDOW_COUNT_SCALAR_NAME =
69 "browser.engagement.max_concurrent_window_count";
70 const TAB_OPEN_EVENT_COUNT_SCALAR_NAME =
71 "browser.engagement.tab_open_event_count";
72 const VERTICAL_TAB_OPEN_EVENT_COUNT_SCALAR_NAME =
73 "browser.engagement.vertical_tab_open_event_count";
74 const MAX_TAB_PINNED_COUNT_SCALAR_NAME =
75 "browser.engagement.max_concurrent_tab_pinned_count";
76 const VERTICAL_MAX_TAB_PINNED_COUNT_SCALAR_NAME =
77 "browser.engagement.max_concurrent_vertical_tab_pinned_count";
78 const TAB_PINNED_EVENT_COUNT_SCALAR_NAME =
79 "browser.engagement.tab_pinned_event_count";
80 const VERTICAL_TAB_PINNED_EVENT_COUNT_SCALAR_NAME =
81 "browser.engagement.vertical_tab_pinned_event_count";
82 const WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME =
83 "browser.engagement.window_open_event_count";
84 const UNIQUE_DOMAINS_COUNT_SCALAR_NAME =
85 "browser.engagement.unique_domains_count";
86 const TOTAL_URI_COUNT_SCALAR_NAME = "browser.engagement.total_uri_count";
87 const UNFILTERED_URI_COUNT_SCALAR_NAME =
88 "browser.engagement.unfiltered_uri_count";
89 const TOTAL_URI_COUNT_NORMAL_AND_PRIVATE_MODE_SCALAR_NAME =
90 "browser.engagement.total_uri_count_normal_and_private_mode";
92 export const MINIMUM_TAB_COUNT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes, in ms
94 // The elements we consider to be interactive.
95 const UI_TARGET_ELEMENTS = [
108 const UI_TARGET_COMPOSED_ELEMENTS_MAP = new Map([["moz-checkbox", "input"]]);
110 // The containers of interactive elements that we care about and their pretty
111 // names. These should be listed in order of most-specific to least-specific,
112 // when iterating JavaScript will guarantee that ordering and so we will find
113 // the most specific area first.
114 const BROWSER_UI_CONTAINER_IDS = {
115 "toolbar-menubar": "menu-bar",
116 TabsToolbar: "tabs-bar",
117 "vertical-tabs": "vertical-tabs-container",
118 PersonalToolbar: "bookmarks-bar",
119 "appMenu-popup": "app-menu",
120 tabContextMenu: "tabs-context",
121 contentAreaContextMenu: "content-context",
122 "widget-overflow-list": "overflow-menu",
123 "widget-overflow-fixed-list": "pinned-overflow-menu",
124 "page-action-buttons": "pageaction-urlbar",
125 pageActionPanel: "pageaction-panel",
126 "unified-extensions-area": "unified-extensions-area",
127 "allTabsMenu-allTabsView": "alltabs-menu",
129 // This should appear last as some of the above are inside the nav bar.
130 "nav-bar": "nav-bar",
133 const ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS = {
134 [BROWSER_UI_CONTAINER_IDS.tabContextMenu]: "tabs-context-entrypoint",
137 // A list of the expected panes in about:preferences
138 const PREFERENCES_PANES = [
147 "paneMoreFromMozilla",
150 const IGNORABLE_EVENTS = new WeakMap();
152 const KNOWN_ADDONS = [];
154 // Buttons that, when clicked, set a preference to true. The convention
155 // is that the preference is named:
157 // browser.engagement.<button id>.has-used
159 // and is defaulted to false.
160 const SET_USAGE_PREF_BUTTONS = [
162 "fxa-toolbar-menu-button",
168 // Buttons that, when clicked, increase a counter. The convention
169 // is that the preference is named:
171 // browser.engagement.<button id>.used-count
173 // and doesn't have a default value.
174 const SET_USAGECOUNT_PREF_BUTTONS = [
175 "pageAction-panel-copyURL",
176 "pageAction-panel-emailLink",
177 "pageAction-panel-pinTab",
178 "pageAction-panel-screenshots_mozilla_org",
179 "pageAction-panel-shareURL",
182 // Places context menu IDs.
183 const PLACES_CONTEXT_MENU_ID = "placesContext";
184 const PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID =
185 "placesContext_open:newcontainertab";
187 // Commands used to open history or bookmark links from places context menu.
188 const PLACES_OPEN_COMMANDS = [
190 "placesCmd_open:window",
191 "placesCmd_open:privatewindow",
192 "placesCmd_open:tab",
195 // How long of a delay between events means the start of a new flow?
196 // Used by Browser UI Interaction event instrumentation.
198 const FLOW_IDLE_TIME = 5 * 60 * 1000;
200 function telemetryId(widgetId, obscureAddons = true) {
201 // Add-on IDs need to be obscured.
202 function addonId(id) {
203 if (!obscureAddons) {
207 let pos = KNOWN_ADDONS.indexOf(id);
209 pos = KNOWN_ADDONS.length;
210 KNOWN_ADDONS.push(id);
212 return `addon${pos}`;
215 if (widgetId.endsWith("-browser-action")) {
217 widgetId.substring(0, widgetId.length - "-browser-action".length)
219 } else if (widgetId.startsWith("pageAction-")) {
221 if (widgetId.startsWith("pageAction-urlbar-")) {
222 actionId = widgetId.substring("pageAction-urlbar-".length);
223 } else if (widgetId.startsWith("pageAction-panel-")) {
224 actionId = widgetId.substring("pageAction-panel-".length);
228 let action = lazy.PageActions.actionForID(actionId);
229 widgetId = action?._isMozillaAction ? actionId : addonId(actionId);
231 } else if (widgetId.startsWith("ext-keyset-id-")) {
232 // Webextension command shortcuts don't have an id on their key element so
233 // we see the id from the keyset that contains them.
234 widgetId = addonId(widgetId.substring("ext-keyset-id-".length));
235 } else if (widgetId.startsWith("ext-key-id-")) {
236 // The command for a webextension sidebar action is an exception to the above rule.
237 widgetId = widgetId.substring("ext-key-id-".length);
238 if (widgetId.endsWith("-sidebar-action")) {
240 widgetId.substring(0, widgetId.length - "-sidebar-action".length)
245 return widgetId.replace(/_/g, "-");
248 function getOpenTabsAndWinsCounts() {
249 let loadedTabCount = 0;
253 for (let win of Services.wm.getEnumerator("navigator:browser")) {
255 tabCount += win.gBrowser.tabs.length;
256 for (const tab of win.gBrowser.tabs) {
257 if (tab.getAttribute("pending") !== "true") {
263 return { loadedTabCount, tabCount, winCount };
266 function getPinnedTabsCount() {
269 for (let win of Services.wm.getEnumerator("navigator:browser")) {
270 pinnedTabs += [...win.ownerGlobal.gBrowser.tabs].filter(
278 export let URICountListener = {
279 // A set containing the visited domains, see bug 1271310.
280 _domainSet: new Set(),
281 // A set containing the visited origins during the last 24 hours (similar to domains, but not quite the same)
282 _domain24hrSet: new Set(),
283 // A map to keep track of the URIs loaded from the restored tabs.
284 _restoredURIsMap: new WeakMap(),
285 // Ongoing expiration timeouts.
286 _timeouts: new Set(),
289 // Only consider http(s) schemas.
290 return uri.schemeIs("http") || uri.schemeIs("https");
293 addRestoredURI(browser, uri) {
294 if (!this.isHttpURI(uri)) {
298 this._restoredURIsMap.set(browser, uri.spec);
301 onLocationChange(browser, webProgress, request, uri, flags) {
303 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) &&
304 webProgress.isTopLevel
306 // By default, assume we no longer need to track this tab.
307 lazy.SearchSERPTelemetry.stopTrackingBrowser(
309 lazy.SearchSERPTelemetryUtils.ABANDONMENTS.NAVIGATION
313 // Don't count this URI if it's an error page.
314 if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
318 // We only care about top level loads.
319 if (!webProgress.isTopLevel) {
323 // The SessionStore sets the URI of a tab first, firing onLocationChange the
324 // first time, then manages content loading using its scheduler. Once content
325 // loads, we will hit onLocationChange again.
326 // We can catch the first case by checking for null requests: be advised that
327 // this can also happen when navigating page fragments, so account for it.
330 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
335 // Don't include URI and domain counts when in private mode.
337 !lazy.PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal) ||
338 Services.prefs.getBoolPref(
339 "browser.engagement.total_uri_count.pbm",
343 // Track URI loads, even if they're not http(s).
348 // If we have troubles parsing the spec, still count this as
349 // an unfiltered URI.
350 if (shouldCountURI) {
351 Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
356 // Don't count about:blank and similar pages, as they would artificially
357 // inflate the counts.
358 if (browser.ownerGlobal.gInitialPages.includes(uriSpec)) {
362 // If the URI we're loading is in the _restoredURIsMap, then it comes from a
363 // restored tab. If so, let's skip it and remove it from the map as we want to
364 // count page refreshes.
365 if (this._restoredURIsMap.get(browser) === uriSpec) {
366 this._restoredURIsMap.delete(browser);
370 // The URI wasn't from a restored tab. Count it among the unfiltered URIs.
371 // If this is an http(s) URI, this also gets counted by the "total_uri_count"
373 if (shouldCountURI) {
374 Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
377 if (!this.isHttpURI(uri)) {
381 if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
382 lazy.SearchSERPTelemetry.updateTrackingStatus(
388 lazy.SearchSERPTelemetry.updateTrackingSinglePageApp(
391 webProgress.loadType,
396 // Update total URI count, including when in private mode.
397 Services.telemetry.scalarAdd(
398 TOTAL_URI_COUNT_NORMAL_AND_PRIVATE_MODE_SCALAR_NAME,
401 Glean.browserEngagement.uriCount.add(1);
403 if (!shouldCountURI) {
407 // Update the URI counts.
408 Services.telemetry.scalarAdd(TOTAL_URI_COUNT_SCALAR_NAME, 1);
411 BrowserUsageTelemetry._recordTabCounts(getOpenTabsAndWinsCounts());
413 // Unique domains should be aggregated by (eTLD + 1): x.test.com and y.test.com
414 // are counted once as test.com.
417 // Even if only considering http(s) URIs, |getBaseDomain| could still throw
418 // due to the URI containing invalid characters or the domain actually being
419 // an ipv4 or ipv6 address.
420 baseDomain = Services.eTLD.getBaseDomain(uri);
425 // We only want to count the unique domains up to MAX_UNIQUE_VISITED_DOMAINS.
426 if (this._domainSet.size < MAX_UNIQUE_VISITED_DOMAINS) {
427 this._domainSet.add(baseDomain);
428 Services.telemetry.scalarSet(
429 UNIQUE_DOMAINS_COUNT_SCALAR_NAME,
434 this._domain24hrSet.add(baseDomain);
435 if (lazy.gRecentVisitedOriginsExpiry) {
436 let timeoutId = lazy.setTimeout(() => {
437 this._domain24hrSet.delete(baseDomain);
438 this._timeouts.delete(timeoutId);
439 }, lazy.gRecentVisitedOriginsExpiry * 1000);
440 this._timeouts.add(timeoutId);
445 * Reset the counts. This should be called when breaking a session in Telemetry.
448 this._domainSet.clear();
452 * Returns the number of unique domains visited in this session during the
455 get uniqueDomainsVisitedInPast24Hours() {
456 return this._domain24hrSet.size;
460 * Resets the number of unique domains visited in this session.
462 resetUniqueDomainsVisitedInPast24Hours() {
463 this._timeouts.forEach(timeoutId => lazy.clearTimeout(timeoutId));
464 this._timeouts.clear();
465 this._domain24hrSet.clear();
468 QueryInterface: ChromeUtils.generateQI([
469 "nsIWebProgressListener",
470 "nsISupportsWeakReference",
474 let gInstallationTelemetryPromise = null;
476 export let BrowserUsageTelemetry = {
478 * This is a policy object used to override behavior for testing.
481 getTelemetryClientId: async () => lazy.ClientID.getClientID(),
482 getUpdateDirectory: () => Services.dirsvc.get("UpdRootD", Ci.nsIFile),
483 readProfileCountFile: async path => IOUtils.readUTF8(path),
484 writeProfileCountFile: async (path, data) => IOUtils.writeUTF8(path, data),
490 this._lastRecordTabCount = 0;
491 this._lastRecordLoadedTabCount = 0;
492 this._setupAfterRestore();
495 Services.prefs.addObserver("browser.tabs.inTitlebar", this);
496 Services.prefs.addObserver(
497 "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled",
501 this._recordUITelemetry();
503 this._onTabsOpenedTask = new lazy.DeferredTask(
504 () => this._onTabsOpened(),
509 get maxTabCountScalarName() {
510 return lazy.sidebarVerticalTabs
511 ? VERTICAL_MAX_TAB_COUNT_SCALAR_NAME
512 : MAX_TAB_COUNT_SCALAR_NAME;
515 get tabOpenEventCountScalarName() {
516 return lazy.sidebarVerticalTabs
517 ? VERTICAL_TAB_OPEN_EVENT_COUNT_SCALAR_NAME
518 : TAB_OPEN_EVENT_COUNT_SCALAR_NAME;
521 get maxTabPinnedCountScalarName() {
522 return lazy.sidebarVerticalTabs
523 ? VERTICAL_MAX_TAB_PINNED_COUNT_SCALAR_NAME
524 : MAX_TAB_PINNED_COUNT_SCALAR_NAME;
527 get tabPinnedEventCountScalarName() {
528 return lazy.sidebarVerticalTabs
529 ? VERTICAL_TAB_PINNED_EVENT_COUNT_SCALAR_NAME
530 : TAB_PINNED_EVENT_COUNT_SCALAR_NAME;
534 * Resets the masked add-on identifiers. Only for use in tests.
537 KNOWN_ADDONS.length = 0;
541 * Handle subsession splits in the parent process.
543 afterSubsessionSplit() {
544 // Scalars just got cleared due to a subsession split. We need to set the maximum
545 // concurrent tab and window counts so that they reflect the correct value for the
547 const counts = getOpenTabsAndWinsCounts();
548 Services.telemetry.scalarSetMaximum(
549 this.maxTabCountScalarName,
552 Services.telemetry.scalarSetMaximum(
553 MAX_WINDOW_COUNT_SCALAR_NAME,
557 // Reset the URI counter.
558 URICountListener.reset();
561 QueryInterface: ChromeUtils.generateQI([
563 "nsISupportsWeakReference",
570 Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC);
571 Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC);
574 observe(subject, topic, data) {
576 case DOMWINDOW_OPENED_TOPIC:
577 this._onWindowOpen(subject);
579 case TELEMETRY_SUBSESSIONSPLIT_TOPIC:
580 this.afterSubsessionSplit();
582 case "nsPref:changed":
584 case "browser.tabs.inTitlebar":
585 this._recordWidgetChange(
587 Services.appinfo.drawInTitlebar ? "off" : "on",
591 case "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled":
592 if (Services.prefs.getBoolPref(data)) {
593 Glean.pictureinpictureSettings.enableAutotriggerSettings.record();
602 switch (event.type) {
610 this._unregisterWindow(event.target);
612 case TAB_RESTORING_TOPIC:
613 // We're restoring a new tab from a previous or crashed session.
614 // We don't want to track the URIs from these tabs, so let
615 // |URICountListener| know about them.
616 let browser = event.target.linkedBrowser;
617 URICountListener.addRestoredURI(browser, browser.currentURI);
619 const { loadedTabCount } = getOpenTabsAndWinsCounts();
620 this._recordTabCounts({ loadedTabCount });
626 * This gets called shortly after the SessionStore has finished restoring
627 * windows and tabs. It counts the open tabs and adds listeners to all the
630 _setupAfterRestore() {
631 // Make sure to catch new chrome windows and subsession splits.
632 Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, true);
633 Services.obs.addObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, true);
635 // Attach the tabopen handlers to the existing Windows.
636 for (let win of Services.wm.getEnumerator("navigator:browser")) {
637 this._registerWindow(win);
640 // Get the initial tab and windows max counts.
641 const counts = getOpenTabsAndWinsCounts();
642 Services.telemetry.scalarSetMaximum(
643 this.maxTabCountScalarName,
646 Services.telemetry.scalarSetMaximum(
647 MAX_WINDOW_COUNT_SCALAR_NAME,
652 _buildWidgetPositions() {
653 let widgetMap = new Map();
655 const toolbarState = nodeId => {
657 if (nodeId == "PersonalToolbar") {
658 value = Services.prefs.getCharPref(
659 "browser.toolbars.bookmarks.visibility",
662 if (value != "newtab") {
663 return value == "never" ? "off" : "on";
667 value = Services.xulStore.getValue(
668 AppConstants.BROWSER_CHROME_URL,
674 return value == "true" ? "off" : "on";
680 BROWSER_UI_CONTAINER_IDS.PersonalToolbar,
681 toolbarState("PersonalToolbar")
685 Services.xulStore.getValue(
686 AppConstants.BROWSER_CHROME_URL,
691 widgetMap.set("menu-toolbar", menuBarHidden ? "off" : "on");
693 // Drawing in the titlebar means not showing the titlebar, hence the negation.
694 widgetMap.set("titlebar", Services.appinfo.drawInTitlebar ? "off" : "on");
696 for (let area of lazy.CustomizableUI.areas) {
697 if (!(area in BROWSER_UI_CONTAINER_IDS)) {
701 let position = BROWSER_UI_CONTAINER_IDS[area];
702 if (area == "nav-bar") {
703 position = `${BROWSER_UI_CONTAINER_IDS[area]}-start`;
706 let widgets = lazy.CustomizableUI.getWidgetsInArea(area);
708 for (let widget of widgets) {
713 if (widget.id.startsWith("customizableui-special-")) {
717 if (area == "nav-bar" && widget.id == "urlbar-container") {
718 position = `${BROWSER_UI_CONTAINER_IDS[area]}-end`;
722 widgetMap.set(widget.id, position);
726 let actions = lazy.PageActions.actions;
727 for (let action of actions) {
728 if (action.pinnedToUrlbar) {
729 widgetMap.set(action.id, "pageaction-urlbar");
737 // We want to find a sensible ID for this element.
742 // See if this is a customizable widget.
743 if (node.ownerDocument.URL == AppConstants.BROWSER_CHROME_URL) {
744 // First find if it is inside one of the customizable areas.
745 for (let area of lazy.CustomizableUI.areas) {
746 if (node.closest(`#${CSS.escape(area)}`)) {
747 for (let widget of lazy.CustomizableUI.getWidgetIdsInArea(area)) {
749 // We care about the buttons on the tabs themselves.
750 widget == "tabbrowser-tabs" ||
751 // We care about the page action and other buttons in here.
752 widget == "urlbar-container" ||
753 // We care about the actual menu items.
754 widget == "menubar-items" ||
755 // We care about individual bookmarks here.
756 widget == "personal-bookmarks"
761 if (node.closest(`#${CSS.escape(widget)}`)) {
774 // A couple of special cases in the tabs.
775 for (let cls of ["bookmark-item", "tab-icon-sound", "tab-close-button"]) {
776 if (!node.classList.contains(cls)) {
779 if (cls == "bookmark-item" && node.parentElement.id.includes("history")) {
780 return "history-item";
785 // One of these will at least let us know what the widget is for.
786 let possibleAttributes = [
793 // The key attribute on key elements is the actual key to listen for.
794 if (node.localName != "key") {
795 possibleAttributes.unshift("key");
798 for (let idAttribute of possibleAttributes) {
799 if (node.hasAttribute(idAttribute)) {
800 return node.getAttribute(idAttribute);
804 return this._getWidgetID(node.parentElement);
807 _getBrowserWidgetContainer(node) {
808 // Find the container holding this element.
809 for (let containerId of Object.keys(BROWSER_UI_CONTAINER_IDS)) {
810 let container = node.ownerDocument.getElementById(containerId);
811 if (container && container.contains(node)) {
812 return BROWSER_UI_CONTAINER_IDS[containerId];
815 // Treat toolbar context menu items that relate to tabs as the tab menu:
817 node.closest("#toolbar-context-menu") &&
818 node.getAttribute("contexttype") == "tabbar"
820 return BROWSER_UI_CONTAINER_IDS.tabContextMenu;
825 _getWidgetContainer(node) {
826 if (node.localName == "key") {
830 const { URL: url } = node.ownerDocument;
831 if (url == AppConstants.BROWSER_CHROME_URL) {
832 return this._getBrowserWidgetContainer(node);
835 url.startsWith("about:preferences") ||
836 url.startsWith("about:settings")
838 // Find the element's category.
839 let container = node.closest("[data-category]");
844 let pane = container.getAttribute("data-category");
846 if (!PREFERENCES_PANES.includes(pane)) {
847 pane = "paneUnknown";
850 return `preferences_${pane}`;
856 lastClickTarget: null,
859 IGNORABLE_EVENTS.set(event, true);
862 _recordCommand(event) {
863 if (IGNORABLE_EVENTS.get(event)) {
867 let sourceEvent = event;
868 while (sourceEvent.sourceEvent) {
869 sourceEvent = sourceEvent.sourceEvent;
872 let lastTarget = this.lastClickTarget?.get();
875 sourceEvent.type == "command" &&
876 sourceEvent.target.contains(lastTarget)
878 // Ignore a command event triggered by a click.
879 this.lastClickTarget = null;
883 this.lastClickTarget = null;
885 if (sourceEvent.type == "click") {
886 // Only care about main button clicks.
887 if (sourceEvent.button != 0) {
891 // This click may trigger a command event so retain the target to be able
892 // to dedupe that event.
893 this.lastClickTarget = Cu.getWeakReference(sourceEvent.target);
896 // We should never see events from web content as they are fired in a
897 // content process, but let's be safe.
898 let url = sourceEvent.target.ownerDocument.documentURIObject;
899 if (!url.schemeIs("chrome") && !url.schemeIs("about")) {
903 // This is what events targetted at content will actually look like.
904 if (sourceEvent.target.localName == "browser") {
908 // Find the actual element we're interested in.
909 let node = sourceEvent.target;
910 const isAboutPreferences =
911 node.ownerDocument.URL.startsWith("about:preferences") ||
912 node.ownerDocument.URL.startsWith("about:settings");
914 !UI_TARGET_ELEMENTS.includes(node.localName) &&
915 !node.classList?.contains("wants-telemetry") &&
916 // We are interested in links on about:preferences as well.
918 isAboutPreferences &&
919 (node.getAttribute("is") === "text-link" || node.localName === "a")
922 node = node.parentNode;
923 if (!node?.parentNode) {
924 // A click on a space or label or top-level document or something we're
925 // not interested in.
930 // When the expected target is a Custom Element with a Shadow Root, there
931 // may be a specific part of the component that click events correspond to
932 // changes. Ignore any other events if requested.
933 let expectedEventTarget = UI_TARGET_COMPOSED_ELEMENTS_MAP.get(
937 event.type == "click" &&
938 expectedEventTarget &&
939 expectedEventTarget != event.composedTarget?.localName
944 if (sourceEvent.type === "command") {
945 const { command, ownerDocument, parentNode } = node;
946 // Check if this command is for a history or bookmark link being opened
947 // from the context menu. In this case, we are interested in the DOM node
948 // for the link, not the menu item itself.
950 PLACES_OPEN_COMMANDS.includes(command) ||
951 parentNode?.parentNode?.id === PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID
953 node = ownerDocument.getElementById(PLACES_CONTEXT_MENU_ID).triggerNode;
957 let item = this._getWidgetID(node);
958 let source = this._getWidgetContainer(node);
960 if (item && source) {
961 this.recordInteractionEvent(item, source);
962 let scalar = `browser.ui.interaction.${source.replace(/-/g, "_")}`;
963 Services.telemetry.keyedScalarAdd(scalar, telemetryId(item), 1);
964 if (SET_USAGECOUNT_PREF_BUTTONS.includes(item)) {
965 let pref = `browser.engagement.${item}.used-count`;
966 Services.prefs.setIntPref(pref, Services.prefs.getIntPref(pref, 0) + 1);
968 if (SET_USAGE_PREF_BUTTONS.includes(item)) {
969 Services.prefs.setBoolPref(`browser.engagement.${item}.has-used`, true);
973 if (ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source]) {
974 let contextMenu = ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source];
975 let triggerContainer = this._getWidgetContainer(
976 node.closest("menupopup")?.triggerNode
978 if (triggerContainer) {
979 this.recordInteractionEvent(item, contextMenu);
980 let scalar = `browser.ui.interaction.${contextMenu.replace(/-/g, "_")}`;
981 Services.telemetry.keyedScalarAdd(
983 telemetryId(triggerContainer),
993 recordInteractionEvent(widgetId, source) {
994 // A note on clocks. Cu.now() is monotonic, but its behaviour across
995 // computer sleeps is different per platform.
996 // We're okay with this for flows because we're looking at idle times
997 // on the order of minutes and within the same machine, so the weirdest
998 // thing we may expect is a flow that accidentally continues across a
999 // sleep. Until we have evidence that this is common, we're in the clear.
1000 if (!this._flowId || this._flowIdTS + FLOW_IDLE_TIME < Cu.now()) {
1001 // We submit the ping full o' events on every new flow,
1002 // including at startup.
1003 GleanPings.prototypeNoCodeEvents.submit();
1004 // We use a GUID here because we need to identify events in a flow
1005 // out of all events from all flows across all clients.
1006 this._flowId = Services.uuid.generateUUID();
1008 this._flowIdTS = Cu.now();
1012 widget_id: telemetryId(widgetId),
1013 flow_id: this._flowId,
1015 Glean.browserUsage.interaction.record(extra);
1019 * Listens for UI interactions in the window.
1021 _addUsageListeners(win) {
1022 // Listen for command events from the UI.
1023 win.addEventListener("command", event => this._recordCommand(event), true);
1024 win.addEventListener("click", event => this._recordCommand(event), true);
1028 * A public version of the private method to take care of the `nav-bar-start`,
1029 * `nav-bar-end` thing that callers shouldn't have to care about. It also
1030 * accepts the DOM ids for the areas rather than the cleaner ones we report
1033 recordWidgetChange(widgetId, newPos, reason) {
1036 newPos = BROWSER_UI_CONTAINER_IDS[newPos];
1039 if (newPos == "nav-bar") {
1040 let { position } = lazy.CustomizableUI.getPlacementOfWidget(widgetId);
1041 let { position: urlPosition } =
1042 lazy.CustomizableUI.getPlacementOfWidget("urlbar-container");
1043 newPos = newPos + (urlPosition > position ? "-start" : "-end");
1046 this._recordWidgetChange(widgetId, newPos, reason);
1052 recordToolbarVisibility(toolbarId, newState, reason) {
1053 if (typeof newState != "string") {
1054 newState = newState ? "on" : "off";
1056 this._recordWidgetChange(
1057 BROWSER_UI_CONTAINER_IDS[toolbarId],
1063 _recordWidgetChange(widgetId, newPos, reason) {
1064 // In some cases (like when add-ons are detected during startup) this gets
1065 // called before we've reported the initial positions. Ignore such cases.
1066 if (!this.widgetMap) {
1070 if (widgetId == "urlbar-container") {
1071 // We don't report the position of the url bar, it is after nav-bar-start
1072 // and before nav-bar-end. But moving it means the widgets around it have
1073 // effectively moved so update those.
1074 let position = "nav-bar-start";
1075 let widgets = lazy.CustomizableUI.getWidgetsInArea("nav-bar");
1077 for (let widget of widgets) {
1082 if (widget.id.startsWith("customizableui-special-")) {
1086 if (widget.id == "urlbar-container") {
1087 position = "nav-bar-end";
1091 // This will do nothing if the position hasn't changed.
1092 this._recordWidgetChange(widget.id, position, reason);
1098 let oldPos = this.widgetMap.get(widgetId);
1099 if (oldPos == newPos) {
1103 let action = "move";
1107 } else if (!newPos) {
1111 let key = `${telemetryId(widgetId, false)}_${action}_${oldPos ?? "na"}_${
1114 Services.telemetry.keyedScalarAdd("browser.ui.customized_widgets", key, 1);
1117 this.widgetMap.set(widgetId, newPos);
1119 this.widgetMap.delete(widgetId);
1123 _recordUITelemetry() {
1124 this.widgetMap = this._buildWidgetPositions();
1126 // FIXME(bug 1883857): object metric type not available in artefact builds.
1127 if ("toolbarWidgets" in Glean.browserUi) {
1128 Glean.browserUi.toolbarWidgets.set(
1131 .map(([widgetId, position]) => {
1132 return { widgetId: telemetryId(widgetId, false), position };
1138 for (let [widgetId, position] of this.widgetMap.entries()) {
1139 let key = `${telemetryId(widgetId, false)}_pinned_${position}`;
1140 Services.telemetry.keyedScalarSet(
1141 "browser.ui.toolbar_widgets",
1149 * Adds listeners to a single chrome window.
1151 _registerWindow(win) {
1152 this._addUsageListeners(win);
1154 win.addEventListener("unload", this);
1155 win.addEventListener("TabOpen", this, true);
1156 win.addEventListener("TabPinned", this, true);
1158 win.gBrowser.tabContainer.addEventListener(TAB_RESTORING_TOPIC, this);
1159 win.gBrowser.addTabsProgressListener(URICountListener);
1163 * Removes listeners from a single chrome window.
1165 _unregisterWindow(win) {
1166 win.removeEventListener("unload", this);
1167 win.removeEventListener("TabOpen", this, true);
1168 win.removeEventListener("TabPinned", this, true);
1170 win.defaultView.gBrowser.tabContainer.removeEventListener(
1171 TAB_RESTORING_TOPIC,
1174 win.defaultView.gBrowser.removeTabsProgressListener(URICountListener);
1178 * Updates the tab counts.
1181 // Update the "tab opened" count and its maximum.
1182 Services.telemetry.scalarAdd(this.tabOpenEventCountScalarName, 1);
1184 // In the case of opening multiple tabs at once, avoid enumerating all open
1185 // tabs and windows each time a tab opens.
1186 this._onTabsOpenedTask.disarm();
1187 this._onTabsOpenedTask.arm();
1191 * Update tab counts after opening multiple tabs.
1194 const { tabCount, loadedTabCount } = getOpenTabsAndWinsCounts();
1195 Services.telemetry.scalarSetMaximum(this.maxTabCountScalarName, tabCount);
1197 this._recordTabCounts({ tabCount, loadedTabCount });
1201 const pinnedTabs = getPinnedTabsCount();
1203 // Update the "tab pinned" count and its maximum.
1204 Services.telemetry.scalarAdd(this.tabPinnedEventCountScalarName, 1);
1205 Services.telemetry.scalarSetMaximum(
1206 this.maxTabPinnedCountScalarName,
1212 * Tracks the window count and registers the listeners for the tab count.
1213 * @param{Object} win The window object.
1215 _onWindowOpen(win) {
1216 // Make sure to have a |nsIDOMWindow|.
1217 if (!(win instanceof Ci.nsIDOMWindow)) {
1221 let onLoad = () => {
1222 win.removeEventListener("load", onLoad);
1224 // Ignore non browser windows.
1226 win.document.documentElement.getAttribute("windowtype") !=
1232 this._registerWindow(win);
1233 // Track the window open event and check the maximum.
1234 const counts = getOpenTabsAndWinsCounts();
1235 Services.telemetry.scalarAdd(WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
1236 Services.telemetry.scalarSetMaximum(
1237 MAX_WINDOW_COUNT_SCALAR_NAME,
1241 // We won't receive the "TabOpen" event for the first tab within a new window.
1242 // Account for that.
1243 this._onTabOpen(counts);
1245 win.addEventListener("load", onLoad);
1249 * Record telemetry about the given tab counts.
1251 * Telemetry for each count will only be recorded if the value isn't
1254 * @param {object} [counts] The tab counts to register with telemetry.
1255 * @param {number} [counts.tabCount] The number of tabs in all browsers.
1256 * @param {number} [counts.loadedTabCount] The number of loaded (i.e., not
1257 * pending) tabs in all browsers.
1259 _recordTabCounts({ tabCount, loadedTabCount }) {
1260 let currentTime = Date.now();
1262 tabCount !== undefined &&
1263 currentTime > this._lastRecordTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1265 Services.telemetry.getHistogramById("TAB_COUNT").add(tabCount);
1266 this._lastRecordTabCount = currentTime;
1270 loadedTabCount !== undefined &&
1272 this._lastRecordLoadedTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1275 .getHistogramById("LOADED_TAB_COUNT")
1276 .add(loadedTabCount);
1277 this._lastRecordLoadedTabCount = currentTime;
1281 _checkProfileCountFileSchema(fileData) {
1282 // Verifies that the schema of the file is the expected schema
1283 if (typeof fileData.version != "string") {
1284 throw new Error("Schema Mismatch Error: Bad type for 'version' field");
1286 if (!Array.isArray(fileData.profileTelemetryIds)) {
1288 "Schema Mismatch Error: Bad type for 'profileTelemetryIds' field"
1291 for (let profileTelemetryId of fileData.profileTelemetryIds) {
1292 if (typeof profileTelemetryId != "string") {
1294 "Schema Mismatch Error: Bad type for an element of 'profileTelemetryIds'"
1300 // Reports the number of Firefox profiles on this machine to telemetry.
1301 async reportProfileCount() {
1303 AppConstants.platform != "win" ||
1304 !AppConstants.MOZ_TELEMETRY_REPORTING
1306 // This is currently a windows-only feature.
1307 // Also, this function writes directly to disk, without using the usual
1308 // telemetry recording functions. So we excplicitly check if telemetry
1309 // reporting was disabled at compile time, and we do not do anything in
1314 // To report only as much data as we need, we will bucket our values.
1315 // Rather than the raw value, we will report the greatest value in the list
1316 // below that is no larger than the raw value.
1317 const buckets = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000, 10000];
1319 // We need both the C:\ProgramData\Mozilla directory and the install
1320 // directory hash to create the profile count file path. We can easily
1321 // reassemble this from the update directory, which looks like:
1322 // C:\ProgramData\Mozilla\updates\hash
1323 // Retrieving the directory this way also ensures that the "Mozilla"
1324 // directory is created with the correct permissions.
1325 // The ProgramData directory, by default, grants write permissions only to
1326 // file creators. The directory service calls GetCommonUpdateDirectory,
1327 // which makes sure the the directory is created with user-writable
1329 const updateDirectory = BrowserUsageTelemetry.Policy.getUpdateDirectory();
1330 const hash = updateDirectory.leafName;
1331 const profileCountFilename = "profile_count_" + hash + ".json";
1332 let profileCountFile = updateDirectory.parent.parent;
1333 profileCountFile.append(profileCountFilename);
1335 let readError = false;
1338 let json = await BrowserUsageTelemetry.Policy.readProfileCountFile(
1339 profileCountFile.path
1341 fileData = JSON.parse(json);
1342 BrowserUsageTelemetry._checkProfileCountFileSchema(fileData);
1344 // Note that since this also catches the "no such file" error, this is
1345 // always the template that we use when writing to the file for the first
1347 fileData = { version: "1", profileTelemetryIds: [] };
1348 if (!(ex.name == "NotFoundError")) {
1350 // Don't just return here on a read error. We need to send the error
1351 // value to telemetry and we want to attempt to fix the file.
1352 // However, we will still report an error for this ping, even if we
1353 // fix the file. This is to prevent always sending a profile count of 1
1354 // if, for some reason, we always get a read error but never a write
1360 let writeError = false;
1361 let currentTelemetryId =
1362 await BrowserUsageTelemetry.Policy.getTelemetryClientId();
1363 // Don't add our telemetry ID to the file if we've already reached the
1364 // largest bucket. This prevents the file size from growing forever.
1366 !fileData.profileTelemetryIds.includes(currentTelemetryId) &&
1367 fileData.profileTelemetryIds.length < Math.max(...buckets)
1369 fileData.profileTelemetryIds.push(currentTelemetryId);
1371 await BrowserUsageTelemetry.Policy.writeProfileCountFile(
1372 profileCountFile.path,
1373 JSON.stringify(fileData)
1381 // Determine the bucketed value to report
1382 let rawProfileCount = fileData.profileTelemetryIds.length;
1383 let valueToReport = 0;
1384 for (let bucket of buckets) {
1385 if (bucket <= rawProfileCount && bucket > valueToReport) {
1386 valueToReport = bucket;
1390 if (readError || writeError) {
1391 // We convey errors via a profile count of 0.
1395 Services.telemetry.scalarSet(
1396 "browser.engagement.profile_count",
1399 // Manually mirror to Glean
1400 Glean.browserEngagement.profileCount.set(valueToReport);
1404 * Check if this is the first run of this profile since installation,
1405 * if so then collect installation telemetry.
1407 * @param {nsIFile} [dataPathOverride] Optional, full data file path, for tests.
1408 * @param {Array<string>} [msixPackagePrefixes] Optional, list of prefixes to
1409 consider "existing" installs when looking at installed MSIX packages.
1410 Defaults to prefixes for builds produced in Firefox automation.
1411 * @return {Promise<Object>} A JSON object containing install telemetry.
1412 * @resolves When the event has been recorded, or if the data file was not found.
1413 * @rejects JavaScript exception on any failure.
1415 async collectInstallationTelemetry(
1417 msixPackagePrefixes = ["Mozilla.Firefox", "Mozilla.MozillaFirefox"]
1419 if (AppConstants.platform != "win") {
1420 // This is a windows-only feature.
1424 const TIMESTAMP_PREF = "app.installation.timestamp";
1425 const lastInstallTime = Services.prefs.getStringPref(TIMESTAMP_PREF, null);
1426 const wpm = Cc["@mozilla.org/windows-package-manager;1"].createInstance(
1427 Ci.nsIWindowsPackageManager
1429 let installer_type = "";
1432 pfn = Services.sysinfo.getProperty("winPackageFamilyName");
1435 function getInstallData() {
1436 // We only care about where _any_ other install existed - no
1437 // need to count more than 1.
1438 const installPaths = lazy.WindowsInstallsInfo.getInstallPaths(
1440 new Set([Services.dirsvc.get("GreBinD", Ci.nsIFile).path])
1442 const msixInstalls = new Set();
1443 // We're just going to eat all errors here -- we don't want the event
1444 // to go unsent if we were unable to look for MSIX installs.
1447 .findUserInstalledPackages(msixPackagePrefixes)
1448 .forEach(i => msixInstalls.add(i));
1450 msixInstalls.delete(pfn);
1462 if (lastInstallTime != null) {
1463 // We've already seen this install
1467 // First time seeing this install, record the timestamp.
1468 Services.prefs.setStringPref(TIMESTAMP_PREF, wpm.getInstalledDate());
1469 let install_data = getInstallData();
1471 installer_type = "msix";
1473 // Build the extra event data
1474 extra.version = AppConstants.MOZ_APP_VERSION;
1475 extra.build_id = AppConstants.MOZ_BUILDID;
1476 // The next few keys are static for the reasons described
1477 // No way to detect whether or not we were installed by an admin
1478 extra.admin_user = "false";
1479 // Always false at the moment, because we create a new profile
1481 extra.profdir_existed = "false";
1482 // Obviously false for MSIX installs
1483 extra.from_msi = "false";
1484 // We have no way of knowing whether we were installed via the GUI,
1485 // through the command line, or some Enterprise management tool.
1486 extra.silent = "false";
1487 // There's no way to change the install path for an MSIX package
1488 extra.default_path = "true";
1489 extra.install_existed = install_data.msixInstalls.has(pfn).toString();
1490 install_data.msixInstalls.delete(pfn);
1491 extra.other_inst = (!!install_data.installPaths.size).toString();
1492 extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
1494 let dataPath = dataPathOverride;
1496 dataPath = Services.dirsvc.get("GreD", Ci.nsIFile);
1497 dataPath.append("installation_telemetry.json");
1502 dataBytes = await IOUtils.read(dataPath.path);
1504 if (ex.name == "NotFoundError") {
1505 // Many systems will not have the data file, return silently if not found as
1506 // there is nothing to record.
1511 const dataString = new TextDecoder("utf-16").decode(dataBytes);
1512 const data = JSON.parse(dataString);
1514 if (lastInstallTime && data.install_timestamp == lastInstallTime) {
1515 // We've already seen this install
1519 // First time seeing this install, record the timestamp.
1520 Services.prefs.setStringPref(TIMESTAMP_PREF, data.install_timestamp);
1521 let install_data = getInstallData();
1523 installer_type = data.installer_type;
1525 // Installation timestamp is not intended to be sent with telemetry,
1526 // remove it to emphasize this point.
1527 delete data.install_timestamp;
1529 // Build the extra event data
1530 extra.version = data.version;
1531 extra.build_id = data.build_id;
1532 extra.admin_user = data.admin_user.toString();
1533 extra.install_existed = data.install_existed.toString();
1534 extra.profdir_existed = data.profdir_existed.toString();
1535 extra.other_inst = (!!install_data.installPaths.size).toString();
1536 extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
1538 if (data.installer_type == "full") {
1539 extra.silent = data.silent.toString();
1540 extra.from_msi = data.from_msi.toString();
1541 extra.default_path = data.default_path.toString();
1544 return { installer_type, extra };
1547 async reportInstallationTelemetry(
1549 msixPackagePrefixes = ["Mozilla.Firefox", "Mozilla.MozillaFirefox"]
1551 // The optional dataPathOverride is only used for testing purposes.
1552 // Use this as a proxy for whether we're in a testing environment.
1553 // If we're in a testing environment we don't want to return the
1554 // same data even if we call this function multiple times in the
1556 if (gInstallationTelemetryPromise && !dataPathOverride) {
1557 return gInstallationTelemetryPromise;
1560 gInstallationTelemetryPromise = (async () => {
1561 let data = await BrowserUsageTelemetry.collectInstallationTelemetry(
1566 if (data?.installer_type) {
1567 let { installer_type, extra } = data;
1569 // Record the event (mirrored to legacy telemetry using GIFFT)
1570 if (installer_type == "full") {
1571 Glean.installation.firstSeenFull.record(extra);
1572 } else if (installer_type == "stub") {
1573 Glean.installation.firstSeenStub.record(extra);
1574 } else if (installer_type == "msix") {
1575 Glean.installation.firstSeenMsix.record(extra);
1578 // Scalars for the new-profile ping. We don't need to collect the build version
1579 // These are mirrored to legacy telemetry using GIFFT
1580 Glean.installationFirstSeen.installerType.set(installer_type);
1581 Glean.installationFirstSeen.version.set(extra.version);
1582 // Convert "true" or "false" strings back into booleans
1583 Glean.installationFirstSeen.adminUser.set(extra.admin_user === "true");
1584 Glean.installationFirstSeen.installExisted.set(
1585 extra.install_existed === "true"
1587 Glean.installationFirstSeen.profdirExisted.set(
1588 extra.profdir_existed === "true"
1590 Glean.installationFirstSeen.otherInst.set(extra.other_inst === "true");
1591 Glean.installationFirstSeen.otherMsixInst.set(
1592 extra.other_msix_inst === "true"
1594 if (installer_type == "full") {
1595 Glean.installationFirstSeen.silent.set(extra.silent === "true");
1596 Glean.installationFirstSeen.fromMsi.set(extra.from_msi === "true");
1597 Glean.installationFirstSeen.defaultPath.set(
1598 extra.default_path === "true"
1605 return gInstallationTelemetryPromise;
1609 // Used by nsIBrowserUsage
1610 export function getUniqueDomainsVisitedInPast24Hours() {
1611 return URICountListener.uniqueDomainsVisitedInPast24Hours;