diff --git a/browser/base/content/browser-commands.js b/browser/base/content/browser-commands.js index 352de44dda36..b0b2383453ef 100644 --- a/browser/base/content/browser-commands.js +++ b/browser/base/content/browser-commands.js @@ -396,7 +396,9 @@ var BrowserCommands = { // In a multi-select context, close all selected tabs if (gBrowser.multiSelectedTabsCount) { - gBrowser.removeMultiSelectedTabs(); + gBrowser.removeMultiSelectedTabs( + gBrowser.TabMetrics.userTriggeredContext() + ); return; } @@ -414,7 +416,10 @@ var BrowserCommands = { } // If the current tab is the last one, this will close the window. - gBrowser.removeCurrentTab({ animate: true }); + gBrowser.removeCurrentTab({ + animate: true, + ...gBrowser.TabMetrics.userTriggeredContext(), + }); }, tryToCloseWindow(event) { diff --git a/browser/base/content/main-popupset.js b/browser/base/content/main-popupset.js index 39f0c97141dd..3f7a07817e39 100644 --- a/browser/base/content/main-popupset.js +++ b/browser/base/content/main-popupset.js @@ -88,16 +88,28 @@ document.addEventListener( TabContextMenu.closeContextTabs(); break; case "context_closeDuplicateTabs": - gBrowser.removeDuplicateTabs(TabContextMenu.contextTab); + gBrowser.removeDuplicateTabs( + TabContextMenu.contextTab, + lazy.TabMetrics.userTriggeredContext() + ); break; case "context_closeTabsToTheStart": - gBrowser.removeTabsToTheStartFrom(TabContextMenu.contextTab); + gBrowser.removeTabsToTheStartFrom( + TabContextMenu.contextTab, + lazy.TabMetrics.userTriggeredContext() + ); break; case "context_closeTabsToTheEnd": - gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab); + gBrowser.removeTabsToTheEndFrom( + TabContextMenu.contextTab, + lazy.TabMetrics.userTriggeredContext() + ); break; case "context_closeOtherTabs": - gBrowser.removeAllTabsBut(TabContextMenu.contextTab); + gBrowser.removeAllTabsBut( + TabContextMenu.contextTab, + lazy.TabMetrics.userTriggeredContext() + ); break; case "context_unloadTab": TabContextMenu.explicitUnloadTabs(); diff --git a/browser/components/firefoxview/opentabs.mjs b/browser/components/firefoxview/opentabs.mjs index fcabcf1a06db..3f7c24fe6e4d 100644 --- a/browser/components/firefoxview/opentabs.mjs +++ b/browser/components/firefoxview/opentabs.mjs @@ -26,6 +26,7 @@ ChromeUtils.defineESModuleGetters(lazy, { NonPrivateTabs: "resource:///modules/OpenTabs.sys.mjs", getTabsTargetForWindow: "resource:///modules/OpenTabs.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + TabMetrics: "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { @@ -503,7 +504,10 @@ class OpenTabsInViewCard extends ViewPageContent { closeTab(event) { const tab = event.originalTarget.tabElement; - tab?.ownerGlobal.gBrowser.removeTab(tab); + tab?.ownerGlobal.gBrowser.removeTab( + tab, + lazy.TabMetrics.userTriggeredContext() + ); Glean.firefoxviewNext.closeOpenTabTabs.record(); } diff --git a/browser/components/tabbrowser/TabMetrics.sys.mjs b/browser/components/tabbrowser/TabMetrics.sys.mjs index 3c74da9dae53..243039c14f04 100644 --- a/browser/components/tabbrowser/TabMetrics.sys.mjs +++ b/browser/components/tabbrowser/TabMetrics.sys.mjs @@ -55,6 +55,7 @@ const METRIC_REOPEN_TYPE = Object.freeze({ * @returns {TabMetricsContext} */ function userTriggeredContext(telemetrySource) { + telemetrySource = telemetrySource || METRIC_SOURCE.UNKNOWN; return { isUserTriggered: true, telemetrySource, diff --git a/browser/components/tabbrowser/TabsList.sys.mjs b/browser/components/tabbrowser/TabsList.sys.mjs index 9a150bced213..d41ab53ef9b1 100644 --- a/browser/components/tabbrowser/TabsList.sys.mjs +++ b/browser/components/tabbrowser/TabsList.sys.mjs @@ -324,9 +324,12 @@ export class TabsPanel extends TabsListBase { break; } if (event.target.classList.contains("all-tabs-close-button")) { - this.gBrowser.removeTab(event.target.tab, { - telemetrySource: lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU, - }); + this.gBrowser.removeTab( + event.target.tab, + lazy.TabMetrics.userTriggeredContext( + lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU + ) + ); break; } if ("tabGroupId" in event.target.dataset) { diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js index f43ab1cf6257..793ba822f087 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -555,14 +555,18 @@ if (event.target.classList.contains("tab-close-button")) { if (this.multiselected) { - gBrowser.removeMultiSelectedTabs({ - telemetrySource: lazy.TabMetrics.METRIC_SOURCE.TAB_STRIP, - }); + gBrowser.removeMultiSelectedTabs( + lazy.TabMetrics.userTriggeredContext( + lazy.TabMetrics.METRIC_SOURCE.TAB_STRIP + ) + ); } else { gBrowser.removeTab(this, { animate: true, triggeringEvent: event, - telemetrySource: lazy.TabMetrics.METRIC_SOURCE.TAB_STRIP, + ...lazy.TabMetrics.userTriggeredContext( + lazy.TabMetrics.METRIC_SOURCE.TAB_STRIP + ), }); } // This enables double-click protection for the tab container diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js index e4bb501043d4..37570c54b06f 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -4139,15 +4139,16 @@ return duplicateTabs; } - removeDuplicateTabs(aTab) { + removeDuplicateTabs(aTab, options) { this._removeDuplicateTabs( aTab, this.getDuplicateTabsToClose(aTab), - this.closingTabsEnum.DUPLICATES + this.closingTabsEnum.DUPLICATES, + options ); } - _removeDuplicateTabs(aConfirmationAnchor, tabs, aCloseTabs) { + _removeDuplicateTabs(aConfirmationAnchor, tabs, aCloseTabs, options) { if (!tabs.length) { return; } @@ -4156,7 +4157,7 @@ return; } - this.removeTabs(tabs); + this.removeTabs(tabs, options); ConfirmationHint.show( aConfirmationAnchor, "confirmation-hint-duplicate-tabs-closed", @@ -4179,7 +4180,7 @@ * In a multi-select context, the tabs (except pinned tabs) that are located to the * left of the leftmost selected tab will be removed. */ - removeTabsToTheStartFrom(aTab) { + removeTabsToTheStartFrom(aTab, options) { let tabs = this._getTabsToTheStartFrom(aTab); if ( !this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_START) @@ -4187,14 +4188,14 @@ return; } - this.removeTabs(tabs); + this.removeTabs(tabs, options); } /** * In a multi-select context, the tabs (except pinned tabs) that are located to the * right of the rightmost selected tab will be removed. */ - removeTabsToTheEndFrom(aTab) { + removeTabsToTheEndFrom(aTab, options) { let tabs = this._getTabsToTheEndFrom(aTab); if ( !this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END) @@ -4202,7 +4203,7 @@ return; } - this.removeTabs(tabs); + this.removeTabs(tabs, options); } /** @@ -4258,7 +4259,7 @@ this.removeTabs(tabsToRemove, aParams); } - removeMultiSelectedTabs({ telemetrySource } = {}) { + removeMultiSelectedTabs({ isUserTriggered, telemetrySource } = {}) { let selectedTabs = this.selectedTabs; if ( !this.warnAboutClosingTabs( @@ -4269,7 +4270,7 @@ return; } - this.removeTabs(selectedTabs, { telemetrySource }); + this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource }); } /** @@ -4313,6 +4314,7 @@ skipPermitUnload, skipRemoves, skipSessionStore, + isUserTriggered, telemetrySource, } ) { @@ -4396,6 +4398,7 @@ prewarmed: true, skipPermitUnload, skipSessionStore, + isUserTriggered, telemetrySource, }); } @@ -4509,6 +4512,7 @@ * @param {boolean} [options.skipGroupCheck] * Skip separate processing of whole tab groups from the set of tabs. * Used by removeTabGroup. + * TODO add docs */ removeTabs( tabs, @@ -4518,6 +4522,7 @@ skipPermitUnload = false, skipSessionStore = false, skipGroupCheck = false, + isUserTriggered = false, telemetrySource, } = {} ) { @@ -4553,6 +4558,8 @@ animate, skipSessionStore, skipPermitUnload, + isUserTriggered, + telemetrySource, }); }); tabs = leftoverTabs; @@ -4565,6 +4572,7 @@ skipPermitUnload, skipRemoves: false, skipSessionStore, + isUserTriggered, telemetrySource, }); @@ -4589,6 +4597,8 @@ prewarmed: true, skipPermitUnload, skipSessionStore, + isUserTriggered, + telemetrySource, }; // Now run again sequentially the beforeunload listeners that will result in a prompt. @@ -4626,6 +4636,7 @@ closeWindowWithLastTab, prewarmed, skipSessionStore, + isUserTriggered, telemetrySource, } = {} ) { @@ -4664,6 +4675,7 @@ closeWindowWithLastTab, prewarmed, skipSessionStore, + isUserTriggered, telemetrySource, }) ) { @@ -4752,6 +4764,7 @@ skipPermitUnload, prewarmed, skipSessionStore = false, + isUserTriggered, telemetrySource, } = {} ) { @@ -4904,7 +4917,12 @@ // inspect the tab that's about to close. let evt = new CustomEvent("TabClose", { bubbles: true, - detail: { adoptedBy: adoptedByTab, skipSessionStore, telemetrySource }, + detail: { + adoptedBy: adoptedByTab, + skipSessionStore, + isUserTriggered, + telemetrySource, + }, }); aTab.dispatchEvent(evt); @@ -9274,13 +9292,17 @@ var TabContextMenu = { closeContextTabs() { if (this.contextTab.multiselected) { - gBrowser.removeMultiSelectedTabs({ - telemetrySource: gBrowser.TabMetrics.METRIC_SOURCE.TAB_STRIP, - }); + gBrowser.removeMultiSelectedTabs( + gBrowser.TabMetrics.userTriggeredContext( + gBrowser.TabMetrics.METRIC_SOURCE.TAB_STRIP + ) + ); } else { gBrowser.removeTab(this.contextTab, { animate: true, - telemetrySource: gBrowser.TabMetrics.METRIC_SOURCE.TAB_STRIP, + ...gBrowser.TabMetrics.userTriggeredContext( + gBrowser.TabMetrics.METRIC_SOURCE.TAB_STRIP + ), }); } }, diff --git a/browser/components/tabbrowser/metrics.yaml b/browser/components/tabbrowser/metrics.yaml index 1bfda379bdd3..63477a6fa46c 100644 --- a/browser/components/tabbrowser/metrics.yaml +++ b/browser/components/tabbrowser/metrics.yaml @@ -396,9 +396,11 @@ tabgroup: bugs: - https://bugzil.la/1938405 - https://bugzil.la/1960360 + - https://bugzil.la/1961161 data_reviews: - https://bugzil.la/1938405 - https://bugzil.la/1960360 + - https://bugzil.la/1961161 data_sensitivity: - interaction labels: @@ -408,6 +410,7 @@ tabgroup: - new - close_tabstrip - close_tabmenu + - close_tab_other - reorder - remove_same_window - remove_other_window diff --git a/browser/components/tabbrowser/test/browser/tabs/browser.toml b/browser/components/tabbrowser/test/browser/tabs/browser.toml index 5abe361dde87..bafa77e8cc4a 100644 --- a/browser/components/tabbrowser/test/browser/tabs/browser.toml +++ b/browser/components/tabbrowser/test/browser/tabs/browser.toml @@ -514,6 +514,8 @@ tags = "vertical-tabs" ["browser_tab_groups_keyboard_focus.js"] tags = "vertical-tabs" +["browser_tab_groups_tab_interactions_telemetry.js"] + ["browser_tab_groups_telemetry.js"] ["browser_tab_label_during_reload.js"] diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_groups_tab_interactions_telemetry.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_groups_tab_interactions_telemetry.js new file mode 100644 index 000000000000..6cf1d3870e45 --- /dev/null +++ b/browser/components/tabbrowser/test/browser/tabs/browser_tab_groups_tab_interactions_telemetry.js @@ -0,0 +1,393 @@ +/* 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/. */ + +const { + openFirefoxViewTab, + closeFirefoxViewTab, + init: FirefoxViewTestUtilsInit, +} = ChromeUtils.importESModule( + "resource://testing-common/FirefoxViewTestUtils.sys.mjs" +); +FirefoxViewTestUtilsInit(this); + +const { TabStateFlusher } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" +); + +const { UrlbarTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" +); + +let resetTelemetry = async () => { + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); +}; + +let assertMetricEmpty = async metricName => { + Assert.equal( + Glean.tabgroup.tabInteractions[metricName].testGetValue(), + null, + `tab_interactions.${metricName} starts empty` + ); +}; + +let assertMetricFoundFor = async (metricName, count = 1) => { + await BrowserTestUtils.waitForCondition(() => { + return Glean.tabgroup.tabInteractions[metricName].testGetValue() == count; + }, `Wait for tab_interactions.${metricName} to be recorded`); + Assert.equal( + Glean.tabgroup.tabInteractions[metricName].testGetValue(), + count, + `tab_interactions.${metricName} was recorded` + ); +}; + +let activateTabContextMenuItem = async ( + selectedTab, + menuItemSelector, + submenuItemSelector +) => { + let submenuItem; + let submenuItemHiddenPromise; + + const tabContextMenu = window.document.getElementById("tabContextMenu"); + Assert.equal( + tabContextMenu.state, + "closed", + "context menu is initially closed" + ); + const contextMenuShown = BrowserTestUtils.waitForEvent( + tabContextMenu, + "popupshown", + false, + ev => ev.target == tabContextMenu + ); + EventUtils.synthesizeMouseAtCenter( + selectedTab, + { type: "contextmenu", button: 2 }, + window + ); + await contextMenuShown; + + if (submenuItemSelector) { + submenuItem = tabContextMenu.querySelector(submenuItemSelector); + + const submenuPopupPromise = BrowserTestUtils.waitForEvent( + submenuItem.menupopup, + "popupshown" + ); + submenuItem.openMenu(true); + await submenuPopupPromise; + + submenuItemHiddenPromise = BrowserTestUtils.waitForEvent( + submenuItem.menupopup, + "popuphidden" + ); + } + + const contextMenuHidden = BrowserTestUtils.waitForEvent( + tabContextMenu, + "popuphidden", + false, + ev => ev.target == tabContextMenu + ); + tabContextMenu.activateItem(tabContextMenu.querySelector(menuItemSelector)); + await contextMenuHidden; + if (submenuItemSelector) { + await submenuItemHiddenPromise; + } + + Assert.equal(tabContextMenu.state, "closed", "context menu is closed"); +}; + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.groups.enabled", true]], + }); + window.gTabsPanel.init(); + registerCleanupFunction(async () => { + await SpecialPowers.popPrefEnv(); + }); +}); + +add_task(async function test_tabInteractionsBasic() { + let initialTab = window.gBrowser.tabs[0]; + await resetTelemetry(); + + let tab = BrowserTestUtils.addTab(window.gBrowser, "https://example.com"); + let group = window.gBrowser.addTabGroup([tab]); + + info( + "Test that selecting a tab in a group records tab_interactions.activate" + ); + await assertMetricEmpty("activate"); + const tabSelectEvent = BrowserTestUtils.waitForEvent(window, "TabSelect"); + window.gBrowser.selectTabAtIndex(1); + await tabSelectEvent; + await assertMetricFoundFor("activate"); + + info( + "Test that moving an existing tab into a tab group records tab_interactions.add" + ); + let tab1 = BrowserTestUtils.addTab(window.gBrowser, "https://example.com"); + await assertMetricEmpty("add"); + window.gBrowser.moveTabToGroup(tab1, group, { isUserTriggered: true }); + await assertMetricFoundFor("add"); + + info( + "Test that adding a new tab to a tab group records tab_interactions.new" + ); + await assertMetricEmpty("new"); + BrowserTestUtils.addTab(window.gBrowser, "https://example.com", { + tabGroup: group, + }); + await assertMetricFoundFor("new"); + + info("Test that moving a tab within a group calls tab_interactions.reorder"); + await assertMetricEmpty("reorder"); + window.gBrowser.moveTabTo(group.tabs[0], { + tabIndex: 3, + isUserTriggered: true, + }); + await assertMetricFoundFor("reorder"); + + info( + "Test that duplicating a tab within a group calls tab_interactions.duplicate" + ); + await assertMetricEmpty("duplicate"); + window.gBrowser.duplicateTab(group.tabs[0], true, { tabIndex: 2 }); + await assertMetricFoundFor("duplicate"); + + window.gBrowser.removeAllTabsBut(initialTab); + await resetTelemetry(); +}); + +add_task(async function test_tabInteractionsClose() { + let initialTab = window.gBrowser.tabs[0]; + await resetTelemetry(); + FirefoxViewTestUtilsInit(this, window); + + let tabs = Array.from({ length: 5 }, () => { + return BrowserTestUtils.addTab(window.gBrowser, "https://example.com", { + skipAnimation: true, + }); + }); + let group = window.gBrowser.addTabGroup(tabs); + + info( + "Test that closing a tab using the tab's close button calls tab_interactions.close_tabstrip" + ); + await assertMetricEmpty("close_tabstrip"); + group.tabs.at(-1).querySelector(".tab-close-button").click(); + await assertMetricFoundFor("close_tabstrip"); + + info( + "Test that closing a tab via the tab context menu calls tab_interactions.close_tabstrip" + ); + await activateTabContextMenuItem(group.tabs[0], "#context_closeTab"); + await assertMetricFoundFor("close_tabstrip", 2); + + info( + "Test that closing a tab via the tab close keyboard shortcut calls tab_interactions.close_tab_other" + ); + window.gBrowser.selectedTab = group.tabs.at(-1); + await assertMetricEmpty("close_tab_other"); + EventUtils.synthesizeKey("w", { accelKey: true }, window); + await assertMetricFoundFor("close_tab_other"); + + info( + "Test that closing a tab via top menu calls tab_interactions.close_tab_other" + ); + window.document.getElementById("cmd_close").doCommand(); + await assertMetricFoundFor("close_tab_other", 2); + + info( + "Test that closing a tab via firefox view calls tab_interactions.close_tab_other" + ); + await openFirefoxViewTab(window).then(async viewTab => { + const openTabs = viewTab.linkedBrowser.contentDocument + .querySelector("named-deck > view-recentbrowsing view-opentabs") + .shadowRoot.querySelector("view-opentabs-card").tabList.rowEls; + const tabElement = Array.from(openTabs).find(t => t.__tabElement.group); + tabElement.shadowRoot.querySelector("moz-button.dismiss-button").click(); + await assertMetricFoundFor("close_tab_other", 3); + }); + await closeFirefoxViewTab(window); + + window.gBrowser.removeAllTabsBut(initialTab); + await resetTelemetry(); +}); + +add_task(async function test_tabInteractionsCloseViaAnotherTabContext() { + let initialTab = window.gBrowser.tabs[0]; + await resetTelemetry(); + + window.gBrowser.addTabGroup([ + BrowserTestUtils.addTab(window.gBrowser, "https://example.com", { + skipAnimation: true, + }), + ]); + + await assertMetricEmpty("close_tab_other"); + + info( + "Test that closing a tab via the tab context menu 'close other tabs' command calls tab_interactions.close_tab_other" + ); + await activateTabContextMenuItem( + initialTab, + "#context_closeOtherTabs", + "#context_closeTabOptions" + ); + await assertMetricFoundFor("close_tab_other"); + + info( + "Test that closing a tab via the tab context menu 'close tabs to left' command calls tab_interactions.close_tab_other" + ); + window.gBrowser.addTabGroup([ + BrowserTestUtils.addTab(window.gBrowser, "https://example.com", { + skipAnimation: true, + }), + ]); + window.gBrowser.moveTabToEnd(initialTab); + await activateTabContextMenuItem( + initialTab, + "#context_closeTabsToTheStart", + "#context_closeTabOptions" + ); + await assertMetricFoundFor("close_tab_other", 2); + + info( + "Test that closing a tab via the tab context menu 'close tabs to right' command calls tab_interactions.close_tab_other" + ); + window.gBrowser.addTabGroup([ + BrowserTestUtils.addTab(window.gBrowser, "https://example.com", { + skipAnimation: true, + }), + ]); + await activateTabContextMenuItem( + initialTab, + "#context_closeTabsToTheEnd", + "#context_closeTabOptions" + ); + await assertMetricFoundFor("close_tab_other", 3); + + info( + "Test that closing a tab via the tab context menu 'close duplicate tabs' command calls tab_interactions.close_tab_other" + ); + let duplicateTabs = [ + BrowserTestUtils.addTab(window.gBrowser, "https://example.com", { + skipAnimation: true, + }), + BrowserTestUtils.addTab(window.gBrowser, "https://example.com", { + skipAnimation: true, + }), + ]; + await Promise.all( + duplicateTabs.map(t => BrowserTestUtils.browserLoaded(t.linkedBrowser)) + ); + window.gBrowser.addTabGroup([duplicateTabs[1]]); + + await activateTabContextMenuItem( + duplicateTabs[0], + "#context_closeDuplicateTabs", + "#context_closeTabOptions" + ); + await assertMetricFoundFor("close_tab_other", 4); + + window.gBrowser.removeAllTabsBut(initialTab); + await resetTelemetry(); +}); + +add_task(async function test_tabInteractionsCloseTabOverflowMenu() { + let initialTab = window.gBrowser.tabs[0]; + await resetTelemetry(); + FirefoxViewTestUtilsInit(this, window); + + let tab = BrowserTestUtils.addTab(window.gBrowser, "https://example.com", { + skipAnimation: true, + }); + window.gBrowser.addTabGroup([tab]); + + info( + "Test that closing a tab from the tab overflow menu calls tab_interactions.close_tabmenu" + ); + let viewShown = BrowserTestUtils.waitForEvent( + window.document.getElementById("allTabsMenu-allTabsView"), + "ViewShown" + ); + window.document.getElementById("alltabs-button").click(); + await viewShown; + + await assertMetricEmpty("close_tabmenu"); + window.document + .querySelector(".all-tabs-item.grouped .all-tabs-close-button") + .click(); + await assertMetricFoundFor("close_tabmenu"); + + let panel = window.document + .getElementById("allTabsMenu-allTabsView") + .closest("panel"); + if (!panel) { + return; + } + let hidden = BrowserTestUtils.waitForPopupEvent(panel, "hidden"); + panel.hidePopup(); + await hidden; + + window.gBrowser.removeAllTabsBut(initialTab); + await resetTelemetry(); +}); + +add_task(async function test_tabInteractionsRemoveFromGroup() { + let initialTab = window.gBrowser.tabs[0]; + await resetTelemetry(); + + let tabs = Array.from({ length: 3 }, () => { + return BrowserTestUtils.addTab(window.gBrowser, "https://example.com", { + skipAnimation: true, + }); + }); + let group = window.gBrowser.addTabGroup(tabs); + + info( + "Test that moving a tab out of a tab group calls tab_interactions.remove_same_window" + ); + await assertMetricEmpty("remove_same_window"); + window.gBrowser.moveTabTo(group.tabs[0], { + tabIndex: 0, + isUserTriggered: true, + }); + await assertMetricFoundFor("remove_same_window"); + + info( + "Test that moving a tab out of a tab group and into a different (existing) window calls tab_interactions.remove_other_window" + ); + await assertMetricEmpty("remove_other_window"); + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + newWin.gBrowser.adoptTab(group.tabs[0]); + await assertMetricFoundFor("remove_other_window"); + await BrowserTestUtils.closeWindow(newWin); + + info( + "Test that moving a tab out of a tab group and into a different (new) window calls tab_interactions.remove_new_window" + ); + await assertMetricEmpty("remove_new_window"); + let newWindowPromise = BrowserTestUtils.waitForNewWindow(); + await EventUtils.synthesizePlainDragAndDrop({ + srcElement: group.tabs[0], + srcWindow: window, + destElement: null, + // don't move horizontally because that could cause a tab move + // animation, and there's code to prevent a tab detaching if + // the dragged tab is released while the animation is running. + stepX: 0, + stepY: 100, + }); + newWin = await newWindowPromise; + await assertMetricFoundFor("remove_new_window"); + await BrowserTestUtils.closeWindow(newWin); + + window.gBrowser.removeAllTabsBut(initialTab); + await resetTelemetry(); +}); diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_groups_telemetry.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_groups_telemetry.js index e243e0bc9831..aa6984c93f36 100644 --- a/browser/components/tabbrowser/test/browser/tabs/browser_tab_groups_telemetry.js +++ b/browser/components/tabbrowser/test/browser/tabs/browser_tab_groups_telemetry.js @@ -715,128 +715,6 @@ add_task(async function test_tabContextMenu_addTabsToGroup() { await resetTelemetry(); }); -add_task(async function test_tabInteractions() { - let assertMetricEmpty = async metricName => { - Assert.equal( - Glean.tabgroup.tabInteractions[metricName].testGetValue(), - null, - `tab_interactions.${metricName} starts empty` - ); - }; - - let assertOneMetricFoundFor = async metricName => { - await BrowserTestUtils.waitForCondition(() => { - return Glean.tabgroup.tabInteractions[metricName].testGetValue() !== null; - }, `Wait for tab_interactions.${metricName} to be recorded`); - Assert.equal( - Glean.tabgroup.tabInteractions[metricName].testGetValue(), - 1, - `tab_interactions.${metricName} was recorded` - ); - }; - - let initialTab = win.gBrowser.tabs[0]; - - await resetTelemetry(); - let group = await makeTabGroup(); - - info( - "Test that selecting a tab in a group records tab_interactions.activate" - ); - await assertMetricEmpty("activate"); - const tabSelectEvent = BrowserTestUtils.waitForEvent(win, "TabSelect"); - win.gBrowser.selectTabAtIndex(1); - await tabSelectEvent; - await assertOneMetricFoundFor("activate"); - - info( - "Test that moving an existing tab into a tab group records tab_interactions.add" - ); - let tab1 = BrowserTestUtils.addTab(win.gBrowser, "https://example.com"); - await assertMetricEmpty("add"); - win.gBrowser.moveTabToGroup(tab1, group, { isUserTriggered: true }); - await assertOneMetricFoundFor("add"); - - info( - "Test that adding a new tab to a tab group records tab_interactions.new" - ); - await assertMetricEmpty("new"); - BrowserTestUtils.addTab(win.gBrowser, "https://example.com", { - tabGroup: group, - }); - await assertOneMetricFoundFor("new"); - - info("Test that moving a tab within a group calls tab_interactions.reorder"); - await assertMetricEmpty("reorder"); - win.gBrowser.moveTabTo(group.tabs[0], { tabIndex: 3, isUserTriggered: true }); - await assertOneMetricFoundFor("reorder"); - - info( - "Test that duplicating a tab within a group calls tab_interactions.duplicate" - ); - await assertMetricEmpty("duplicate"); - win.gBrowser.duplicateTab(group.tabs[0], true, { tabIndex: 2 }); - await assertOneMetricFoundFor("duplicate"); - - info( - "Test that closing a tab using the tab's close button calls tab_interactions.close_tabstrip" - ); - await assertMetricEmpty("close_tabstrip"); - group.tabs.at(-1).querySelector(".tab-close-button").click(); - await assertOneMetricFoundFor("close_tabstrip"); - - info( - "Test that closing a tab from the tab overflow menu calls tab_interactions.close_tabmenu" - ); - await openTabsMenu(); - await assertMetricEmpty("close_tabmenu"); - win.document - .querySelector(".all-tabs-item.grouped .all-tabs-close-button") - .click(); - await assertOneMetricFoundFor("close_tabmenu"); - await closeTabsMenu(); - - info( - "Test that moving a tab out of a tab group calls tab_interactions.remove_same_window" - ); - await assertMetricEmpty("remove_same_window"); - win.gBrowser.moveTabTo(group.tabs[0], { tabIndex: 0, isUserTriggered: true }); - await assertOneMetricFoundFor("remove_same_window"); - - info( - "Test that moving a tab out of a tab group and into a different (existing) window calls tab_interactions.remove_other_window" - ); - await assertMetricEmpty("remove_other_window"); - let tab2 = BrowserTestUtils.addTab(win.gBrowser, "https://example.com"); - win.gBrowser.moveTabToGroup(tab2, group, { isUserTriggered: true }); - let newWin = await BrowserTestUtils.openNewBrowserWindow(); - newWin.gBrowser.adoptTab(tab2); - await assertOneMetricFoundFor("remove_other_window"); - await BrowserTestUtils.closeWindow(newWin); - - info( - "Test that moving a tab out of a tab group and into a different (new) window calls tab_interactions.remove_new_window" - ); - await assertMetricEmpty("remove_new_window"); - let newWindowPromise = BrowserTestUtils.waitForNewWindow(); - await EventUtils.synthesizePlainDragAndDrop({ - srcElement: group.tabs[0], - srcWindow: win, - destElement: null, - // don't move horizontally because that could cause a tab move - // animation, and there's code to prevent a tab detaching if - // the dragged tab is released while the animation is running. - stepX: 0, - stepY: 100, - }); - newWin = await newWindowPromise; - await assertOneMetricFoundFor("remove_new_window"); - await BrowserTestUtils.closeWindow(newWin); - - win.gBrowser.removeAllTabsBut(initialTab); - await resetTelemetry(); -}); - add_task(async function test_groupInteractions() { await resetTelemetry(); let group = await makeTabGroup(); diff --git a/browser/modules/BrowserUsageTelemetry.sys.mjs b/browser/modules/BrowserUsageTelemetry.sys.mjs index 50ee0630fe7c..e52d6db3663b 100644 --- a/browser/modules/BrowserUsageTelemetry.sys.mjs +++ b/browser/modules/BrowserUsageTelemetry.sys.mjs @@ -1269,13 +1269,16 @@ export let BrowserUsageTelemetry = { _onTabClosed(event) { const group = event.target?.group; + const isUserTriggered = event.detail?.isUserTriggered; const source = event.detail?.telemetrySource; - if (group) { + if (group && isUserTriggered) { if (source == lazy.TabMetrics.METRIC_SOURCE.TAB_STRIP) { Glean.tabgroup.tabInteractions.close_tabstrip.add(); } else if (source == lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU) { Glean.tabgroup.tabInteractions.close_tabmenu.add(); + } else { + Glean.tabgroup.tabInteractions.close_tab_other.add(); } } },