forked from mirrors/gecko-dev
		
	 6d7585cc1e
			
		
	
	
		6d7585cc1e
		
	
	
	
	
		
			
			Backs out Bug 1815023's patch: https://hg.mozilla.org/mozilla-central/rev/e9af7f1b2a01 Depends on D189591 Differential Revision: https://phabricator.services.mozilla.com/D189592
		
			
				
	
	
		
			1420 lines
		
	
	
	
		
			43 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1420 lines
		
	
	
	
		
			43 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
 | |
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   ClientID: "resource://gre/modules/ClientID.sys.mjs",
 | |
|   CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
 | |
|   DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
 | |
|   PageActions: "resource:///modules/PageActions.sys.mjs",
 | |
|   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | |
|   SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs",
 | |
|   SearchSERPTelemetryUtils: "resource:///modules/SearchSERPTelemetry.sys.mjs",
 | |
| 
 | |
|   WindowsInstallsInfo:
 | |
|     "resource://gre/modules/components-utils/WindowsInstallsInfo.sys.mjs",
 | |
| 
 | |
|   clearTimeout: "resource://gre/modules/Timer.sys.mjs",
 | |
|   setTimeout: "resource://gre/modules/Timer.sys.mjs",
 | |
| });
 | |
| 
 | |
| // This pref is in seconds!
 | |
| XPCOMUtils.defineLazyPreferenceGetter(
 | |
|   lazy,
 | |
|   "gRecentVisitedOriginsExpiry",
 | |
|   "browser.engagement.recent_visited_origins.expiry"
 | |
| );
 | |
| 
 | |
| // The upper bound for the count of the visited unique domain names.
 | |
| const MAX_UNIQUE_VISITED_DOMAINS = 100;
 | |
| 
 | |
| // Observed topic names.
 | |
| const TAB_RESTORING_TOPIC = "SSTabRestoring";
 | |
| const TELEMETRY_SUBSESSIONSPLIT_TOPIC =
 | |
|   "internal-telemetry-after-subsession-split";
 | |
| const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
 | |
| 
 | |
| // Probe names.
 | |
| const MAX_TAB_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_tab_count";
 | |
| const MAX_WINDOW_COUNT_SCALAR_NAME =
 | |
|   "browser.engagement.max_concurrent_window_count";
 | |
| const TAB_OPEN_EVENT_COUNT_SCALAR_NAME =
 | |
|   "browser.engagement.tab_open_event_count";
 | |
| const MAX_TAB_PINNED_COUNT_SCALAR_NAME =
 | |
|   "browser.engagement.max_concurrent_tab_pinned_count";
 | |
| const TAB_PINNED_EVENT_COUNT_SCALAR_NAME =
 | |
|   "browser.engagement.tab_pinned_event_count";
 | |
| const WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME =
 | |
|   "browser.engagement.window_open_event_count";
 | |
| const UNIQUE_DOMAINS_COUNT_SCALAR_NAME =
 | |
|   "browser.engagement.unique_domains_count";
 | |
| const TOTAL_URI_COUNT_SCALAR_NAME = "browser.engagement.total_uri_count";
 | |
| const UNFILTERED_URI_COUNT_SCALAR_NAME =
 | |
|   "browser.engagement.unfiltered_uri_count";
 | |
| const TOTAL_URI_COUNT_NORMAL_AND_PRIVATE_MODE_SCALAR_NAME =
 | |
|   "browser.engagement.total_uri_count_normal_and_private_mode";
 | |
| 
 | |
| export const MINIMUM_TAB_COUNT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes, in ms
 | |
| 
 | |
| // The elements we consider to be interactive.
 | |
| const UI_TARGET_ELEMENTS = [
 | |
|   "menuitem",
 | |
|   "toolbarbutton",
 | |
|   "key",
 | |
|   "command",
 | |
|   "checkbox",
 | |
|   "input",
 | |
|   "button",
 | |
|   "image",
 | |
|   "radio",
 | |
|   "richlistitem",
 | |
| ];
 | |
| 
 | |
| // The containers of interactive elements that we care about and their pretty
 | |
| // names. These should be listed in order of most-specific to least-specific,
 | |
| // when iterating JavaScript will guarantee that ordering and so we will find
 | |
| // the most specific area first.
 | |
| const BROWSER_UI_CONTAINER_IDS = {
 | |
|   "toolbar-menubar": "menu-bar",
 | |
|   TabsToolbar: "tabs-bar",
 | |
|   PersonalToolbar: "bookmarks-bar",
 | |
|   "appMenu-popup": "app-menu",
 | |
|   tabContextMenu: "tabs-context",
 | |
|   contentAreaContextMenu: "content-context",
 | |
|   "widget-overflow-list": "overflow-menu",
 | |
|   "widget-overflow-fixed-list": "pinned-overflow-menu",
 | |
|   "page-action-buttons": "pageaction-urlbar",
 | |
|   pageActionPanel: "pageaction-panel",
 | |
|   "unified-extensions-area": "unified-extensions-area",
 | |
|   "allTabsMenu-allTabsView": "alltabs-menu",
 | |
| 
 | |
|   // This should appear last as some of the above are inside the nav bar.
 | |
|   "nav-bar": "nav-bar",
 | |
| };
 | |
| 
 | |
| const ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS = {
 | |
|   [BROWSER_UI_CONTAINER_IDS.tabContextMenu]: "tabs-context-entrypoint",
 | |
| };
 | |
| 
 | |
| // A list of the expected panes in about:preferences
 | |
| const PREFERENCES_PANES = [
 | |
|   "paneHome",
 | |
|   "paneGeneral",
 | |
|   "panePrivacy",
 | |
|   "paneSearch",
 | |
|   "paneSearchResults",
 | |
|   "paneSync",
 | |
|   "paneContainers",
 | |
|   "paneExperimental",
 | |
|   "paneMoreFromMozilla",
 | |
| ];
 | |
| 
 | |
| const IGNORABLE_EVENTS = new WeakMap();
 | |
| 
 | |
| const KNOWN_ADDONS = [];
 | |
| 
 | |
| // Buttons that, when clicked, set a preference to true. The convention
 | |
| // is that the preference is named:
 | |
| //
 | |
| // browser.engagement.<button id>.has-used
 | |
| //
 | |
| // and is defaulted to false.
 | |
| const SET_USAGE_PREF_BUTTONS = [
 | |
|   "downloads-button",
 | |
|   "fxa-toolbar-menu-button",
 | |
|   "home-button",
 | |
|   "sidebar-button",
 | |
|   "library-button",
 | |
| ];
 | |
| 
 | |
| // Buttons that, when clicked, increase a counter. The convention
 | |
| // is that the preference is named:
 | |
| //
 | |
| // browser.engagement.<button id>.used-count
 | |
| //
 | |
| // and doesn't have a default value.
 | |
| const SET_USAGECOUNT_PREF_BUTTONS = [
 | |
|   "pageAction-panel-copyURL",
 | |
|   "pageAction-panel-emailLink",
 | |
|   "pageAction-panel-pinTab",
 | |
|   "pageAction-panel-screenshots_mozilla_org",
 | |
|   "pageAction-panel-shareURL",
 | |
| ];
 | |
