diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index 1bd7850d60ce..a08e335e5df7 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -102,6 +102,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "SelfSupportBackend", XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", "resource:///modules/sessionstore/SessionStore.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUsageTelemetry", + "resource:///modules/BrowserUsageTelemetry.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"); @@ -781,6 +784,7 @@ BrowserGlue.prototype = { NewTabMessages.init(); SessionStore.init(); + BrowserUsageTelemetry.init(); BrowserUITelemetry.init(); ContentSearch.init(); FormValidationHandler.init(); @@ -1174,6 +1178,7 @@ BrowserGlue.prototype = { Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e); } + BrowserUsageTelemetry.uninit(); SelfSupportBackend.uninit(); NewTabMessages.uninit(); diff --git a/browser/modules/BrowserUsageTelemetry.jsm b/browser/modules/BrowserUsageTelemetry.jsm new file mode 100644 index 000000000000..2c6563f9b671 --- /dev/null +++ b/browser/modules/BrowserUsageTelemetry.jsm @@ -0,0 +1,175 @@ +/* -*- 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["BrowserUsageTelemetry"]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); + +// Observed topic names. +const WINDOWS_RESTORED_TOPIC = "sessionstore-windows-restored"; +const TELEMETRY_SUBSESSIONSPLIT_TOPIC = "internal-telemetry-after-subsession-split"; +const DOMWINDOW_OPENED_TOPIC = "domwindowopened"; +const DOMWINDOW_CLOSED_TOPIC = "domwindowclosed"; + +// 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 WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME = "browser.engagement.window_open_event_count"; + +function getOpenTabsAndWinsCounts() { + let tabCount = 0; + let winCount = 0; + + let browserEnum = Services.wm.getEnumerator("navigator:browser"); + while (browserEnum.hasMoreElements()) { + let win = browserEnum.getNext(); + winCount++; + tabCount += win.gBrowser.tabs.length; + } + + return { tabCount, winCount }; +} + +let BrowserUsageTelemetry = { + init() { + Services.obs.addObserver(this, WINDOWS_RESTORED_TOPIC, false); + }, + + /** + * 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); + }, + + uninit() { + Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC, false); + Services.obs.removeObserver(this, DOMWINDOW_CLOSED_TOPIC, false); + Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, false); + Services.obs.removeObserver(this, WINDOWS_RESTORED_TOPIC, false); + }, + + observe(subject, topic, data) { + switch(topic) { + case WINDOWS_RESTORED_TOPIC: + this._setupAfterRestore(); + break; + case DOMWINDOW_OPENED_TOPIC: + this._onWindowOpen(subject); + break; + case DOMWINDOW_CLOSED_TOPIC: + this._unregisterWindow(subject); + break; + case TELEMETRY_SUBSESSIONSPLIT_TOPIC: + this.afterSubsessionSplit(); + break; + } + }, + + handleEvent(event) { + switch(event.type) { + case "TabOpen": + this._onTabOpen(); + 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, false); + Services.obs.addObserver(this, DOMWINDOW_CLOSED_TOPIC, false); + Services.obs.addObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, false); + + // Attach the tabopen handlers to the existing Windows. + let browserEnum = Services.wm.getEnumerator("navigator:browser"); + while (browserEnum.hasMoreElements()) { + this._registerWindow(browserEnum.getNext()); + } + + // 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); + }, + + /** + * Adds listeners to a single chrome window. + */ + _registerWindow(win) { + win.addEventListener("TabOpen", this, true); + }, + + /** + * Removes listeners from a single chrome window. + */ + _unregisterWindow(win) { + // Ignore non-browser windows. + if (!(win instanceof Ci.nsIDOMWindow) || + win.document.documentElement.getAttribute("windowtype") != "navigator:browser") { + return; + } + + win.removeEventListener("TabOpen", this, true); + }, + + /** + * Updates the tab counts. + * @param {Number} [newTabCount=0] The count of the opened tabs across all windows. This + * is computed manually if not provided. + */ + _onTabOpen(tabCount = 0) { + // Use the provided tab count if available. Otherwise, go on and compute it. + tabCount = tabCount || getOpenTabsAndWinsCounts().tabCount; + // Update the "tab opened" count and its maximum. + Services.telemetry.scalarAdd(TAB_OPEN_EVENT_COUNT_SCALAR_NAME, 1); + Services.telemetry.scalarSetMaximum(MAX_TAB_COUNT_SCALAR_NAME, tabCount); + }, + + /** + * 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, false); + + // 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.tabCount); + }; + win.addEventListener("load", onLoad, false); + }, +}; diff --git a/browser/modules/moz.build b/browser/modules/moz.build index 1fece271b48c..434b46f99cdb 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -14,6 +14,7 @@ EXTRA_JS_MODULES += [ 'AboutHome.jsm', 'AboutNewTab.jsm', 'BrowserUITelemetry.jsm', + 'BrowserUsageTelemetry.jsm', 'CaptivePortalWatcher.jsm', 'CastingApps.jsm', 'Chat.jsm', diff --git a/toolkit/components/telemetry/Scalars.yaml b/toolkit/components/telemetry/Scalars.yaml index 7357ecd431a7..1fa7e249e03f 100644 --- a/toolkit/components/telemetry/Scalars.yaml +++ b/toolkit/components/telemetry/Scalars.yaml @@ -70,3 +70,54 @@ telemetry.test: notification_emails: - telemetry-client-dev@mozilla.com release_channel_collection: opt-out + +# The following section contains the browser engagement scalars. +browser.engagement: + max_concurrent_tab_count: + bug_numbers: + - 1271304 + description: > + The count of maximum number of tabs open during a subsession, + across all windows, including tabs in private windows and restored + at startup. + expires: "55" + kind: uint + notification_emails: + - rweiss@mozilla.com + release_channel_collection: opt-out + + tab_open_event_count: + bug_numbers: + - 1271304 + description: > + The count of tab open events per subsession, across all windows, after the + session has been restored. This includes tab open events from private windows. + expires: "55" + kind: uint + notification_emails: + - rweiss@mozilla.com + release_channel_collection: opt-out + + max_concurrent_window_count: + bug_numbers: + - 1271304 + description: > + The count of maximum number of browser windows open during a subsession. This + includes private windows and the ones opened when starting the browser. + expires: "55" + kind: uint + notification_emails: + - rweiss@mozilla.com + release_channel_collection: opt-out + + window_open_event_count: + bug_numbers: + - 1271304 + description: > + The count of browser window open events per subsession, after the session + has been restored. The count includes the private windows. + expires: "55" + kind: uint + notification_emails: + - rweiss@mozilla.com + release_channel_collection: opt-out diff --git a/toolkit/components/telemetry/TelemetrySession.jsm b/toolkit/components/telemetry/TelemetrySession.jsm index 9d530e844bc1..704c8077c151 100644 --- a/toolkit/components/telemetry/TelemetrySession.jsm +++ b/toolkit/components/telemetry/TelemetrySession.jsm @@ -1351,6 +1351,10 @@ var Impl = { // Persist session data to disk (don't wait until it completes). let sessionData = this._getSessionDataObject(); TelemetryStorage.saveSessionData(sessionData); + + // Notify that there was a subsession split in the parent process. This is an + // internal topic and is only meant for internal Telemetry usage. + Services.obs.notifyObservers(null, "internal-telemetry-after-subsession-split", null); } } diff --git a/toolkit/components/telemetry/docs/main-ping.rst b/toolkit/components/telemetry/docs/main-ping.rst index 5aeb4d9388de..7ee0ace5d742 100644 --- a/toolkit/components/telemetry/docs/main-ping.rst +++ b/toolkit/components/telemetry/docs/main-ping.rst @@ -15,6 +15,8 @@ This ping is triggered by different scenarios, which is documented by the ``reas Most reasons lead to a session split, initiating a new *subsession*. We reset important measurements for those subsessions. +After a new subsession split, the ``internal-telemetry-after-subsession-split`` topic is notified to all the observers. *This is an internal topic and is only meant for internal Telemetry usage.* + *Note:* ``saved-session`` is sent with a different ping type (``saved-session``, not ``main``), but otherwise has the same format as discussed here. Structure::