Source code

Revision control

Copy as Markdown

Other Tools

/* 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, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs",
UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
});
// `contextId` is a unique identifier used by Contextual Services
const CONTEXT_ID_PREF = "browser.contextual-services.contextId";
ChromeUtils.defineLazyGetter(lazy, "contextId", () => {
let _contextId = Services.prefs.getStringPref(CONTEXT_ID_PREF, null);
if (!_contextId) {
_contextId = Services.uuid.generateUUID().toString();
Services.prefs.setStringPref(CONTEXT_ID_PREF, _contextId);
}
return _contextId;
});
// 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 only care about MouseEvent subclasses here when the
// event type is "click", or else the subclasses are associated with
// non-click interactions.
let isClick =
event &&
(ChromeUtils.getClassName(event) == "MouseEvent" ||
event.type == "click" ||
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 label = lazy.UrlbarSearchUtils.getSearchModeScalarKey(searchMode);
let name = searchMode.entry.replace(/_([a-z])/g, (m, p) => p.toUpperCase());
Glean.urlbarSearchmode[name]?.[label].add(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 = {}) {
if (engine.clickUrl) {
this.#reportSearchInGlean(engine.clickUrl);
}
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, source, "enter");
break;
default:
this._recordSearch(browser, engine, 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, source, action);
}
_recordSearch(browser, engine, source, action = null) {
let scalarSource = KNOWN_SEARCH_SOURCES.get(source);
lazy.SearchSERPTelemetry.recordBrowserSource(browser, scalarSource);
let label = action ? "search_" + action : "search";
let name = scalarSource.replace(/_([a-z])/g, (m, p) => p.toUpperCase());
Glean.browserEngagementNavigation[name][label].add(1);
}
/**
* Records the search in Glean for contextual services.
*
* @param {string} reportingUrl
* The url to be sent to contextual services.
*/
#reportSearchInGlean(reportingUrl) {
let defaultValuesByGleanKey = {
contextId: lazy.contextId,
};
let sendGleanPing = valuesByGleanKey => {
valuesByGleanKey = { ...defaultValuesByGleanKey, ...valuesByGleanKey };
for (let [gleanKey, value] of Object.entries(valuesByGleanKey)) {
let glean = Glean.searchWith[gleanKey];
if (value !== undefined && value !== "") {
glean.set(value);
}
}
GleanPings.searchWith.submit();
};
sendGleanPing({
reportingUrl,
});
}
}
export var BrowserSearchTelemetry = new BrowserSearchTelemetryHandler();