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"
34 // The upper bound for the count of the visited unique domain names.
35 const MAX_UNIQUE_VISITED_DOMAINS = 100;
37 // Observed topic names.
38 const TAB_RESTORING_TOPIC = "SSTabRestoring";
39 const TELEMETRY_SUBSESSIONSPLIT_TOPIC =
40 "internal-telemetry-after-subsession-split";
41 const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
44 const MAX_TAB_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_tab_count";
45 const MAX_WINDOW_COUNT_SCALAR_NAME =
46 "browser.engagement.max_concurrent_window_count";
47 const TAB_OPEN_EVENT_COUNT_SCALAR_NAME =
48 "browser.engagement.tab_open_event_count";
49 const MAX_TAB_PINNED_COUNT_SCALAR_NAME =
50 "browser.engagement.max_concurrent_tab_pinned_count";
51 const TAB_PINNED_EVENT_COUNT_SCALAR_NAME =
52 "browser.engagement.tab_pinned_event_count";
53 const WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME =
54 "browser.engagement.window_open_event_count";
55 const UNIQUE_DOMAINS_COUNT_SCALAR_NAME =
56 "browser.engagement.unique_domains_count";
57 const TOTAL_URI_COUNT_SCALAR_NAME = "browser.engagement.total_uri_count";
58 const UNFILTERED_URI_COUNT_SCALAR_NAME =
59 "browser.engagement.unfiltered_uri_count";
60 const TOTAL_URI_COUNT_NORMAL_AND_PRIVATE_MODE_SCALAR_NAME =
61 "browser.engagement.total_uri_count_normal_and_private_mode";
63 export const MINIMUM_TAB_COUNT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes, in ms
65 // The elements we consider to be interactive.
66 const UI_TARGET_ELEMENTS = [
79 // The containers of interactive elements that we care about and their pretty
80 // names. These should be listed in order of most-specific to least-specific,
81 // when iterating JavaScript will guarantee that ordering and so we will find
82 // the most specific area first.
83 const BROWSER_UI_CONTAINER_IDS = {
84 "toolbar-menubar": "menu-bar",
85 TabsToolbar: "tabs-bar",
86 PersonalToolbar: "bookmarks-bar",
87 "appMenu-popup": "app-menu",
88 tabContextMenu: "tabs-context",
89 contentAreaContextMenu: "content-context",
90 "widget-overflow-list": "overflow-menu",
91 "widget-overflow-fixed-list": "pinned-overflow-menu",
92 "page-action-buttons": "pageaction-urlbar",
93 pageActionPanel: "pageaction-panel",
94 "unified-extensions-area": "unified-extensions-area",
95 "allTabsMenu-allTabsView": "alltabs-menu",
97 // This should appear last as some of the above are inside the nav bar.
101 const ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS = {
102 [BROWSER_UI_CONTAINER_IDS.tabContextMenu]: "tabs-context-entrypoint",
105 // A list of the expected panes in about:preferences
106 const PREFERENCES_PANES = [
115 "paneMoreFromMozilla",
118 const IGNORABLE_EVENTS = new WeakMap();
120 const KNOWN_ADDONS = [];
122 // Buttons that, when clicked, set a preference to true. The convention
123 // is that the preference is named:
125 // browser.engagement.<button id>.has-used
127 // and is defaulted to false.
128 const SET_USAGE_PREF_BUTTONS = [
130 "fxa-toolbar-menu-button",
136 // Buttons that, when clicked, increase a counter. The convention
137 // is that the preference is named:
139 // browser.engagement.<button id>.used-count
141 // and doesn't have a default value.
142 const SET_USAGECOUNT_PREF_BUTTONS = [
143 "pageAction-panel-copyURL",
144 "pageAction-panel-emailLink",
145 "pageAction-panel-pinTab",
146 "pageAction-panel-screenshots_mozilla_org",
147 "pageAction-panel-shareURL",
150 // Places context menu IDs.
151 const PLACES_CONTEXT_MENU_ID = "placesContext";
152 const PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID =
153 "placesContext_open:newcontainertab";
155 // Commands used to open history or bookmark links from places context menu.
156 const PLACES_OPEN_COMMANDS = [
158 "placesCmd_open:window",
159 "placesCmd_open:privatewindow",
160 "placesCmd_open:tab",
163 function telemetryId(widgetId, obscureAddons = true) {
164 // Add-on IDs need to be obscured.
165 function addonId(id) {
166 if (!obscureAddons) {
170 let pos = KNOWN_ADDONS.indexOf(id);
172 pos = KNOWN_ADDONS.length;
173 KNOWN_ADDONS.push(id);
175 return `addon${pos}`;
178 if (widgetId.endsWith("-browser-action")) {
180 widgetId.substring(0, widgetId.length - "-browser-action".length)
182 } else if (widgetId.startsWith("pageAction-")) {
184 if (widgetId.startsWith("pageAction-urlbar-")) {
185 actionId = widgetId.substring("pageAction-urlbar-".length);
186 } else if (widgetId.startsWith("pageAction-panel-")) {
187 actionId = widgetId.substring("pageAction-panel-".length);
191 let action = lazy.PageActions.actionForID(actionId);
192 widgetId = action?._isMozillaAction ? actionId : addonId(actionId);
194 } else if (widgetId.startsWith("ext-keyset-id-")) {
195 // Webextension command shortcuts don't have an id on their key element so
196 // we see the id from the keyset that contains them.
197 widgetId = addonId(widgetId.substring("ext-keyset-id-".length));
198 } else if (widgetId.startsWith("ext-key-id-")) {
199 // The command for a webextension sidebar action is an exception to the above rule.
200 widgetId = widgetId.substring("ext-key-id-".length);
201 if (widgetId.endsWith("-sidebar-action")) {
203 widgetId.substring(0, widgetId.length - "-sidebar-action".length)
208 return widgetId.replace(/_/g, "-");
211 function getOpenTabsAndWinsCounts() {
212 let loadedTabCount = 0;
216 for (let win of Services.wm.getEnumerator("navigator:browser")) {
218 tabCount += win.gBrowser.tabs.length;
219 for (const tab of win.gBrowser.tabs) {
220 if (tab.getAttribute("pending") !== "true") {
226 return { loadedTabCount, tabCount, winCount };
229 function getPinnedTabsCount() {
232 for (let win of Services.wm.getEnumerator("navigator:browser")) {
233 pinnedTabs += [...win.ownerGlobal.gBrowser.tabs].filter(
241 export let URICountListener = {
242 // A set containing the visited domains, see bug 1271310.
243 _domainSet: new Set(),
244 // A set containing the visited origins during the last 24 hours (similar to domains, but not quite the same)
245 _domain24hrSet: new Set(),
246 // A map to keep track of the URIs loaded from the restored tabs.
247 _restoredURIsMap: new WeakMap(),
248 // Ongoing expiration timeouts.
249 _timeouts: new Set(),
252 // Only consider http(s) schemas.
253 return uri.schemeIs("http") || uri.schemeIs("https");
256 addRestoredURI(browser, uri) {
257 if (!this.isHttpURI(uri)) {
261 this._restoredURIsMap.set(browser, uri.spec);
264 onLocationChange(browser, webProgress, request, uri, flags) {
266 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) &&
267 webProgress.isTopLevel
269 // By default, assume we no longer need to track this tab.
270 lazy.SearchSERPTelemetry.stopTrackingBrowser(
272 lazy.SearchSERPTelemetryUtils.ABANDONMENTS.NAVIGATION
276 // Don't count this URI if it's an error page.
277 if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
281 // We only care about top level loads.
282 if (!webProgress.isTopLevel) {
286 // The SessionStore sets the URI of a tab first, firing onLocationChange the
287 // first time, then manages content loading using its scheduler. Once content
288 // loads, we will hit onLocationChange again.
289 // We can catch the first case by checking for null requests: be advised that
290 // this can also happen when navigating page fragments, so account for it.
293 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
298 // Don't include URI and domain counts when in private mode.
300 !lazy.PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal) ||
301 Services.prefs.getBoolPref(
302 "browser.engagement.total_uri_count.pbm",
306 // Track URI loads, even if they're not http(s).
311 // If we have troubles parsing the spec, still count this as
312 // an unfiltered URI.
313 if (shouldCountURI) {
314 Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
319 // Don't count about:blank and similar pages, as they would artificially
320 // inflate the counts.
321 if (browser.ownerGlobal.gInitialPages.includes(uriSpec)) {
325 // If the URI we're loading is in the _restoredURIsMap, then it comes from a
326 // restored tab. If so, let's skip it and remove it from the map as we want to
327 // count page refreshes.
328 if (this._restoredURIsMap.get(browser) === uriSpec) {
329 this._restoredURIsMap.delete(browser);
333 // The URI wasn't from a restored tab. Count it among the unfiltered URIs.
334 // If this is an http(s) URI, this also gets counted by the "total_uri_count"
336 if (shouldCountURI) {
337 Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
340 if (!this.isHttpURI(uri)) {
344 if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
345 lazy.SearchSERPTelemetry.updateTrackingStatus(
351 lazy.SearchSERPTelemetry.updateTrackingSinglePageApp(
354 webProgress.loadType,
359 // Update total URI count, including when in private mode.
360 Services.telemetry.scalarAdd(
361 TOTAL_URI_COUNT_NORMAL_AND_PRIVATE_MODE_SCALAR_NAME,
364 Glean.browserEngagement.uriCount.add(1);
366 if (!shouldCountURI) {
370 // Update the URI counts.
371 Services.telemetry.scalarAdd(TOTAL_URI_COUNT_SCALAR_NAME, 1);
374 BrowserUsageTelemetry._recordTabCounts(getOpenTabsAndWinsCounts());
376 // Unique domains should be aggregated by (eTLD + 1): x.test.com and y.test.com
377 // are counted once as test.com.
380 // Even if only considering http(s) URIs, |getBaseDomain| could still throw
381 // due to the URI containing invalid characters or the domain actually being
382 // an ipv4 or ipv6 address.
383 baseDomain = Services.eTLD.getBaseDomain(uri);
388 // We only want to count the unique domains up to MAX_UNIQUE_VISITED_DOMAINS.
389 if (this._domainSet.size < MAX_UNIQUE_VISITED_DOMAINS) {
390 this._domainSet.add(baseDomain);
391 Services.telemetry.scalarSet(
392 UNIQUE_DOMAINS_COUNT_SCALAR_NAME,
397 this._domain24hrSet.add(baseDomain);
398 if (lazy.gRecentVisitedOriginsExpiry) {
399 let timeoutId = lazy.setTimeout(() => {
400 this._domain24hrSet.delete(baseDomain);
401 this._timeouts.delete(timeoutId);
402 }, lazy.gRecentVisitedOriginsExpiry * 1000);
403 this._timeouts.add(timeoutId);
408 * Reset the counts. This should be called when breaking a session in Telemetry.
411 this._domainSet.clear();
415 * Returns the number of unique domains visited in this session during the
418 get uniqueDomainsVisitedInPast24Hours() {
419 return this._domain24hrSet.size;
423 * Resets the number of unique domains visited in this session.
425 resetUniqueDomainsVisitedInPast24Hours() {
426 this._timeouts.forEach(timeoutId => lazy.clearTimeout(timeoutId));
427 this._timeouts.clear();
428 this._domain24hrSet.clear();
431 QueryInterface: ChromeUtils.generateQI([
432 "nsIWebProgressListener",
433 "nsISupportsWeakReference",
437 export let BrowserUsageTelemetry = {
439 * This is a policy object used to override behavior for testing.
442 getTelemetryClientId: async () => lazy.ClientID.getClientID(),
443 getUpdateDirectory: () => Services.dirsvc.get("UpdRootD", Ci.nsIFile),
444 readProfileCountFile: async path => IOUtils.readUTF8(path),
445 writeProfileCountFile: async (path, data) => IOUtils.writeUTF8(path, data),
451 this._lastRecordTabCount = 0;
452 this._lastRecordLoadedTabCount = 0;
453 this._setupAfterRestore();
456 Services.prefs.addObserver("browser.tabs.inTitlebar", this);
458 this._recordUITelemetry();
460 this._onTabsOpenedTask = new lazy.DeferredTask(
461 () => this._onTabsOpened(),
467 * Resets the masked add-on identifiers. Only for use in tests.
470 KNOWN_ADDONS.length = 0;
474 * Handle subsession splits in the parent process.
476 afterSubsessionSplit() {
477 // Scalars just got cleared due to a subsession split. We need to set the maximum
478 // concurrent tab and window counts so that they reflect the correct value for the
480 const counts = getOpenTabsAndWinsCounts();
481 Services.telemetry.scalarSetMaximum(
482 MAX_TAB_COUNT_SCALAR_NAME,
485 Services.telemetry.scalarSetMaximum(
486 MAX_WINDOW_COUNT_SCALAR_NAME,
490 // Reset the URI counter.
491 URICountListener.reset();
494 QueryInterface: ChromeUtils.generateQI([
496 "nsISupportsWeakReference",
503 Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC);
504 Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC);
507 observe(subject, topic, data) {
509 case DOMWINDOW_OPENED_TOPIC:
510 this._onWindowOpen(subject);
512 case TELEMETRY_SUBSESSIONSPLIT_TOPIC:
513 this.afterSubsessionSplit();
515 case "nsPref:changed":
517 case "browser.tabs.inTitlebar":
518 this._recordWidgetChange(
520 Services.appinfo.drawInTitlebar ? "off" : "on",
530 switch (event.type) {
538 this._unregisterWindow(event.target);
540 case TAB_RESTORING_TOPIC:
541 // We're restoring a new tab from a previous or crashed session.
542 // We don't want to track the URIs from these tabs, so let
543 // |URICountListener| know about them.
544 let browser = event.target.linkedBrowser;
545 URICountListener.addRestoredURI(browser, browser.currentURI);
547 const { loadedTabCount } = getOpenTabsAndWinsCounts();
548 this._recordTabCounts({ loadedTabCount });
554 * This gets called shortly after the SessionStore has finished restoring
555 * windows and tabs. It counts the open tabs and adds listeners to all the
558 _setupAfterRestore() {
559 // Make sure to catch new chrome windows and subsession splits.
560 Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, true);
561 Services.obs.addObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, true);
563 // Attach the tabopen handlers to the existing Windows.
564 for (let win of Services.wm.getEnumerator("navigator:browser")) {
565 this._registerWindow(win);
568 // Get the initial tab and windows max counts.
569 const counts = getOpenTabsAndWinsCounts();
570 Services.telemetry.scalarSetMaximum(
571 MAX_TAB_COUNT_SCALAR_NAME,
574 Services.telemetry.scalarSetMaximum(
575 MAX_WINDOW_COUNT_SCALAR_NAME,
580 _buildWidgetPositions() {
581 let widgetMap = new Map();
583 const toolbarState = nodeId => {
585 if (nodeId == "PersonalToolbar") {
586 value = Services.prefs.getCharPref(
587 "browser.toolbars.bookmarks.visibility",
590 if (value != "newtab") {
591 return value == "never" ? "off" : "on";
595 value = Services.xulStore.getValue(
596 AppConstants.BROWSER_CHROME_URL,
602 return value == "true" ? "off" : "on";
608 BROWSER_UI_CONTAINER_IDS.PersonalToolbar,
609 toolbarState("PersonalToolbar")
613 Services.xulStore.getValue(
614 AppConstants.BROWSER_CHROME_URL,
619 widgetMap.set("menu-toolbar", menuBarHidden ? "off" : "on");
621 // Drawing in the titlebar means not showing the titlebar, hence the negation.
622 widgetMap.set("titlebar", Services.appinfo.drawInTitlebar ? "off" : "on");
624 for (let area of lazy.CustomizableUI.areas) {
625 if (!(area in BROWSER_UI_CONTAINER_IDS)) {
629 let position = BROWSER_UI_CONTAINER_IDS[area];
630 if (area == "nav-bar") {
631 position = `${BROWSER_UI_CONTAINER_IDS[area]}-start`;
634 let widgets = lazy.CustomizableUI.getWidgetsInArea(area);
636 for (let widget of widgets) {
641 if (widget.id.startsWith("customizableui-special-")) {
645 if (area == "nav-bar" && widget.id == "urlbar-container") {
646 position = `${BROWSER_UI_CONTAINER_IDS[area]}-end`;
650 widgetMap.set(widget.id, position);
654 let actions = lazy.PageActions.actions;
655 for (let action of actions) {
656 if (action.pinnedToUrlbar) {
657 widgetMap.set(action.id, "pageaction-urlbar");
665 // We want to find a sensible ID for this element.
670 // See if this is a customizable widget.
671 if (node.ownerDocument.URL == AppConstants.BROWSER_CHROME_URL) {
672 // First find if it is inside one of the customizable areas.
673 for (let area of lazy.CustomizableUI.areas) {
674 if (node.closest(`#${CSS.escape(area)}`)) {
675 for (let widget of lazy.CustomizableUI.getWidgetIdsInArea(area)) {
677 // We care about the buttons on the tabs themselves.
678 widget == "tabbrowser-tabs" ||
679 // We care about the page action and other buttons in here.
680 widget == "urlbar-container" ||
681 // We care about the actual menu items.
682 widget == "menubar-items" ||
683 // We care about individual bookmarks here.
684 widget == "personal-bookmarks"
689 if (node.closest(`#${CSS.escape(widget)}`)) {
702 // A couple of special cases in the tabs.
703 for (let cls of ["bookmark-item", "tab-icon-sound", "tab-close-button"]) {
704 if (!node.classList.contains(cls)) {
707 if (cls == "bookmark-item" && node.parentElement.id.includes("history")) {
708 return "history-item";
713 // One of these will at least let us know what the widget is for.
714 let possibleAttributes = [
721 // The key attribute on key elements is the actual key to listen for.
722 if (node.localName != "key") {
723 possibleAttributes.unshift("key");
726 for (let idAttribute of possibleAttributes) {
727 if (node.hasAttribute(idAttribute)) {
728 return node.getAttribute(idAttribute);
732 return this._getWidgetID(node.parentElement);
735 _getBrowserWidgetContainer(node) {
736 // Find the container holding this element.
737 for (let containerId of Object.keys(BROWSER_UI_CONTAINER_IDS)) {
738 let container = node.ownerDocument.getElementById(containerId);
739 if (container && container.contains(node)) {
740 return BROWSER_UI_CONTAINER_IDS[containerId];
743 // Treat toolbar context menu items that relate to tabs as the tab menu:
745 node.closest("#toolbar-context-menu") &&
746 node.getAttribute("contexttype") == "tabbar"
748 return BROWSER_UI_CONTAINER_IDS.tabContextMenu;
753 _getWidgetContainer(node) {
754 if (node.localName == "key") {
758 const { URL } = node.ownerDocument;
759 if (URL == AppConstants.BROWSER_CHROME_URL) {
760 return this._getBrowserWidgetContainer(node);
763 URL.startsWith("about:preferences") ||
764 URL.startsWith("about:settings")
766 // Find the element's category.
767 let container = node.closest("[data-category]");
772 let pane = container.getAttribute("data-category");
774 if (!PREFERENCES_PANES.includes(pane)) {
775 pane = "paneUnknown";
778 return `preferences_${pane}`;
784 lastClickTarget: null,
787 IGNORABLE_EVENTS.set(event, true);
790 _recordCommand(event) {
791 if (IGNORABLE_EVENTS.get(event)) {
795 let sourceEvent = event;
796 while (sourceEvent.sourceEvent) {
797 sourceEvent = sourceEvent.sourceEvent;
800 let lastTarget = this.lastClickTarget?.get();
803 sourceEvent.type == "command" &&
804 sourceEvent.target.contains(lastTarget)
806 // Ignore a command event triggered by a click.
807 this.lastClickTarget = null;
811 this.lastClickTarget = null;
813 if (sourceEvent.type == "click") {
814 // Only care about main button clicks.
815 if (sourceEvent.button != 0) {
819 // This click may trigger a command event so retain the target to be able
820 // to dedupe that event.
821 this.lastClickTarget = Cu.getWeakReference(sourceEvent.target);
824 // We should never see events from web content as they are fired in a
825 // content process, but let's be safe.
826 let url = sourceEvent.target.ownerDocument.documentURIObject;
827 if (!url.schemeIs("chrome") && !url.schemeIs("about")) {
831 // This is what events targetted at content will actually look like.
832 if (sourceEvent.target.localName == "browser") {
836 // Find the actual element we're interested in.
837 let node = sourceEvent.target;
838 const isAboutPreferences =
839 node.ownerDocument.URL.startsWith("about:preferences") ||
840 node.ownerDocument.URL.startsWith("about:settings");
842 !UI_TARGET_ELEMENTS.includes(node.localName) &&
843 !node.classList?.contains("wants-telemetry") &&
844 // We are interested in links on about:preferences as well.
846 isAboutPreferences &&
847 (node.getAttribute("is") === "text-link" || node.localName === "a")
850 node = node.parentNode;
851 if (!node?.parentNode) {
852 // A click on a space or label or top-level document or something we're
853 // not interested in.
858 if (sourceEvent.type === "command") {
859 const { command, ownerDocument, parentNode } = node;
860 // Check if this command is for a history or bookmark link being opened
861 // from the context menu. In this case, we are interested in the DOM node
862 // for the link, not the menu item itself.
864 PLACES_OPEN_COMMANDS.includes(command) ||
865 parentNode?.parentNode?.id === PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID
867 node = ownerDocument.getElementById(PLACES_CONTEXT_MENU_ID).triggerNode;
871 let item = this._getWidgetID(node);
872 let source = this._getWidgetContainer(node);
874 if (item && source) {
875 let scalar = `browser.ui.interaction.${source.replace(/-/g, "_")}`;
876 Services.telemetry.keyedScalarAdd(scalar, telemetryId(item), 1);
877 if (SET_USAGECOUNT_PREF_BUTTONS.includes(item)) {
878 let pref = `browser.engagement.${item}.used-count`;
879 Services.prefs.setIntPref(pref, Services.prefs.getIntPref(pref, 0) + 1);
881 if (SET_USAGE_PREF_BUTTONS.includes(item)) {
882 Services.prefs.setBoolPref(`browser.engagement.${item}.has-used`, true);
886 if (ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source]) {
887 let contextMenu = ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source];
888 let triggerContainer = this._getWidgetContainer(
889 node.closest("menupopup")?.triggerNode
891 if (triggerContainer) {
892 let scalar = `browser.ui.interaction.${contextMenu.replace(/-/g, "_")}`;
893 Services.telemetry.keyedScalarAdd(
895 telemetryId(triggerContainer),
903 * Listens for UI interactions in the window.
905 _addUsageListeners(win) {
906 // Listen for command events from the UI.
907 win.addEventListener("command", event => this._recordCommand(event), true);
908 win.addEventListener("click", event => this._recordCommand(event), true);
912 * A public version of the private method to take care of the `nav-bar-start`,
913 * `nav-bar-end` thing that callers shouldn't have to care about. It also
914 * accepts the DOM ids for the areas rather than the cleaner ones we report
917 recordWidgetChange(widgetId, newPos, reason) {
920 newPos = BROWSER_UI_CONTAINER_IDS[newPos];
923 if (newPos == "nav-bar") {
924 let { position } = lazy.CustomizableUI.getPlacementOfWidget(widgetId);
925 let { position: urlPosition } =
926 lazy.CustomizableUI.getPlacementOfWidget("urlbar-container");
927 newPos = newPos + (urlPosition > position ? "-start" : "-end");
930 this._recordWidgetChange(widgetId, newPos, reason);
936 recordToolbarVisibility(toolbarId, newState, reason) {
937 if (typeof newState != "string") {
938 newState = newState ? "on" : "off";
940 this._recordWidgetChange(
941 BROWSER_UI_CONTAINER_IDS[toolbarId],
947 _recordWidgetChange(widgetId, newPos, reason) {
948 // In some cases (like when add-ons are detected during startup) this gets
949 // called before we've reported the initial positions. Ignore such cases.
950 if (!this.widgetMap) {
954 if (widgetId == "urlbar-container") {
955 // We don't report the position of the url bar, it is after nav-bar-start
956 // and before nav-bar-end. But moving it means the widgets around it have
957 // effectively moved so update those.
958 let position = "nav-bar-start";
959 let widgets = lazy.CustomizableUI.getWidgetsInArea("nav-bar");
961 for (let widget of widgets) {
966 if (widget.id.startsWith("customizableui-special-")) {
970 if (widget.id == "urlbar-container") {
971 position = "nav-bar-end";
975 // This will do nothing if the position hasn't changed.
976 this._recordWidgetChange(widget.id, position, reason);
982 let oldPos = this.widgetMap.get(widgetId);
983 if (oldPos == newPos) {
991 } else if (!newPos) {
995 let key = `${telemetryId(widgetId, false)}_${action}_${oldPos ?? "na"}_${
998 Services.telemetry.keyedScalarAdd("browser.ui.customized_widgets", key, 1);
1001 this.widgetMap.set(widgetId, newPos);
1003 this.widgetMap.delete(widgetId);
1007 _recordUITelemetry() {
1008 this.widgetMap = this._buildWidgetPositions();
1010 for (let [widgetId, position] of this.widgetMap.entries()) {
1011 let key = `${telemetryId(widgetId, false)}_pinned_${position}`;
1012 Services.telemetry.keyedScalarSet(
1013 "browser.ui.toolbar_widgets",
1021 * Adds listeners to a single chrome window.
1023 _registerWindow(win) {
1024 this._addUsageListeners(win);
1026 win.addEventListener("unload", this);
1027 win.addEventListener("TabOpen", this, true);
1028 win.addEventListener("TabPinned", this, true);
1030 win.gBrowser.tabContainer.addEventListener(TAB_RESTORING_TOPIC, this);
1031 win.gBrowser.addTabsProgressListener(URICountListener);
1035 * Removes listeners from a single chrome window.
1037 _unregisterWindow(win) {
1038 win.removeEventListener("unload", this);
1039 win.removeEventListener("TabOpen", this, true);
1040 win.removeEventListener("TabPinned", this, true);
1042 win.defaultView.gBrowser.tabContainer.removeEventListener(
1043 TAB_RESTORING_TOPIC,
1046 win.defaultView.gBrowser.removeTabsProgressListener(URICountListener);
1050 * Updates the tab counts.
1053 // Update the "tab opened" count and its maximum.
1054 Services.telemetry.scalarAdd(TAB_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
1056 // In the case of opening multiple tabs at once, avoid enumerating all open
1057 // tabs and windows each time a tab opens.
1058 this._onTabsOpenedTask.disarm();
1059 this._onTabsOpenedTask.arm();
1063 * Update tab counts after opening multiple tabs.
1066 const { tabCount, loadedTabCount } = getOpenTabsAndWinsCounts();
1067 Services.telemetry.scalarSetMaximum(MAX_TAB_COUNT_SCALAR_NAME, tabCount);
1069 this._recordTabCounts({ tabCount, loadedTabCount });
1073 const pinnedTabs = getPinnedTabsCount();
1075 // Update the "tab pinned" count and its maximum.
1076 Services.telemetry.scalarAdd(TAB_PINNED_EVENT_COUNT_SCALAR_NAME, 1);
1077 Services.telemetry.scalarSetMaximum(
1078 MAX_TAB_PINNED_COUNT_SCALAR_NAME,
1084 * Tracks the window count and registers the listeners for the tab count.
1085 * @param{Object} win The window object.
1087 _onWindowOpen(win) {
1088 // Make sure to have a |nsIDOMWindow|.
1089 if (!(win instanceof Ci.nsIDOMWindow)) {
1093 let onLoad = () => {
1094 win.removeEventListener("load", onLoad);
1096 // Ignore non browser windows.
1098 win.document.documentElement.getAttribute("windowtype") !=
1104 this._registerWindow(win);
1105 // Track the window open event and check the maximum.
1106 const counts = getOpenTabsAndWinsCounts();
1107 Services.telemetry.scalarAdd(WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
1108 Services.telemetry.scalarSetMaximum(
1109 MAX_WINDOW_COUNT_SCALAR_NAME,
1113 // We won't receive the "TabOpen" event for the first tab within a new window.
1114 // Account for that.
1115 this._onTabOpen(counts);
1117 win.addEventListener("load", onLoad);
1121 * Record telemetry about the given tab counts.
1123 * Telemetry for each count will only be recorded if the value isn't
1126 * @param {object} [counts] The tab counts to register with telemetry.
1127 * @param {number} [counts.tabCount] The number of tabs in all browsers.
1128 * @param {number} [counts.loadedTabCount] The number of loaded (i.e., not
1129 * pending) tabs in all browsers.
1131 _recordTabCounts({ tabCount, loadedTabCount }) {
1132 let currentTime = Date.now();
1134 tabCount !== undefined &&
1135 currentTime > this._lastRecordTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1137 Services.telemetry.getHistogramById("TAB_COUNT").add(tabCount);
1138 this._lastRecordTabCount = currentTime;
1142 loadedTabCount !== undefined &&
1144 this._lastRecordLoadedTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
1147 .getHistogramById("LOADED_TAB_COUNT")
1148 .add(loadedTabCount);
1149 this._lastRecordLoadedTabCount = currentTime;
1153 _checkProfileCountFileSchema(fileData) {
1154 // Verifies that the schema of the file is the expected schema
1155 if (typeof fileData.version != "string") {
1156 throw new Error("Schema Mismatch Error: Bad type for 'version' field");
1158 if (!Array.isArray(fileData.profileTelemetryIds)) {
1160 "Schema Mismatch Error: Bad type for 'profileTelemetryIds' field"
1163 for (let profileTelemetryId of fileData.profileTelemetryIds) {
1164 if (typeof profileTelemetryId != "string") {
1166 "Schema Mismatch Error: Bad type for an element of 'profileTelemetryIds'"
1172 // Reports the number of Firefox profiles on this machine to telemetry.
1173 async reportProfileCount() {
1175 AppConstants.platform != "win" ||
1176 !AppConstants.MOZ_TELEMETRY_REPORTING
1178 // This is currently a windows-only feature.
1179 // Also, this function writes directly to disk, without using the usual
1180 // telemetry recording functions. So we excplicitly check if telemetry
1181 // reporting was disabled at compile time, and we do not do anything in
1186 // To report only as much data as we need, we will bucket our values.
1187 // Rather than the raw value, we will report the greatest value in the list
1188 // below that is no larger than the raw value.
1189 const buckets = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000, 10000];
1191 // We need both the C:\ProgramData\Mozilla directory and the install
1192 // directory hash to create the profile count file path. We can easily
1193 // reassemble this from the update directory, which looks like:
1194 // C:\ProgramData\Mozilla\updates\hash
1195 // Retrieving the directory this way also ensures that the "Mozilla"
1196 // directory is created with the correct permissions.
1197 // The ProgramData directory, by default, grants write permissions only to
1198 // file creators. The directory service calls GetCommonUpdateDirectory,
1199 // which makes sure the the directory is created with user-writable
1201 const updateDirectory = BrowserUsageTelemetry.Policy.getUpdateDirectory();
1202 const hash = updateDirectory.leafName;
1203 const profileCountFilename = "profile_count_" + hash + ".json";
1204 let profileCountFile = updateDirectory.parent.parent;
1205 profileCountFile.append(profileCountFilename);
1207 let readError = false;
1210 let json = await BrowserUsageTelemetry.Policy.readProfileCountFile(
1211 profileCountFile.path
1213 fileData = JSON.parse(json);
1214 BrowserUsageTelemetry._checkProfileCountFileSchema(fileData);
1216 // Note that since this also catches the "no such file" error, this is
1217 // always the template that we use when writing to the file for the first
1219 fileData = { version: "1", profileTelemetryIds: [] };
1220 if (!(ex.name == "NotFoundError")) {
1222 // Don't just return here on a read error. We need to send the error
1223 // value to telemetry and we want to attempt to fix the file.
1224 // However, we will still report an error for this ping, even if we
1225 // fix the file. This is to prevent always sending a profile count of 1
1226 // if, for some reason, we always get a read error but never a write
1232 let writeError = false;
1233 let currentTelemetryId =
1234 await BrowserUsageTelemetry.Policy.getTelemetryClientId();
1235 // Don't add our telemetry ID to the file if we've already reached the
1236 // largest bucket. This prevents the file size from growing forever.
1238 !fileData.profileTelemetryIds.includes(currentTelemetryId) &&
1239 fileData.profileTelemetryIds.length < Math.max(...buckets)
1241 fileData.profileTelemetryIds.push(currentTelemetryId);
1243 await BrowserUsageTelemetry.Policy.writeProfileCountFile(
1244 profileCountFile.path,
1245 JSON.stringify(fileData)
1253 // Determine the bucketed value to report
1254 let rawProfileCount = fileData.profileTelemetryIds.length;
1255 let valueToReport = 0;
1256 for (let bucket of buckets) {
1257 if (bucket <= rawProfileCount && bucket > valueToReport) {
1258 valueToReport = bucket;
1262 if (readError || writeError) {
1263 // We convey errors via a profile count of 0.
1267 Services.telemetry.scalarSet(
1268 "browser.engagement.profile_count",
1271 // Manually mirror to Glean
1272 Glean.browserEngagement.profileCount.set(valueToReport);
1276 * Check if this is the first run of this profile since installation,
1277 * if so then send installation telemetry.
1279 * @param {nsIFile} [dataPathOverride] Optional, full data file path, for tests.
1280 * @param {Array<string>} [msixPackagePrefixes] Optional, list of prefixes to
1281 consider "existing" installs when looking at installed MSIX packages.
1282 Defaults to prefixes for builds produced in Firefox automation.
1284 * @resolves When the event has been recorded, or if the data file was not found.
1285 * @rejects JavaScript exception on any failure.
1287 async reportInstallationTelemetry(
1289 msixPackagePrefixes = ["Mozilla.Firefox", "Mozilla.MozillaFirefox"]
1291 if (AppConstants.platform != "win") {
1292 // This is a windows-only feature.
1296 const TIMESTAMP_PREF = "app.installation.timestamp";
1297 const lastInstallTime = Services.prefs.getStringPref(TIMESTAMP_PREF, null);
1298 const wpm = Cc["@mozilla.org/windows-package-manager;1"].createInstance(
1299 Ci.nsIWindowsPackageManager
1301 let installer_type = "";
1304 pfn = Services.sysinfo.getProperty("winPackageFamilyName");
1307 function getInstallData() {
1308 // We only care about where _any_ other install existed - no
1309 // need to count more than 1.
1310 const installPaths = lazy.WindowsInstallsInfo.getInstallPaths(
1312 new Set([Services.dirsvc.get("GreBinD", Ci.nsIFile).path])
1314 const msixInstalls = new Set();
1315 // We're just going to eat all errors here -- we don't want the event
1316 // to go unsent if we were unable to look for MSIX installs.
1319 .findUserInstalledPackages(msixPackagePrefixes)
1320 .forEach(i => msixInstalls.add(i));
1322 msixInstalls.delete(pfn);
1334 if (lastInstallTime != null) {
1335 // We've already seen this install
1339 // First time seeing this install, record the timestamp.
1340 Services.prefs.setStringPref(TIMESTAMP_PREF, wpm.getInstalledDate());
1341 let install_data = getInstallData();
1343 installer_type = "msix";
1345 // Build the extra event data
1346 extra.version = AppConstants.MOZ_APP_VERSION;
1347 extra.build_id = AppConstants.MOZ_BUILDID;
1348 // The next few keys are static for the reasons described
1349 // No way to detect whether or not we were installed by an admin
1350 extra.admin_user = "false";
1351 // Always false at the moment, because we create a new profile
1353 extra.profdir_existed = "false";
1354 // Obviously false for MSIX installs
1355 extra.from_msi = "false";
1356 // We have no way of knowing whether we were installed via the GUI,
1357 // through the command line, or some Enterprise management tool.
1358 extra.silent = "false";
1359 // There's no way to change the install path for an MSIX package
1360 extra.default_path = "true";
1361 extra.install_existed = install_data.msixInstalls.has(pfn).toString();
1362 install_data.msixInstalls.delete(pfn);
1363 extra.other_inst = (!!install_data.installPaths.size).toString();
1364 extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
1366 let dataPath = dataPathOverride;
1368 dataPath = Services.dirsvc.get("GreD", Ci.nsIFile);
1369 dataPath.append("installation_telemetry.json");
1374 dataBytes = await IOUtils.read(dataPath.path);
1376 if (ex.name == "NotFoundError") {
1377 // Many systems will not have the data file, return silently if not found as
1378 // there is nothing to record.
1383 const dataString = new TextDecoder("utf-16").decode(dataBytes);
1384 const data = JSON.parse(dataString);
1386 if (lastInstallTime && data.install_timestamp == lastInstallTime) {
1387 // We've already seen this install
1391 // First time seeing this install, record the timestamp.
1392 Services.prefs.setStringPref(TIMESTAMP_PREF, data.install_timestamp);
1393 let install_data = getInstallData();
1395 installer_type = data.installer_type;
1397 // Installation timestamp is not intended to be sent with telemetry,
1398 // remove it to emphasize this point.
1399 delete data.install_timestamp;
1401 // Build the extra event data
1402 extra.version = data.version;
1403 extra.build_id = data.build_id;
1404 extra.admin_user = data.admin_user.toString();
1405 extra.install_existed = data.install_existed.toString();
1406 extra.profdir_existed = data.profdir_existed.toString();
1407 extra.other_inst = (!!install_data.installPaths.size).toString();
1408 extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
1410 if (data.installer_type == "full") {
1411 extra.silent = data.silent.toString();
1412 extra.from_msi = data.from_msi.toString();
1413 extra.default_path = data.default_path.toString();
1417 Services.telemetry.setEventRecordingEnabled("installation", true);
1418 Services.telemetry.recordEvent(
1428 // Used by nsIBrowserUsage
1429 export function getUniqueDomainsVisitedInPast24Hours() {
1430 return URICountListener.uniqueDomainsVisitedInPast24Hours;