commit 80328f6b2fe1b2b784590446b442cf928839ea84
parent a3f2b955ffe575ff63834121063b60c6e454aaa2
Author: Stephen Thompson <sthompson@mozilla.com>
Date: Tue, 14 Oct 2025 21:00:36 +0000
Bug 1988855 Part 3 - telemetry for moving tabs based on where those tabs were opened r=dwalker,tabbrowser-reviewers
When a user moves a tab, we want to record whether that tab was originally opened inside of Firefox or from outside of Firefox. For tabs originally opened from outside of Firefox, we want to know whether the tab was originally opened next to the active tab or at the end of the tab strip.
This telemetry event is intended to measure the success of the new about:preferences setting "Open links from apps next to your active tab." Our hypothesis is that if users are happier with where externally opened tabs are placed, then they will be less likely to move those tabs.
Differential Revision: https://phabricator.services.mozilla.com/D266918
Diffstat:
4 files changed, 233 insertions(+), 8 deletions(-)
diff --git a/browser/components/tabbrowser/metrics.yaml b/browser/components/tabbrowser/metrics.yaml
@@ -114,8 +114,6 @@ browser.ui.interaction:
- mconley@mozilla.com
expires: never
telemetry_mirror: BROWSER_UI_INTERACTION_ALL_TABS_PANEL_DRAGSTART_TAB_EVENT_COUNT
- no_lint:
- - COMMON_PREFIX
all_tabs_panel_entrypoint:
type: labeled_counter
@@ -133,8 +131,25 @@ browser.ui.interaction:
- mconley@mozilla.com
expires: never
telemetry_mirror: BROWSER_UI_INTERACTION_ALL_TABS_PANEL_ENTRYPOINT
- no_lint:
- - COMMON_PREFIX
+
+ tab_movement:
+ type: labeled_counter
+ disabled: true
+ description: >
+ Records information about user tab movements within the tab strip.
+ bugs:
+ - https://bugzil.la/1988855
+ data_reviews:
+ - https://bugzil.la/1988855
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - sthompson@mozilla.com
+ expires: never
+ labels:
+ - not_from_external_app
+ - from_external_app_next_to_active_tab
+ - from_external_app_tab_strip_end
tabgroup:
create_group:
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser.toml b/browser/components/tabbrowser/test/browser/tabs/browser.toml
@@ -186,6 +186,8 @@ tags = "vertical-tabs"
tags = "vertical-tabs"
fail-if = ["vertical_tab"] # Bug 1932787, fails in the "vertical-tabs" variant
+["browser_move_externally_opened_tabs_telemetry.js"]
+
["browser_multiselect_tabs_active_tab_selected_by_default.js"]
tags = "vertical-tabs"
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_move_externally_opened_tabs_telemetry.js b/browser/components/tabbrowser/test/browser/tabs/browser_move_externally_opened_tabs_telemetry.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let resetTelemetry = async () => {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+ // `browser.ui.interaction.tab_movement` is disabled by default and enabled
+ // by server knobs, so this test needs to enable it manually in order
+ // to test it.
+ Services.fog.applyServerKnobsConfig(
+ JSON.stringify({
+ metrics_enabled: {
+ "browser.ui.interaction.tab_movement": true,
+ },
+ })
+ );
+};
+
+/**
+ * Simulates opening a link external to Firefox, returning the newly created tab.
+ *
+ * @returns {Promise<MozTabbrowserTab>}
+ */
+async function openExternalLink() {
+ const tabOpen = BrowserTestUtils.waitForEvent(window, "TabOpen");
+ window.browserDOMWindow.openURI(
+ Services.io.newURI("data:text/plain,external%20URL"),
+ null,
+ Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW,
+ Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL,
+ Services.scriptSecurityManager.createNullPrincipal({})
+ );
+ return (await tabOpen).target;
+}
+
+/**
+ * @returns {Promise<MozTabbrowserTab>}
+ */
+async function openInternalLink() {
+ return BrowserTestUtils.addTab(gBrowser, "data:text/plain,internal%20URL");
+}
+
+/**
+ * @param {MozTabbrowserTab} tab
+ */
+async function moveTab(tab) {
+ const tabMove = BrowserTestUtils.waitForEvent(tab, "TabMove");
+ gBrowser.moveTabTo(tab, {
+ tabIndex: 0,
+ isUserTriggered: true,
+ });
+ await tabMove;
+}
+
+/**
+ * @param {1|null} notFromExternalApp
+ * @param {1|null} fromExternalAppNextToActiveTab
+ * @param {1|null} fromExternalAppTabStripEnd
+ */
+function assertTabMovementCounters(
+ notFromExternalApp,
+ fromExternalAppNextToActiveTab,
+ fromExternalAppTabStripEnd
+) {
+ Assert.equal(
+ Glean.browserUiInteraction.tabMovement.not_from_external_app.testGetValue(),
+ notFromExternalApp,
+ "should have recorded appropriate internally opened tab move"
+ );
+ Assert.equal(
+ Glean.browserUiInteraction.tabMovement.from_external_app_next_to_active_tab.testGetValue(),
+ fromExternalAppNextToActiveTab,
+ "should have recorded appropriate externally opened tab move (next to active tab)"
+ );
+ Assert.equal(
+ Glean.browserUiInteraction.tabMovement.from_external_app_tab_strip_end.testGetValue(),
+ fromExternalAppTabStripEnd,
+ "should have recorded appropriate externally opened tab move (end of tab strip)"
+ );
+}
+
+add_task(async function test_move_interally_opened_link() {
+ await resetTelemetry();
+
+ const tab = await openInternalLink();
+ await moveTab(tab);
+
+ await TestUtils.waitForCondition(
+ () =>
+ Glean.browserUiInteraction.tabMovement.not_from_external_app.testGetValue(),
+ "wait until a metric is recorded"
+ );
+
+ assertTabMovementCounters(1, null, null);
+
+ BrowserTestUtils.removeTab(tab);
+ await resetTelemetry();
+});
+
+add_task(async function test_move_exterally_opened_link_end_of_tab_strip() {
+ await resetTelemetry();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.link.open_newwindow.override.external", -1]],
+ });
+
+ const tab = await openExternalLink();
+ await moveTab(tab);
+
+ await TestUtils.waitForCondition(
+ () =>
+ Glean.browserUiInteraction.tabMovement.from_external_app_tab_strip_end.testGetValue(),
+ "wait until a metric is recorded"
+ );
+
+ assertTabMovementCounters(null, null, 1);
+
+ BrowserTestUtils.removeTab(tab);
+ await resetTelemetry();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_move_exterally_opened_link_next_to_active_tab() {
+ await resetTelemetry();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.link.open_newwindow.override.external",
+ Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT,
+ ],
+ ],
+ });
+
+ const tab = await openExternalLink();
+ await moveTab(tab);
+
+ await TestUtils.waitForCondition(
+ () =>
+ Glean.browserUiInteraction.tabMovement.from_external_app_next_to_active_tab.testGetValue(),
+ "wait until a metric is recorded"
+ );
+
+ assertTabMovementCounters(null, 1, null);
+
+ BrowserTestUtils.removeTab(tab);
+ await resetTelemetry();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/modules/BrowserUsageTelemetry.sys.mjs b/browser/modules/BrowserUsageTelemetry.sys.mjs
@@ -205,6 +205,12 @@ const PLACES_OPEN_COMMANDS = [
// Default: 5min.
const FLOW_IDLE_TIME = 5 * 60 * 1000;
+const externalTabMovementRegistry = {
+ internallyOpenedTabs: new WeakSet(),
+ externallyOpenedTabsNextToActiveTab: new WeakSet(),
+ externallyOpenedTabsAtEndOfTabStrip: new WeakSet(),
+};
+
function telemetryId(widgetId, obscureAddons = true) {
// Add-on IDs need to be obscured.
function addonId(id) {
@@ -1318,10 +1324,27 @@ export let BrowserUsageTelemetry = {
Glean.tabgroup.tabInteractions.new.add();
}
- if (event?.detail?.fromExternal) {
- Glean.linkHandling.openFromExternalApp.record({
- next_to_active_tab: this._isOpenNextToActiveTabSettingEnabled(),
- });
+ if (event) {
+ if (event.detail?.fromExternal) {
+ const wasOpenedNextToActiveTab =
+ this._isOpenNextToActiveTabSettingEnabled();
+
+ Glean.linkHandling.openFromExternalApp.record({
+ next_to_active_tab: wasOpenedNextToActiveTab,
+ });
+
+ if (wasOpenedNextToActiveTab) {
+ externalTabMovementRegistry.externallyOpenedTabsNextToActiveTab.add(
+ event.target
+ );
+ } else {
+ externalTabMovementRegistry.externallyOpenedTabsAtEndOfTabStrip.add(
+ event.target
+ );
+ }
+ } else {
+ externalTabMovementRegistry.internallyOpenedTabs.add(event.target);
+ }
}
const userContextId = event?.target?.getAttribute("usercontextid");
@@ -1350,6 +1373,11 @@ export let BrowserUsageTelemetry = {
this._recordTabCounts({ tabCount, loadedTabCount });
},
+ /**
+ *
+ * @param {CustomEvent} event
+ * TabClose event.
+ */
_onTabClosed(event) {
const group = event.target?.group;
const isUserTriggered = event.detail?.isUserTriggered;
@@ -1379,6 +1407,13 @@ export let BrowserUsageTelemetry = {
layout: lazy.sidebarVerticalTabs ? "vertical" : "horizontal",
});
}
+
+ if (event.target) {
+ // Stop tracking any tabs that have been tracked since their `TabOpen` events.
+ Object.values(externalTabMovementRegistry).forEach(set => {
+ set.delete(event.target);
+ });
+ }
},
_onTabPinned(event) {
@@ -1611,6 +1646,8 @@ export let BrowserUsageTelemetry = {
this._updateTabMovementsRecord(tabMovementsRecord, event);
tabMovementsRecord.deferredTask.arm();
}
+
+ this._recordExternalTabMovement(event);
},
/**
@@ -1638,6 +1675,28 @@ export let BrowserUsageTelemetry = {
}
},
+ /**
+ * @param {CustomEvent} event
+ * TabMove event
+ */
+ _recordExternalTabMovement(event) {
+ if (externalTabMovementRegistry.internallyOpenedTabs.has(event.target)) {
+ Glean.browserUiInteraction.tabMovement.not_from_external_app.add();
+ } else if (
+ externalTabMovementRegistry.externallyOpenedTabsNextToActiveTab.has(
+ event.target
+ )
+ ) {
+ Glean.browserUiInteraction.tabMovement.from_external_app_next_to_active_tab.add();
+ } else if (
+ externalTabMovementRegistry.externallyOpenedTabsAtEndOfTabStrip.has(
+ event.target
+ )
+ ) {
+ Glean.browserUiInteraction.tabMovement.from_external_app_tab_strip_end.add();
+ }
+ },
+
_onTabSelect(event) {
if (event.target.group) {
let interaction = event.target.group.collapsed