gecko-dev/browser/components/urlbar/UrlbarProviderQuickSuggest.jsm

317 lines
10 KiB
JavaScript

/* 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["UrlbarProviderQuickSuggest"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
CONTEXTUAL_SERVICES_PING_TYPES:
"resource:///modules/PartnerLinkAttribution.jsm",
NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm",
PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.jsm",
Services: "resource://gre/modules/Services.jsm",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
UrlbarQuickSuggest: "resource:///modules/UrlbarQuickSuggest.jsm",
UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
UrlbarResult: "resource:///modules/UrlbarResult.jsm",
UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
});
// These prefs are relative to the `browser.urlbar` branch.
const SUGGEST_PREF = "suggest.quicksuggest";
const FEATURE_NAME = "Firefox Suggest";
const NONSPONSORED_ACTION_TEXT = FEATURE_NAME;
const HELP_TITLE = `Learn more about ${FEATURE_NAME}`;
const TELEMETRY_SCALAR_IMPRESSION =
"contextual.services.quicksuggest.impression";
const TELEMETRY_SCALAR_CLICK = "contextual.services.quicksuggest.click";
const TELEMETRY_SCALAR_HELP = "contextual.services.quicksuggest.help";
const TELEMETRY_EVENT_CATEGORY = "contextservices.quicksuggest";
/**
* A provider that returns a suggested url to the user based on what
* they have currently typed so they can navigate directly.
*/
class ProviderQuickSuggest extends UrlbarProvider {
constructor(...args) {
super(...args);
this._updateExperimentState();
UrlbarPrefs.addObserver(this);
NimbusFeatures.urlbar.onUpdate(this._updateExperimentState);
}
/**
* Returns the name of this provider.
* @returns {string} the name of this provider.
*/
get name() {
return "UrlbarProviderQuickSuggest";
}
/**
* The type of the provider.
*/
get type() {
return UrlbarUtils.PROVIDER_TYPE.NETWORK;
}
/**
* @returns {string} The name of the Firefox Suggest feature, suitable for
* display to the user. en-US only for now.
*/
get featureName() {
return FEATURE_NAME;
}
/**
* @returns {string} The help URL for the Quick Suggest feature.
*/
get helpUrl() {
return (
this._helpUrl ||
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"firefox-suggest"
);
}
/**
* Whether this provider should be invoked for the given context.
* If this method returns false, the providers manager won't start a query
* with this provider, to save on resources.
* @param {UrlbarQueryContext} queryContext The query context object
* @returns {boolean} Whether this provider should be invoked for the search.
*/
isActive(queryContext) {
this._addedResultInLastQuery = false;
// If the sources don't include search or the user used a restriction
// character other than search, don't allow any suggestions.
if (
!queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH) ||
(queryContext.restrictSource &&
queryContext.restrictSource != UrlbarUtils.RESULT_SOURCE.SEARCH)
) {
return false;
}
return (
queryContext.trimmedSearchString &&
!queryContext.searchMode &&
UrlbarPrefs.get("quickSuggestEnabled") &&
(UrlbarPrefs.get("quicksuggest.showedOnboardingDialog") ||
!UrlbarPrefs.get("quickSuggestShouldShowOnboardingDialog")) &&
UrlbarPrefs.get(SUGGEST_PREF) &&
UrlbarPrefs.get("suggest.searches") &&
UrlbarPrefs.get("browser.search.suggest.enabled") &&
(!queryContext.isPrivate ||
UrlbarPrefs.get("browser.search.suggest.enabled.private"))
);
}
/**
* Starts querying.
* @param {UrlbarQueryContext} queryContext The query context object
* @param {function} addCallback Callback invoked by the provider to add a new
* result. A UrlbarResult should be passed to it.
* @note Extended classes should return a Promise resolved when the provider
* is done searching AND returning results.
*/
async startQuery(queryContext, addCallback) {
let instance = this.queryInstance;
let suggestion = await UrlbarQuickSuggest.query(
queryContext.searchString.trimStart()
);
if (!suggestion || instance != this.queryInstance) {
return;
}
let payload = {
qsSuggestion: [suggestion.fullKeyword, UrlbarUtils.HIGHLIGHT.SUGGESTED],
title: suggestion.title,
url: suggestion.url,
icon: suggestion.icon,
sponsoredImpressionUrl: suggestion.impression_url,
sponsoredClickUrl: suggestion.click_url,
sponsoredBlockId: suggestion.block_id,
sponsoredAdvertiser: suggestion.advertiser,
isSponsored: true,
helpUrl: this.helpUrl,
helpTitle: HELP_TITLE,
};
if (!suggestion.isSponsored) {
// In addition to the view, we also use `sponsoredText` in the muxer to
// tell whether the result is sponsored or non-sponsored, so be careful
// about changing it. See also bug 1695302 re: these property names.
payload.sponsoredText = NONSPONSORED_ACTION_TEXT;
}
let result = new UrlbarResult(
UrlbarUtils.RESULT_TYPE.URL,
UrlbarUtils.RESULT_SOURCE.SEARCH,
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
);
addCallback(this, result);
this._addedResultInLastQuery = true;
}
/**
* Called when the user starts and ends an engagement with the urlbar. For
* details on parameters, see UrlbarProvider.onEngagement().
*
* @param {boolean} isPrivate
* True if the engagement is in a private context.
* @param {string} state
* The state of the engagement, one of: start, engagement, abandonment,
* discard
* @param {UrlbarQueryContext} queryContext
* The engagement's query context. This is *not* guaranteed to be defined
* when `state` is "start". It will always be defined for "engagement" and
* "abandonment".
* @param {object} details
* This is defined only when `state` is "engagement" or "abandonment", and
* it describes the search string and picked result.
*/
onEngagement(isPrivate, state, queryContext, details) {
if (!this._addedResultInLastQuery) {
return;
}
this._addedResultInLastQuery = false;
// Per spec, we update telemetry only when the user picks a result, i.e.,
// when `state` is "engagement".
if (state != "engagement") {
return;
}
// Get the index of the quick suggest result. Usually it will be last, so to
// avoid an O(n) lookup in the common case, check the last result first. It
// may not be last if `browser.urlbar.showSearchSuggestionsFirst` is false
// or its position is configured differently via Nimbus.
let resultIndex = queryContext.results.length - 1;
let result = queryContext.results[resultIndex];
if (result.providerName != this.name) {
resultIndex = queryContext.results.findIndex(
r => r.providerName == this.name
);
if (resultIndex < 0) {
Cu.reportError(`Could not find quick suggest result`);
return;
}
result = queryContext.results[resultIndex];
}
// Record telemetry. We want to record the 1-based index of the result, so
// add 1 to the 0-based resultIndex.
let telemetryResultIndex = resultIndex + 1;
// impression scalar
Services.telemetry.keyedScalarAdd(
TELEMETRY_SCALAR_IMPRESSION,
telemetryResultIndex,
1
);
if (details.selIndex == resultIndex) {
// click or help scalar
Services.telemetry.keyedScalarAdd(
details.selType == "help"
? TELEMETRY_SCALAR_HELP
: TELEMETRY_SCALAR_CLICK,
telemetryResultIndex,
1
);
}
// Send the custom impression and click pings
if (!isPrivate) {
let isQuickSuggestLinkClicked =
details.selIndex == resultIndex && details.selType !== "help";
let {
sponsoredAdvertiser,
sponsoredImpressionUrl,
sponsoredClickUrl,
sponsoredBlockId,
} = result.payload;
// impression
PartnerLinkAttribution.sendContextualServicesPing(
{
search_query: details.searchString,
matched_keywords: details.searchString,
advertiser: sponsoredAdvertiser,
block_id: sponsoredBlockId,
position: telemetryResultIndex,
reporting_url: sponsoredImpressionUrl,
is_clicked: isQuickSuggestLinkClicked,
},
CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION
);
// click
if (isQuickSuggestLinkClicked) {
PartnerLinkAttribution.sendContextualServicesPing(
{
advertiser: sponsoredAdvertiser,
block_id: sponsoredBlockId,
position: telemetryResultIndex,
reporting_url: sponsoredClickUrl,
},
CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION
);
}
}
}
/**
* Called when a urlbar pref changes. We use this to listen for changes to
* `browser.urlbar.suggest.quicksuggest` so we can record a telemetry event.
*
* @param {string} pref
* The name of the pref relative to `browser.urlbar`.
*/
onPrefChanged(pref) {
switch (pref) {
case SUGGEST_PREF:
Services.telemetry.recordEvent(
TELEMETRY_EVENT_CATEGORY,
"enable_toggled",
UrlbarPrefs.get(SUGGEST_PREF) ? "enabled" : "disabled"
);
break;
}
}
/**
* Updates state based on the `browser.urlbar.quicksuggest.enabled` pref.
* Enable/disable event telemetry and ensure QuickSuggest module is loaded
* when enabled.
*/
_updateExperimentState() {
Services.telemetry.setEventRecordingEnabled(
TELEMETRY_EVENT_CATEGORY,
UrlbarPrefs.get("quickSuggestEnabled")
);
// QuickSuggest is only loaded by the UrlBar on it's first query, however
// there is work it can preload when idle instead of starting it on user
// input. Referencing it here will trigger its import and init.
if (UrlbarPrefs.get("quickSuggestEnabled")) {
UrlbarQuickSuggest; // eslint-disable-line no-unused-expressions
}
}
// Whether we added a result during the most recent query.
_addedResultInLastQuery = false;
// This is intended for tests and allows them to set a different help URL.
_helpUrl = undefined;
}
var UrlbarProviderQuickSuggest = new ProviderQuickSuggest();