BrowserUsageTelemetry.sys.mjs (66125B)
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/. */ 5 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 8 9 const lazy = {}; 10 11 ChromeUtils.defineESModuleGetters(lazy, { 12 ClientID: "resource://gre/modules/ClientID.sys.mjs", 13 CustomizableUI: 14 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", 15 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", 16 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 17 PageActions: "resource:///modules/PageActions.sys.mjs", 18 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 19 SearchSERPTelemetry: 20 "moz-src:///browser/components/search/SearchSERPTelemetry.sys.mjs", 21 SearchSERPTelemetryUtils: 22 "moz-src:///browser/components/search/SearchSERPTelemetry.sys.mjs", 23 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", 24 TabMetrics: "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs", 25 WindowsInstallsInfo: 26 "resource://gre/modules/components-utils/WindowsInstallsInfo.sys.mjs", 27 28 clearTimeout: "resource://gre/modules/Timer.sys.mjs", 29 setTimeout: "resource://gre/modules/Timer.sys.mjs", 30 }); 31 32 // This pref is in seconds! 33 XPCOMUtils.defineLazyPreferenceGetter( 34 lazy, 35 "gRecentVisitedOriginsExpiry", 36 "browser.engagement.recent_visited_origins.expiry" 37 ); 38 XPCOMUtils.defineLazyPreferenceGetter( 39 lazy, 40 "sidebarVerticalTabs", 41 "sidebar.verticalTabs", 42 false, 43 (_aPreference, _previousValue, isVertical) => { 44 let tabCount = getOpenTabsAndWinsCounts().tabCount; 45 BrowserUsageTelemetry.maxTabCount = tabCount; 46 let pinnedTabCount = getPinnedTabsCount(); 47 BrowserUsageTelemetry.maxTabPinnedCount = pinnedTabCount; 48 if (isVertical) { 49 Glean.browserEngagement.maxConcurrentVerticalTabCount.set(tabCount); 50 Glean.browserEngagement.maxConcurrentVerticalTabPinnedCount.set( 51 pinnedTabCount 52 ); 53 } else { 54 Glean.browserEngagement.maxConcurrentTabCount.set(tabCount); 55 Glean.browserEngagement.maxConcurrentTabPinnedCount.set(pinnedTabCount); 56 } 57 BrowserUsageTelemetry.recordPinnedTabsCount(pinnedTabCount); 58 } 59 ); 60 61 // The upper bound for the count of the visited unique domain names. 62 const MAX_UNIQUE_VISITED_DOMAINS = 100; 63 64 // Observed topic names. 65 const TAB_RESTORING_TOPIC = "SSTabRestoring"; 66 const TELEMETRY_SUBSESSIONSPLIT_TOPIC = 67 "internal-telemetry-after-subsession-split"; 68 const DOMWINDOW_OPENED_TOPIC = "domwindowopened"; 69 const SESSION_STORE_SAVED_TAB_GROUPS_TOPIC = 70 "sessionstore-saved-tab-groups-changed"; 71 72 export const MINIMUM_TAB_COUNT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes, in ms 73 74 // The elements we consider to be interactive. 75 const UI_TARGET_CHANGE_ELEMENTS = new Set([ 76 "moz-checkbox", 77 "moz-select", 78 "moz-radio", 79 "moz-toggle", 80 "moz-input-email", 81 "moz-input-folder", 82 "moz-input-number", 83 "moz-input-password", 84 "moz-input-search", 85 "moz-input-tel", 86 "moz-input-text", 87 "moz-input-url", 88 "moz-visual-picker-item", 89 "sync-device-name", 90 ]); 91 const UI_TARGET_COMMAND_ELEMENTS = new Set([ 92 "menuitem", 93 "toolbarbutton", 94 "key", 95 "command", 96 "checkbox", 97 "input", 98 "button", 99 "image", 100 "radio", 101 "richlistitem", 102 "moz-button", 103 "moz-box-button", 104 "moz-box-link", 105 "dialog-button", 106 ]); 107 const UI_TARGET_ELEMENTS = new Map([ 108 ["change", UI_TARGET_CHANGE_ELEMENTS], 109 ["click", UI_TARGET_COMMAND_ELEMENTS], 110 ["command", UI_TARGET_COMMAND_ELEMENTS], 111 ]); 112 113 // The containers of interactive elements that we care about and their pretty 114 // names. These should be listed in order of most-specific to least-specific, 115 // when iterating JavaScript will guarantee that ordering and so we will find 116 // the most specific area first. 117 const BROWSER_UI_CONTAINER_IDS = { 118 "toolbar-menubar": "menu-bar", 119 TabsToolbar: "tabs-bar", 120 "vertical-tabs": "vertical-tabs-container", 121 PersonalToolbar: "bookmarks-bar", 122 "appMenu-popup": "app-menu", 123 tabContextMenu: "tabs-context", 124 contentAreaContextMenu: "content-context", 125 "widget-overflow-list": "overflow-menu", 126 "widget-overflow-fixed-list": "pinned-overflow-menu", 127 "page-action-buttons": "pageaction-urlbar", 128 pageActionPanel: "pageaction-panel", 129 "unified-extensions-area": "unified-extensions-area", 130 "allTabsMenu-allTabsView": "alltabs-menu", 131 // Historically, panels opened from a button on any toolbar have been 132 // considered part of the nav-bar. Due to a technical change these panels 133 // are no longer descendants of the nav-bar; this entry just preserves 134 // continuity for telemetry. 135 "customizationui-widget-panel": "nav-bar", 136 137 // This should appear last as some of the above are inside the nav bar. 138 "nav-bar": "nav-bar", 139 }; 140 141 const ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS = { 142 [BROWSER_UI_CONTAINER_IDS.tabContextMenu]: "tabs-context-entrypoint", 143 }; 144 145 // A list of the expected panes in about:preferences 146 const PREFERENCES_PANES = [ 147 "paneHome", 148 "paneGeneral", 149 "panePrivacy", 150 "paneSearch", 151 "paneSearchResults", 152 "paneSync", 153 "paneContainers", 154 "paneExperimental", 155 "paneMoreFromMozilla", 156 "paneAiFeatures", 157 ]; 158 159 const IGNORABLE_EVENTS = new WeakMap(); 160 161 const KNOWN_ADDONS = []; 162 163 // Buttons that, when clicked, set a preference to true. The convention 164 // is that the preference is named: 165 // 166 // browser.engagement.<button id>.has-used 167 // 168 // and is defaulted to false. 169 const SET_USAGE_PREF_BUTTONS = [ 170 "downloads-button", 171 "fxa-toolbar-menu-button", 172 "home-button", 173 "sidebar-button", 174 "library-button", 175 ]; 176 177 // Buttons that, when clicked, increase a counter. The convention 178 // is that the preference is named: 179 // 180 // browser.engagement.<button id>.used-count 181 // 182 // and doesn't have a default value. 183 const SET_USAGECOUNT_PREF_BUTTONS = [ 184 "pageAction-panel-copyURL", 185 "pageAction-panel-emailLink", 186 "pageAction-panel-pinTab", 187 "pageAction-panel-screenshots_mozilla_org", 188 "pageAction-panel-shareURL", 189 ]; 190 191 // Places context menu IDs. 192 const PLACES_CONTEXT_MENU_ID = "placesContext"; 193 const PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID = 194 "placesContext_open:newcontainertab"; 195 196 // Commands used to open history or bookmark links from places context menu. 197 const PLACES_OPEN_COMMANDS = [ 198 "placesCmd_open", 199 "placesCmd_open:window", 200 "placesCmd_open:privatewindow", 201 "placesCmd_open:tab", 202 ]; 203 204 // How long of a delay between events means the start of a new flow? 205 // Used by Browser UI Interaction event instrumentation. 206 // Default: 5min. 207 const FLOW_IDLE_TIME = 5 * 60 * 1000; 208 209 const externalTabMovementRegistry = { 210 internallyOpenedTabs: new WeakSet(), 211 externallyOpenedTabsNextToActiveTab: new WeakSet(), 212 externallyOpenedTabsAtEndOfTabStrip: new WeakSet(), 213 }; 214 215 function telemetryId(widgetId, obscureAddons = true) { 216 // Add-on IDs need to be obscured. 217 function addonId(id) { 218 if (!obscureAddons) { 219 return id; 220 } 221 222 let pos = KNOWN_ADDONS.indexOf(id); 223 if (pos < 0) { 224 pos = KNOWN_ADDONS.length; 225 KNOWN_ADDONS.push(id); 226 } 227 return `addon${pos}`; 228 } 229 230 if (widgetId.endsWith("-browser-action")) { 231 widgetId = addonId( 232 widgetId.substring(0, widgetId.length - "-browser-action".length) 233 ); 234 } else if (widgetId.startsWith("pageAction-")) { 235 let actionId; 236 if (widgetId.startsWith("pageAction-urlbar-")) { 237 actionId = widgetId.substring("pageAction-urlbar-".length); 238 } else if (widgetId.startsWith("pageAction-panel-")) { 239 actionId = widgetId.substring("pageAction-panel-".length); 240 } 241 242 if (actionId) { 243 let action = lazy.PageActions.actionForID(actionId); 244 widgetId = action?._isMozillaAction ? actionId : addonId(actionId); 245 } 246 } else if (widgetId.startsWith("ext-keyset-id-")) { 247 // Webextension command shortcuts don't have an id on their key element so 248 // we see the id from the keyset that contains them. 249 widgetId = addonId(widgetId.substring("ext-keyset-id-".length)); 250 } else if (widgetId.startsWith("ext-key-id-")) { 251 // The command for a webextension sidebar action is an exception to the above rule. 252 widgetId = widgetId.substring("ext-key-id-".length); 253 if (widgetId.endsWith("-sidebar-action")) { 254 widgetId = addonId( 255 widgetId.substring(0, widgetId.length - "-sidebar-action".length) 256 ); 257 } 258 } 259 260 return widgetId.replace(/_/g, "-"); 261 } 262 263 function getOpenTabsAndWinsCounts() { 264 let loadedTabCount = 0; 265 let tabCount = 0; 266 let tabsInGroupsCount = 0; 267 let winCount = 0; 268 269 for (let win of Services.wm.getEnumerator("navigator:browser")) { 270 winCount++; 271 tabCount += win.gBrowser.tabs.length; 272 for (const tab of win.gBrowser.tabs) { 273 if (tab.getAttribute("pending") !== "true") { 274 loadedTabCount += 1; 275 } 276 277 if (tab.getAttribute("group")) { 278 tabsInGroupsCount += 1; 279 } 280 } 281 } 282 283 let tabsNotInGroupsCount = tabCount - tabsInGroupsCount; 284 285 return { 286 loadedTabCount, 287 tabCount, 288 winCount, 289 tabsInGroupsCount, 290 tabsNotInGroupsCount, 291 }; 292 } 293 294 function getPinnedTabsCount() { 295 let pinnedTabs = 0; 296 297 for (let win of Services.wm.getEnumerator("navigator:browser")) { 298 pinnedTabs += [...win.ownerGlobal.gBrowser.tabs].filter( 299 t => t.pinned 300 ).length; 301 } 302 303 return pinnedTabs; 304 } 305 306 export let URICountListener = { 307 // A set containing the visited domains, see bug 1271310. 308 _domainSet: new Set(), 309 // A map containing the visited origins during the last 24 hours (similar 310 // to domains, but not quite the same), mapping to a timeoutId or 0. 311 _domain24hrSet: new Map(), 312 // A map to keep track of the URIs loaded from the restored tabs. 313 _restoredURIsMap: new WeakMap(), 314 315 isHttpURI(uri) { 316 // Only consider http(s) schemas. 317 return uri.schemeIs("http") || uri.schemeIs("https"); 318 }, 319 320 addRestoredURI(browser, uri) { 321 if (!this.isHttpURI(uri)) { 322 return; 323 } 324 325 this._restoredURIsMap.set(browser, uri.spec); 326 }, 327 328 onLocationChange(browser, webProgress, request, uri, flags) { 329 if ( 330 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) && 331 webProgress.isTopLevel 332 ) { 333 // By default, assume we no longer need to track this tab. 334 lazy.SearchSERPTelemetry.stopTrackingBrowser( 335 browser, 336 lazy.SearchSERPTelemetryUtils.ABANDONMENTS.NAVIGATION 337 ); 338 } 339 340 // Don't count this URI if it's an error page. 341 if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) { 342 return; 343 } 344 345 // We only care about top level loads. 346 if (!webProgress.isTopLevel) { 347 return; 348 } 349 350 // The SessionStore sets the URI of a tab first, firing onLocationChange the 351 // first time, then manages content loading using its scheduler. Once content 352 // loads, we will hit onLocationChange again. 353 // We can catch the first case by checking for null requests: be advised that 354 // this can also happen when navigating page fragments, so account for it. 355 if ( 356 !request && 357 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) 358 ) { 359 return; 360 } 361 362 // Don't include URI and domain counts when in private mode. 363 let shouldCountURI = 364 !lazy.PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal) || 365 Services.prefs.getBoolPref( 366 "browser.engagement.total_uri_count.pbm", 367 false 368 ); 369 370 // Track URI loads, even if they're not http(s). 371 let uriSpec = null; 372 try { 373 uriSpec = uri.spec; 374 } catch (e) { 375 // If we have troubles parsing the spec, still count this as 376 // an unfiltered URI. 377 if (shouldCountURI) { 378 Glean.browserEngagement.unfilteredUriCount.add(1); 379 } 380 return; 381 } 382 383 // Don't count about:blank and similar pages, as they would artificially 384 // inflate the counts. 385 if (browser.ownerGlobal.gInitialPages.includes(uriSpec)) { 386 return; 387 } 388 389 // If the URI we're loading is in the _restoredURIsMap, then it comes from a 390 // restored tab. If so, let's skip it and remove it from the map as we want to 391 // count page refreshes. 392 if (this._restoredURIsMap.get(browser) === uriSpec) { 393 this._restoredURIsMap.delete(browser); 394 return; 395 } 396 397 // The URI wasn't from a restored tab. Count it among the unfiltered URIs. 398 // If this is an http(s) URI, this also gets counted by the "total_uri_count" 399 // probe. 400 if (shouldCountURI) { 401 Glean.browserEngagement.unfilteredUriCount.add(1); 402 } 403 404 if (!this.isHttpURI(uri)) { 405 return; 406 } 407 408 if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) { 409 lazy.SearchSERPTelemetry.updateTrackingStatus( 410 browser, 411 uriSpec, 412 webProgress.loadType 413 ); 414 } else { 415 lazy.SearchSERPTelemetry.updateTrackingSinglePageApp( 416 browser, 417 uriSpec, 418 webProgress.loadType, 419 flags 420 ); 421 } 422 423 // Update total URI count, including when in private mode. 424 Glean.browserEngagement.uriCount.add(1); 425 426 if (!shouldCountURI) { 427 return; 428 } 429 430 // Update the URI counts. 431 Glean.browserEngagement.uriCountNormalMode.add(1); 432 433 // Update tab count 434 BrowserUsageTelemetry._recordTabCounts(getOpenTabsAndWinsCounts()); 435 436 // Unique domains should be aggregated by (eTLD + 1): x.test.com and y.test.com 437 // are counted once as test.com. 438 let baseDomain; 439 try { 440 // Even if only considering http(s) URIs, |getBaseDomain| could still throw 441 // due to the URI containing invalid characters or the domain actually being 442 // an ipv4 or ipv6 address. 443 baseDomain = Services.eTLD.getBaseDomain(uri); 444 } catch (e) { 445 return; 446 } 447 448 // We only want to count the unique domains up to MAX_UNIQUE_VISITED_DOMAINS. 449 if (this._domainSet.size < MAX_UNIQUE_VISITED_DOMAINS) { 450 this._domainSet.add(baseDomain); 451 Glean.browserEngagement.uniqueDomainsCount.set(this._domainSet.size); 452 } 453 454 // Clear and re-add the expiration timeout for this base domain, if any. 455 let timeoutId = this._domain24hrSet.get(baseDomain); 456 if (timeoutId) { 457 lazy.clearTimeout(timeoutId); 458 } 459 if (lazy.gRecentVisitedOriginsExpiry) { 460 timeoutId = lazy.setTimeout(() => { 461 this._domain24hrSet.delete(baseDomain); 462 }, lazy.gRecentVisitedOriginsExpiry * 1000); 463 } else { 464 timeoutId = 0; 465 } 466 this._domain24hrSet.set(baseDomain, timeoutId); 467 }, 468 469 /** 470 * Reset the counts. This should be called when breaking a session in Telemetry. 471 */ 472 reset() { 473 this._domainSet.clear(); 474 }, 475 476 /** 477 * Returns the number of unique domains visited in this session during the 478 * last 24 hours. 479 */ 480 get uniqueDomainsVisitedInPast24Hours() { 481 return this._domain24hrSet.size; 482 }, 483 484 /** 485 * Resets the number of unique domains visited in this session. 486 */ 487 resetUniqueDomainsVisitedInPast24Hours() { 488 this._domain24hrSet.forEach(value => lazy.clearTimeout(value)); 489 this._domain24hrSet.clear(); 490 }, 491 492 QueryInterface: ChromeUtils.generateQI([ 493 "nsIWebProgressListener", 494 "nsISupportsWeakReference", 495 ]), 496 }; 497 498 let gInstallationTelemetryPromise = null; 499 500 export let BrowserUsageTelemetry = { 501 /** 502 * This is a policy object used to override behavior for testing. 503 */ 504 Policy: { 505 getTelemetryClientId: async () => lazy.ClientID.getClientID(), 506 getUpdateDirectory: () => Services.dirsvc.get("UpdRootD", Ci.nsIFile), 507 readProfileCountFile: async path => IOUtils.readUTF8(path), 508 writeProfileCountFile: async (path, data) => IOUtils.writeUTF8(path, data), 509 }, 510 511 _inited: false, 512 513 /** 514 * @typedef {object} TabMovementsRecord 515 * @property {DeferredTask} deferredTask 516 * The `DeferredTask` that will report this record's metrics once all 517 * tab movement events with the same `telemetrySource` have been received 518 * in the current event loop. 519 * @property {number} numberAddedToTabGroup 520 * The number of tabs from `tabs` which started out as ungrouped tabs but 521 * moved into a tab group during the tab movement operation. 522 */ 523 524 /** @type {Map<string, TabMovementsRecord>} */ 525 _tabMovementsBySegment: new Map(), 526 527 init() { 528 this._lastRecordTabCount = 0; 529 this._lastRecordLoadedTabCount = 0; 530 this._setupAfterRestore(); 531 this._inited = true; 532 533 Services.prefs.addObserver("browser.tabs.inTitlebar", this); 534 Services.prefs.addObserver("idle-daily", this); 535 536 this._recordUITelemetry(); 537 this._recordInitialPrefValues(); 538 this.recordPinnedTabsCount(); 539 540 this._onTabsOpenedTask = new lazy.DeferredTask( 541 () => this._onTabsOpened(), 542 0 543 ); 544 545 this._onTabGroupChangeTask = new lazy.DeferredTask( 546 () => this._doOnTabGroupChange(), 547 0 548 ); 549 550 this._onTabGroupExpandOrCollapseTask = new lazy.DeferredTask( 551 () => this._doOnTabGroupExpandOrCollapse(), 552 0 553 ); 554 555 this._onSavedTabGroupsChangedTask = new lazy.DeferredTask( 556 () => this._doOnSavedTabGroupsChange(), 557 0 558 ); 559 this._onSavedTabGroupsChangedTask.arm(); 560 }, 561 562 maxWindowCount: 0, 563 maxTabCount: 0, 564 get maxTabCountGleanQuantity() { 565 return lazy.sidebarVerticalTabs 566 ? Glean.browserEngagement.maxConcurrentVerticalTabCount 567 : Glean.browserEngagement.maxConcurrentTabCount; 568 }, 569 570 maxTabPinnedCount: 0, 571 updateMaxTabPinnedCount(pinnedTabs) { 572 if (pinnedTabs > this.maxTabPinnedCount) { 573 this.maxTabPinnedCount = pinnedTabs; 574 if (lazy.sidebarVerticalTabs) { 575 Glean.browserEngagement.maxConcurrentVerticalTabPinnedCount.set( 576 pinnedTabs 577 ); 578 } else { 579 Glean.browserEngagement.maxConcurrentTabPinnedCount.set(pinnedTabs); 580 } 581 } 582 }, 583 584 recordPinnedTabsCount(count = getPinnedTabsCount()) { 585 if (lazy.sidebarVerticalTabs) { 586 Glean.pinnedTabs.count.sidebar.set(count); 587 } else { 588 Glean.pinnedTabs.count.horizontalBar.set(count); 589 } 590 }, 591 592 /** 593 * Resets the masked add-on identifiers. Only for use in tests. 594 */ 595 _resetAddonIds() { 596 KNOWN_ADDONS.length = 0; 597 }, 598 599 /** 600 * Handle subsession splits in the parent process. 601 */ 602 afterSubsessionSplit() { 603 // Scalars just got cleared due to a subsession split. We need to set the maximum 604 // concurrent tab and window counts so that they reflect the correct value for the 605 // new subsession. 606 this._initMaxTabAndWindowCounts(); 607 608 // Reset the URI counter. 609 URICountListener.reset(); 610 }, 611 612 QueryInterface: ChromeUtils.generateQI([ 613 "nsIObserver", 614 "nsISupportsWeakReference", 615 ]), 616 617 uninit() { 618 if (!this._inited) { 619 return; 620 } 621 Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC); 622 Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC); 623 Services.obs.removeObserver(this, SESSION_STORE_SAVED_TAB_GROUPS_TOPIC); 624 }, 625 626 observe(subject, topic, data) { 627 switch (topic) { 628 case DOMWINDOW_OPENED_TOPIC: 629 this._onWindowOpen(subject); 630 break; 631 case TELEMETRY_SUBSESSIONSPLIT_TOPIC: 632 this.afterSubsessionSplit(); 633 break; 634 case SESSION_STORE_SAVED_TAB_GROUPS_TOPIC: 635 this._onSavedTabGroupsChange(); 636 break; 637 case "nsPref:changed": 638 switch (data) { 639 case "browser.tabs.inTitlebar": 640 this._recordWidgetChange( 641 "titlebar", 642 Services.appinfo.drawInTitlebar ? "off" : "on", 643 "pref" 644 ); 645 break; 646 case "idle-daily": 647 this._recordInitialPrefValues(); 648 break; 649 } 650 break; 651 } 652 }, 653 654 handleEvent(event) { 655 switch (event.type) { 656 case "TabOpen": 657 this._onTabOpen(event); 658 break; 659 case "TabClose": 660 this._onTabClosed(event); 661 break; 662 case "TabPinned": 663 this._onTabPinned(event); 664 break; 665 case "TabUnpinned": 666 this._onTabUnpinned(); 667 break; 668 case "TabGroupCreateByUser": 669 this._onTabGroupCreateByUser(event); 670 break; 671 case "TabGrouped": 672 case "TabUngrouped": 673 this._onTabGroupChange(); 674 break; 675 case "TabGroupCollapse": 676 case "TabGroupExpand": 677 this._onTabGroupExpandOrCollapse(); 678 break; 679 case "TabMove": 680 this._onTabMove(event); 681 break; 682 case "TabSelect": 683 this._onTabSelect(event); 684 break; 685 case "TabGroupRemoveRequested": 686 this._onTabGroupRemoveRequested(event); 687 break; 688 case "TabGroupSaved": 689 this._onTabGroupSave(event); 690 break; 691 case "TabGroupUngroup": 692 this._onTabGroupUngroup(event); 693 break; 694 695 case "unload": 696 this._unregisterWindow(event.target); 697 break; 698 case TAB_RESTORING_TOPIC: { 699 // We're restoring a new tab from a previous or crashed session. 700 // We don't want to track the URIs from these tabs, so let 701 // |URICountListener| know about them. 702 let browser = event.target.linkedBrowser; 703 URICountListener.addRestoredURI(browser, browser.currentURI); 704 705 const { loadedTabCount } = getOpenTabsAndWinsCounts(); 706 this._recordTabCounts({ loadedTabCount }); 707 break; 708 } 709 } 710 }, 711 712 _initMaxTabAndWindowCounts() { 713 const counts = getOpenTabsAndWinsCounts(); 714 this.maxTabCount = counts.tabCount; 715 this.maxTabCountGleanQuantity.set(counts.tabCount); 716 this.maxWindowCount = counts.winCount; 717 Glean.browserEngagement.maxConcurrentWindowCount.set(counts.winCount); 718 }, 719 720 /** 721 * This gets called shortly after the SessionStore has finished restoring 722 * windows and tabs. It counts the open tabs and adds listeners to all the 723 * windows. 724 */ 725 _setupAfterRestore() { 726 // Make sure to catch new chrome windows and subsession splits. 727 Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, true); 728 Services.obs.addObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, true); 729 Services.obs.addObserver(this, SESSION_STORE_SAVED_TAB_GROUPS_TOPIC, true); 730 731 // Attach the tabopen handlers to the existing Windows. 732 for (let win of Services.wm.getEnumerator("navigator:browser")) { 733 this._registerWindow(win); 734 } 735 736 // Get the initial tab and windows max counts. 737 this._initMaxTabAndWindowCounts(); 738 }, 739 740 _buildWidgetPositions() { 741 let widgetMap = new Map(); 742 743 const toolbarState = nodeId => { 744 let value; 745 if (nodeId == "PersonalToolbar") { 746 value = Services.prefs.getCharPref( 747 "browser.toolbars.bookmarks.visibility", 748 "newtab" 749 ); 750 if (value != "newtab") { 751 return value == "never" ? "off" : "on"; 752 } 753 return value; 754 } 755 value = Services.xulStore.getValue( 756 AppConstants.BROWSER_CHROME_URL, 757 nodeId, 758 "collapsed" 759 ); 760 761 if (value) { 762 return value == "true" ? "off" : "on"; 763 } 764 return "off"; 765 }; 766 767 widgetMap.set( 768 BROWSER_UI_CONTAINER_IDS.PersonalToolbar, 769 toolbarState("PersonalToolbar") 770 ); 771 772 let menuBarHidden = 773 Services.xulStore.getValue( 774 AppConstants.BROWSER_CHROME_URL, 775 "toolbar-menubar", 776 "autohide" 777 ) != "false"; 778 779 widgetMap.set("menu-toolbar", menuBarHidden ? "off" : "on"); 780 781 // Drawing in the titlebar means not showing the titlebar, hence the negation. 782 widgetMap.set("titlebar", Services.appinfo.drawInTitlebar ? "off" : "on"); 783 784 for (let area of lazy.CustomizableUI.areas) { 785 if (!(area in BROWSER_UI_CONTAINER_IDS)) { 786 continue; 787 } 788 789 let position = BROWSER_UI_CONTAINER_IDS[area]; 790 if (area == "nav-bar") { 791 position = `${BROWSER_UI_CONTAINER_IDS[area]}-start`; 792 } 793 794 let widgets = lazy.CustomizableUI.getWidgetsInArea(area); 795 796 for (let widget of widgets) { 797 if (!widget) { 798 continue; 799 } 800 801 if (widget.id.startsWith("customizableui-special-")) { 802 continue; 803 } 804 805 if (area == "nav-bar" && widget.id == "urlbar-container") { 806 position = `${BROWSER_UI_CONTAINER_IDS[area]}-end`; 807 continue; 808 } 809 810 widgetMap.set(widget.id, position); 811 } 812 } 813 814 let actions = lazy.PageActions.actions; 815 for (let action of actions) { 816 if (action.pinnedToUrlbar) { 817 widgetMap.set(action.id, "pageaction-urlbar"); 818 } 819 } 820 821 return widgetMap; 822 }, 823 824 _getWidgetID(node) { 825 // We want to find a sensible ID for this element. 826 if (!node) { 827 return null; 828 } 829 830 // See if this is a customizable widget. 831 if (node.ownerDocument.URL == AppConstants.BROWSER_CHROME_URL) { 832 // First find if it is inside one of the customizable areas. 833 for (let area of lazy.CustomizableUI.areas) { 834 if (node.closest(`#${CSS.escape(area)}`)) { 835 for (let widget of lazy.CustomizableUI.getWidgetIdsInArea(area)) { 836 if ( 837 // We care about the buttons on the tabs themselves. 838 widget == "tabbrowser-tabs" || 839 // We care about the page action and other buttons in here. 840 widget == "urlbar-container" || 841 // We care about the actual menu items. 842 widget == "menubar-items" || 843 // We care about individual bookmarks here. 844 widget == "personal-bookmarks" 845 ) { 846 continue; 847 } 848 849 if (node.closest(`#${CSS.escape(widget)}`)) { 850 return widget; 851 } 852 } 853 break; 854 } 855 } 856 } 857 858 if (node.id) { 859 return node.id; 860 } 861 862 // A couple of special cases in the tabs. 863 for (let cls of ["bookmark-item", "tab-icon-sound", "tab-close-button"]) { 864 if (!node.classList.contains(cls)) { 865 continue; 866 } 867 if (cls == "bookmark-item" && node.parentElement.id.includes("history")) { 868 return "history-item"; 869 } 870 return cls; 871 } 872 873 // One of these will at least let us know what the widget is for. 874 let possibleAttributes = [ 875 "preference", 876 "command", 877 "observes", 878 "data-l10n-id", 879 ]; 880 881 // The key attribute on key elements is the actual key to listen for. 882 if (node.localName != "key") { 883 possibleAttributes.unshift("key"); 884 } 885 886 for (let idAttribute of possibleAttributes) { 887 if (node.hasAttribute(idAttribute)) { 888 return node.getAttribute(idAttribute); 889 } 890 } 891 892 return this._getWidgetID(node.parentElement); 893 }, 894 895 _getBrowserWidgetContainer(node) { 896 // Find the container holding this element. 897 for (let containerId of Object.keys(BROWSER_UI_CONTAINER_IDS)) { 898 let container = node.ownerDocument.getElementById(containerId); 899 if (container && container.contains(node)) { 900 return BROWSER_UI_CONTAINER_IDS[containerId]; 901 } 902 } 903 // Treat toolbar context menu items that relate to tabs as the tab menu: 904 if ( 905 node.closest("#toolbar-context-menu") && 906 node.getAttribute("contexttype") == "tabbar" 907 ) { 908 return BROWSER_UI_CONTAINER_IDS.tabContextMenu; 909 } 910 return null; 911 }, 912 913 _getWidgetContainer(node) { 914 if (node.localName == "key") { 915 return "keyboard"; 916 } 917 918 const { URL: url } = node.ownerDocument; 919 if (url == AppConstants.BROWSER_CHROME_URL) { 920 return this._getBrowserWidgetContainer(node); 921 } 922 if ( 923 url.startsWith("about:preferences") || 924 url.startsWith("about:settings") 925 ) { 926 // Find the element's category. 927 let container = node.closest("[data-category]"); 928 if (!container) { 929 return null; 930 } 931 932 let pane = container.getAttribute("data-category"); 933 934 if (!PREFERENCES_PANES.includes(pane)) { 935 pane = "paneUnknown"; 936 } 937 938 return `preferences_${pane}`; 939 } 940 941 return null; 942 }, 943 944 lastClickTarget: null, 945 946 ignoreEvent(event) { 947 IGNORABLE_EVENTS.set(event, true); 948 }, 949 950 _recordCommand(event) { 951 if (IGNORABLE_EVENTS.get(event)) { 952 return; 953 } 954 955 let sourceEvent = event; 956 while (sourceEvent.sourceEvent) { 957 sourceEvent = sourceEvent.sourceEvent; 958 } 959 960 let lastTarget = this.lastClickTarget?.get(); 961 if ( 962 lastTarget && 963 sourceEvent.type == "command" && 964 sourceEvent.target.contains(lastTarget) 965 ) { 966 // Ignore a command event triggered by a click. 967 this.lastClickTarget = null; 968 return; 969 } 970 971 this.lastClickTarget = null; 972 973 if (sourceEvent.type == "click") { 974 // Only care about main button clicks. 975 if (sourceEvent.button != 0) { 976 return; 977 } 978 979 // This click may trigger a command event so retain the target to be able 980 // to dedupe that event. 981 this.lastClickTarget = Cu.getWeakReference(sourceEvent.target); 982 } 983 984 // We should never see events from web content as they are fired in a 985 // content process, but let's be safe. 986 let url = sourceEvent.target.ownerDocument.documentURIObject; 987 if (!url.schemeIs("chrome") && !url.schemeIs("about")) { 988 return; 989 } 990 991 // This is what events targetted at content will actually look like. 992 if (sourceEvent.target.localName == "browser") { 993 return; 994 } 995 996 // Find the actual element we're interested in. 997 let node = sourceEvent.target; 998 const isAboutPreferences = 999 node.ownerDocument.URL.startsWith("about:preferences") || 1000 node.ownerDocument.URL.startsWith("about:settings"); 1001 let targetElements = UI_TARGET_ELEMENTS.get(event.type); 1002 1003 while ( 1004 !targetElements.has(node.localName) && 1005 !node.classList?.contains("wants-telemetry") && 1006 // We are interested in links on about:preferences as well. 1007 !( 1008 isAboutPreferences && 1009 (node.getAttribute("is") === "text-link" || node.localName === "a") 1010 ) 1011 ) { 1012 node = node.parentNode; 1013 if (!node?.parentNode) { 1014 // A click on a space or label or top-level document or something we're 1015 // not interested in. 1016 return; 1017 } 1018 } 1019 1020 if (sourceEvent.type === "command") { 1021 const { command, ownerDocument, parentNode } = node; 1022 // Check if this command is for a history or bookmark link being opened 1023 // from the context menu. In this case, we are interested in the DOM node 1024 // for the link, not the menu item itself. 1025 if ( 1026 PLACES_OPEN_COMMANDS.includes(command) || 1027 parentNode?.parentNode?.id === PLACES_OPEN_IN_CONTAINER_TAB_MENU_ID 1028 ) { 1029 node = ownerDocument.getElementById(PLACES_CONTEXT_MENU_ID).triggerNode; 1030 } 1031 } 1032 1033 let item = this._getWidgetID(node); 1034 let source = this._getWidgetContainer(node); 1035 1036 if (item && source) { 1037 this.recordInteractionEvent(item, source); 1038 let name = source 1039 .replace(/-/g, "_") 1040 .replace(/_([a-z])/g, (m, p) => p.toUpperCase()); 1041 Glean.browserUiInteraction[name]?.[telemetryId(item)].add(1); 1042 if (SET_USAGECOUNT_PREF_BUTTONS.includes(item)) { 1043 let pref = `browser.engagement.${item}.used-count`; 1044 Services.prefs.setIntPref(pref, Services.prefs.getIntPref(pref, 0) + 1); 1045 } 1046 if (SET_USAGE_PREF_BUTTONS.includes(item)) { 1047 Services.prefs.setBoolPref(`browser.engagement.${item}.has-used`, true); 1048 } 1049 } 1050 1051 if (ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source]) { 1052 let contextMenu = ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS[source]; 1053 let triggerContainer = this._getWidgetContainer( 1054 node.closest("menupopup")?.triggerNode 1055 ); 1056 if (triggerContainer) { 1057 this.recordInteractionEvent(item, contextMenu); 1058 let name = contextMenu 1059 .replace(/-/g, "_") 1060 .replace(/_([a-z])/g, (m, p) => p.toUpperCase()); 1061 Glean.browserUiInteraction[name]?.[telemetryId(triggerContainer)].add( 1062 1 1063 ); 1064 } 1065 } 1066 }, 1067 1068 _flowId: null, 1069 _flowIdTS: 0, 1070 1071 recordInteractionEvent(widgetId, source) { 1072 // A note on clocks. ChromeUtils.now() is monotonic, but its behaviour across 1073 // computer sleeps is different per platform. 1074 // We're okay with this for flows because we're looking at idle times 1075 // on the order of minutes and within the same machine, so the weirdest 1076 // thing we may expect is a flow that accidentally continues across a 1077 // sleep. Until we have evidence that this is common, we're in the clear. 1078 if (!this._flowId || this._flowIdTS + FLOW_IDLE_TIME < ChromeUtils.now()) { 1079 // We submit the ping full o' events on every new flow, 1080 // including at startup. 1081 GleanPings.prototypeNoCodeEvents.submit(); 1082 // We use a GUID here because we need to identify events in a flow 1083 // out of all events from all flows across all clients. 1084 this._flowId = Services.uuid.generateUUID(); 1085 } 1086 this._flowIdTS = ChromeUtils.now(); 1087 1088 const extra = { 1089 source, 1090 widget_id: telemetryId(widgetId), 1091 flow_id: this._flowId, 1092 }; 1093 Glean.browserUsage.interaction.record(extra); 1094 }, 1095 1096 /** 1097 * Listens for UI interactions in the window. 1098 */ 1099 _addUsageListeners(win) { 1100 // Listen for events that UI_TARGET_ELEMENTS expect from the UI. 1101 UI_TARGET_ELEMENTS.keys().forEach(type => 1102 win.addEventListener(type, event => this._recordCommand(event), true) 1103 ); 1104 }, 1105 1106 /** 1107 * A public version of the private method to take care of the `nav-bar-start`, 1108 * `nav-bar-end` thing that callers shouldn't have to care about. It also 1109 * accepts the DOM ids for the areas rather than the cleaner ones we report 1110 * to telemetry. 1111 */ 1112 recordWidgetChange(widgetId, newPos, reason) { 1113 try { 1114 if (newPos) { 1115 newPos = BROWSER_UI_CONTAINER_IDS[newPos]; 1116 } 1117 1118 if (newPos == "nav-bar") { 1119 let { position } = lazy.CustomizableUI.getPlacementOfWidget(widgetId); 1120 let { position: urlPosition } = 1121 lazy.CustomizableUI.getPlacementOfWidget("urlbar-container"); 1122 newPos = newPos + (urlPosition > position ? "-start" : "-end"); 1123 } 1124 1125 this._recordWidgetChange(widgetId, newPos, reason); 1126 } catch (e) { 1127 console.error(e); 1128 } 1129 }, 1130 1131 recordToolbarVisibility(toolbarId, newState, reason) { 1132 if (typeof newState != "string") { 1133 newState = newState ? "on" : "off"; 1134 } 1135 this._recordWidgetChange( 1136 BROWSER_UI_CONTAINER_IDS[toolbarId], 1137 newState, 1138 reason 1139 ); 1140 }, 1141 1142 _recordWidgetChange(widgetId, newPos, reason) { 1143 // In some cases (like when add-ons are detected during startup) this gets 1144 // called before we've reported the initial positions. Ignore such cases. 1145 if (!this.widgetMap) { 1146 return; 1147 } 1148 1149 if (widgetId == "urlbar-container") { 1150 // We don't report the position of the url bar, it is after nav-bar-start 1151 // and before nav-bar-end. But moving it means the widgets around it have 1152 // effectively moved so update those. 1153 let position = "nav-bar-start"; 1154 let widgets = lazy.CustomizableUI.getWidgetsInArea("nav-bar"); 1155 1156 for (let widget of widgets) { 1157 if (!widget) { 1158 continue; 1159 } 1160 1161 if (widget.id.startsWith("customizableui-special-")) { 1162 continue; 1163 } 1164 1165 if (widget.id == "urlbar-container") { 1166 position = "nav-bar-end"; 1167 continue; 1168 } 1169 1170 // This will do nothing if the position hasn't changed. 1171 this._recordWidgetChange(widget.id, position, reason); 1172 } 1173 1174 return; 1175 } 1176 1177 let oldPos = this.widgetMap.get(widgetId); 1178 if (oldPos == newPos) { 1179 return; 1180 } 1181 1182 let action = "move"; 1183 1184 if (!oldPos) { 1185 action = "add"; 1186 } else if (!newPos) { 1187 action = "remove"; 1188 } 1189 1190 let key = `${telemetryId(widgetId, false)}_${action}_${oldPos ?? "na"}_${ 1191 newPos ?? "na" 1192 }_${reason}`; 1193 Glean.browserUi.customizedWidgets[key].add(1); 1194 1195 if (newPos) { 1196 this.widgetMap.set(widgetId, newPos); 1197 } else { 1198 this.widgetMap.delete(widgetId); 1199 } 1200 }, 1201 1202 _recordUITelemetry() { 1203 this.widgetMap = this._buildWidgetPositions(); 1204 1205 // FIXME(bug 1883857): object metric type not available in artefact builds. 1206 if ("toolbarWidgets" in Glean.browserUi) { 1207 Glean.browserUi.toolbarWidgets.set( 1208 this.widgetMap 1209 .entries() 1210 .map(([widgetId, position]) => { 1211 return { widgetId: telemetryId(widgetId, false), position }; 1212 }) 1213 .toArray() 1214 ); 1215 } 1216 1217 for (let [widgetId, position] of this.widgetMap.entries()) { 1218 let key = `${telemetryId(widgetId, false)}_pinned_${position}`; 1219 Glean.browserUi.mirrorForToolbarWidgets[key].set(true); 1220 } 1221 }, 1222 1223 /** 1224 * Records the startup values of prefs that govern important browser behavior 1225 * options. 1226 */ 1227 _recordInitialPrefValues() { 1228 this._recordOpenNextToActiveTabSettingValue(); 1229 }, 1230 1231 /** 1232 * @returns {boolean} 1233 */ 1234 _isOpenNextToActiveTabSettingEnabled() { 1235 /** @type {number} proxy for `browser.link.open_newwindow.override.external` */ 1236 const externalLinkOpeningBehavior = 1237 lazy.NimbusFeatures.externalLinkHandling.getVariable("openBehavior"); 1238 return ( 1239 externalLinkOpeningBehavior == 1240 Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT 1241 ); 1242 }, 1243 1244 _recordOpenNextToActiveTabSettingValue() { 1245 Glean.linkHandling.openNextToActiveTabSettingsEnabled.set( 1246 this._isOpenNextToActiveTabSettingEnabled() 1247 ); 1248 }, 1249 1250 /** 1251 * Adds listeners to a single chrome window. 1252 * 1253 * @param {Window} win 1254 */ 1255 _registerWindow(win) { 1256 this._addUsageListeners(win); 1257 1258 win.addEventListener("unload", this); 1259 win.addEventListener("TabMove", this); 1260 win.addEventListener("TabOpen", this, true); 1261 win.addEventListener("TabClose", this, true); 1262 win.addEventListener("TabPinned", this, true); 1263 win.addEventListener("TabUnpinned", this, true); 1264 win.addEventListener("TabSelect", this); 1265 win.addEventListener("TabGroupCreateByUser", this); 1266 win.addEventListener("TabGroupRemoveRequested", this); 1267 win.addEventListener("TabGrouped", this); 1268 win.addEventListener("TabUngrouped", this); 1269 win.addEventListener("TabGroupCollapse", this); 1270 win.addEventListener("TabGroupExpand", this); 1271 win.addEventListener("TabGroupSaved", this); 1272 win.addEventListener("TabGroupUngroup", this); 1273 1274 win.gBrowser.tabContainer.addEventListener(TAB_RESTORING_TOPIC, this); 1275 win.gBrowser.addTabsProgressListener(URICountListener); 1276 }, 1277 1278 /** 1279 * Removes listeners from a single chrome window. 1280 */ 1281 _unregisterWindow(win) { 1282 win.removeEventListener("unload", this); 1283 win.removeEventListener("TabMove", this); 1284 win.removeEventListener("TabOpen", this, true); 1285 win.removeEventListener("TabClose", this, true); 1286 win.removeEventListener("TabPinned", this, true); 1287 win.removeEventListener("TabUnpinned", this, true); 1288 win.removeEventListener("TabSelect", this); 1289 win.removeEventListener("TabGroupCreateByUser", this); 1290 win.removeEventListener("TabGroupRemoveRequested", this); 1291 win.removeEventListener("TabGrouped", this); 1292 win.removeEventListener("TabUngrouped", this); 1293 win.removeEventListener("TabGroupCollapse", this); 1294 win.removeEventListener("TabGroupExpand", this); 1295 win.removeEventListener("TabGroupSaved", this); 1296 win.removeEventListener("TabGroupUngroup", this); 1297 1298 win.defaultView.gBrowser.tabContainer.removeEventListener( 1299 TAB_RESTORING_TOPIC, 1300 this 1301 ); 1302 win.defaultView.gBrowser.removeTabsProgressListener(URICountListener); 1303 }, 1304 1305 /** 1306 * Updates the tab counts. 1307 * 1308 * @param {CustomEvent} [event] 1309 * `TabOpen` event 1310 */ 1311 _onTabOpen(event) { 1312 // Update the "tab opened" count and its maximum. 1313 if (lazy.sidebarVerticalTabs) { 1314 Glean.browserEngagement.verticalTabOpenEventCount.add(1); 1315 } else { 1316 Glean.browserEngagement.tabOpenEventCount.add(1); 1317 } 1318 1319 if (event?.target?.group) { 1320 Glean.tabgroup.tabInteractions.new.add(); 1321 } 1322 1323 if (event) { 1324 if (event.detail?.fromExternal) { 1325 const wasOpenedNextToActiveTab = 1326 this._isOpenNextToActiveTabSettingEnabled(); 1327 1328 Glean.linkHandling.openFromExternalApp.record({ 1329 next_to_active_tab: wasOpenedNextToActiveTab, 1330 }); 1331 1332 if (wasOpenedNextToActiveTab) { 1333 externalTabMovementRegistry.externallyOpenedTabsNextToActiveTab.add( 1334 event.target 1335 ); 1336 } else { 1337 externalTabMovementRegistry.externallyOpenedTabsAtEndOfTabStrip.add( 1338 event.target 1339 ); 1340 } 1341 } else { 1342 externalTabMovementRegistry.internallyOpenedTabs.add(event.target); 1343 } 1344 } 1345 1346 const userContextId = event?.target?.getAttribute("usercontextid"); 1347 if (userContextId) { 1348 Glean.containers.containerTabOpened.record({ 1349 container_id: String(userContextId), 1350 }); 1351 } 1352 1353 // In the case of opening multiple tabs at once, avoid enumerating all open 1354 // tabs and windows each time a tab opens. 1355 this._onTabsOpenedTask.disarm(); 1356 this._onTabsOpenedTask.arm(); 1357 }, 1358 1359 /** 1360 * Update tab counts after opening multiple tabs. 1361 */ 1362 _onTabsOpened() { 1363 const { tabCount, loadedTabCount } = getOpenTabsAndWinsCounts(); 1364 if (tabCount > this.maxTabCount) { 1365 this.maxTabCount = tabCount; 1366 this.maxTabCountGleanQuantity.set(tabCount); 1367 } 1368 1369 this._recordTabCounts({ tabCount, loadedTabCount }); 1370 }, 1371 1372 /** 1373 * 1374 * @param {CustomEvent} event 1375 * TabClose event. 1376 */ 1377 _onTabClosed(event) { 1378 const group = event.target?.group; 1379 const isUserTriggered = event.detail?.isUserTriggered; 1380 const source = event.detail?.telemetrySource; 1381 1382 if (group && isUserTriggered) { 1383 if (source == lazy.TabMetrics.METRIC_SOURCE.TAB_STRIP) { 1384 Glean.tabgroup.tabInteractions.close_tabstrip.add(); 1385 } else if (source == lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU) { 1386 Glean.tabgroup.tabInteractions.close_tabmenu.add(); 1387 } else { 1388 Glean.tabgroup.tabInteractions.close_tab_other.add(); 1389 } 1390 } 1391 1392 const userContextId = event?.target?.getAttribute("usercontextid"); 1393 if (userContextId) { 1394 Glean.containers.containerTabClosed.record({ 1395 container_id: String(userContextId), 1396 }); 1397 } 1398 1399 if (event.target?.pinned) { 1400 const pinnedTabs = getPinnedTabsCount(); 1401 this.recordPinnedTabsCount(pinnedTabs - 1); 1402 Glean.pinnedTabs.close.record({ 1403 layout: lazy.sidebarVerticalTabs ? "vertical" : "horizontal", 1404 }); 1405 } 1406 1407 if (event.target) { 1408 // Stop tracking any tabs that have been tracked since their `TabOpen` events. 1409 Object.values(externalTabMovementRegistry).forEach(set => { 1410 set.delete(event.target); 1411 }); 1412 } 1413 }, 1414 1415 _onTabPinned(event) { 1416 const pinnedTabs = getPinnedTabsCount(); 1417 1418 // Update the "tab pinned" count and its maximum. 1419 if (lazy.sidebarVerticalTabs) { 1420 Glean.browserEngagement.verticalTabPinnedEventCount.add(1); 1421 } else { 1422 Glean.browserEngagement.tabPinnedEventCount.add(1); 1423 } 1424 this.updateMaxTabPinnedCount(pinnedTabs); 1425 this.recordPinnedTabsCount(pinnedTabs); 1426 Glean.pinnedTabs.pin.record({ 1427 layout: lazy.sidebarVerticalTabs ? "vertical" : "horizontal", 1428 source: event.detail?.telemetrySource, 1429 }); 1430 }, 1431 1432 _onTabUnpinned() { 1433 this.recordPinnedTabsCount(); 1434 }, 1435 1436 _onTabGroupCreateByUser(event) { 1437 Glean.tabgroup.createGroup.record({ 1438 id: event.target.id, 1439 layout: lazy.sidebarVerticalTabs 1440 ? lazy.TabMetrics.METRIC_TABS_LAYOUT.VERTICAL 1441 : lazy.TabMetrics.METRIC_TABS_LAYOUT.HORIZONTAL, 1442 source: event.detail.telemetryUserCreateSource, 1443 tabs: event.target.tabs.length, 1444 }); 1445 1446 this._onTabGroupChange(); 1447 }, 1448 1449 _onTabGroupSave(event) { 1450 const { isUserTriggered } = event.detail; 1451 1452 Glean.tabgroup.save.record({ 1453 user_triggered: isUserTriggered, 1454 id: event.target.id, 1455 }); 1456 1457 if (isUserTriggered) { 1458 Glean.tabgroup.groupInteractions.save.add(1); 1459 } 1460 1461 this._onTabGroupChange(); 1462 }, 1463 1464 _onTabGroupChange() { 1465 this._onTabGroupChangeTask.disarm(); 1466 this._onTabGroupChangeTask.arm(); 1467 }, 1468 1469 /** 1470 * @param {CustomEvent} event `TabGroupUngroup` event 1471 */ 1472 _onTabGroupUngroup(event) { 1473 const { isUserTriggered, telemetrySource } = event.detail; 1474 if (isUserTriggered) { 1475 Glean.tabgroup.ungroup.record({ source: telemetrySource }); 1476 // Only count explicit user actions (i.e. "Ungroup tabs" in the tab group 1477 // context menu) toward the total number of tab group ungroup interations. 1478 // This excludes implicit user actions, e.g. canceling tab group creation. 1479 if (telemetrySource == lazy.TabMetrics.METRIC_SOURCE.TAB_GROUP_MENU) { 1480 Glean.tabgroup.groupInteractions.ungroup.add(1); 1481 } 1482 } 1483 }, 1484 1485 /** 1486 * Returns summary statistics of a set of numbers. 1487 * 1488 * @param {number[]} data 1489 * @returns {{max: number, min: number, median: number, average: number}} 1490 */ 1491 _getSummaryStats(data) { 1492 let count = data.length; 1493 data.sort((a, b) => a - b); 1494 let middleIndex = Math.floor(count / 2); 1495 1496 return { 1497 max: data.at(-1), 1498 min: data.at(0), 1499 median: 1500 count % 2 == 0 1501 ? (data[middleIndex - 1] + data[middleIndex]) / 2 1502 : data[middleIndex], 1503 average: data.reduce((a, b) => a + b, 0) / count, 1504 }; 1505 }, 1506 1507 _doOnTabGroupChange() { 1508 let totalTabs = 0; 1509 let totalTabsInGroups = 0; 1510 1511 // Used for calculation of average and median 1512 let tabGroupLengths = []; 1513 1514 for (let win of Services.wm.getEnumerator("navigator:browser")) { 1515 totalTabs += win.gBrowser.tabs.length; 1516 for (let group of win.gBrowser.tabGroups) { 1517 totalTabsInGroups += group.tabs.length; 1518 tabGroupLengths.push(group.tabs.length); 1519 } 1520 } 1521 1522 let { max, min, median, average } = this._getSummaryStats(tabGroupLengths); 1523 1524 Glean.tabgroup.tabCountInGroups.inside.set(totalTabsInGroups); 1525 Glean.tabgroup.tabCountInGroups.outside.set(totalTabs - totalTabsInGroups); 1526 1527 Glean.tabgroup.tabsPerActiveGroup.median.set(median); 1528 Glean.tabgroup.tabsPerActiveGroup.average.set(average); 1529 Glean.tabgroup.tabsPerActiveGroup.max.set(max); 1530 Glean.tabgroup.tabsPerActiveGroup.min.set(min); 1531 }, 1532 1533 _onSavedTabGroupsChange() { 1534 this._onSavedTabGroupsChangedTask.disarm(); 1535 this._onSavedTabGroupsChangedTask.arm(); 1536 }, 1537 1538 _doOnSavedTabGroupsChange() { 1539 let savedGroups = lazy.SessionStore.getSavedTabGroups(); 1540 let tabCounts = savedGroups.map(group => group.tabs.length); 1541 let { max, min, median, average } = this._getSummaryStats(tabCounts); 1542 1543 Glean.tabgroup.savedGroups.set(savedGroups.length); 1544 1545 Glean.tabgroup.tabsPerSavedGroup.median.set(median); 1546 Glean.tabgroup.tabsPerSavedGroup.average.set(average); 1547 Glean.tabgroup.tabsPerSavedGroup.max.set(max); 1548 Glean.tabgroup.tabsPerSavedGroup.min.set(min); 1549 }, 1550 1551 _onTabGroupExpandOrCollapse() { 1552 this._onTabGroupExpandOrCollapseTask.disarm(); 1553 this._onTabGroupExpandOrCollapseTask.arm(); 1554 }, 1555 1556 _doOnTabGroupExpandOrCollapse() { 1557 let expanded = 0, 1558 collapsed = 0; 1559 1560 for (let win of Services.wm.getEnumerator("navigator:browser")) { 1561 for (let group of win.gBrowser.tabGroups) { 1562 if (group.collapsed) { 1563 collapsed += 1; 1564 } else { 1565 expanded += 1; 1566 } 1567 } 1568 } 1569 1570 Glean.tabgroup.activeGroups.collapsed.set(collapsed); 1571 Glean.tabgroup.activeGroups.expanded.set(expanded); 1572 }, 1573 1574 /** 1575 * @param {CustomEvent} event 1576 */ 1577 _onTabGroupRemoveRequested(event) { 1578 let { 1579 isUserTriggered = false, 1580 telemetrySource = lazy.TabMetrics.METRIC_SOURCE.UNKNOWN, 1581 } = event.detail; 1582 1583 if (isUserTriggered) { 1584 Glean.tabgroup.delete.record({ 1585 id: event.target.id, 1586 source: telemetrySource, 1587 }); 1588 Glean.tabgroup.groupInteractions.delete.add(1); 1589 } 1590 }, 1591 1592 /** 1593 * Accumulates `TabMove` events in order to record 1 metrics event per frame 1594 * per telemetry source. 1595 * 1596 * For example, dragging and dropping 4 tabs should listen for 4 `TabMove` 1597 * events but result in 1 metrics event being recorded with a source of 1598 * `drag` and a tab count of 4. 1599 * 1600 * @param {CustomEvent} event 1601 */ 1602 _onTabMove(event) { 1603 let { isUserTriggered, telemetrySource } = event.detail; 1604 1605 if (!isUserTriggered) { 1606 return; 1607 } 1608 1609 let groupType = ""; 1610 if (event.target.group) { 1611 groupType = event.target.group.collapsed 1612 ? lazy.TabMetrics.METRIC_GROUP_TYPE.COLLAPSED 1613 : lazy.TabMetrics.METRIC_GROUP_TYPE.EXPANDED; 1614 } 1615 1616 let segmentKey = [telemetrySource, groupType].join(","); 1617 1618 let tabMovementsRecord = this._tabMovementsBySegment.get(segmentKey); 1619 if (!tabMovementsRecord) { 1620 let deferredTask = new lazy.DeferredTask(() => { 1621 if (tabMovementsRecord.numberAddedToTabGroup) { 1622 Glean.tabgroup.addTab.record({ 1623 source: telemetrySource, 1624 tabs: tabMovementsRecord.numberAddedToTabGroup, 1625 layout: lazy.sidebarVerticalTabs 1626 ? lazy.TabMetrics.METRIC_TABS_LAYOUT.VERTICAL 1627 : lazy.TabMetrics.METRIC_TABS_LAYOUT.HORIZONTAL, 1628 group_type: groupType, 1629 }); 1630 } 1631 this._tabMovementsBySegment.delete(segmentKey); 1632 }, 0); 1633 tabMovementsRecord = { 1634 deferredTask, 1635 numberAddedToTabGroup: 0, 1636 }; 1637 this._tabMovementsBySegment.set(segmentKey, tabMovementsRecord); 1638 this._updateTabMovementsRecord(tabMovementsRecord, event); 1639 deferredTask.arm(); 1640 } else { 1641 tabMovementsRecord.deferredTask.disarm(); 1642 this._updateTabMovementsRecord(tabMovementsRecord, event); 1643 tabMovementsRecord.deferredTask.arm(); 1644 } 1645 1646 this._recordExternalTabMovement(event); 1647 }, 1648 1649 /** 1650 * @param {TabMovementsRecord} record 1651 * @param {CustomEvent} event 1652 */ 1653 _updateTabMovementsRecord(record, event) { 1654 let { previousTabState, currentTabState } = event.detail; 1655 1656 if (!previousTabState.tabGroupId && currentTabState.tabGroupId) { 1657 Glean.tabgroup.tabInteractions.add.add(); 1658 record.numberAddedToTabGroup += 1; 1659 } 1660 1661 if ( 1662 previousTabState.tabGroupId && 1663 previousTabState.tabGroupId == currentTabState.tabGroupId && 1664 previousTabState.tabIndex != currentTabState.tabIndex 1665 ) { 1666 Glean.tabgroup.tabInteractions.reorder.add(); 1667 } 1668 1669 if (previousTabState.tabGroupId && !currentTabState.tabGroupId) { 1670 Glean.tabgroup.tabInteractions.remove_same_window.add(); 1671 } 1672 }, 1673 1674 /** 1675 * @param {CustomEvent} event 1676 * TabMove event 1677 */ 1678 _recordExternalTabMovement(event) { 1679 if (externalTabMovementRegistry.internallyOpenedTabs.has(event.target)) { 1680 Glean.browserUiInteraction.tabMovement.not_from_external_app.add(); 1681 } else if ( 1682 externalTabMovementRegistry.externallyOpenedTabsNextToActiveTab.has( 1683 event.target 1684 ) 1685 ) { 1686 Glean.browserUiInteraction.tabMovement.from_external_app_next_to_active_tab.add(); 1687 } else if ( 1688 externalTabMovementRegistry.externallyOpenedTabsAtEndOfTabStrip.has( 1689 event.target 1690 ) 1691 ) { 1692 Glean.browserUiInteraction.tabMovement.from_external_app_tab_strip_end.add(); 1693 } 1694 }, 1695 1696 _onTabSelect(event) { 1697 if (event.target.group) { 1698 let interaction = event.target.group.collapsed 1699 ? Glean.tabgroup.tabInteractions.activate_collapsed 1700 : Glean.tabgroup.tabInteractions.activate_expanded; 1701 interaction.add(); 1702 } 1703 if (event.target.pinned) { 1704 const counter = lazy.sidebarVerticalTabs 1705 ? Glean.pinnedTabs.activations.sidebar 1706 : Glean.pinnedTabs.activations.horizontalBar; 1707 counter.add(); 1708 } 1709 }, 1710 1711 /** 1712 * Tracks the window count and registers the listeners for the tab count. 1713 * 1714 * @param {object} win The window object. 1715 */ 1716 _onWindowOpen(win) { 1717 // Make sure to have a |nsIDOMWindow|. 1718 if (!(win instanceof Ci.nsIDOMWindow)) { 1719 return; 1720 } 1721 1722 let onLoad = () => { 1723 win.removeEventListener("load", onLoad); 1724 1725 // Ignore non browser windows. 1726 if ( 1727 win.document.documentElement.getAttribute("windowtype") != 1728 "navigator:browser" 1729 ) { 1730 return; 1731 } 1732 1733 this._registerWindow(win); 1734 // Track the window open event and check the maximum. 1735 const counts = getOpenTabsAndWinsCounts(); 1736 Glean.browserEngagement.windowOpenEventCount.add(1); 1737 1738 if (counts.winCount > this.maxWindowCount) { 1739 this.maxWindowCount = counts.winCount; 1740 Glean.browserEngagement.maxConcurrentWindowCount.set(counts.winCount); 1741 } 1742 1743 // We won't receive the "TabOpen" event for the first tab within a new window. 1744 // Account for that. 1745 this._onTabOpen(); 1746 }; 1747 win.addEventListener("load", onLoad); 1748 }, 1749 1750 /** 1751 * Record telemetry about the given tab counts. 1752 * 1753 * Telemetry for each count will only be recorded if the value isn't 1754 * `undefined`. 1755 * 1756 * @param {object} [counts] The tab counts to register with telemetry. 1757 * @param {number} [counts.tabCount] The number of tabs in all browsers. 1758 * @param {number} [counts.loadedTabCount] The number of loaded (i.e., not 1759 * pending) tabs in all browsers. 1760 */ 1761 _recordTabCounts({ tabCount, loadedTabCount }) { 1762 let currentTime = Date.now(); 1763 if ( 1764 tabCount !== undefined && 1765 currentTime > this._lastRecordTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS 1766 ) { 1767 Glean.browserEngagement.tabCount.accumulateSingleSample(tabCount); 1768 this._lastRecordTabCount = currentTime; 1769 } 1770 1771 if ( 1772 loadedTabCount !== undefined && 1773 currentTime > 1774 this._lastRecordLoadedTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS 1775 ) { 1776 Glean.browserEngagement.loadedTabCount.accumulateSingleSample( 1777 loadedTabCount 1778 ); 1779 this._lastRecordLoadedTabCount = currentTime; 1780 } 1781 }, 1782 1783 _checkProfileCountFileSchema(fileData) { 1784 // Verifies that the schema of the file is the expected schema 1785 if (typeof fileData.version != "string") { 1786 throw new Error("Schema Mismatch Error: Bad type for 'version' field"); 1787 } 1788 if (!Array.isArray(fileData.profileTelemetryIds)) { 1789 throw new Error( 1790 "Schema Mismatch Error: Bad type for 'profileTelemetryIds' field" 1791 ); 1792 } 1793 for (let profileTelemetryId of fileData.profileTelemetryIds) { 1794 if (typeof profileTelemetryId != "string") { 1795 throw new Error( 1796 "Schema Mismatch Error: Bad type for an element of 'profileTelemetryIds'" 1797 ); 1798 } 1799 } 1800 }, 1801 1802 // Reports the number of Firefox profiles on this machine to telemetry. 1803 async reportProfileCount() { 1804 // Note: this is currently a windows-only feature. 1805 1806 // To report only as much data as we need, we will bucket our values. 1807 // Rather than the raw value, we will report the greatest value in the list 1808 // below that is no larger than the raw value. 1809 const buckets = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000, 10000]; 1810 1811 // We need both the C:\ProgramData\Mozilla directory and the install 1812 // directory hash to create the profile count file path. We can easily 1813 // reassemble this from the update directory, which looks like: 1814 // C:\ProgramData\Mozilla\updates\hash 1815 // Retrieving the directory this way also ensures that the "Mozilla" 1816 // directory is created with the correct permissions. 1817 // The ProgramData directory, by default, grants write permissions only to 1818 // file creators. The directory service calls GetCommonUpdateDirectory, 1819 // which makes sure the the directory is created with user-writable 1820 // permissions. 1821 const updateDirectory = BrowserUsageTelemetry.Policy.getUpdateDirectory(); 1822 const hash = updateDirectory.leafName; 1823 const profileCountFilename = "profile_count_" + hash + ".json"; 1824 let profileCountFile = updateDirectory.parent.parent; 1825 profileCountFile.append(profileCountFilename); 1826 1827 let readError = false; 1828 let fileData; 1829 try { 1830 let json = await BrowserUsageTelemetry.Policy.readProfileCountFile( 1831 profileCountFile.path 1832 ); 1833 fileData = JSON.parse(json); 1834 BrowserUsageTelemetry._checkProfileCountFileSchema(fileData); 1835 } catch (ex) { 1836 // Note that since this also catches the "no such file" error, this is 1837 // always the template that we use when writing to the file for the first 1838 // time. 1839 fileData = { version: "1", profileTelemetryIds: [] }; 1840 if (!(ex.name == "NotFoundError")) { 1841 console.error(ex); 1842 // Don't just return here on a read error. We need to send the error 1843 // value to telemetry and we want to attempt to fix the file. 1844 // However, we will still report an error for this ping, even if we 1845 // fix the file. This is to prevent always sending a profile count of 1 1846 // if, for some reason, we always get a read error but never a write 1847 // error. 1848 readError = true; 1849 } 1850 } 1851 1852 let writeError = false; 1853 let currentTelemetryId = 1854 await BrowserUsageTelemetry.Policy.getTelemetryClientId(); 1855 // Don't add our telemetry ID to the file if we've already reached the 1856 // largest bucket. This prevents the file size from growing forever. 1857 if ( 1858 !fileData.profileTelemetryIds.includes(currentTelemetryId) && 1859 fileData.profileTelemetryIds.length < Math.max(...buckets) 1860 ) { 1861 fileData.profileTelemetryIds.push(currentTelemetryId); 1862 try { 1863 await BrowserUsageTelemetry.Policy.writeProfileCountFile( 1864 profileCountFile.path, 1865 JSON.stringify(fileData) 1866 ); 1867 } catch (ex) { 1868 console.error(ex); 1869 writeError = true; 1870 } 1871 } 1872 1873 // Determine the bucketed value to report 1874 let rawProfileCount = fileData.profileTelemetryIds.length; 1875 let valueToReport = 0; 1876 for (let bucket of buckets) { 1877 if (bucket <= rawProfileCount && bucket > valueToReport) { 1878 valueToReport = bucket; 1879 } 1880 } 1881 1882 if (readError || writeError) { 1883 // We convey errors via a profile count of 0. 1884 valueToReport = 0; 1885 } 1886 1887 Glean.browserEngagement.profileCount.set(valueToReport); 1888 }, 1889 1890 /** 1891 * Check if this is the first run of this profile since installation, 1892 * if so then collect installation telemetry. 1893 * 1894 * @param {nsIFile} [dataPathOverride] Optional, full data file path, for tests. 1895 * @param {Array<string>} [msixPackagePrefixes] Optional, list of prefixes to 1896 consider "existing" installs when looking at installed MSIX packages. 1897 Defaults to prefixes for builds produced in Firefox automation. 1898 * @returns {Promise<object>} 1899 * Resolves to a JSON object containing install telemetry when the event has 1900 * been recorded, or if the data file was not found. 1901 * @rejects JavaScript exception on any failure. 1902 */ 1903 async collectInstallationTelemetry( 1904 dataPathOverride, 1905 msixPackagePrefixes = ["Mozilla.Firefox", "Mozilla.MozillaFirefox"] 1906 ) { 1907 if (AppConstants.platform != "win") { 1908 // This is a windows-only feature. 1909 return {}; 1910 } 1911 1912 const TIMESTAMP_PREF = "app.installation.timestamp"; 1913 const lastInstallTime = Services.prefs.getStringPref(TIMESTAMP_PREF, null); 1914 const wpm = Cc["@mozilla.org/windows-package-manager;1"].createInstance( 1915 Ci.nsIWindowsPackageManager 1916 ); 1917 let installer_type = ""; 1918 let pfn; 1919 try { 1920 pfn = Services.sysinfo.getProperty("winPackageFamilyName"); 1921 } catch (e) {} 1922 1923 function getInstallData() { 1924 // We only care about where _any_ other install existed - no 1925 // need to count more than 1. 1926 const installPaths = lazy.WindowsInstallsInfo.getInstallPaths( 1927 1, 1928 new Set([Services.dirsvc.get("GreBinD", Ci.nsIFile).path]) 1929 ); 1930 const msixInstalls = new Set(); 1931 // We're just going to eat all errors here -- we don't want the event 1932 // to go unsent if we were unable to look for MSIX installs. 1933 try { 1934 wpm 1935 .findUserInstalledPackages(msixPackagePrefixes) 1936 .forEach(i => msixInstalls.add(i)); 1937 if (pfn) { 1938 msixInstalls.delete(pfn); 1939 } 1940 } catch (ex) {} 1941 return { 1942 installPaths, 1943 msixInstalls, 1944 }; 1945 } 1946 1947 let extra = {}; 1948 1949 if (pfn) { 1950 if (lastInstallTime != null) { 1951 // We've already seen this install 1952 return {}; 1953 } 1954 1955 // First time seeing this install, record the timestamp. 1956 Services.prefs.setStringPref(TIMESTAMP_PREF, wpm.getInstalledDate()); 1957 let install_data = getInstallData(); 1958 1959 installer_type = "msix"; 1960 1961 // Build the extra event data 1962 extra.version = AppConstants.MOZ_APP_VERSION; 1963 extra.build_id = AppConstants.MOZ_BUILDID; 1964 // The next few keys are static for the reasons described 1965 // No way to detect whether or not we were installed by an admin 1966 extra.admin_user = "false"; 1967 // Always false at the moment, because we create a new profile 1968 // on first launch 1969 extra.profdir_existed = "false"; 1970 // Obviously false for MSIX installs 1971 extra.from_msi = "false"; 1972 // We have no way of knowing whether we were installed via the GUI, 1973 // through the command line, or some Enterprise management tool. 1974 extra.silent = "false"; 1975 // There's no way to change the install path for an MSIX package 1976 extra.default_path = "true"; 1977 extra.install_existed = install_data.msixInstalls.has(pfn).toString(); 1978 install_data.msixInstalls.delete(pfn); 1979 extra.other_inst = (!!install_data.installPaths.size).toString(); 1980 extra.other_msix_inst = (!!install_data.msixInstalls.size).toString(); 1981 } else { 1982 let dataPath = dataPathOverride; 1983 if (!dataPath) { 1984 dataPath = Services.dirsvc.get("GreD", Ci.nsIFile); 1985 dataPath.append("installation_telemetry.json"); 1986 } 1987 1988 let dataBytes; 1989 try { 1990 dataBytes = await IOUtils.read(dataPath.path); 1991 } catch (ex) { 1992 if (ex.name == "NotFoundError") { 1993 // Many systems will not have the data file, return silently if not found as 1994 // there is nothing to record. 1995 return {}; 1996 } 1997 throw ex; 1998 } 1999 const dataString = new TextDecoder("utf-16").decode(dataBytes); 2000 const data = JSON.parse(dataString); 2001 2002 if (lastInstallTime && data.install_timestamp == lastInstallTime) { 2003 // We've already seen this install 2004 return {}; 2005 } 2006 2007 // First time seeing this install, record the timestamp. 2008 Services.prefs.setStringPref(TIMESTAMP_PREF, data.install_timestamp); 2009 let install_data = getInstallData(); 2010 2011 installer_type = data.installer_type; 2012 2013 // Installation timestamp is not intended to be sent with telemetry, 2014 // remove it to emphasize this point. 2015 delete data.install_timestamp; 2016 2017 // Build the extra event data 2018 extra.version = data.version; 2019 extra.build_id = data.build_id; 2020 extra.admin_user = data.admin_user.toString(); 2021 extra.install_existed = data.install_existed.toString(); 2022 extra.profdir_existed = data.profdir_existed.toString(); 2023 extra.other_inst = (!!install_data.installPaths.size).toString(); 2024 extra.other_msix_inst = (!!install_data.msixInstalls.size).toString(); 2025 2026 if (data.installer_type == "full") { 2027 extra.silent = data.silent.toString(); 2028 extra.from_msi = data.from_msi.toString(); 2029 extra.default_path = data.default_path.toString(); 2030 } 2031 } 2032 return { installer_type, extra }; 2033 }, 2034 2035 async reportInstallationTelemetry( 2036 dataPathOverride, 2037 msixPackagePrefixes = ["Mozilla.Firefox", "Mozilla.MozillaFirefox"] 2038 ) { 2039 // The optional dataPathOverride is only used for testing purposes. 2040 // Use this as a proxy for whether we're in a testing environment. 2041 // If we're in a testing environment we don't want to return the 2042 // same data even if we call this function multiple times in the 2043 // same instance. 2044 if (gInstallationTelemetryPromise && !dataPathOverride) { 2045 return gInstallationTelemetryPromise; 2046 } 2047 2048 gInstallationTelemetryPromise = (async () => { 2049 let data = await BrowserUsageTelemetry.collectInstallationTelemetry( 2050 dataPathOverride, 2051 msixPackagePrefixes 2052 ); 2053 2054 if (data?.installer_type) { 2055 let { installer_type, extra } = data; 2056 2057 // Record the event (mirrored to legacy telemetry using GIFFT) 2058 if (installer_type == "full") { 2059 Glean.installation.firstSeenFull.record(extra); 2060 } else if (installer_type == "stub") { 2061 Glean.installation.firstSeenStub.record(extra); 2062 } else if (installer_type == "msix") { 2063 Glean.installation.firstSeenMsix.record(extra); 2064 } 2065 2066 // Scalars for the new-profile ping. We don't need to collect the build version 2067 // These are mirrored to legacy telemetry using GIFFT 2068 Glean.installationFirstSeen.installerType.set(installer_type); 2069 Glean.installationFirstSeen.version.set(extra.version); 2070 // Convert "true" or "false" strings back into booleans 2071 Glean.installationFirstSeen.adminUser.set(extra.admin_user === "true"); 2072 Glean.installationFirstSeen.installExisted.set( 2073 extra.install_existed === "true" 2074 ); 2075 Glean.installationFirstSeen.profdirExisted.set( 2076 extra.profdir_existed === "true" 2077 ); 2078 Glean.installationFirstSeen.otherInst.set(extra.other_inst === "true"); 2079 Glean.installationFirstSeen.otherMsixInst.set( 2080 extra.other_msix_inst === "true" 2081 ); 2082 if (installer_type == "full") { 2083 Glean.installationFirstSeen.silent.set(extra.silent === "true"); 2084 Glean.installationFirstSeen.fromMsi.set(extra.from_msi === "true"); 2085 Glean.installationFirstSeen.defaultPath.set( 2086 extra.default_path === "true" 2087 ); 2088 } 2089 } 2090 return data; 2091 })(); 2092 2093 return gInstallationTelemetryPromise; 2094 }, 2095 }; 2096 2097 // Used by nsIBrowserUsage 2098 export function getUniqueDomainsVisitedInPast24Hours() { 2099 return URICountListener.uniqueDomainsVisitedInPast24Hours; 2100 }