| 
 | |
| // Places context menu IDs.
 | |
| const PLACES_CONTEXT_MENU_ID = "placesContext";
 | |
| const PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID =
 | |
|   "placesContext_open:newcontainertab";
 | |
| 
 | |
| // Commands used to open history or bookmark links from places context menu.
 | |
| const PLACES_OPEN_COMMANDS = [
 | |
|   "placesCmd_open",
 | |
|   "placesCmd_open:window",
 | |
|   "placesCmd_open:privatewindow",
 | |
|   "placesCmd_open:tab",
 | |
| ];
 | |
| 
 | |
| function telemetryId(widgetId, obscureAddons = true) {
 | |
|   // Add-on IDs need to be obscured.
 | |
|   function addonId(id) {
 | |
|     if (!obscureAddons) {
 | |
|       return id;
 | |
|     }
 | |
| 
 | |
|     let pos = KNOWN_ADDONS.indexOf(id);
 | |
|     if (pos < 0) {
 | |
|       pos = KNOWN_ADDONS.length;
 | |
|       KNOWN_ADDONS.push(id);
 | |
|     }
 | |
|     return `addon${pos}`;
 | |
|   }
 | |
| 
 | |
|   if (widgetId.endsWith("-browser-action")) {
 | |
|     widgetId = addonId(
 | |
|       widgetId.substring(0, widgetId.length - "-browser-action".length)
 | |
|     );
 | |
|   } else if (widgetId.startsWith("pageAction-")) {
 | |
|     let actionId;
 | |
|     if (widgetId.startsWith("pageAction-urlbar-")) {
 | |
|       actionId = widgetId.substring("pageAction-urlbar-".length);
 | |
|     } else if (widgetId.startsWith("pageAction-panel-")) {
 | |
|       actionId = widgetId.substring("pageAction-panel-".length);
 | |
|     }
 | |
| 
 | |
|     if (actionId) {
 | |
|       let action = lazy.PageActions.actionForID(actionId);
 | |
|       widgetId = action?._isMozillaAction ? actionId : addonId(actionId);
 | |
|     }
 | |
|   } else if (widgetId.startsWith("ext-keyset-id-")) {
 | |
|     // Webextension command shortcuts don't have an id on their key element so
 | |
|     // we see the id from the keyset that contains them.
 | |
|     widgetId = addonId(widgetId.substring("ext-keyset-id-".length));
 | |
|   } else if (widgetId.startsWith("ext-key-id-")) {
 | |
|     // The command for a webextension sidebar action is an exception to the above rule.
 | |
|     widgetId = widgetId.substring("ext-key-id-".length);
 | |
|     if (widgetId.endsWith("-sidebar-action")) {
 | |
|       widgetId = addonId(
 | |
|         widgetId.substring(0, widgetId.length - "-sidebar-action".length)
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return widgetId.replace(/_/g, "-");
 | |
| }
 | |
| 
 | |
| function getOpenTabsAndWinsCounts() {
 | |
|   let loadedTabCount = 0;
 | |
|   let tabCount = 0;
 | |
|   let winCount = 0;
 | |
| 
 | |
|   for (let win of Services.wm.getEnumerator("navigator:browser")) {
 | |
|     winCount++;
 | |
|     tabCount += win.gBrowser.tabs.length;
 | |
|     for (const tab of win.gBrowser.tabs) {
 | |
|       if (tab.getAttribute("pending") !== "true") {
 | |
|         loadedTabCount += 1;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return { loadedTabCount, tabCount, winCount };
 | |
| }
 | |
| 
 | |
| function getPinnedTabsCount() {
 | |
|   let pinnedTabs = 0;
 | |
| 
 | |
|   for (let win of Services.wm.getEnumerator("navigator:browser")) {
 | |
|     pinnedTabs += [...win.ownerGlobal.gBrowser.tabs].filter(
 | |
|       t => t.pinned
 | |
|     ).length;
 | |
|   }
 | |
| 
 | |
|   return pinnedTabs;
 | |
| }
 | |
| 
 | |
| export let URICountListener = {
 | |
|   // A set containing the visited domains, see bug 1271310.
 | |
|   _domainSet: new Set(),
 | |
|   // A set containing the visited origins during the last 24 hours (similar to domains, but not quite the same)
 | |
|   _domain24hrSet: new Set(),
 | |
|   // A map to keep track of the URIs loaded from the restored tabs.
 | |
|   _restoredURIsMap: new WeakMap(),
 | |
|   // Ongoing expiration timeouts.
 | |
|   _timeouts: new Set(),
 | |
| 
 | |
|   isHttpURI(uri) {
 | |
|     // Only consider http(s) schemas.
 | |
|     return uri.schemeIs("http") || uri.schemeIs("https");
 | |
|   },
 | |
| 
 | |
|   addRestoredURI(browser, uri) {
 | |
|     if (!this.isHttpURI(uri)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._restoredURIsMap.set(browser, uri.spec);
 | |
|   },
 | |
| 
 | |
|   onLocationChange(browser, webProgress, request, uri, flags) {
 | |
|     if (
 | |
|       !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) &&
 | |
|       webProgress.isTopLevel
 | |
|     ) {
 | |
|       // By default, assume we no longer need to track this tab.
 | |
|       lazy.SearchSERPTelemetry.stopTrackingBrowser(
 | |
|         browser,
 | |
|         lazy.SearchSERPTelemetryUtils.ABANDONMENTS.NAVIGATION
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // Don't count this URI if it's an error page.
 | |
|     if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // We only care about top level loads.
 | |
|     if (!webProgress.isTopLevel) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // The SessionStore sets the URI of a tab first, firing onLocationChange the
 | |
|     // first time, then manages content loading using its scheduler. Once content
 | |
|     // loads, we will hit onLocationChange again.
 | |
|     // We can catch the first case by checking for null requests: be advised that
 | |
|     // this can also happen when navigating page fragments, so account for it.
 | |
|     if (
 | |
|       !request &&
 | |
|       !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Don't include URI and domain counts when in private mode.
 | |
|     let shouldCountURI =
 | |
|       !lazy.PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal) ||
 | |
|       Services.prefs.getBoolPref(
 | |
|         "browser.engagement.total_uri_count.pbm",
 | |
|         false
 | |
|       );
 | |
| 
 | |
|     // Track URI loads, even if they're not http(s).
 | |
|     let uriSpec = null;
 | |
|     try {
 | |
|       uriSpec = uri.spec;
 | |
|     } catch (e) {
 | |
|       // If we have troubles parsing the spec, still count this as
 | |
|       // an unfiltered URI.
 | |
|       if (shouldCountURI) {
 | |
|         Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
 | |
|       }
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Don't count about:blank and similar pages, as they would artificially
 | |
|     // inflate the counts.
 | |
|     if (browser.ownerGlobal.gInitialPages.includes(uriSpec)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // If the URI we're loading is in the _restoredURIsMap, then it comes from a
 | |
|     // restored tab. If so, let's skip it and remove it from the map as we want to
 | |
|     // count page refreshes.
 | |
|     if (this._restoredURIsMap.get(browser) === uriSpec) {
 | |
|       this._restoredURIsMap.delete(browser);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // The URI wasn't from a restored tab. Count it among the unfiltered URIs.
 | |
|     // If this is an http(s) URI, this also gets counted by the "total_uri_count"
 | |
|     // probe.
 | |
|     if (shouldCountURI) {
 | |
|       Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1);
 | |
|     }
 | |
| 
 | |
|     if (!this.isHttpURI(uri)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
 | |
|       lazy.SearchSERPTelemetry.updateTrackingStatus(
 | |
|         browser,
 | |
|         uriSpec,
 | |
|         webProgress.loadType
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // Update total URI count, including when in private mode.
 | |
|     Services.telemetry.scalarAdd(
 | |
|       TOTAL_URI_COUNT_NORMAL_AND_PRIVATE_MODE_SCALAR_NAME,
 | |
|       1
 | |
|     );
 | |
|     Glean.browserEngagement.uriCount.add(1);
 | |
| 
 | |
|     if (!shouldCountURI) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Update the URI counts.
 | |
|     Services.telemetry.scalarAdd(TOTAL_URI_COUNT_SCALAR_NAME, 1);
 | |
| 
 | |
|     // Update tab count
 | |
|     BrowserUsageTelemetry._recordTabCounts(getOpenTabsAndWinsCounts());
 | |
| 
 | |
|     // Unique domains should be aggregated by (eTLD + 1): x.test.com and y.test.com
 | |
|     // are counted once as test.com.
 | |
|     let baseDomain;
 | |
|     try {
 | |
|       // Even if only considering http(s) URIs, |getBaseDomain| could still throw
 | |
|       // due to the URI containing invalid characters or the domain actually being
 | |
|       // an ipv4 or ipv6 address.
 | |
|       baseDomain = Services.eTLD.getBaseDomain(uri);
 | |
|     } catch (e) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // We only want to count the unique domains up to MAX_UNIQUE_VISITED_DOMAINS.
 | |
|     if (this._domainSet.size < MAX_UNIQUE_VISITED_DOMAINS) {
 | |
|       this._domainSet.add(baseDomain);
 | |
|       Services.telemetry.scalarSet(
 | |
|         UNIQUE_DOMAINS_COUNT_SCALAR_NAME,
 | |
|         this._domainSet.size
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this._domain24hrSet.add(baseDomain);
 | |
|     if (lazy.gRecentVisitedOriginsExpiry) {
 | |
|       let timeoutId = lazy.setTimeout(() => {
 | |
|         this._domain24hrSet.delete(baseDomain);
 | |
|         this._timeouts.delete(timeoutId);
 | |
|       }, lazy.gRecentVisitedOriginsExpiry * 1000);
 | |
|       this._timeouts.add(timeoutId);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Reset the counts. This should be called when breaking a session in Telemetry.
 | |
|    */
 | |
|   reset() {
 | |
|     this._domainSet.clear();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns the number of unique domains visited in this session during the
 | |
|    * last 24 hours.
 | |
|    */
 | |
|   get uniqueDomainsVisitedInPast24Hours() {
 | |
|     return this._domain24hrSet.size;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Resets the number of unique domains visited in this session.
 | |
|    */
 | |
|   resetUniqueDomainsVisitedInPast24Hours() {
 | |
|     this._timeouts.forEach(timeoutId => lazy.clearTimeout(timeoutId));
 | |
|     this._timeouts.clear();
 | |
|     this._domain24hrSet.clear();
 | |
|   },
 | |
| 
 | |
|   QueryInterface: ChromeUtils.generateQI([
 | |
|     "nsIWebProgressListener",
 | |
|     "nsISupportsWeakReference",
 | |
|   ]),
 | |
| };
 | |
| 
 | |
| export let BrowserUsageTelemetry = {
 | |
|   /**
 | |
|    * This is a policy object used to override behavior for testing.
 | |
|    */
 | |
|   Policy: {
 | |
|     getTelemetryClientId: async () => lazy.ClientID.getClientID(),
 | |
|     getUpdateDirectory: () => Services.dirsvc.get("UpdRootD", Ci.nsIFile),
 | |
|     readProfileCountFile: async path => IOUtils.readUTF8(path),
 | |
|     writeProfileCountFile: async (path, data) => IOUtils.writeUTF8(path, data),
 | |
|   },
 | |
| 
 | |
|   _inited: false,
 | |
| 
 | |
|   init() {
 | |
|     this._lastRecordTabCount = 0;
 | |
|     this._lastRecordLoadedTabCount = 0;
 | |
|     this._setupAfterRestore();
 | |
|     this._inited = true;
 | |
| 
 | |
|     Services.prefs.addObserver("browser.tabs.inTitlebar", this);
 | |
| 
 | |
|     this._recordUITelemetry();
 | |
| 
 | |
|     this._onTabsOpenedTask = new lazy.DeferredTask(
 | |
|       () => this._onTabsOpened(),
 | |
|       0
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Resets the masked add-on identifiers. Only for use in tests.
 | |
|    */
 | |
|   _resetAddonIds() {
 | |
|     KNOWN_ADDONS.length = 0;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Handle subsession splits in the parent process.
 | |
|    */
 | |
|   afterSubsessionSplit() {
 | |
|     // Scalars just got cleared due to a subsession split. We need to set the maximum
 | |
|     // concurrent tab and window counts so that they reflect the correct value for the
 | |
|     // new subsession.
 | |
|     const counts = getOpenTabsAndWinsCounts();
 | |
|     Services.telemetry.scalarSetMaximum(
 | |
|       MAX_TAB_COUNT_SCALAR_NAME,
 | |
|       counts.tabCount
 | |
|     );
 | |
|     Services.telemetry.scalarSetMaximum(
 | |
|       MAX_WINDOW_COUNT_SCALAR_NAME,
 | |
|       counts.winCount
 | |
|     );
 | |
| 
 | |
|     // Reset the URI counter.
 | |
|     URICountListener.reset();
 | |
|   },
 | |
| 
 | |
|   QueryInterface: ChromeUtils.generateQI([
 | |
|     "nsIObserver",
 | |
|     "nsISupportsWeakReference",
 | |
|   ]),
 | |
| 
 | |
|   uninit() {
 | |
|     if (!this._inited) {
 | |
|       return;
 | |
|     }
 | |
|     Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC);
 | |
|     Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC);
 | |
|   },
 | |
| 
 | |
|   observe(subject, topic, data) {
 | |
|     switch (topic) {
 | |
|       case DOMWINDOW_OPENED_TOPIC:
 | |
|         this._onWindowOpen(subject);
 | |
|         break;
 | |
|       case TELEMETRY_SUBSESSIONSPLIT_TOPIC:
 | |
|         this.afterSubsessionSplit();
 | |
|         break;
 | |
|       case "nsPref:changed":
 | |
|         switch (data) {
 | |
|           case "browser.tabs.inTitlebar":
 | |
|             this._recordWidgetChange(
 | |
|               "titlebar",
 | |
|               Services.appinfo.drawInTitlebar ? "off" : "on",
 | |
|               "pref"
 | |
|             );
 | |
|             break;
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   handleEvent(event) {
 | |
|     switch (event.type) {
 | |
|       case "TabOpen":
 | |
|         this._onTabOpen();
 | |
|         break;
 | |
|       case "TabPinned":
 | |
|         this._onTabPinned();
 | |
|         break;
 | |
|       case "unload":
 | |
|         this._unregisterWindow(event.target);
 | |
|         break;
 | |
|       case TAB_RESTORING_TOPIC:
 | |
|         // We're restoring a new tab from a previous or crashed session.
 | |
|         // We don't want to track the URIs from these tabs, so let
 | |
|         // |URICountListener| know about them.
 | |
|         let browser = event.target.linkedBrowser;
 | |
|         URICountListener.addRestoredURI(browser, browser.currentURI);
 | |
| 
 | |
|         const { loadedTabCount } = getOpenTabsAndWinsCounts();
 | |
|         this._recordTabCounts({ loadedTabCount });
 | |
|         break;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * This gets called shortly after the SessionStore has finished restoring
 | |
|    * windows and tabs. It counts the open tabs and adds listeners to all the
 | |
|    * windows.
 | |
|    */
 | |
|   _setupAfterRestore() {
 | |
|     // Make sure to catch new chrome windows and subsession splits.
 | |
|     Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, true);
 | |
|     Services.obs.addObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, true);
 | |
| 
 | |
|     // Attach the tabopen handlers to the existing Windows.
 | |
|     for (let win of Services.wm.getEnumerator("navigator:browser")) {
 | |
|       this._registerWindow(win);
 | |
|     }
 | |
| 
 | |
|     // Get the initial tab and windows max counts.
 | |
|     const counts = getOpenTabsAndWinsCounts();
 | |
|     Services.telemetry.scalarSetMaximum(
 | |
|       MAX_TAB_COUNT_SCALAR_NAME,
 | |
|       counts.tabCount
 | |
|     );
 | |
|     Services.telemetry.scalarSetMaximum(
 | |
|       MAX_WINDOW_COUNT_SCALAR_NAME,
 | |
|       counts.winCount
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   _buildWidgetPositions() {
 | |
|     let widgetMap = new Map();
 | |
| 
 | |
|     const toolbarState = nodeId => {
 | |
|       let value;
 | |
|       if (nodeId == "PersonalToolbar") {
 | |
|         value = Services.prefs.getCharPref(
 | |
|           "browser.toolbars.bookmarks.visibility",
 | |
|           "newtab"
 | |
|         );
 | |
|         if (value != "newtab") {
 | |
|           return value == "never" ? "off" : "on";
 | |
|         }
 | |
|         return value;
 | |
|       }
 | |
|       value = Services.xulStore.getValue(
 | |
|         AppConstants.BROWSER_CHROME_URL,
 | |
|         nodeId,
 | |
|         "collapsed"
 | |
|       );
 | |
| 
 | |
|       if (value) {
 | |
|         return value == "true" ? "off" : "on";
 | |
|       }
 | |
|       return "off";
 | |
|     };
 | |
| 
 | |
|     widgetMap.set(
 | |
|       BROWSER_UI_CONTAINER_IDS.PersonalToolbar,
 | |
|       toolbarState("PersonalToolbar")
 | |
|     );
 | |
| 
 | |
|     let menuBarHidden =
 | |
|       Services.xulStore.getValue(
 | |
|         AppConstants.BROWSER_CHROME_URL,
 | |
|         "toolbar-menubar",
 | |
|         "autohide"
 | |
|       ) != "false";
 | |
| 
 | |
|     widgetMap.set("menu-toolbar", menuBarHidden ? "off" : "on");
 | |
| 
 | |
|     // Drawing in the titlebar means not showing the titlebar, hence the negation.
 | |
|     widgetMap.set("titlebar", Services.appinfo.drawInTitlebar ? "off" : "on");
 | |
| 
 | |
|     for (let area of lazy.CustomizableUI.areas) {
 | |
|       if (!(area in BROWSER_UI_CONTAINER_IDS)) {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       let position = BROWSER_UI_CONTAINER_IDS[area];
 | |
|       if (area == "nav-bar") {
 | |
|         position = `${BROWSER_UI_CONTAINER_IDS[area]}-start`;
 | |
|       }
 | |
| 
 | |
|       let widgets = lazy.CustomizableUI.getWidgetsInArea(area);
 | |
| 
 | |
|       for (let widget of widgets) {
 | |
|         if (!widget) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         if (widget.id.startsWith("customizableui-special-")) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         if (area == "nav-bar" && widget.id == "urlbar-container") {
 | |
|           position = `${BROWSER_UI_CONTAINER_IDS[area]}-end`;
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         widgetMap.set(widget.id, position);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let actions = lazy.PageActions.actions;
 | |
|     for (let action of actions) {
 | |
|       if (action.pinnedToUrlbar) {
 | |
|         widgetMap.set(action.id, "pageaction-urlbar");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return widgetMap;
 | |
|   },
 | |
| 
 | |
|   _getWidgetID(node) {
 | |
|     // We want to find a sensible ID for this element.
 | |
|     if (!node) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     // See if this is a customizable widget.
 | |
|     if (node.ownerDocument.URL == AppConstants.BROWSER_CHROME_URL) {
 | |
|       // First find if it is inside one of the customizable areas.
 | |
|       for (let area of lazy.CustomizableUI.areas) {
 | |
|         if (node.closest(`#${CSS.escape(area)}`)) {
 | |
|           for (let widget of lazy.CustomizableUI.getWidgetIdsInArea(area)) {
 | |
|             if (
 | |
|               // We care about the buttons on the tabs themselves.
 | |
|               widget == "tabbrowser-tabs" ||
 | |
|               // We care about the page action and other buttons in here.
 | |
|               widget == "urlbar-container" ||
 | |
|               // We care about the actual menu items.
 | |
|               widget == "menubar-items" ||
 | |
|               // We care about individual bookmarks here.
 | |
|               widget == "personal-bookmarks"
 | |
|             ) {
 | |
|               continue;
 | |
|             }
 | |
| 
 | |
|             if (node.closest(`#${CSS.escape(widget)}`)) {
 | |
|               return widget;
 | |
|             }
 | |
|           }
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (node.id) {
 | |
|       return node.id;
 | |
|     }
 | |
| 
 | |
|     // A couple of special cases in the tabs.
 | |
|     for (let cls of ["bookmark-item", "tab-icon-sound", "tab-close-button"]) {
 | |
|       if (!node.classList.contains(cls)) {
 | |
|         continue;
 | |
|       }
 | |
|       if (cls == "bookmark-item" && node.parentElement.id.includes("history")) {
 | |
|         return "history-item";
 | |
|       }
 | |
|       return cls;
 | |
|     }
 | |
| 
 | |
|     // One of these will at least let us know what the widget is for.
 | |
|     let possibleAttributes = [
 | |
|       "preference",
 | |
|       "command",
 | |
|       "observes",
 | |
|       "data-l10n-id",
 | |
|     ];
 | |
| 
 | |
|     // The key attribute on key elements is the actual key to listen for.
 | |
|     if (node.localName != "key") {
 | |
|       possibleAttributes.unshift("key");
 | |
|     }
 | |
| 
 | |
|     for (let idAttribute of possibleAttributes) {
 | |
|       if (node.hasAttribute(idAttribute)) {
 | |
|         return node.getAttribute(idAttribute);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return this._getWidgetID(node.parentElement);
 | |
|   },
 | |
| 
 | |
|   _getBrowserWidgetContainer(node) {
 | |
|     // Find the container holding this element.
 | |
|     for (let containerId of Object.keys(BROWSER_UI_CONTAINER_IDS)) {
 | |
|       let container = node.ownerDocument.getElementById(containerId);
 | |
|       if (container && container.contains(node)) {
 | |
|         return BROWSER_UI_CONTAINER_IDS[containerId];
 | |
|       }
 | |
|     }
 | |
|     // Treat toolbar context menu items that relate to tabs as the tab menu:
 | |
|     if (
 | |
|       node.closest("#toolbar-context-menu") &&
 | |
|       node.getAttribute("contexttype") == "tabbar"
 | |
|     ) {
 | |
|       return BROWSER_UI_CONTAINER_IDS.tabContextMenu;
 | |
|     }
 | |
|     return null;
 | |
|   },
 | |
| 
 | |
|   _getWidgetContainer(node) {
 | |
|     if (node.localName == "key") {
 | |
|       return "keyboard";
 | |
|     }
 | |
| 
 | |
|     const { URL } = node.ownerDocument;
 | |
|     if (URL == AppConstants.BROWSER_CHROME_URL) {
 | |
|       return this._getBrowserWidgetContainer(node);
 | |
|     }
 | |
|     if (URL.startsWith("about:preferences")) {
 | |
|       // Find the element's category.
 | |
|       let container = node.closest("[data-category]");
 | |
|       if (!container) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       let pane = container.getAttribute("data-category");
 | |
| 
 | |
|       if (!PREFERENCES_PANES.includes(pane)) {
 | |
|         pane = "paneUnknown";
 | |
|       }
 | |
| 
 | |
|       return `preferences_${pane}`;
 | |
|     }
 | |
| 
 | |
|     return null;
 | |
|   },
 | |
| 
 | |
|   lastClickTarget: null,
 | |
| 
 | |
|   ignoreEvent(event) {
 | |
|     IGNORABLE_EVENTS.set(event, true);
 | |
|   },
 | |
| 
 | |
|   _recordCommand(event) {
 | |
|     if (IGNORABLE_EVENTS.get(event)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let sourceEvent = event;
 | |
|     while (sourceEvent.sourceEvent) {
 | |
|       sourceEvent = sourceEvent.sourceEvent;
 | |
|     }
 | |
| 
 | |
|     let lastTarget = this.lastClickTarget?.get();
 | |
|     if (
 | |
|       lastTarget &&
 | |
|       sourceEvent.type == "command" &&
 | |
|       sourceEvent.target.contains(lastTarget)
 | |
|     ) {
 | |
|       // Ignore a command event triggered by a click.
 | |
|       this.lastClickTarget = null;
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.lastClickTarget = null;
 | |
| 
 | |
|     if (sourceEvent.type == "click") {
 | |
|       // Only care about main button clicks.
 | |
|       if (sourceEvent.button != 0) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // This click may trigger a command event so retain the target to be able
 | |
|       // to dedupe that event.
 | |
|       this.lastClickTarget = Cu.getWeakReference(sourceEvent.target);
 | |
|     }
 | |
| 
 | |
|     // We should never see events from web content as they are fired in a
 | |
|     // content process, but let's be safe.
 | |
|     let url = sourceEvent.target.ownerDocument.documentURIObject;
 | |
|     if (!url.schemeIs("chrome") && !url.schemeIs("about")) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // This is what events targetted  at content will actually look like.
 | |
|     if (sourceEvent.target.localName == "browser") {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Find the actual element we're interested in.
 | |
|     let node = sourceEvent.target;
 | |
|     const isAboutPreferences =
 | |
|       node.ownerDocument.URL.startsWith("about:preferences");
 | |
|     while (
 | |
|       !UI_TARGET_ELEMENTS.includes(node.localName) &&
 | |
|       !node.classList?.contains("wants-telemetry") &&
 | |
|       // We are interested in links on about:preferences as well.
 | |
|       !(
 | |
|         isAboutPreferences &&
 | |
|         (node.getAttribute("is") === "text-link" || node.localName === "a")
 | |
|       )
 | |
|     ) {
 | |
|       node = node.parentNode;
 | |
|       if (!node?.parentNode) {
 | |
|         // A click on a space or label or top-level document or something we're
 | |
|         // not interested in.
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (sourceEvent.type === "command") {
 | |
|       const { command, ownerDocument, parentNode } = node;
 | |
|       // Check if this command is for a history or bookmark link being opened
 | |
|       // from the context menu. In this case, we are interested in the DOM node
 | |
|       // for the link, not the menu item itself.
 | |
|       if (
 | |
|         PLACES_OPEN_COMMANDS.includes(command) ||
 | |
|         parentNode?.parentNode?.id === PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID
 | |
|       ) {
 | |
|         node = ownerDocument.getElementById(PLACES_CONTEXT_MENU_ID).triggerNode;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let item = this._getWidgetID(node);
 | |
|     let source = this._getWidgetContainer(node);
 | |
| 
 | |
|     if (item && source) {
 | |
|       let scalar = `browser.ui.interaction.${source.replace(/-/g, "_")}`;
 | |
|       Services.telemetry.keyedScalarAdd(scalar, telemetryId(item), 1);
 | |
|       if (SET_USAGECOUNT_PREF_BUTTONS.includes(item)) {
 | |
|         let pref = `browser.engagement.${item}.used-count`;
 | |
|         Services.prefs.setIntPref(pref, Services.prefs.getIntPref(pref, 0) + 1);
 | |
|       }
 | |
|       if (SET_USAGE_PREF_BUTTONS.includes(item)) {
 | |
|         Services.prefs.setBoolPref(`browser.engagement.${item}.has-used`, true);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source]) {
 | |
|       let contextMenu = ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source];
 | |
|       let triggerContainer = this._getWidgetContainer(
 | |
|         node.closest("menupopup")?.triggerNode
 | |
|       );
 | |
|       if (triggerContainer) {
 | |
|         let scalar = `browser.ui.interaction.${contextMenu.replace(/-/g, "_")}`;
 | |
|         Services.telemetry.keyedScalarAdd(
 | |
|           scalar,
 | |
|           telemetryId(triggerContainer),
 | |
|           1
 | |
|         );
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Listens for UI interactions in the window.
 | |
|    */
 | |
|   _addUsageListeners(win) {
 | |
|     // Listen for command events from the UI.
 | |
|     win.addEventListener("command", event => this._recordCommand(event), true);
 | |
|     win.addEventListener("click", event => this._recordCommand(event), true);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * A public version of the private method to take care of the `nav-bar-start`,
 | |
|    * `nav-bar-end` thing that callers shouldn't have to care about. It also
 | |
|    * accepts the DOM ids for the areas rather than the cleaner ones we report
 | |
|    * to telemetry.
 | |
|    */
 | |
|   recordWidgetChange(widgetId, newPos, reason) {
 | |
|     try {
 | |
|       if (newPos) {
 | |
|         newPos = BROWSER_UI_CONTAINER_IDS[newPos];
 | |
|       }
 | |
| 
 | |
|       if (newPos == "nav-bar") {
 | |
|         let { position } = lazy.CustomizableUI.getPlacementOfWidget(widgetId);
 | |
|         let { position: urlPosition } =
 | |
|           lazy.CustomizableUI.getPlacementOfWidget("urlbar-container");
 | |
|         newPos = newPos + (urlPosition > position ? "-start" : "-end");
 | |
|       }
 | |
| 
 | |
|       this._recordWidgetChange(widgetId, newPos, reason);
 | |
|     } catch (e) {
 | |
|       console.error(e);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   recordToolbarVisibility(toolbarId, newState, reason) {
 | |
|     if (typeof newState != "string") {
 | |
|       newState = newState ? "on" : "off";
 | |
|     }
 | |
|     this._recordWidgetChange(
 | |
|       BROWSER_UI_CONTAINER_IDS[toolbarId],
 | |
|       newState,
 | |
|       reason
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   _recordWidgetChange(widgetId, newPos, reason) {
 | |
|     // In some cases (like when add-ons are detected during startup) this gets
 | |
|     // called before we've reported the initial positions. Ignore such cases.
 | |
|     if (!this.widgetMap) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (widgetId == "urlbar-container") {
 | |
|       // We don't report the position of the url bar, it is after nav-bar-start
 | |
|       // and before nav-bar-end. But moving it means the widgets around it have
 | |
|       // effectively moved so update those.
 | |
|       let position = "nav-bar-start";
 | |
|       let widgets = lazy.CustomizableUI.getWidgetsInArea("nav-bar");
 | |
| 
 | |
|       for (let widget of widgets) {
 | |
|         if (!widget) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         if (widget.id.startsWith("customizableui-special-")) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         if (widget.id == "urlbar-container") {
 | |
|           position = "nav-bar-end";
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         // This will do nothing if the position hasn't changed.
 | |
|         this._recordWidgetChange(widget.id, position, reason);
 | |
|       }
 | |
| 
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let oldPos = this.widgetMap.get(widgetId);
 | |
|     if (oldPos == newPos) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let action = "move";
 | |
| 
 | |
|     if (!oldPos) {
 | |
|       action = "add";
 | |
|     } else if (!newPos) {
 | |
|       action = "remove";
 | |
|     }
 | |
| 
 | |
|     let key = `${telemetryId(widgetId, false)}_${action}_${oldPos ?? "na"}_${
 | |
|       newPos ?? "na"
 | |
|     }_${reason}`;
 | |
|     Services.telemetry.keyedScalarAdd("browser.ui.customized_widgets", key, 1);
 | |
| 
 | |
|     if (newPos) {
 | |
|       this.widgetMap.set(widgetId, newPos);
 | |
|     } else {
 | |
|       this.widgetMap.delete(widgetId);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _recordUITelemetry() {
 | |
|     this.widgetMap = this._buildWidgetPositions();
 | |
| 
 | |
|     for (let [widgetId, position] of this.widgetMap.entries()) {
 | |
|       let key = `${telemetryId(widgetId, false)}_pinned_${position}`;
 | |
|       Services.telemetry.keyedScalarSet(
 | |
|         "browser.ui.toolbar_widgets",
 | |
|         key,
 | |
|         true
 | |
|       );
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Adds listeners to a single chrome window.
 | |
|    */
 | |
|   _registerWindow(win) {
 | |
|     this._addUsageListeners(win);
 | |
| 
 | |
|     win.addEventListener("unload", this);
 | |
|     win.addEventListener("TabOpen", this, true);
 | |
|     win.addEventListener("TabPinned", this, true);
 | |
| 
 | |
|     win.gBrowser.tabContainer.addEventListener(TAB_RESTORING_TOPIC, this);
 | |
|     win.gBrowser.addTabsProgressListener(URICountListener);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes listeners from a single chrome window.
 | |
|    */
 | |
|   _unregisterWindow(win) {
 | |
|     win.removeEventListener("unload", this);
 | |
|     win.removeEventListener("TabOpen", this, true);
 | |
|     win.removeEventListener("TabPinned", this, true);
 | |
| 
 | |
|     win.defaultView.gBrowser.tabContainer.removeEventListener(
 | |
|       TAB_RESTORING_TOPIC,
 | |
|       this
 | |
|     );
 | |
|     win.defaultView.gBrowser.removeTabsProgressListener(URICountListener);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Updates the tab counts.
 | |
|    */
 | |
|   _onTabOpen() {
 | |
|     // Update the "tab opened" count and its maximum.
 | |
|     Services.telemetry.scalarAdd(TAB_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
 | |
| 
 | |
|     // In the case of opening multiple tabs at once, avoid enumerating all open
 | |
|     // tabs and windows each time a tab opens.
 | |
|     this._onTabsOpenedTask.disarm();
 | |
|     this._onTabsOpenedTask.arm();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Update tab counts after opening multiple tabs.
 | |
|    */
 | |
|   _onTabsOpened() {
 | |
|     const { tabCount, loadedTabCount } = getOpenTabsAndWinsCounts();
 | |
|     Services.telemetry.scalarSetMaximum(MAX_TAB_COUNT_SCALAR_NAME, tabCount);
 | |
| 
 | |
|     this._recordTabCounts({ tabCount, loadedTabCount });
 | |
|   },
 | |
| 
 | |
|   _onTabPinned(target) {
 | |
|     const pinnedTabs = getPinnedTabsCount();
 | |
| 
 | |
|     // Update the "tab pinned" count and its maximum.
 | |
|     Services.telemetry.scalarAdd(TAB_PINNED_EVENT_COUNT_SCALAR_NAME, 1);
 | |
|     Services.telemetry.scalarSetMaximum(
 | |
|       MAX_TAB_PINNED_COUNT_SCALAR_NAME,
 | |
|       pinnedTabs
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Tracks the window count and registers the listeners for the tab count.
 | |
|    * @param{Object} win The window object.
 | |
|    */
 | |
|   _onWindowOpen(win) {
 | |
|     // Make sure to have a |nsIDOMWindow|.
 | |
|     if (!(win instanceof Ci.nsIDOMWindow)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let onLoad = () => {
 | |
|       win.removeEventListener("load", onLoad);
 | |
| 
 | |
|       // Ignore non browser windows.
 | |
|       if (
 | |
|         win.document.documentElement.getAttribute("windowtype") !=
 | |
|         "navigator:browser"
 | |
|       ) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this._registerWindow(win);
 | |
|       // Track the window open event and check the maximum.
 | |
|       const counts = getOpenTabsAndWinsCounts();
 | |
|       Services.telemetry.scalarAdd(WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME, 1);
 | |
|       Services.telemetry.scalarSetMaximum(
 | |
|         MAX_WINDOW_COUNT_SCALAR_NAME,
 | |
|         counts.winCount
 | |
|       );
 | |
| 
 | |
|       // We won't receive the "TabOpen" event for the first tab within a new window.
 | |
|       // Account for that.
 | |
|       this._onTabOpen(counts);
 | |
|     };
 | |
|     win.addEventListener("load", onLoad);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Record telemetry about the given tab counts.
 | |
|    *
 | |
|    * Telemetry for each count will only be recorded if the value isn't
 | |
|    * `undefined`.
 | |
|    *
 | |
|    * @param {object} [counts] The tab counts to register with telemetry.
 | |
|    * @param {number} [counts.tabCount] The number of tabs in all browsers.
 | |
|    * @param {number} [counts.loadedTabCount] The number of loaded (i.e., not
 | |
|    *                                         pending) tabs in all browsers.
 | |
|    */
 | |
|   _recordTabCounts({ tabCount, loadedTabCount }) {
 | |
|     let currentTime = Date.now();
 | |
|     if (
 | |
|       tabCount !== undefined &&
 | |
|       currentTime > this._lastRecordTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
 | |
|     ) {
 | |
|       Services.telemetry.getHistogramById("TAB_COUNT").add(tabCount);
 | |
|       this._lastRecordTabCount = currentTime;
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       loadedTabCount !== undefined &&
 | |
|       currentTime >
 | |
|         this._lastRecordLoadedTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
 | |
|     ) {
 | |
|       Services.telemetry
 | |
|         .getHistogramById("LOADED_TAB_COUNT")
 | |
|         .add(loadedTabCount);
 | |
|       this._lastRecordLoadedTabCount = currentTime;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _checkProfileCountFileSchema(fileData) {
 | |
|     // Verifies that the schema of the file is the expected schema
 | |
|     if (typeof fileData.version != "string") {
 | |
|       throw new Error("Schema Mismatch Error: Bad type for 'version' field");
 | |
|     }
 | |
|     if (!Array.isArray(fileData.profileTelemetryIds)) {
 | |
|       throw new Error(
 | |
|         "Schema Mismatch Error: Bad type for 'profileTelemetryIds' field"
 | |
|       );
 | |
|     }
 | |
|     for (let profileTelemetryId of fileData.profileTelemetryIds) {
 | |
|       if (typeof profileTelemetryId != "string") {
 | |
|         throw new Error(
 | |
|           "Schema Mismatch Error: Bad type for an element of 'profileTelemetryIds'"
 | |
|         );
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // Reports the number of Firefox profiles on this machine to telemetry.
 | |
|   async reportProfileCount() {
 | |
|     if (
 | |
|       AppConstants.platform != "win" ||
 | |
|       !AppConstants.MOZ_TELEMETRY_REPORTING
 | |
|     ) {
 | |
|       // This is currently a windows-only feature.
 | |
|       // Also, this function writes directly to disk, without using the usual
 | |
|       // telemetry recording functions. So we excplicitly check if telemetry
 | |
|       // reporting was disabled at compile time, and we do not do anything in
 | |
|       // case.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // To report only as much data as we need, we will bucket our values.
 | |
|     // Rather than the raw value, we will report the greatest value in the list
 | |
|     // below that is no larger than the raw value.
 | |
|     const buckets = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000, 10000];
 | |
| 
 | |
|     // We need both the C:\ProgramData\Mozilla directory and the install
 | |
|     // directory hash to create the profile count file path. We can easily
 | |
|     // reassemble this from the update directory, which looks like:
 | |
|     // C:\ProgramData\Mozilla\updates\hash
 | |
|     // Retrieving the directory this way also ensures that the "Mozilla"
 | |
|     // directory is created with the correct permissions.
 | |
|     // The ProgramData directory, by default, grants write permissions only to
 | |
|     // file creators. The directory service calls GetCommonUpdateDirectory,
 | |
|     // which makes sure the the directory is created with user-writable
 | |
|     // permissions.
 | |
|     const updateDirectory = BrowserUsageTelemetry.Policy.getUpdateDirectory();
 | |
|     const hash = updateDirectory.leafName;
 | |
|     const profileCountFilename = "profile_count_" + hash + ".json";
 | |
|     let profileCountFile = updateDirectory.parent.parent;
 | |
|     profileCountFile.append(profileCountFilename);
 | |
| 
 | |
|     let readError = false;
 | |
|     let fileData;
 | |
|     try {
 | |
|       let json = await BrowserUsageTelemetry.Policy.readProfileCountFile(
 | |
|         profileCountFile.path
 | |
|       );
 | |
|       fileData = JSON.parse(json);
 | |
|       BrowserUsageTelemetry._checkProfileCountFileSchema(fileData);
 | |
|     } catch (ex) {
 | |
|       // Note that since this also catches the "no such file" error, this is
 | |
|       // always the template that we use when writing to the file for the first
 | |
|       // time.
 | |
|       fileData = { version: "1", profileTelemetryIds: [] };
 | |
|       if (!(ex.name == "NotFoundError")) {
 | |
|         console.error(ex);
 | |
|         // Don't just return here on a read error. We need to send the error
 | |
|         // value to telemetry and we want to attempt to fix the file.
 | |
|         // However, we will still report an error for this ping, even if we
 | |
|         // fix the file. This is to prevent always sending a profile count of 1
 | |
|         // if, for some reason, we always get a read error but never a write
 | |
|         // error.
 | |
|         readError = true;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let writeError = false;
 | |
|     let currentTelemetryId =
 | |
|       await BrowserUsageTelemetry.Policy.getTelemetryClientId();
 | |
|     // Don't add our telemetry ID to the file if we've already reached the
 | |
|     // largest bucket. This prevents the file size from growing forever.
 | |
|     if (
 | |
|       !fileData.profileTelemetryIds.includes(currentTelemetryId) &&
 | |
|       fileData.profileTelemetryIds.length < Math.max(...buckets)
 | |
|     ) {
 | |
|       fileData.profileTelemetryIds.push(currentTelemetryId);
 | |
|       try {
 | |
|         await BrowserUsageTelemetry.Policy.writeProfileCountFile(
 | |
|           profileCountFile.path,
 | |
|           JSON.stringify(fileData)
 | |
|         );
 | |
|       } catch (ex) {
 | |
|         console.error(ex);
 | |
|         writeError = true;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Determine the bucketed value to report
 | |
|     let rawProfileCount = fileData.profileTelemetryIds.length;
 | |
|     let valueToReport = 0;
 | |
|     for (let bucket of buckets) {
 | |
|       if (bucket <= rawProfileCount && bucket > valueToReport) {
 | |
|         valueToReport = bucket;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (readError || writeError) {
 | |
|       // We convey errors via a profile count of 0.
 | |
|       valueToReport = 0;
 | |
|     }
 | |
| 
 | |
|     Services.telemetry.scalarSet(
 | |
|       "browser.engagement.profile_count",
 | |
|       valueToReport
 | |
|     );
 | |
|     // Manually mirror to Glean
 | |
|     Glean.browserEngagement.profileCount.set(valueToReport);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Check if this is the first run of this profile since installation,
 | |
|    * if so then send installation telemetry.
 | |
|    *
 | |
|    * @param {nsIFile} [dataPathOverride] Optional, full data file path, for tests.
 | |
|    * @param {Array<string>} [msixPackagePrefixes] Optional, list of prefixes to
 | |
|             consider "existing" installs when looking at installed MSIX packages.
 | |
|             Defaults to prefixes for builds produced in Firefox automation.
 | |
|    * @return {Promise}
 | |
|    * @resolves When the event has been recorded, or if the data file was not found.
 | |
|    * @rejects JavaScript exception on any failure.
 | |
|    */
 | |
|   async reportInstallationTelemetry(
 | |
|     dataPathOverride,
 | |
|     msixPackagePrefixes = ["Mozilla.Firefox", "Mozilla.MozillaFirefox"]
 | |
|   ) {
 | |
|     if (AppConstants.platform != "win") {
 | |
|       // This is a windows-only feature.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const TIMESTAMP_PREF = "app.installation.timestamp";
 | |
|     const lastInstallTime = Services.prefs.getStringPref(TIMESTAMP_PREF, null);
 | |
|     const wpm = Cc["@mozilla.org/windows-package-manager;1"].createInstance(
 | |
|       Ci.nsIWindowsPackageManager
 | |
|     );
 | |
|     let installer_type = "";
 | |
|     let pfn;
 | |
|     try {
 | |
|       pfn = Services.sysinfo.getProperty("winPackageFamilyName");
 | |
|     } catch (e) {}
 | |
| 
 | |
|     function getInstallData() {
 | |
|       // We only care about where _any_ other install existed - no
 | |
|       // need to count more than 1.
 | |
|       const installPaths = lazy.WindowsInstallsInfo.getInstallPaths(
 | |
|         1,
 | |
|         new Set([Services.dirsvc.get("GreBinD", Ci.nsIFile).path])
 | |
|       );
 | |
|       const msixInstalls = new Set();
 | |
|       // We're just going to eat all errors here -- we don't want the event
 | |
|       // to go unsent if we were unable to look for MSIX installs.
 | |
|       try {
 | |
|         wpm
 | |
|           .findUserInstalledPackages(msixPackagePrefixes)
 | |
|           .forEach(i => msixInstalls.add(i));
 | |
|         if (pfn) {
 | |
|           msixInstalls.delete(pfn);
 | |
|         }
 | |
|       } catch (ex) {}
 | |
|       return {
 | |
|         installPaths,
 | |
|         msixInstalls,
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     let extra = {};
 | |
| 
 | |
|     if (pfn) {
 | |
|       if (lastInstallTime != null) {
 | |
|         // We've already seen this install
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // First time seeing this install, record the timestamp.
 | |
|       Services.prefs.setStringPref(TIMESTAMP_PREF, wpm.getInstalledDate());
 | |
|       let install_data = getInstallData();
 | |
| 
 | |
|       installer_type = "msix";
 | |
| 
 | |
|       // Build the extra event data
 | |
|       extra.version = AppConstants.MOZ_APP_VERSION;
 | |
|       extra.build_id = AppConstants.MOZ_BUILDID;
 | |
|       // The next few keys are static for the reasons described
 | |
|       // No way to detect whether or not we were installed by an admin
 | |
|       extra.admin_user = "false";
 | |
|       // Always false at the moment, because we create a new profile
 | |
|       // on first launch
 | |
|       extra.profdir_existed = "false";
 | |
|       // Obviously false for MSIX installs
 | |
|       extra.from_msi = "false";
 | |
|       // We have no way of knowing whether we were installed via the GUI,
 | |
|       // through the command line, or some Enterprise management tool.
 | |
|       extra.silent = "false";
 | |
|       // There's no way to change the install path for an MSIX package
 | |
|       extra.default_path = "true";
 | |
|       extra.install_existed = install_data.msixInstalls.has(pfn).toString();
 | |
|       install_data.msixInstalls.delete(pfn);
 | |
|       extra.other_inst = (!!install_data.installPaths.size).toString();
 | |
|       extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
 | |
|     } else {
 | |
|       let dataPath = dataPathOverride;
 | |
|       if (!dataPath) {
 | |
|         dataPath = Services.dirsvc.get("GreD", Ci.nsIFile);
 | |
|         dataPath.append("installation_telemetry.json");
 | |
|       }
 | |
| 
 | |
|       let dataBytes;
 | |
|       try {
 | |
|         dataBytes = await IOUtils.read(dataPath.path);
 | |
|       } catch (ex) {
 | |
|         if (ex.name == "NotFoundError") {
 | |
|           // Many systems will not have the data file, return silently if not found as
 | |
|           // there is nothing to record.
 | |
|           return;
 | |
|         }
 | |
|         throw ex;
 | |
|       }
 | |
|       const dataString = new TextDecoder("utf-16").decode(dataBytes);
 | |
|       const data = JSON.parse(dataString);
 | |
| 
 | |
|       if (lastInstallTime && data.install_timestamp == lastInstallTime) {
 | |
|         // We've already seen this install
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // First time seeing this install, record the timestamp.
 | |
|       Services.prefs.setStringPref(TIMESTAMP_PREF, data.install_timestamp);
 | |
|       let install_data = getInstallData();
 | |
| 
 | |
|       installer_type = data.installer_type;
 | |
| 
 | |
|       // Installation timestamp is not intended to be sent with telemetry,
 | |
|       // remove it to emphasize this point.
 | |
|       delete data.install_timestamp;
 | |
| 
 | |
|       // Build the extra event data
 | |
|       extra.version = data.version;
 | |
|       extra.build_id = data.build_id;
 | |
|       extra.admin_user = data.admin_user.toString();
 | |
|       extra.install_existed = data.install_existed.toString();
 | |
|       extra.profdir_existed = data.profdir_existed.toString();
 | |
|       extra.other_inst = (!!install_data.installPaths.size).toString();
 | |
|       extra.other_msix_inst = (!!install_data.msixInstalls.size).toString();
 | |
| 
 | |
|       if (data.installer_type == "full") {
 | |
|         extra.silent = data.silent.toString();
 | |
|         extra.from_msi = data.from_msi.toString();
 | |
|         extra.default_path = data.default_path.toString();
 | |
|       }
 | |
|     }
 | |
|     // Record the event
 | |
|     Services.telemetry.setEventRecordingEnabled("installation", true);
 | |
|     Services.telemetry.recordEvent(
 | |
|       "installation",
 | |
|       "first_seen",
 | |
|       installer_type,
 | |
|       null,
 | |
|       extra
 | |
|     );
 | |
|   },
 | |
| };
 | |
| 
 | |
| // Used by nsIBrowserUsage
 | |
| export function getUniqueDomainsVisitedInPast24Hours() {
 | |
|   return URICountListener.uniqueDomainsVisitedInPast24Hours;
 | |
| }
 |