tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 309094844ef89311db58001760275fb46f96b662
parent ac30d4d7974faeacc0816c65d88d75e016979d91
Author: Stephen Thompson <sthompson@mozilla.com>
Date:   Tue,  6 Jan 2026 19:32:47 +0000

Bug 2003702 - telemetry event when tab note is added r=jswinarton,tabbrowser-reviewers,toolkit-telemetry-reviewers

The new `tab_notes.added` event requires that we record the UI surface entrypoint that the user used to create the tab note. Since both the tab context menu and tab hover preview panel are entrypoints to the tab note panel, I ended up threading that context from entrypoint -> tab note panel -> TabNotes.set call -> TabNote:Created event -> TabNoteController.handleEvent so that that `source` can be recorded.

I think it would be possible to pass around telemetry context to fewer places, but this is what I came up with.

I put the tests for the tab hover preview panel into the existing omnibus browser_tab_preview test module since it has a lot of test helpers that keep tab hover preview tests sane.

Differential Revision: https://phabricator.services.mozilla.com/D275904

Diffstat:
Mbrowser/base/content/main-popupset.js | 5++++-
Mbrowser/components/tabbrowser/content/tab-hover-preview.mjs | 4+++-
Mbrowser/components/tabbrowser/content/tabnote-menu.js | 14++++++++++++--
Mbrowser/components/tabbrowser/test/browser/tabs/browser_tab_preview.js | 14+++++++++++++-
Mbrowser/components/tabnotes/TabNotes.sys.mjs | 17+++++++++++++++--
Mbrowser/components/tabnotes/TabNotesController.sys.mjs | 6++++++
Abrowser/components/tabnotes/metrics.yaml | 34++++++++++++++++++++++++++++++++++
Mbrowser/components/tabnotes/test/browser/browser.toml | 2++
Mbrowser/components/tabnotes/test/browser/browser_tab_notes_menu.js | 45+++++++++------------------------------------
Abrowser/components/tabnotes/test/browser/browser_tab_notes_telemetry.js | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/tabnotes/test/browser/head.js | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/tabnotes/types/tabnotes.ts | 11+++++++++++
Mtoolkit/components/glean/metrics_index.py | 1+
13 files changed, 253 insertions(+), 43 deletions(-)

