diff options
Diffstat (limited to 'browser/components/search/BrowserSearchTelemetry.sys.mjs')
-rw-r--r-- | browser/components/search/BrowserSearchTelemetry.sys.mjs | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/browser/components/search/BrowserSearchTelemetry.sys.mjs b/browser/components/search/BrowserSearchTelemetry.sys.mjs new file mode 100644 index 0000000000..15163e38c7 --- /dev/null +++ b/browser/components/search/BrowserSearchTelemetry.sys.mjs @@ -0,0 +1,293 @@ +/* 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 lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", + UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs", +}); + +// A map of known search origins. +// The keys of this map are used in the calling code to recordSearch, and in +// the SEARCH_COUNTS histogram. +// The values of this map are used in the names of scalars for the following +// scalar groups: +// browser.engagement.navigation.* +// browser.search.content.* +// browser.search.withads.* +// browser.search.adclicks.* +const KNOWN_SEARCH_SOURCES = new Map([ + ["abouthome", "about_home"], + ["contextmenu", "contextmenu"], + ["newtab", "about_newtab"], + ["searchbar", "searchbar"], + ["system", "system"], + ["urlbar", "urlbar"], + ["urlbar-handoff", "urlbar_handoff"], + ["urlbar-persisted", "urlbar_persisted"], + ["urlbar-searchmode", "urlbar_searchmode"], + ["webextension", "webextension"], +]); + +/** + * This class handles saving search telemetry related to the url bar, + * search bar and other areas as per the sources above. + */ +class BrowserSearchTelemetryHandler { + KNOWN_SEARCH_SOURCES = KNOWN_SEARCH_SOURCES; + + /** + * Determines if we should record a search for this browser instance. + * Private Browsing mode is normally skipped. + * + * @param {browser} browser + * The browser where the search was loaded. + * @returns {boolean} + * True if the search should be recorded, false otherwise. + */ + shouldRecordSearchCount(browser) { + return ( + !lazy.PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal) || + !Services.prefs.getBoolPref("browser.engagement.search_counts.pbm", false) + ); + } + + /** + * Records the method by which the user selected a result from the urlbar or + * searchbar. + * + * @param {Event} event + * The event that triggered the selection. + * @param {string} source + * Either "urlbar" or "searchbar" depending on the source. + * @param {number} index + * The index that the user chose in the popup, or -1 if there wasn't a + * selection. + * @param {string} userSelectionBehavior + * How the user cycled through results before picking the current match. + * Could be one of "tab", "arrow" or "none". + */ + recordSearchSuggestionSelectionMethod( + event, + source, + index, + userSelectionBehavior = "none" + ) { + // If the contents of the histogram are changed then + // `UrlbarTestUtils.SELECTED_RESULT_METHODS` should also be updated. + if (source == "searchbar" && userSelectionBehavior != "none") { + throw new Error("Did not expect a selection behavior for the searchbar."); + } + + let histogram = Services.telemetry.getHistogramById( + source == "urlbar" + ? "FX_URLBAR_SELECTED_RESULT_METHOD" + : "FX_SEARCHBAR_SELECTED_RESULT_METHOD" + ); + // command events are from the one-off context menu. Treat them as clicks. + // Note that we don't care about MouseEvent subclasses here, since + // those are not clicks. + let isClick = + event && + (ChromeUtils.getClassName(event) == "MouseEvent" || + event.type == "command"); + let category; + if (isClick) { + category = "click"; + } else if (index >= 0) { + switch (userSelectionBehavior) { + case "tab": + category = "tabEnterSelection"; + break; + case "arrow": + category = "arrowEnterSelection"; + break; + case "rightClick": + // Selected by right mouse button. + category = "rightClickEnter"; + break; + default: + category = "enterSelection"; + } + } else { + category = "enter"; + } + histogram.add(category); + } + + /** + * Records entry into the Urlbar's search mode. + * + * Telemetry records only which search mode is entered and how it was entered. + * It does not record anything pertaining to searches made within search mode. + * + * @param {object} searchMode + * A search mode object. See UrlbarInput.setSearchMode documentation for + * details. + */ + recordSearchMode(searchMode) { + // Search mode preview is not search mode. Recording it would just create + // noise. + if (searchMode.isPreview) { + return; + } + + let scalarKey = lazy.UrlbarSearchUtils.getSearchModeScalarKey(searchMode); + Services.telemetry.keyedScalarAdd( + "urlbar.searchmode." + searchMode.entry, + scalarKey, + 1 + ); + } + + /** + * The main entry point for recording search related Telemetry. This includes + * search counts and engagement measurements. + * + * Telemetry records only search counts per engine and action origin, but + * nothing pertaining to the search contents themselves. + * + * @param {browser} browser + * The browser where the search originated. + * @param {nsISearchEngine} engine + * The engine handling the search. + * @param {string} source + * Where the search originated from. See KNOWN_SEARCH_SOURCES for allowed + * values. + * @param {object} [details] Options object. + * @param {boolean} [details.isOneOff=false] + * true if this event was generated by a one-off search. + * @param {boolean} [details.isSuggestion=false] + * true if this event was generated by a suggested search. + * @param {boolean} [details.isFormHistory=false] + * true if this event was generated by a form history result. + * @param {string} [details.alias=null] + * The search engine alias used in the search, if any. + * @param {string} [details.newtabSessionId=undefined] + * The newtab session that prompted this search, if any. + * @throws if source is not in the known sources list. + */ + recordSearch(browser, engine, source, details = {}) { + try { + if (!this.shouldRecordSearchCount(browser)) { + return; + } + if (!KNOWN_SEARCH_SOURCES.has(source)) { + console.error("Unknown source for search: ", source); + return; + } + + const countIdPrefix = `${engine.telemetryId}.`; + const countIdSource = countIdPrefix + source; + let histogram = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS"); + + if ( + details.alias && + engine.isAppProvided && + engine.aliases.includes(details.alias) + ) { + // This is a keyword search using an AppProvided engine. + // Record the source as "alias", not "urlbar". + histogram.add(countIdPrefix + "alias"); + } else { + histogram.add(countIdSource); + } + + // Dispatch the search signal to other handlers. + switch (source) { + case "urlbar": + case "searchbar": + case "urlbar-searchmode": + case "urlbar-persisted": + case "urlbar-handoff": + this._handleSearchAndUrlbar(browser, engine, source, details); + break; + case "abouthome": + case "newtab": + this._recordSearch(browser, engine, details.url, source, "enter"); + break; + default: + this._recordSearch(browser, engine, details.url, source); + break; + } + if (["urlbar-handoff", "abouthome", "newtab"].includes(source)) { + Glean.newtabSearch.issued.record({ + newtab_visit_id: details.newtabSessionId, + search_access_point: KNOWN_SEARCH_SOURCES.get(source), + telemetry_id: engine.telemetryId, + }); + lazy.SearchSERPTelemetry.recordBrowserNewtabSession( + browser, + details.newtabSessionId + ); + } + } catch (ex) { + // Catch any errors here, so that search actions are not broken if + // telemetry is broken for some reason. + console.error(ex); + } + } + + /** + * This function handles the "urlbar", "urlbar-oneoff", "searchbar" and + * "searchbar-oneoff" sources. + * + * @param {browser} browser + * The browser where the search originated. + * @param {nsISearchEngine} engine + * The engine handling the search. + * @param {string} source + * Where the search originated from. + * @param {object} details + * See {@link BrowserSearchTelemetryHandler.recordSearch} + */ + _handleSearchAndUrlbar(browser, engine, source, details) { + const isOneOff = !!details.isOneOff; + let action = "enter"; + if (isOneOff) { + action = "oneoff"; + } else if (details.isFormHistory) { + action = "formhistory"; + } else if (details.isSuggestion) { + action = "suggestion"; + } else if (details.alias) { + action = "alias"; + } + + this._recordSearch(browser, engine, details.url, source, action); + } + + _recordSearch(browser, engine, url, source, action = null) { + if (url) { + lazy.PartnerLinkAttribution.makeSearchEngineRequest(engine, url).catch( + console.error + ); + } + + let scalarSource = KNOWN_SEARCH_SOURCES.get(source); + + lazy.SearchSERPTelemetry.recordBrowserSource(browser, scalarSource); + + let scalarKey = action ? "search_" + action : "search"; + Services.telemetry.keyedScalarAdd( + "browser.engagement.navigation." + scalarSource, + scalarKey, + 1 + ); + Services.telemetry.recordEvent( + "navigation", + "search", + scalarSource, + action, + { + engine: engine.telemetryId, + } + ); + } +} + +export var BrowserSearchTelemetry = new BrowserSearchTelemetryHandler(); |