BrowserSearchTelemetry.sys.mjs (13164B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 ContextId: "moz-src:///browser/modules/ContextId.sys.mjs", 9 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 10 SearchSERPTelemetry: 11 "moz-src:///browser/components/search/SearchSERPTelemetry.sys.mjs", 12 SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs", 13 UrlbarSearchUtils: 14 "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs", 15 }); 16 17 /** 18 * @import {SearchEngine} from "moz-src:///toolkit/components/search/SearchEngine.sys.mjs" 19 */ 20 21 /** 22 * This class handles saving search telemetry related to the url bar, 23 * search bar and other areas as per the sources above. 24 */ 25 class BrowserSearchTelemetryHandler { 26 /** 27 * A map of known search origins. The values of this map should be used for all 28 * current telemetry, except for sap.deprecatedCounts. 29 * 30 * The keys of this map are used in the calling code to recordSearch, and in 31 * the sap.deprecatedCounts labelled counter (and the mirrored SEARCH_COUNTS 32 * histogram). 33 * 34 * When legacy telemetry stops being reported, we should remove this map, and 35 * update the callers to use the values directly. We might still want to keep 36 * a list of valid sources, to help ensure that telemetry reporting is updated 37 * correctly if new sources are added. 38 */ 39 KNOWN_SEARCH_SOURCES = new Map([ 40 ["abouthome", "about_home"], 41 ["contextmenu", "contextmenu"], 42 ["contextmenu_visual", "contextmenu_visual"], 43 ["newtab", "about_newtab"], 44 ["searchbar", "searchbar"], 45 ["system", "system"], 46 ["urlbar", "urlbar"], 47 ["urlbar-handoff", "urlbar_handoff"], 48 ["urlbar-persisted", "urlbar_persisted"], 49 ["urlbar-searchmode", "urlbar_searchmode"], 50 ["webextension", "webextension"], 51 ["aiwindow_assistant", "aiwindow_assistant"], 52 ]); 53 54 /** 55 * Determines if we should record a search for this browser instance. 56 * Private Browsing mode is normally skipped. 57 * 58 * @param {MozBrowser} browser 59 * The browser where the search was loaded. 60 * @returns {boolean} 61 * True if the search should be recorded, false otherwise. 62 */ 63 shouldRecordSearchCount(browser) { 64 return ( 65 !lazy.PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal) || 66 !Services.prefs.getBoolPref("browser.engagement.search_counts.pbm", false) 67 ); 68 } 69 70 /** 71 * Records the method by which the user selected a result from the searchbar. 72 * 73 * @param {Event} event 74 * The event that triggered the selection. 75 * @param {number} index 76 * The index that the user chose in the popup, or -1 if there wasn't a 77 * selection. 78 */ 79 recordSearchSuggestionSelectionMethod(event, index) { 80 // command events are from the one-off context menu. Treat them as clicks. 81 // Note that we only care about MouseEvent subclasses here when the 82 // event type is "click", or else the subclasses are associated with 83 // non-click interactions. 84 let isClick = 85 event && 86 (ChromeUtils.getClassName(event) == "MouseEvent" || 87 event.type == "click" || 88 event.type == "command"); 89 let category; 90 if (isClick) { 91 category = "click"; 92 } else if (index >= 0) { 93 category = "enterSelection"; 94 } else { 95 category = "enter"; 96 } 97 98 Glean.searchbar.selectedResultMethod[category].add(1); 99 } 100 101 /** 102 * Records entry into the Urlbar's search mode. 103 * 104 * Telemetry records only which search mode is entered and how it was entered. 105 * It does not record anything pertaining to searches made within search mode. 106 * 107 * @param {object} searchMode 108 * A search mode object. See UrlbarInput.setSearchMode documentation for 109 * details. 110 */ 111 recordSearchMode(searchMode) { 112 // Search mode preview is not search mode. Recording it would just create 113 // noise. 114 if (searchMode.isPreview) { 115 return; 116 } 117 118 let label = lazy.UrlbarSearchUtils.getSearchModeScalarKey(searchMode); 119 let name = searchMode.entry.replace(/_([a-z])/g, (m, p) => p.toUpperCase()); 120 Glean.urlbarSearchmode[name]?.[label].add(1); 121 } 122 123 /** 124 * The main entry point for recording search related Telemetry. This includes 125 * search counts and engagement measurements. 126 * 127 * Telemetry records only search counts per engine and action origin, but 128 * nothing pertaining to the search contents themselves. 129 * 130 * @param {MozBrowser} browser 131 * The browser where the search originated. 132 * @param {nsISearchEngine} engine 133 * The engine handling the search. 134 * @param {string} source 135 * Where the search originated from. See KNOWN_SEARCH_SOURCES for allowed 136 * values. 137 * @param {object} [details] Options object. 138 * @param {boolean} [details.isOneOff=false] 139 * true if this event was generated by a one-off search. 140 * @param {boolean} [details.isSuggestion=false] 141 * true if this event was generated by a suggested search. 142 * @param {boolean} [details.isFormHistory=false] 143 * true if this event was generated by a form history result. 144 * @param {string} [details.alias=null] 145 * The search engine alias used in the search, if any. 146 * @param {string} [details.newtabSessionId=undefined] 147 * The newtab session that prompted this search, if any. 148 * @param {string} [details.searchUrlType=undefined] 149 * A `SearchUtils.URL_TYPE` value that indicates the type of search. 150 * Defaults to `SearchUtils.URL_TYPE.SEARCH`, a plain old search. 151 * @throws if source is not in the known sources list. 152 */ 153 recordSearch(browser, engine, source, details = {}) { 154 if (engine.clickUrl) { 155 this.#reportSearchInGlean(engine.clickUrl); 156 } 157 158 try { 159 if (!this.shouldRecordSearchCount(browser)) { 160 return; 161 } 162 if (!this.KNOWN_SEARCH_SOURCES.has(source)) { 163 console.error("Unknown source for search: ", source); 164 return; 165 } 166 167 if (source.startsWith("urlbar")) { 168 Services.prefs.setIntPref( 169 "browser.urlbar.lastUrlbarSearchSeconds", 170 Math.round(Date.now() / 1000) 171 ); 172 } 173 174 if (source != "contextmenu_visual") { 175 const countIdPrefix = `${engine.telemetryId}.`; 176 const countIdSource = countIdPrefix + source; 177 178 // NOTE: When removing the sap.deprecatedCounts telemetry, see the note 179 // above KNOWN_SEARCH_SOURCES. 180 if ( 181 details.alias && 182 engine.isConfigEngine && 183 engine.aliases.includes(details.alias) 184 ) { 185 // This is a keyword search using a config engine. 186 // Record the source as "alias", not "urlbar". 187 Glean.sap.deprecatedCounts[countIdPrefix + "alias"].add(); 188 } else { 189 Glean.sap.deprecatedCounts[countIdSource].add(); 190 } 191 } 192 193 // When an engine is overridden by a third party, then we report the 194 // override and skip reporting the partner code, since we don't have 195 // a requirement to report the partner code in that case. 196 let isOverridden = !!engine.overriddenById; 197 198 let searchUrlType = 199 details.searchUrlType ?? lazy.SearchUtils.URL_TYPE.SEARCH; 200 201 let unwrappedEngine = /** @type {SearchEngine} */ ( 202 engine.wrappedJSObject 203 ); 204 205 // Strict equality is used because we want to only match against the 206 // empty string and not other values. We would have `engine.partnerCode` 207 // return `undefined`, but the XPCOM interfaces force us to return an 208 // empty string. 209 let reportPartnerCode = 210 !isOverridden && 211 engine.partnerCode !== "" && 212 !unwrappedEngine.getURLOfType(searchUrlType) 213 ?.excludePartnerCodeFromTelemetry; 214 215 Glean.sap.counts.record({ 216 source: this.KNOWN_SEARCH_SOURCES.get(source), 217 provider_id: engine.isConfigEngine ? engine.id : "other", 218 provider_name: engine.name, 219 // If no code is reported, we must returned undefined, Glean will then 220 // not report the field. 221 partner_code: reportPartnerCode ? engine.partnerCode : undefined, 222 overridden_by_third_party: isOverridden.toString(), 223 }); 224 225 // Dispatch the search signal to other handlers. 226 switch (source) { 227 case "urlbar": 228 case "searchbar": 229 case "urlbar-searchmode": 230 case "urlbar-persisted": 231 case "urlbar-handoff": 232 this._handleSearchAndUrlbar(browser, engine, source, details); 233 break; 234 case "abouthome": 235 case "newtab": 236 this._recordSearch(browser, engine, source, "enter"); 237 break; 238 default: 239 this._recordSearch(browser, engine, source); 240 break; 241 } 242 if (["urlbar-handoff", "abouthome", "newtab"].includes(source)) { 243 Glean.newtabSearch.issued.record({ 244 newtab_visit_id: details.newtabSessionId, 245 search_access_point: this.KNOWN_SEARCH_SOURCES.get(source), 246 telemetry_id: engine.telemetryId, 247 }); 248 lazy.SearchSERPTelemetry.recordBrowserNewtabSession( 249 browser, 250 details.newtabSessionId 251 ); 252 } 253 } catch (ex) { 254 // Catch any errors here, so that search actions are not broken if 255 // telemetry is broken for some reason. 256 console.error(ex); 257 } 258 } 259 260 /** 261 * Records visits to a search engine's search form. 262 * 263 * @param {nsISearchEngine} engine 264 * The engine whose search form is being visited. 265 * @param {string} source 266 * Where the search form was opened from. 267 * This can be "urlbar" or "searchbar". 268 */ 269 recordSearchForm(engine, source) { 270 Glean.sap.searchFormCounts.record({ 271 source, 272 provider_id: engine.isConfigEngine ? engine.id : "other", 273 }); 274 } 275 276 /** 277 * Records an impression of a search access point. 278 * 279 * @param {MozBrowser} browser 280 * The browser associated with the SAP. 281 * @param {nsISearchEngine|null} engine 282 * The engine handling the search, or null if this doesn't apply to the SAP 283 * (e.g., the engine isn't known or selected yet). The counter's label will 284 * be `engine.id` if `engine` is a non-null, app-provided engine. Otherwise 285 * the label will be "none". 286 * @param {string} source 287 * The name of the SAP. See `KNOWN_SEARCH_SOURCES` for allowed values. 288 */ 289 recordSapImpression(browser, engine, source) { 290 if (!this.shouldRecordSearchCount(browser)) { 291 return; 292 } 293 if (!this.KNOWN_SEARCH_SOURCES.has(source)) { 294 console.error("Unknown source for SAP impression:", source); 295 return; 296 } 297 298 let scalarSource = this.KNOWN_SEARCH_SOURCES.get(source); 299 let name = scalarSource.replace(/_([a-z])/g, (m, p) => p.toUpperCase()); 300 let label = engine?.isConfigEngine ? engine.id : "none"; 301 Glean.sapImpressionCounts[name][label].add(1); 302 } 303 304 /** 305 * This function handles the "urlbar", "urlbar-oneoff", "searchbar" and 306 * "searchbar-oneoff" sources. 307 * 308 * @param {MozBrowser} browser 309 * The browser where the search originated. 310 * @param {nsISearchEngine} engine 311 * The engine handling the search. 312 * @param {string} source 313 * Where the search originated from. 314 * @param {object} details 315 * See {@link BrowserSearchTelemetryHandler.recordSearch} 316 */ 317 _handleSearchAndUrlbar(browser, engine, source, details) { 318 const isOneOff = !!details.isOneOff; 319 let action = "enter"; 320 if (isOneOff) { 321 action = "oneoff"; 322 } else if (details.isFormHistory) { 323 action = "formhistory"; 324 } else if (details.isSuggestion) { 325 action = "suggestion"; 326 } else if (details.alias) { 327 action = "alias"; 328 } 329 330 this._recordSearch(browser, engine, source, action); 331 } 332 333 _recordSearch(browser, engine, source, action = null) { 334 let scalarSource = this.KNOWN_SEARCH_SOURCES.get(source); 335 lazy.SearchSERPTelemetry.recordBrowserSource(browser, scalarSource); 336 337 let label = action ? "search_" + action : "search"; 338 let name = scalarSource.replace(/_([a-z])/g, (m, p) => p.toUpperCase()); 339 Glean.browserEngagementNavigation[name][label].add(1); 340 } 341 342 /** 343 * Records the search in Glean for contextual services. 344 * 345 * @param {string} reportingUrl 346 * The url to be sent to contextual services. 347 */ 348 async #reportSearchInGlean(reportingUrl) { 349 let defaultValuesByGleanKey = { 350 contextId: await lazy.ContextId.request(), 351 }; 352 353 let sendGleanPing = valuesByGleanKey => { 354 valuesByGleanKey = { ...defaultValuesByGleanKey, ...valuesByGleanKey }; 355 for (let [gleanKey, value] of Object.entries(valuesByGleanKey)) { 356 let glean = Glean.searchWith[gleanKey]; 357 if (value !== undefined && value !== "") { 358 glean.set(value); 359 } 360 } 361 GleanPings.searchWith.submit(); 362 }; 363 364 sendGleanPing({ 365 reportingUrl, 366 }); 367 } 368 } 369 370 export var BrowserSearchTelemetry = new BrowserSearchTelemetryHandler();