diff --git a/browser/base/content/main-popupset.js b/browser/base/content/main-popupset.js @@ -9,6 +9,7 @@ document.addEventListener( const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { TabMetrics: "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs", + TabNotes: "moz-src:///browser/components/tabnotes/TabNotes.sys.mjs", }); let mainPopupSet = document.getElementById("mainPopupSet"); // eslint-disable-next-line complexity @@ -86,7 +87,9 @@ document.addEventListener( break; case "context_addNote": case "context_editNote": - gBrowser.tabNoteMenu.openPanel(TabContextMenu.contextTab); + gBrowser.tabNoteMenu.openPanel(TabContextMenu.contextTab, { + telemetrySource: lazy.TabNotes.TELEMETRY_SOURCE.TAB_CONTEXT_MENU, + }); break; case "context_deleteNote": TabContextMenu.deleteTabNotes(); diff --git a/browser/components/tabbrowser/content/tab-hover-preview.mjs b/browser/components/tabbrowser/content/tab-hover-preview.mjs @@ -507,7 +507,9 @@ class TabPanel extends HoverPanel { * panel. */ #openTabNotePanel() { - this.win.gBrowser.tabNoteMenu.openPanel(this.#tab); + this.win.gBrowser.tabNoteMenu.openPanel(this.#tab, { + telemetrySource: lazy.TabNotes.TELEMETRY_SOURCE.TAB_HOVER_PREVIEW_PANEL, + }); this.deactivate(this.#tab, { force: true }); } diff --git a/browser/components/tabbrowser/content/tabnote-menu.js b/browser/components/tabbrowser/content/tabnote-menu.js @@ -88,6 +88,8 @@ #cancelButton; #saveButton; #overflowIndicator; + /** @type {TabNoteTelemetrySource|null} */ + #telemetrySource = null; connectedCallback() { if (this.#initialized) { @@ -145,6 +147,7 @@ on_popuphidden() { this.#currentTab = null; this.#noteField.value = ""; + this.#telemetrySource = null; } get createMode() { @@ -210,12 +213,17 @@ /** * @param {MozTabbrowserTab} tab + * The tab whose note this panel will control. + * @param {object} [options] + * @param {TabNoteTelemetrySource} [options.telemetrySource] + * The UI surface that requested to open this panel. */ - openPanel(tab) { + openPanel(tab, options = {}) { if (!TabNotes.isEligible(tab)) { return; } this.#currentTab = tab; + this.#telemetrySource = options.telemetrySource; this.#updatePanel(); @@ -246,7 +254,9 @@ let note = this.#noteField.value; if (TabNotes.isEligible(this.#currentTab) && note.length) { - TabNotes.set(this.#currentTab, note); + TabNotes.set(this.#currentTab, note, { + telemetrySource: this.#telemetrySource, + }); } this.#panel.hidePopup(); diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_preview.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_preview.js @@ -605,10 +605,22 @@ add_task(async function tabNotesTests() { EventUtils.sendString(noteText, window); await input; let menuHidden = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "hidden"); - const tabNoteCreated = BrowserTestUtils.waitForEvent(tab, "TabNote:Created"); + let tabNoteCreated = BrowserTestUtils.waitForEvent(tab, "TabNote:Created"); tabNotePanel.querySelector("#tab-note-editor-button-save").click(); await Promise.all([menuHidden, tabNoteCreated]); + await BrowserTestUtils.waitForCondition( + () => Glean.tabNotes.added.testGetValue()?.length, + "wait for event to be recorded" + ); + + const [addedEvent] = Glean.tabNotes.added.testGetValue(); + Assert.deepEqual( + addedEvent.extra, + { source: "hover_menu" }, + "added event extra data should say the tab note was added from the tab hover preview menu" + ); + await closeTabPreviews(); info("validate the presentation of an eligible tab with a tab note"); diff --git a/browser/components/tabnotes/TabNotes.sys.mjs b/browser/components/tabnotes/TabNotes.sys.mjs @@ -72,6 +72,10 @@ RETURNING */ export class TabNotesStorage { DATABASE_FILE_NAME = Object.freeze("tabnotes.sqlite"); + TELEMETRY_SOURCE = Object.freeze({ + TAB_CONTEXT_MENU: "context_menu", + TAB_HOVER_PREVIEW_PANEL: "hover_menu", + }); /** @type {OpenedConnection|undefined} */ #connection; @@ -167,12 +171,15 @@ export class TabNotesStorage { * The tab that the note should be associated with * @param {string} note * The note itself + * @param {object} [options] + * @param {TabNoteTelemetrySource} [options.telemetrySource] + * The UI surface that requested to set a note. * @returns {Promise<TabNoteRecord>} * The actual note that was set after sanitization * @throws {RangeError} * if `tab` is not eligible for a tab note or `note` is empty */ - async set(tab, note) { + async set(tab, note, options = {}) { if (!this.isEligible(tab)) { throw new RangeError("Tab notes must be associated to an eligible tab"); } @@ -200,6 +207,7 @@ export class TabNotesStorage { bubbles: true, detail: { note: insertedRecord, + telemetrySource: options.telemetrySource, }, }) ); @@ -217,6 +225,7 @@ export class TabNotesStorage { bubbles: true, detail: { note: updatedRecord, + telemetrySource: options.telemetrySource, }, }) ); @@ -229,10 +238,13 @@ export class TabNotesStorage { * * @param {MozTabbrowserTab} tab * The tab that has a note + * @param {object} [options] + * @param {TabNoteTelemetrySource} [options.telemetrySource] + * The UI surface that requested to delete a note. * @returns {Promise<boolean>} * True if there was a note and it was deleted; false otherwise */ - async delete(tab) { + async delete(tab, options = {}) { /** @type {mozIStorageRow[]} */ const deleteResult = await this.#connection.executeCached(DELETE_NOTE, { url: tab.canonicalUrl, @@ -245,6 +257,7 @@ export class TabNotesStorage { bubbles: true, detail: { note: deletedRecord, + telemetrySource: options.telemetrySource, }, }) ); diff --git a/browser/components/tabnotes/TabNotesController.sys.mjs b/browser/components/tabnotes/TabNotesController.sys.mjs @@ -126,6 +126,12 @@ class TabNotesControllerClass { break; case "TabNote:Created": { + const { telemetrySource } = event.detail; + if (telemetrySource) { + Glean.tabNotes.added.record({ + source: telemetrySource, + }); + } // A new tab note was created for a specific canonical URL. Ensure that // all tabs with the same canonical URL also indicate that there is a // tab note. diff --git a/browser/components/tabnotes/metrics.yaml b/browser/components/tabnotes/metrics.yaml @@ -0,0 +1,34 @@ +# 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/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Firefox :: Tabbed Browser' + +tab_notes: + added: + type: event + description: > + Recorded when a user creates a new note for a tab. + notification_emails: + - sthompson@mozilla.com + bugs: + - https://bugzil.la/2003702 + data_reviews: + - https://bugzil.la/2003702 + data_sensitivity: + - interaction + extra_keys: + source: + description: > + Identifies the user interface entry point that resulted in this tab + note being added. Expected values: + - `context_menu` # Tab context menu's "Add Note" menu item + - `hover_menu` # Tab hover preview panel's "Add Note" button + type: string + expires: never diff --git a/browser/components/tabnotes/test/browser/browser.toml b/browser/components/tabnotes/test/browser/browser.toml @@ -9,3 +9,5 @@ support-files = [ ["browser_tab_notes_adopt.js"] ["browser_tab_notes_menu.js"] + +["browser_tab_notes_telemetry.js"] diff --git a/browser/components/tabnotes/test/browser/browser_tab_notes_menu.js b/browser/components/tabnotes/test/browser/browser_tab_notes_menu.js @@ -13,28 +13,10 @@ registerCleanupFunction(async () => { */ /** - * @param {Node} triggerNode - * @param {string} contextMenuId - * @returns {Promise<XULMenuElement|XULPopupElement>} + * @param {MozTabbrowserTab} selectedTab + * @param {string} menuItemSelector + * @param {string} [submenuItemSelector] */ -async function getContextMenu(triggerNode, contextMenuId) { - let win = triggerNode.ownerGlobal; - triggerNode.scrollIntoView({ behavior: "instant" }); - const contextMenu = win.document.getElementById(contextMenuId); - const contextMenuShown = BrowserTestUtils.waitForPopupEvent( - contextMenu, - "shown" - ); - - EventUtils.synthesizeMouseAtCenter( - triggerNode, - { type: "contextmenu", button: 2 }, - win - ); - await contextMenuShown; - return contextMenu; -} - let activateTabContextMenuItem = async ( selectedTab, menuItemSelector, @@ -88,15 +70,9 @@ let activateTabContextMenuItem = async ( }; /** - * @param {XULMenuElement|XULPopupElement} contextMenu - * @returns {Promise<void>} + * @param {MozTabbrowserTab} tab + * @returns {Promise<XULPanelElement>} */ -async function closeContextMenu(contextMenu) { - let menuHidden = BrowserTestUtils.waitForPopupEvent(contextMenu, "hidden"); - contextMenu.hidePopup(); - await menuHidden; -} - async function openTabNoteMenuByAddNote(tab) { let tabNotePanel = document.getElementById("tabNotePanel"); let panelShown = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "shown"); @@ -105,6 +81,10 @@ async function openTabNoteMenuByAddNote(tab) { return tabNotePanel; } +/** + * @param {MozTabbrowserTab} tab + * @returns {Promise<XULPanelElement>} + */ async function openTabNoteMenuByEditNote(tab) { let tabNotePanel = document.getElementById("tabNotePanel"); let panelShown = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "shown"); @@ -113,13 +93,6 @@ async function openTabNoteMenuByEditNote(tab) { return tabNotePanel; } -async function closeTabNoteMenu() { - let tabNotePanel = document.getElementById("tabNotePanel"); - let menuHidden = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "hidden"); - tabNotePanel.hidePopup(); - return menuHidden; -} - add_task(async function test_tabContextMenu_prefDisabled() { // open context menu with tab notes disabled await SpecialPowers.pushPrefEnv({ diff --git a/browser/components/tabnotes/test/browser/browser_tab_notes_telemetry.js b/browser/components/tabnotes/test/browser/browser_tab_notes_telemetry.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(() => { + Services.fog.initializeFOG(); +}); + +registerCleanupFunction(async () => { + await TabNotes.reset(); +}); + +afterEach(async () => { + await resetTelemetry(); +}); + +async function resetTelemetry() { + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); +} + +/** + * Tab note telemetry tests + */ + +add_task(async function test_tabNoteAddedTabContextMenu() { + let tab = BrowserTestUtils.addTab(gBrowser, "https://www.example.com"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + let tabContextMenu = await getContextMenu(tab, "tabContextMenu"); + let addNoteMenuItem = document.getElementById("context_addNote"); + let tabNotePanel = await openPanel( + document.getElementById("tabNotePanel"), + () => tabContextMenu.activateItem(addNoteMenuItem) + ); + + Assert.equal( + document.activeElement, + tabNotePanel.querySelector("textarea"), + "tab note textarea should be focused" + ); + const input = BrowserTestUtils.waitForEvent(document.activeElement, "input"); + EventUtils.sendString("Lorem ipsum dolor", window); + await input; + let menuHidden = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "hidden"); + let tabNoteCreated = BrowserTestUtils.waitForEvent(tab, "TabNote:Created"); + tabNotePanel.querySelector("#tab-note-editor-button-save").click(); + await Promise.all([menuHidden, tabNoteCreated]); + + await BrowserTestUtils.waitForCondition( + () => Glean.tabNotes.added.testGetValue()?.length, + "wait for event to be recorded" + ); + + const [addedEvent] = Glean.tabNotes.added.testGetValue(); + Assert.deepEqual( + addedEvent.extra, + { source: "context_menu" }, + "added event extra data should say the tab note was added from the tab context menu" + ); + + await closeTabNoteMenu(); + await TabNotes.delete(tab); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/tabnotes/test/browser/head.js b/browser/components/tabnotes/test/browser/head.js @@ -5,3 +5,80 @@ const { TabNotes } = ChromeUtils.importESModule( "moz-src:///browser/components/tabnotes/TabNotes.sys.mjs" ); + +/** + * @param {Node} triggerNode + * @param {string} contextMenuId + * @returns {Promise<XULMenuElement|XULPopupElement>} + */ +async function getContextMenu(triggerNode, contextMenuId) { + let win = triggerNode.ownerGlobal; + triggerNode.scrollIntoView({ behavior: "instant" }); + const contextMenu = win.document.getElementById(contextMenuId); + const contextMenuShown = BrowserTestUtils.waitForPopupEvent( + contextMenu, + "shown" + ); + + EventUtils.synthesizeMouseAtCenter( + triggerNode, + { type: "contextmenu", button: 2 }, + win + ); + await contextMenuShown; + return contextMenu; +} + +/** + * @param {XULMenuElement|XULPopupElement} contextMenu + * @returns {Promise<void>} + */ +async function closeContextMenu(contextMenu) { + let menuHidden = BrowserTestUtils.waitForPopupEvent(contextMenu, "hidden"); + contextMenu.hidePopup(); + await menuHidden; +} + +/** + * @param {Element} panel + * @param {() => Promise<void>} opener + * @returns {Promise<Element>} + * The panel element that was opened. + */ +async function openPanel(panel, opener) { + let panelShown = BrowserTestUtils.waitForPopupEvent(panel, "shown"); + Assert.equal(panel.state, "closed", "Panel starts hidden"); + await Promise.all([opener(), panelShown]); + Assert.equal(panel.state, "open", "Panel is now open"); + return panel; +} + +/** + * Open the tab note creation panel by choosing "Add note" from the + * tab context menu. + * + * @param {MozTabbrowserTab} tab + * @returns {Promise<Element>} + * `<tabnote-menu>` element. + */ +async function openTabNoteMenu(tab) { + let tabContextMenu = await getContextMenu(tab, "tabContextMenu"); + let tabNotePanel = document.getElementById("tabNotePanel"); + let panelShown = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "shown"); + tabContextMenu.activateItem(document.getElementById("context_addNote")); + await panelShown; + return tabNotePanel; +} + +/** + * Closes the tab note panel. + * + * @returns {Promise<Event>} + * `popuphidden` event from closing this menu. + */ +function closeTabNoteMenu() { + let tabNotePanel = document.getElementById("tabNotePanel"); + let menuHidden = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "hidden"); + tabNotePanel.hidePopup(); + return menuHidden; +} diff --git a/browser/components/tabnotes/types/tabnotes.ts b/browser/components/tabnotes/types/tabnotes.ts @@ -30,6 +30,7 @@ interface TabNoteCreatedEvent extends CustomEvent { target: MozTabbrowserTab; detail: { note: TabNoteRecord; + telemetrySource?: TabNoteTelemetrySource; }; } @@ -38,6 +39,7 @@ interface TabNoteEditedEvent extends CustomEvent { target: MozTabbrowserTab; detail: { note: TabNoteRecord; + telemetrySource?: TabNoteTelemetrySource; }; } @@ -46,6 +48,7 @@ interface TabNoteRemovedEvent extends CustomEvent { target: MozTabbrowserTab; detail: { note: TabNoteRecord; + telemetrySource?: TabNoteTelemetrySource; }; } @@ -55,3 +58,11 @@ type TabbrowserWebProgressListener< > = F extends (...args: any) => any ? (aBrowser: MozBrowser, ...rest: Parameters<F>) => ReturnType<F> : never; + +/** + * Constant values used to record the UI surface when a user interacted + * with tab notes. + */ +type TabNoteTelemetrySource = + | "context_menu" // tab context menu + | "hover_menu"; // tab hover preview panel diff --git a/toolkit/components/glean/metrics_index.py b/toolkit/components/glean/metrics_index.py @@ -141,6 +141,7 @@ firefox_desktop_metrics = [ "browser/components/sessionstore/metrics.yaml", "browser/components/sidebar/metrics.yaml", "browser/components/tabbrowser/metrics.yaml", + "browser/components/tabnotes/metrics.yaml", "browser/components/taskbartabs/metrics.yaml", "browser/components/textrecognition/metrics.yaml", "browser/components/urlbar/metrics.yaml",