Bug 1932502 - Remove the Suggest JS backend: Part 2: Remove the backend. r=daisuke

This is part 2 of 4, the main part. It removes the backend and the code in
`BaseFeature` and feature subclasses that hook into it. As a consequence it also
makes the other following changes:

* Remove `QuickSuggest.backend` (eventually I'd like to add a Merino backend and
  then add a new `QuickSuggest.backends` list that has the Rust, Merino, and ML
  backends)
* Add `QuickSuggest.config`
* Replace uses of `QuickSuggest.backend.config` and `QuickSuggest.jsBackend.config`
  with `QuickSuggest.config`
* Remove the `quickSuggestRustEnabled` Nimbus variable
* Update most remaining tests so that they do not assume the JS backend exists

I left the `browser.urlbar.quicksuggest.rustEnabled` pref. I would like to
preserve the ability to toggle off Rust suggestions while keeping other parts of
Suggest enabled even if we don't need it now. That seems like a wise thing to do
and is similar to how both Merino and the ML backends can be toggled separately.

Depends on D231465

Differential Revision: https://phabricator.services.mozilla.com/D231466
This commit is contained in:
Drew Willcoxon
2024-12-11 20:50:45 +00:00
parent b76d20ad1a
commit 2033165d91
29 changed files with 91 additions and 1060 deletions
+7 -17
View File
@@ -28,8 +28,6 @@ const FEATURES = {
MDNSuggestions: "resource:///modules/urlbar/private/MDNSuggestions.sys.mjs",
PocketSuggestions:
"resource:///modules/urlbar/private/PocketSuggestions.sys.mjs",
SuggestBackendJs:
"resource:///modules/urlbar/private/SuggestBackendJs.sys.mjs",
SuggestBackendMl:
"resource:///modules/urlbar/private/SuggestBackendMl.sys.mjs",
SuggestBackendRust:
@@ -98,30 +96,22 @@ class _QuickSuggest {
return ONBOARDING_URI;
}
/**
* @returns {SuggestBackendJs|SuggestBackendRust}
* The currently active backend.
*/
get backend() {
return lazy.UrlbarPrefs.get("quickSuggestRustEnabled")
? this.rustBackend
: this.jsBackend;
}
/**
* @returns {SuggestBackendRust}
* The Rust backend. Not used when the JS backend is enabled.
* The Rust backend, which manages the Rust component.
*/
get rustBackend() {
return this.#features.SuggestBackendRust;
}
/**
* @returns {SuggestBackendJs}
* The JS backend. Not used when the Rust backend is enabled.
* @returns {object}
* Global Suggest configuration stored in remote settings and ingested by
* the Rust component. See remote settings or the Rust component for the
* latest schema.
*/
get jsBackend() {
return this.#features.SuggestBackendJs;
get config() {
return this.rustBackend?.config || {};
}
/**
@@ -128,9 +128,8 @@ class ProviderQuickSuggest extends UrlbarProvider {
// Fetch suggestions from all enabled sources.
let promises = [];
let { backend } = lazy.QuickSuggest;
if (backend?.isEnabled) {
promises.push(backend.query(searchString));
if (lazy.QuickSuggest.rustBackend?.isEnabled) {
promises.push(lazy.QuickSuggest.rustBackend.query(searchString));
}
if (
lazy.UrlbarPrefs.get("quicksuggest.dataCollection.enabled") &&
@@ -633,10 +632,7 @@ class ProviderQuickSuggest extends UrlbarProvider {
*/
cancelQuery() {
// Cancel the Rust query.
let backend = lazy.QuickSuggest.getFeature("SuggestBackendRust");
if (backend?.isEnabled) {
backend.cancelQuery();
}
lazy.QuickSuggest.rustBackend?.cancelQuery();
// Cancel the Merino timeout timer so it doesn't fire and record a timeout.
// If it's already canceled or has fired, this is a no-op.
-1
View File
@@ -76,7 +76,6 @@ EXTRA_JS_MODULES["urlbar/private"] += [
"private/MDNSuggestions.sys.mjs",
"private/MLSuggest.sys.mjs",
"private/PocketSuggestions.sys.mjs",
"private/SuggestBackendJs.sys.mjs",
"private/SuggestBackendMl.sys.mjs",
"private/SuggestBackendRust.sys.mjs",
"private/Weather.sys.mjs",
@@ -9,7 +9,6 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
SuggestionsMap: "resource:///modules/urlbar/private/SuggestBackendJs.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
@@ -51,58 +50,6 @@ export class AddonSuggestions extends BaseFeature {
return ["Amo"];
}
enable(enabled) {
if (enabled) {
lazy.QuickSuggest.jsBackend.register(this);
} else {
lazy.QuickSuggest.jsBackend.unregister(this);
this.#suggestionsMap?.clear();
}
}
queryRemoteSettings(searchString) {
const suggestions = this.#suggestionsMap?.get(searchString);
if (!suggestions) {
return [];
}
return suggestions.map(suggestion => ({
icon: suggestion.icon,
url: suggestion.url,
title: suggestion.title,
description: suggestion.description,
guid: suggestion.guid,
score: suggestion.score,
}));
}
async onRemoteSettingsSync(rs) {
const records = await rs.get({ filters: { type: "amo-suggestions" } });
if (!this.isEnabled) {
return;
}
const suggestionsMap = new lazy.SuggestionsMap();
for (const record of records) {
const { buffer } = await rs.attachments.download(record);
if (!this.isEnabled) {
return;
}
const results = JSON.parse(new TextDecoder("utf-8").decode(buffer));
await suggestionsMap.add(results, {
mapKeyword:
lazy.SuggestionsMap.MAP_KEYWORD_PREFIXES_STARTING_AT_FIRST_WORD,
});
if (!this.isEnabled) {
return;
}
}
this.#suggestionsMap = suggestionsMap;
}
async makeResult(queryContext, suggestion, searchString) {
if (!this.isEnabled) {
// The feature is disabled on the client, but Merino may still return
@@ -270,10 +217,8 @@ export class AddonSuggestions extends BaseFeature {
get canShowLessFrequently() {
const cap =
lazy.UrlbarPrefs.get("addonsShowLessFrequentlyCap") ||
lazy.QuickSuggest.backend.config?.showLessFrequentlyCap ||
lazy.QuickSuggest.config.showLessFrequentlyCap ||
0;
return !cap || this.showLessFrequentlyCount < cap;
}
#suggestionsMap = null;
}
@@ -8,7 +8,6 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
SuggestionsMap: "resource:///modules/urlbar/private/SuggestBackendJs.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
@@ -21,11 +20,6 @@ const NONSPONSORED_IAB_CATEGORIES = new Set(["5 - Education"]);
* called "expanded Wikipedia") suggestions in remote settings.
*/
export class AdmWikipedia extends BaseFeature {
constructor() {
super();
this.#suggestionsMap = new lazy.SuggestionsMap();
}
get shouldEnable() {
return (
lazy.UrlbarPrefs.get("suggest.quicksuggest.nonsponsored") ||
@@ -69,78 +63,6 @@ export class AdmWikipedia extends BaseFeature {
return false;
}
enable(enabled) {
if (enabled) {
lazy.QuickSuggest.jsBackend.register(this);
} else {
lazy.QuickSuggest.jsBackend.unregister(this);
this.#suggestionsMap.clear();
}
}
async queryRemoteSettings(searchString) {
let suggestions = this.#suggestionsMap.get(searchString);
if (!suggestions) {
return [];
}
// Start each icon fetch at the same time and wait for them all to finish.
let icons = await Promise.all(
suggestions.map(({ icon }) => this.#fetchIcon(icon))
);
return suggestions.map(suggestion => ({
full_keyword: this.#getFullKeyword(searchString, suggestion.keywords),
title: suggestion.title,
url: suggestion.url,
click_url: suggestion.click_url,
impression_url: suggestion.impression_url,
block_id: suggestion.id,
advertiser: suggestion.advertiser,
iab_category: suggestion.iab_category,
is_sponsored: !NONSPONSORED_IAB_CATEGORIES.has(suggestion.iab_category),
score: suggestion.score,
position: suggestion.position,
icon: icons.shift(),
}));
}
async onRemoteSettingsSync(rs) {
let dataType = lazy.UrlbarPrefs.get("quickSuggestRemoteSettingsDataType");
this.logger.debug("Loading remote settings", { dataType });
let [data] = await Promise.all([
rs.get({ filters: { type: dataType } }),
rs
.get({ filters: { type: "icon" } })
.then(icons =>
Promise.all(icons.map(i => rs.attachments.downloadToDisk(i)))
),
]);
if (!this.isEnabled) {
return;
}
let suggestionsMap = new lazy.SuggestionsMap();
this.logger.debug("Got remote settings data", { recordCount: data.length });
for (let record of data) {
let { buffer } = await rs.attachments.download(record);
if (!this.isEnabled) {
return;
}
let results = JSON.parse(new TextDecoder("utf-8").decode(buffer));
this.logger.debug("Adding results", { resultCount: results.length });
await suggestionsMap.add(results);
if (!this.isEnabled) {
return;
}
}
this.#suggestionsMap = suggestionsMap;
}
makeResult(queryContext, suggestion) {
let originalUrl;
if (suggestion.source == "rust") {
@@ -241,88 +163,4 @@ export class AdmWikipedia extends BaseFeature {
return result;
}
/**
* Gets the "full keyword" (i.e., suggestion) for a query from a list of
* keywords. The suggestions data doesn't include full keywords, so we make
* our own based on the result's keyword phrases and a particular query. We
* use two heuristics:
*
* (1) Find the first keyword phrase that has more words than the query. Use
* its first `queryWords.length` words as the full keyword. e.g., if the
* query is "moz" and `keywords` is ["moz", "mozi", "mozil", "mozill",
* "mozilla", "mozilla firefox"], pick "mozilla firefox", pop off the
* "firefox" and use "mozilla" as the full keyword.
* (2) If there isn't any keyword phrase with more words, then pick the
* longest phrase. e.g., pick "mozilla" in the previous example (assuming
* the "mozilla firefox" phrase isn't there). That might be the query
* itself.
*
* @param {string} query
* The query string.
* @param {Array} keywords
* An array of suggestion keywords.
* @returns {string}
* The full keyword.
*/
#getFullKeyword(query, keywords) {
let longerPhrase;
let trimmedQuery = query.toLocaleLowerCase().trim();
let queryWords = trimmedQuery.split(" ");
for (let phrase of keywords) {
if (phrase.startsWith(query)) {
let trimmedPhrase = phrase.trim();
let phraseWords = trimmedPhrase.split(" ");
// As an exception to (1), if the query ends with a space, then look for
// phrases with one more word so that the suggestion includes a word
// following the space.
let extra = query.endsWith(" ") ? 1 : 0;
let len = queryWords.length + extra;
if (len < phraseWords.length) {
// We found a phrase with more words.
return phraseWords.slice(0, len).join(" ");
}
if (
query.length < phrase.length &&
(!longerPhrase || longerPhrase.length < trimmedPhrase.length)
) {
// We found a longer phrase with the same number of words.
longerPhrase = trimmedPhrase;
}
}
}
return longerPhrase || trimmedQuery;
}
/**
* Fetch the icon from RemoteSettings attachments.
*
* @param {string} path
* The icon's remote settings path.
* @returns {string}
* The absolute file path to the downloaded attachment.
*/
async #fetchIcon(path) {
if (!path) {
return null;
}
let { rs } = lazy.QuickSuggest.jsBackend;
if (!rs) {
return null;
}
let record = (
await rs.get({
filters: { id: `icon-${path}` },
})
).pop();
if (!record) {
return null;
}
return rs.attachments.downloadToDisk(record);
}
#suggestionsMap;
}
@@ -27,14 +27,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
* place, without mixing it with unrelated code and cluttering up
* `QuickSuggest`. You can also test it in isolation from `QuickSuggest`.
*
* - Remote settings management. You can register your feature with
* `QuickSuggestRemoteSettings` and it will be called at appropriate times to
* sync from remote settings.
*
* - If your feature also serves suggestions from remote settings, you can
* implement one method, `queryRemoteSettings()`, to hook into
* `UrlbarProviderQuickSuggest`.
*
* - Your feature will automatically get its own logger.
*
* To register your subclass with `QuickSuggest`, add it to the `FEATURES` const
@@ -116,31 +108,6 @@ export class BaseFeature {
*/
enable(_enabled) {}
/**
* If the feature manages suggestions from remote settings that should be
* returned by UrlbarProviderQuickSuggest, the subclass should override this
* method. It should return remote settings suggestions matching the given
* search string.
*
* @param {string} _searchString
* The search string.
* @returns {Array}
* An array of matching suggestions, or null if not implemented.
*/
async queryRemoteSettings(_searchString) {
return null;
}
/**
* If the feature manages data in remote settings, the subclass should
* override this method. It should fetch the data and build whatever data
* structures are necessary to support the feature.
*
* @param {RemoteSettings} _rs
* The `RemoteSettings` client object.
*/
async onRemoteSettingsSync(_rs) {}
/**
* If the feature manages suggestions that either aren't served by Merino or
* whose telemetry type is different from `merinoProvider`, the subclass
@@ -108,7 +108,7 @@ export class FakespotSuggestions extends BaseFeature {
get canShowLessFrequently() {
let cap =
lazy.UrlbarPrefs.get("fakespotShowLessFrequentlyCap") ||
lazy.QuickSuggest.backend.config?.showLessFrequentlyCap ||
lazy.QuickSuggest.config.showLessFrequentlyCap ||
0;
return !cap || this.showLessFrequentlyCount < cap;
}
@@ -52,7 +52,7 @@ export class ImpressionCaps extends BaseFeature {
this.logger.debug("Starting impression stats update", {
type,
currentStats: this.#stats,
impression_caps: lazy.QuickSuggest.jsBackend.config.impression_caps,
impression_caps: lazy.QuickSuggest.config.impression_caps,
});
// Don't bother recording anything if caps are disabled.
@@ -151,7 +151,9 @@ export class ImpressionCaps extends BaseFeature {
// Validate stats against any changes to the impression caps in the config.
this._onConfigSet = () => this.#validateStats();
lazy.QuickSuggest.jsBackend.emitter.on("config-set", this._onConfigSet);
// TODO: If impression caps are ever enabled again, this will need to be
// fixed.
// lazy.QuickSuggest.jsBackend.emitter.on("config-set", this._onConfigSet);
// Periodically record impression counters reset telemetry.
this.#setCountersResetInterval();
@@ -165,7 +167,9 @@ export class ImpressionCaps extends BaseFeature {
}
#uninit() {
lazy.QuickSuggest.jsBackend.emitter.off("config-set", this._onConfigSet);
// TODO: If impression caps are ever enabled again, this will need to be
// fixed.
// lazy.QuickSuggest.jsBackend.emitter.off("config-set", this._onConfigSet);
this._onConfigSet = null;
lazy.clearInterval(this._impressionCountersResetInterval);
@@ -207,7 +211,7 @@ export class ImpressionCaps extends BaseFeature {
* corresponding to each impression cap. See the `#stats` comment for info.
*/
#validateStats() {
let { impression_caps } = lazy.QuickSuggest.jsBackend.config;
let { impression_caps } = lazy.QuickSuggest.config;
this.logger.debug("Validating impression stats", {
impression_caps,
@@ -325,7 +329,7 @@ export class ImpressionCaps extends BaseFeature {
#resetElapsedCounters() {
this.logger.debug("Checking for elapsed impression cap intervals", {
currentStats: this.#stats,
impression_caps: lazy.QuickSuggest.jsBackend.config.impression_caps,
impression_caps: lazy.QuickSuggest.config.impression_caps,
});
let now = Date.now();
@@ -446,8 +450,7 @@ export class ImpressionCaps extends BaseFeature {
// cap is the one with the same suggestion type (sponsored or non-sponsored)
// and interval. See `#validateStats()` for more.
//
// Impression caps are stored in the remote settings config. See
// `SuggestBackendJs.config.impression_caps`.
// Impression caps are stored in the Suggest remote settings global config.
#stats = {};
// Whether impression stats are currently being updated.
@@ -8,7 +8,6 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
SuggestionsMap: "resource:///modules/urlbar/private/SuggestBackendJs.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
@@ -48,49 +47,6 @@ export class MDNSuggestions extends BaseFeature {
return ["Mdn"];
}
enable(enabled) {
if (enabled) {
lazy.QuickSuggest.jsBackend.register(this);
} else {
lazy.QuickSuggest.jsBackend.unregister(this);
this.#suggestionsMap?.clear();
}
}
queryRemoteSettings(searchString) {
const suggestions = this.#suggestionsMap?.get(searchString);
return suggestions
? suggestions.map(suggestion => ({ ...suggestion }))
: [];
}
async onRemoteSettingsSync(rs) {
const records = await rs.get({ filters: { type: "mdn-suggestions" } });
if (!this.isEnabled) {
return;
}
const suggestionsMap = new lazy.SuggestionsMap();
for (const record of records) {
const { buffer } = await rs.attachments.download(record);
if (!this.isEnabled) {
return;
}
const results = JSON.parse(new TextDecoder("utf-8").decode(buffer));
await suggestionsMap.add(results, {
mapKeyword:
lazy.SuggestionsMap.MAP_KEYWORD_PREFIXES_STARTING_AT_FIRST_WORD,
});
if (!this.isEnabled) {
return;
}
}
this.#suggestionsMap = suggestionsMap;
}
async makeResult(queryContext, suggestion) {
if (!this.isEnabled) {
// The feature is disabled on the client, but Merino may still return
@@ -193,6 +149,4 @@ export class MDNSuggestions extends BaseFeature {
break;
}
}
#suggestionsMap = null;
}
@@ -8,7 +8,6 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
SuggestionsMap: "resource:///modules/urlbar/private/SuggestBackendJs.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
@@ -25,12 +24,6 @@ const RESULT_MENU_COMMAND = {
* A feature that manages Pocket suggestions in remote settings.
*/
export class PocketSuggestions extends BaseFeature {
constructor() {
super();
this.#lowConfidenceSuggestionsMap = new lazy.SuggestionsMap();
this.#highConfidenceSuggestionsMap = new lazy.SuggestionsMap();
}
get shouldEnable() {
return (
lazy.UrlbarPrefs.get("pocketFeatureGate") &&
@@ -59,94 +52,11 @@ export class PocketSuggestions extends BaseFeature {
get canShowLessFrequently() {
let cap =
lazy.UrlbarPrefs.get("pocketShowLessFrequentlyCap") ||
lazy.QuickSuggest.backend.config?.showLessFrequentlyCap ||
lazy.QuickSuggest.config.showLessFrequentlyCap ||
0;
return !cap || this.showLessFrequentlyCount < cap;
}
enable(enabled) {
if (enabled) {
lazy.QuickSuggest.jsBackend.register(this);
} else {
lazy.QuickSuggest.jsBackend.unregister(this);
this.#lowConfidenceSuggestionsMap.clear();
this.#highConfidenceSuggestionsMap.clear();
}
}
async queryRemoteSettings(searchString) {
// If the search string matches high confidence suggestions, they should be
// treated as top picks. Otherwise try to match low confidence suggestions.
let is_top_pick = false;
let suggestions = this.#highConfidenceSuggestionsMap.get(searchString);
if (suggestions.length) {
is_top_pick = true;
} else {
suggestions = this.#lowConfidenceSuggestionsMap.get(searchString);
}
let lowerSearchString = searchString.toLocaleLowerCase();
return suggestions.map(suggestion => {
// Add `full_keyword` to each matched suggestion. It should be the longest
// keyword that starts with the user's search string.
let full_keyword = lowerSearchString;
let keywords = is_top_pick
? suggestion.highConfidenceKeywords
: suggestion.lowConfidenceKeywords;
for (let keyword of keywords) {
if (
keyword.startsWith(lowerSearchString) &&
full_keyword.length < keyword.length
) {
full_keyword = keyword;
}
}
return { ...suggestion, is_top_pick, full_keyword };
});
}
async onRemoteSettingsSync(rs) {
let records = await rs.get({ filters: { type: "pocket-suggestions" } });
if (!this.isEnabled) {
return;
}
let lowMap = new lazy.SuggestionsMap();
let highMap = new lazy.SuggestionsMap();
this.logger.debug("Got records", { recordCount: records.length });
for (let record of records) {
let { buffer } = await rs.attachments.download(record);
if (!this.isEnabled) {
return;
}
let suggestions = JSON.parse(new TextDecoder("utf-8").decode(buffer));
this.logger.debug("Adding suggestions", {
suggestionsCount: suggestions.length,
});
await lowMap.add(suggestions, {
keywordsProperty: "lowConfidenceKeywords",
mapKeyword:
lazy.SuggestionsMap.MAP_KEYWORD_PREFIXES_STARTING_AT_FIRST_WORD,
});
if (!this.isEnabled) {
return;
}
await highMap.add(suggestions, {
keywordsProperty: "highConfidenceKeywords",
});
if (!this.isEnabled) {
return;
}
}
this.#lowConfidenceSuggestionsMap = lowMap;
this.#highConfidenceSuggestionsMap = highMap;
}
makeResult(queryContext, suggestion, searchString) {
if (!this.isEnabled) {
// The feature is disabled on the client, but Merino may still return
@@ -320,7 +230,4 @@ export class PocketSuggestions extends BaseFeature {
);
}
}
#lowConfidenceSuggestionsMap;
#highConfidenceSuggestionsMap;
}
@@ -1,442 +0,0 @@
/* 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/. */
import { BaseFeature } from "resource:///modules/urlbar/private/BaseFeature.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
});
const RS_COLLECTION = "quicksuggest";
// Entries are added to `SuggestionsMap` map in chunks, and each chunk will add
// at most this many entries.
const SUGGESTIONS_MAP_CHUNK_SIZE = 1000;
const TELEMETRY_LATENCY = "FX_URLBAR_QUICK_SUGGEST_REMOTE_SETTINGS_LATENCY_MS";
// See `SuggestionsMap.MAP_KEYWORD_PREFIXES_STARTING_AT_FIRST_WORD`. When a full
// keyword starts with one of the prefixes in this list, the user must type the
// entire prefix to start triggering matches based on that full keyword, instead
// of only the first word.
const KEYWORD_PREFIXES_TO_TREAT_AS_SINGLE_WORDS = ["how to"];
/**
* The Suggest JS backend. Not used when the Rust backend is enabled.
*/
export class SuggestBackendJs extends BaseFeature {
constructor(...args) {
super(...args);
this.#emitter = new lazy.EventEmitter();
}
get shouldEnable() {
return !lazy.UrlbarPrefs.get("quickSuggestRustEnabled");
}
/**
* @returns {RemoteSettings}
* The underlying `RemoteSettings` client object.
*/
get rs() {
return this.#rs;
}
/**
* @returns {EventEmitter}
* The client will emit events on this object.
*/
get emitter() {
return this.#emitter;
}
/**
* @returns {object}
* Global quick suggest configuration stored in remote settings. When the
* config changes the `emitter` property will emit a "config-set" event. The
* config is an object that looks like this:
*
* {
* impression_caps: {
* nonsponsored: {
* lifetime,
* custom: [
* { interval_s, max_count },
* ],
* },
* sponsored: {
* lifetime,
* custom: [
* { interval_s, max_count },
* ],
* },
* },
* show_less_frequently_cap,
* }
*/
get config() {
return this.#config;
}
/**
* @returns {Array}
* Array of `BasicFeature` instances.
*/
get features() {
return [...this.#features];
}
enable(enabled) {
if (!enabled) {
this.#enableSettings(false);
} else if (this.#features.size) {
this.#enableSettings(true);
this.#syncAll();
}
}
/**
* Registers a quick suggest feature that uses remote settings.
*
* @param {BaseFeature} feature
* An instance of a `BaseFeature` subclass. See `BaseFeature` for methods
* that the subclass must implement.
*/
register(feature) {
this.logger.debug("Registering feature: " + feature.name);
this.#features.add(feature);
if (this.isEnabled) {
if (this.#features.size == 1) {
this.#enableSettings(true);
}
this.#syncFeature(feature);
}
}
/**
* Unregisters a quick suggest feature that uses remote settings.
*
* @param {BaseFeature} feature
* An instance of a `BaseFeature` subclass.
*/
unregister(feature) {
this.logger.debug("Unregistering feature: " + feature.name);
this.#features.delete(feature);
if (!this.#features.size) {
this.#enableSettings(false);
}
}
/**
* Queries remote settings suggestions from all registered features.
*
* @param {string} searchString
* The search string.
* @returns {Array}
* The remote settings suggestions. If there are no matches, an empty array
* is returned.
*/
async query(searchString) {
let suggestions;
let stopwatchInstance = {};
TelemetryStopwatch.start(TELEMETRY_LATENCY, stopwatchInstance);
try {
suggestions = await this.#queryHelper(searchString);
TelemetryStopwatch.finish(TELEMETRY_LATENCY, stopwatchInstance);
} catch (error) {
TelemetryStopwatch.cancel(TELEMETRY_LATENCY, stopwatchInstance);
this.logger.error("Query error: " + error);
}
return suggestions || [];
}
async #queryHelper(searchString) {
this.logger.info("Handling query: " + JSON.stringify(searchString));
let results = await Promise.all(
[...this.#features].map(async feature => {
let suggestions = await feature.queryRemoteSettings(searchString);
return [feature, suggestions ?? []];
})
);
let allSuggestions = [];
for (let [feature, suggestions] of results) {
for (let suggestion of suggestions) {
// Features typically return suggestion objects straight from their
// suggestion maps. We don't want consumers to modify those objects
// since they are the source of truth (tests especially tend to do
// this), so return copies to consumers.
allSuggestions.push({
...suggestion,
source: "remote-settings",
provider: feature.name,
});
}
}
return allSuggestions;
}
async #enableSettings(enabled) {
if (enabled && !this.#rs) {
this.logger.debug("Creating RemoteSettings client");
this.#onSettingsSync = event => this.#syncAll({ event });
this.#rs = lazy.RemoteSettings(RS_COLLECTION);
this.#rs.on("sync", this.#onSettingsSync);
await this.#syncConfig();
} else if (!enabled && this.#rs) {
this.logger.debug("Destroying RemoteSettings client");
this.#rs.off("sync", this.#onSettingsSync);
this.#rs = null;
this.#onSettingsSync = null;
}
}
async #syncConfig() {
this.logger.debug("Syncing config");
let rs = this.#rs;
let configArray = await rs.get({ filters: { type: "configuration" } });
if (rs != this.#rs) {
return;
}
this.logger.debug("Got config array: " + JSON.stringify(configArray));
this.#setConfig(configArray?.[0]?.configuration || {});
}
async #syncFeature(feature) {
this.logger.debug("Syncing feature: " + feature.name);
await feature.onRemoteSettingsSync(this.#rs);
}
async #syncAll({ event = null } = {}) {
this.logger.debug("Syncing all");
let rs = this.#rs;
// Remove local files of deleted records
if (event?.data?.deleted) {
await Promise.all(
event.data.deleted
.filter(d => d.attachment)
.map(entry =>
Promise.all([
this.#rs.attachments.deleteDownloaded(entry), // type: data
this.#rs.attachments.deleteFromDisk(entry), // type: icon
])
)
);
if (rs != this.#rs) {
return;
}
}
let promises = [this.#syncConfig()];
for (let feature of this.#features) {
promises.push(this.#syncFeature(feature));
}
await Promise.all(promises);
}
/**
* Sets the quick suggest config and emits a "config-set" event.
*
* @param {object} config
* The config object.
*/
#setConfig(config) {
config = lazy.UrlbarUtils.copySnakeKeysToCamel(config ?? {});
this.logger.debug("Setting config: " + JSON.stringify(config));
this.#config = config;
this.#emitter.emit("config-set");
}
async _test_syncAll() {
if (this.#rs) {
// `RemoteSettingsClient` won't start another import if it's already
// importing. Wait for it to finish before starting the new one.
await this.#rs._importingPromise;
await this.#syncAll();
}
}
// The `RemoteSettings` client.
#rs = null;
// Registered `BaseFeature` instances.
#features = new Set();
// Configuration data synced from remote settings. See the `config` getter.
#config = {};
#emitter = null;
#onSettingsSync = null;
}
/**
* A wrapper around `Map` that handles quick suggest suggestions from remote
* settings. It maps keywords to suggestions. It has two benefits over `Map`:
*
* - The main benefit is that map entries are added in batches on idle to avoid
* blocking the main thread for too long, since there can be many suggestions
* and keywords.
* - A secondary benefit is that the interface is tailored to quick suggest
* suggestions, which have a `keywords` property.
*/
export class SuggestionsMap {
/**
* Returns the list of suggestions for a keyword.
*
* @param {string} keyword
* The keyword.
* @returns {Array}
* The array of suggestions for the keyword. If the keyword isn't in the
* map, the array will be empty.
*/
get(keyword) {
let object = this.#suggestionsByKeyword.get(keyword.toLocaleLowerCase());
if (!object) {
return [];
}
return Array.isArray(object) ? object : [object];
}
/**
* Adds a list of suggestion objects to the results map. Each suggestion must
* have a property whose value is an array of keyword strings. The
* suggestion's keywords will be taken from this array either exactly as they
* are specified or by generating new keywords from them; see `mapKeyword`.
*
* @param {Array} suggestions
* Array of suggestion objects.
* @param {object} options
* Options object.
* @param {string} options.keywordsProperty
* The name of the keywords property in each suggestion.
* @param {Function} options.mapKeyword
* If null, the keywords for each suggestion will be taken from the keywords
* array exactly as they are specified. Otherwise, this function will be
* called for each string in the array, and it should return an array of
* strings. The suggestion's final list of keywords will be the union of all
* strings returned by this function. See also the `MAP_KEYWORD_*` consts.
*/
async add(
suggestions,
{ keywordsProperty = "keywords", mapKeyword = null } = {}
) {
// There can be many suggestions, and each suggestion can have many
// keywords. To avoid blocking the main thread for too long, update the map
// in chunks, and to avoid blocking the UI and other higher priority work,
// do each chunk only when the main thread is idle. During each chunk, we'll
// add at most `chunkSize` entries to the map.
let suggestionIndex = 0;
let keywordIndex = 0;
// Keep adding chunks until all suggestions have been fully added.
while (suggestionIndex < suggestions.length) {
await new Promise(resolve => {
Services.tm.idleDispatchToMainThread(() => {
// Keep updating the map until the current chunk is done.
let indexInChunk = 0;
while (
indexInChunk < SuggestionsMap.chunkSize &&
suggestionIndex < suggestions.length
) {
let suggestion = suggestions[suggestionIndex];
let keywords = suggestion[keywordsProperty];
if (keywordIndex == keywords.length) {
// We've added entries for all keywords of the current suggestion.
// Move on to the next suggestion.
suggestionIndex++;
keywordIndex = 0;
continue;
}
// As a convenience, allow `mapKeyword` to return a string even
// though the JSDoc says an array must be returned.
let originalKeyword = keywords[keywordIndex];
let mappedKeywords =
mapKeyword?.(originalKeyword) ?? originalKeyword;
if (typeof mappedKeywords == "string") {
mappedKeywords = [mappedKeywords];
}
for (let keyword of mappedKeywords) {
// If the keyword's only suggestion is `suggestion`, store it
// directly as the value. Otherwise store an array of unique
// suggestions. See the `#suggestionsByKeyword` comment.
let object = this.#suggestionsByKeyword.get(keyword);
if (!object) {
this.#suggestionsByKeyword.set(keyword, suggestion);
} else {
let isArray = Array.isArray(object);
if (!isArray && object != suggestion) {
this.#suggestionsByKeyword.set(keyword, [object, suggestion]);
} else if (isArray && !object.includes(suggestion)) {
object.push(suggestion);
}
}
}
keywordIndex++;
indexInChunk++;
}
// The current chunk is done.
resolve();
});
});
}
}
clear() {
this.#suggestionsByKeyword.clear();
}
/**
* @returns {Function}
* A `mapKeyword` function that maps a keyword to an array containing the
* keyword's first word plus every subsequent prefix of the keyword. The
* strings in `KEYWORD_PREFIXES_TO_TREAT_AS_SINGLE_WORDS` will modify this
* behavior: When a full keyword starts with one of the prefixes in that
* list, the generated prefixes will start at that prefix instead of the
* first word.
*/
static get MAP_KEYWORD_PREFIXES_STARTING_AT_FIRST_WORD() {
return fullKeyword => {
let prefix = KEYWORD_PREFIXES_TO_TREAT_AS_SINGLE_WORDS.find(p =>
fullKeyword.startsWith(p + " ")
);
let spaceIndex = prefix ? prefix.length : fullKeyword.indexOf(" ");
let keywords = [fullKeyword];
if (spaceIndex >= 0) {
for (let i = spaceIndex; i < fullKeyword.length; i++) {
keywords.push(fullKeyword.substring(0, i));
}
}
return keywords;
};
}
// Maps each keyword in the dataset to one or more suggestions for the
// keyword. If only one suggestion uses a keyword, the keyword's value in the
// map will be the suggestion object. If more than one suggestion uses the
// keyword, the value will be an array of the suggestions. The reason for not
// always using an array is that we expect the vast majority of keywords to be
// used by only one suggestion, and since there are potentially very many
// keywords and suggestions and we keep them in memory all the time, we want
// to save as much memory as possible.
#suggestionsByKeyword = new Map();
// This is only defined as a property so that tests can override it.
static chunkSize = SUGGESTIONS_MAP_CHUNK_SIZE;
}
@@ -108,6 +108,14 @@ export class SuggestBackendRust extends BaseFeature {
}
}
get enablingPreferences() {
return ["quicksuggest.rustEnabled"];
}
get shouldEnable() {
return lazy.UrlbarPrefs.get("quicksuggest.rustEnabled");
}
/**
* @returns {object}
* The global Suggest config from the Rust component as returned from
@@ -125,10 +133,6 @@ export class SuggestBackendRust extends BaseFeature {
return this.#ingestQueue.emptyPromise;
}
get shouldEnable() {
return lazy.UrlbarPrefs.get("quickSuggestRustEnabled");
}
enable(enabled) {
if (enabled) {
this.#init();
@@ -173,7 +173,7 @@ export class Weather extends BaseFeature {
get canShowLessFrequently() {
const cap =
lazy.UrlbarPrefs.get("weatherShowLessFrequentlyCap") ||
lazy.QuickSuggest.backend.config?.showLessFrequentlyCap ||
lazy.QuickSuggest.config.showLessFrequentlyCap ||
0;
return !cap || this.showLessFrequentlyCount < cap;
}
@@ -64,7 +64,7 @@ export class YelpSuggestions extends BaseFeature {
get canShowLessFrequently() {
const cap =
lazy.UrlbarPrefs.get("yelpShowLessFrequentlyCap") ||
lazy.QuickSuggest.backend.config?.showLessFrequentlyCap ||
lazy.QuickSuggest.config.showLessFrequentlyCap ||
0;
return !cap || this.showLessFrequentlyCount < cap;
}
@@ -215,9 +215,7 @@ add_task(async function suggest() {
assertAbandonmentTelemetry([
{
groups: "heuristic,suggest",
results: UrlbarPrefs.get("quickSuggestRustEnabled")
? "search_engine,rust_adm_nonsponsored"
: "search_engine,rs_adm_nonsponsored",
results: "search_engine,rust_adm_nonsponsored",
n_results: 2,
},
]),
@@ -231,9 +231,7 @@ add_task(async function suggest() {
assertEngagementTelemetry([
{
groups: "heuristic,suggest",
results: UrlbarPrefs.get("quickSuggestRustEnabled")
? "search_engine,rust_adm_nonsponsored"
: "search_engine,rs_adm_nonsponsored",
results: "search_engine,rust_adm_nonsponsored",
n_results: 2,
},
]),
@@ -68,6 +68,5 @@ async function getRowByType(type) {
}
function suggestResultType(typeWithoutSource) {
let source = UrlbarPrefs.get("quickSuggestRustEnabled") ? "rust" : "rs";
return `${source}_${typeWithoutSource}`;
return `rust_${typeWithoutSource}`;
}
@@ -1,7 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/valid-lazy */
/* eslint-disable jsdoc/require-param */
const lazy = {};
@@ -9,28 +8,15 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
Region: "resource://gre/modules/Region.sys.mjs",
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
RemoteSettingsConfig: "resource://gre/modules/RustRemoteSettings.sys.mjs",
RemoteSettingsServer:
"resource://testing-common/RemoteSettingsServer.sys.mjs",
SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
SuggestBackendRust:
"resource:///modules/urlbar/private/SuggestBackendRust.sys.mjs",
Suggestion: "resource://gre/modules/RustSuggest.sys.mjs",
SuggestionProvider: "resource://gre/modules/RustSuggest.sys.mjs",
SuggestStore: "resource://gre/modules/RustSuggest.sys.mjs",
TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarProviderQuickSuggest:
"resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
sinon: "resource://testing-common/Sinon.sys.mjs",
});
let gTestScope;
@@ -42,6 +28,7 @@ let gTestScope;
// xpcshell tests.
Object.defineProperty(lazy, "UrlbarTestUtils", {
get: () => {
// eslint-disable-next-line mozilla/valid-lazy
if (!lazy._UrlbarTestUtils) {
const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
"resource://testing-common/UrlbarTestUtils.sys.mjs"
@@ -49,10 +36,13 @@ Object.defineProperty(lazy, "UrlbarTestUtils", {
module.init(gTestScope);
gTestScope.registerCleanupFunction(() => {
// Make sure the utils are re-initialized during the next test.
// eslint-disable-next-line mozilla/valid-lazy
lazy._UrlbarTestUtils = null;
});
// eslint-disable-next-line mozilla/valid-lazy
lazy._UrlbarTestUtils = module;
}
// eslint-disable-next-line mozilla/valid-lazy
return lazy._UrlbarTestUtils;
},
});
@@ -64,6 +54,7 @@ Object.defineProperty(lazy, "UrlbarTestUtils", {
// xpcshell tests.
Object.defineProperty(lazy, "MerinoTestUtils", {
get: () => {
// eslint-disable-next-line mozilla/valid-lazy
if (!lazy._MerinoTestUtils) {
const { MerinoTestUtils: module } = ChromeUtils.importESModule(
"resource://testing-common/MerinoTestUtils.sys.mjs"
@@ -71,10 +62,13 @@ Object.defineProperty(lazy, "MerinoTestUtils", {
module.init(gTestScope);
gTestScope.registerCleanupFunction(() => {
// Make sure the utils are re-initialized during the next test.
// eslint-disable-next-line mozilla/valid-lazy
lazy._MerinoTestUtils = null;
});
// eslint-disable-next-line mozilla/valid-lazy
lazy._MerinoTestUtils = module;
}
// eslint-disable-next-line mozilla/valid-lazy
return lazy._MerinoTestUtils;
},
});
@@ -222,20 +216,8 @@ class _QuickSuggestTestUtils {
await this.#remoteSettingsServer.start();
this.#log("ensureQuickSuggestInit", "Remote settings server started");
// Get the cached `RemoteSettings` client used by the JS backend and tell it
// to ignore signatures and to always force sync. Otherwise it won't sync if
// the previous sync was recent enough, which is incompatible with testing.
let rs = lazy.RemoteSettings("quicksuggest");
let { get, verifySignature } = rs;
rs.verifySignature = false;
rs.get = opts => get.call(rs, { forceSync: true, ...opts });
this.#restoreRemoteSettings = () => {
rs.verifySignature = verifySignature;
rs.get = get;
};
// Finally, init Suggest and set prefs. Do this after setting up remote
// settings because the current backend will immediately try to sync.
// Init Suggest and set prefs. Do this after setting up remote settings
// because the Rust backend will immediately try to sync.
this.#log(
"ensureQuickSuggestInit",
"Calling QuickSuggest.init() and setting prefs"
@@ -251,7 +233,7 @@ class _QuickSuggestTestUtils {
serverUrl: this.#remoteSettingsServer.url.toString(),
});
// Wait for the current backend to finish syncing.
// Wait for the Rust backend to finish syncing.
await this.forceSync();
// Set up Merino. This can happen any time relative to Suggest init.
@@ -279,8 +261,8 @@ class _QuickSuggestTestUtils {
async #uninitQuickSuggest(prefs, clearDataCollectionEnabled) {
this.#log("#uninitQuickSuggest", "Started");
// Reset prefs, which can cause the current backend to start syncing. Wait
// for it to finish.
// Reset prefs, which can cause the Rust backend to start syncing. Wait for
// it to finish.
for (let [name] of prefs) {
lazy.UrlbarPrefs.clear(name);
}
@@ -288,7 +270,6 @@ class _QuickSuggestTestUtils {
this.#log("#uninitQuickSuggest", "Stopping remote settings server");
await this.#remoteSettingsServer.stop();
this.#restoreRemoteSettings();
if (clearDataCollectionEnabled) {
lazy.UrlbarPrefs.clear("quicksuggest.dataCollection.enabled");
@@ -356,11 +337,6 @@ class _QuickSuggestTestUtils {
await lazy.QuickSuggest.rustBackend._test_ingest();
this.#log("forceSync", "Done syncing Rust backend");
}
if (lazy.QuickSuggest.jsBackend.isEnabled) {
this.#log("forceSync", "Syncing JS backend");
await lazy.QuickSuggest.jsBackend._test_syncAll();
this.#log("forceSync", "Done syncing JS backend");
}
this.#log("forceSync", "Done");
}
@@ -378,7 +354,7 @@ class _QuickSuggestTestUtils {
* @see {@link setConfig}
*/
async withConfig({ config, callback }) {
let original = lazy.QuickSuggest.jsBackend.config;
let original = lazy.QuickSuggest.config;
await this.setConfig(config);
await callback();
await this.setConfig(original);
@@ -413,15 +389,14 @@ class _QuickSuggestTestUtils {
/**
* Returns an expected AMP (sponsored) result that can be passed to
* `check_results()` in xpcshell tests regardless of whether the Rust backend
* is enabled.
* `check_results()` in xpcshell tests.
*
* @returns {object}
* An object that can be passed to `check_results()`.
*/
ampResult({
source,
provider,
source = "rust",
provider = "Amp",
keyword = "amp",
fullKeyword = keyword,
title = "Amp Suggestion",
@@ -452,6 +427,8 @@ class _QuickSuggestTestUtils {
url,
originalUrl,
requestId,
source,
provider,
displayUrl: url.replace(/^https:\/\//, ""),
isSponsored: true,
qsSuggestion: fullKeyword ?? keyword,
@@ -473,17 +450,9 @@ class _QuickSuggestTestUtils {
result.payload.descriptionL10n = descriptionL10n;
}
if (lazy.UrlbarPrefs.get("quickSuggestRustEnabled")) {
result.payload.source = source || "rust";
result.payload.provider = provider || "Amp";
if (result.payload.source == "rust") {
result.payload.iconBlob = iconBlob;
} else {
result.payload.icon = icon;
}
if (result.payload.source == "rust") {
result.payload.iconBlob = iconBlob;
} else {
result.payload.source = source || "remote-settings";
result.payload.provider = provider || "AdmWikipedia";
result.payload.icon = icon;
}
@@ -519,31 +488,24 @@ class _QuickSuggestTestUtils {
/**
* Returns an expected Wikipedia (non-sponsored) result that can be passed to
* `check_results()` in xpcshell tests regardless of whether the Rust backend
* is enabled.
* `check_results()` in xpcshell tests.
*
* @returns {object}
* An object that can be passed to `check_results()`.
*/
wikipediaResult({
source,
provider,
source = "rust",
provider = "Wikipedia",
keyword = "wikipedia",
fullKeyword = keyword,
title = "Wikipedia Suggestion",
url = "https://example.com/wikipedia",
originalUrl = url,
icon = null,
iconBlob = null,
impressionUrl = "https://example.com/wikipedia-impression",
clickUrl = "https://example.com/wikipedia-click",
blockId = 2,
advertiser = "Wikipedia",
iabCategory = "5 - Education",
suggestedIndex = -1,
isSuggestedIndexRelativeToGroup = true,
} = {}) {
let result = {
return {
suggestedIndex,
isSuggestedIndexRelativeToGroup,
type: lazy.UrlbarUtils.RESULT_TYPE.URL,
@@ -553,6 +515,9 @@ class _QuickSuggestTestUtils {
title,
url,
originalUrl,
iconBlob,
source,
provider,
displayUrl: url.replace(/^https:\/\//, ""),
isSponsored: false,
qsSuggestion: fullKeyword ?? keyword,
@@ -566,23 +531,6 @@ class _QuickSuggestTestUtils {
telemetryType: "adm_nonsponsored",
},
};
if (lazy.UrlbarPrefs.get("quickSuggestRustEnabled")) {
result.payload.source = source || "rust";
result.payload.provider = provider || "Wikipedia";
result.payload.iconBlob = iconBlob;
} else {
result.payload.source = source || "remote-settings";
result.payload.provider = provider || "AdmWikipedia";
result.payload.icon = icon;
result.payload.sponsoredImpressionUrl = impressionUrl;
result.payload.sponsoredClickUrl = clickUrl;
result.payload.sponsoredBlockId = blockId;
result.payload.sponsoredAdvertiser = advertiser;
result.payload.sponsoredIabCategory = iabCategory;
}
return result;
}
/**
@@ -847,15 +795,14 @@ class _QuickSuggestTestUtils {
/**
* Returns an expected AMO (addons) result that can be passed to
* `check_results()` in xpcshell tests regardless of whether the Rust backend
* is enabled.
* `check_results()` in xpcshell tests.
*
* @returns {object}
* An object that can be passed to `check_results()`.
*/
amoResult({
source,
provider,
source = "rust",
provider = "Amo",
title = "Amo Suggestion",
description = "Amo description",
url = "https://example.com/amo",
@@ -870,7 +817,7 @@ class _QuickSuggestTestUtils {
url = url.href;
}
let result = {
return {
isBestMatch: true,
suggestedIndex: 1,
type: lazy.UrlbarUtils.RESULT_TYPE.URL,
@@ -891,21 +838,11 @@ class _QuickSuggestTestUtils {
telemetryType: "amo",
},
};
if (lazy.UrlbarPrefs.get("quickSuggestRustEnabled")) {
result.payload.source = source || "rust";
result.payload.provider = provider || "Amo";
} else {
result.payload.source = source || "remote-settings";
result.payload.provider = provider || "AddonSuggestions";
}
return result;
}
/**
* Returns an expected MDN result that can be passed to `check_results()` in
* xpcshell tests regardless of whether the Rust backend is enabled.
* xpcshell tests.
*
* @returns {object}
* An object that can be passed to `check_results()`.
@@ -920,7 +857,7 @@ class _QuickSuggestTestUtils {
);
finalUrl.searchParams.set("utm_content", "treatment");
let result = {
return {
isBestMatch: true,
suggestedIndex: 1,
type: lazy.UrlbarUtils.RESULT_TYPE.URL,
@@ -936,33 +873,24 @@ class _QuickSuggestTestUtils {
icon: "chrome://global/skin/icons/mdn.svg",
shouldShowUrl: true,
bottomTextL10n: { id: "firefox-suggest-mdn-bottom-text" },
source: "rust",
provider: "Mdn",
},
};
if (lazy.UrlbarPrefs.get("quickSuggestRustEnabled")) {
result.payload.source = "rust";
result.payload.provider = "Mdn";
} else {
result.payload.source = "remote-settings";
result.payload.provider = "MDNSuggestions";
}
return result;
}
/**
* Returns an expected weather result that can be passed to `check_results()`
* in xpcshell tests regardless of whether the Rust backend is enabled.
* in xpcshell tests.
*
* @returns {object}
* An object that can be passed to `check_results()`.
*/
weatherResult({
source,
provider,
source = "rust",
provider = "Weather",
city = null,
region = null,
telemetryType = undefined,
temperatureUnit = undefined,
} = {}) {
if (!temperatureUnit) {
@@ -970,19 +898,19 @@ class _QuickSuggestTestUtils {
Services.locale.regionalPrefsLocales[0] == "en-US" ? "f" : "c";
}
let result = {
return {
type: lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC,
source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
heuristic: false,
suggestedIndex: 1,
payload: {
temperatureUnit,
source,
provider,
url: lazy.MerinoTestUtils.WEATHER_SUGGESTION.url,
input: lazy.MerinoTestUtils.WEATHER_SUGGESTION.url,
iconId: "6",
requestId: lazy.MerinoTestUtils.server.response.body.request_id,
source: "merino",
provider: "accuweather",
dynamicType: "weather",
city: city || lazy.MerinoTestUtils.WEATHER_SUGGESTION.city_name,
region: region || lazy.MerinoTestUtils.WEATHER_SUGGESTION.region_code,
@@ -998,20 +926,9 @@ class _QuickSuggestTestUtils {
low: lazy.MerinoTestUtils.WEATHER_SUGGESTION.forecast.low[
temperatureUnit
],
telemetryType: "weather",
},
};
if (lazy.UrlbarPrefs.get("quickSuggestRustEnabled")) {
result.payload.source = source || "rust";
result.payload.provider = provider || "Weather";
if (telemetryType !== null) {
result.payload.telemetryType = telemetryType || "weather";
}
} else {
throw new Error("Weather result not supported when Rust disabled");
}
return result;
}
/**
@@ -1437,7 +1354,6 @@ class _QuickSuggestTestUtils {
}
#remoteSettingsServer;
#restoreRemoteSettings;
}
export var QuickSuggestTestUtils = new _QuickSuggestTestUtils();
@@ -93,10 +93,7 @@ async function doOneBasicBlockTest({ result, block }) {
// The suggested index is -1 even for sponsored since search suggestions are
// disabled.
let suggested_index = -1;
let expectedBlockId =
UrlbarPrefs.get("quicksuggest.rustEnabled") && !isSponsored
? null
: result.id;
let expectedBlockId = !isSponsored ? null : result.id;
let pingsSubmitted = 0;
GleanPings.quickSuggest.testBeforeNextSubmit(() => {
@@ -48,10 +48,7 @@ add_task(async function basic() {
UrlbarProviderQuickSuggest.name,
"The result should be from the expected provider"
);
Assert.equal(
result.payload.provider,
UrlbarPrefs.get("quickSuggestRustEnabled") ? "Mdn" : "MDNSuggestions"
);
Assert.equal(result.payload.provider, "Mdn");
const onLoad = BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
@@ -27,7 +27,6 @@ add_setup(async function () {
await QuickSuggestTestUtils.ensureQuickSuggestInit({
remoteSettingsRecords: REMOTE_SETTINGS_RECORDS,
prefs: [
["quicksuggest.rustEnabled", true],
["suggest.quicksuggest.sponsored", true],
["suggest.yelp", true],
["yelp.featureGate", true],
@@ -39,10 +39,7 @@ add_task(async function nonsponsored() {
let match_type = "firefox-suggest";
let advertiser = REMOTE_SETTINGS_RESULT.advertiser.toLowerCase();
let reporting_url = undefined;
let source = UrlbarPrefs.get("quicksuggest.rustEnabled")
? "rust"
: "remote-settings";
let block_id = source == "rust" ? undefined : REMOTE_SETTINGS_RESULT.id;
let source = "rust";
// Make sure `improve_suggest_experience_checked` is recorded correctly
// depending on the value of the related pref.
@@ -66,7 +63,6 @@ add_task(async function nonsponsored() {
source,
match_type,
position,
block_id,
advertiser,
reporting_url,
suggested_index: -1,
@@ -85,7 +81,6 @@ add_task(async function nonsponsored() {
source,
match_type,
position,
block_id,
advertiser,
reporting_url,
suggested_index: -1,
@@ -100,7 +95,6 @@ add_task(async function nonsponsored() {
source,
match_type,
position,
block_id,
advertiser,
reporting_url,
suggested_index: -1,
@@ -121,7 +115,6 @@ add_task(async function nonsponsored() {
source,
match_type,
position,
block_id,
advertiser,
reporting_url,
suggested_index: -1,
@@ -136,7 +129,6 @@ add_task(async function nonsponsored() {
source,
match_type,
position,
block_id,
advertiser,
suggested_index: -1,
suggested_index_relative_to_group: true,
@@ -156,7 +148,6 @@ add_task(async function nonsponsored() {
source,
match_type,
position,
block_id,
advertiser,
reporting_url,
suggested_index: -1,
@@ -40,9 +40,7 @@ add_setup(async function () {
// sponsored
add_task(async function sponsored() {
let match_type = "firefox-suggest";
let source = UrlbarPrefs.get("quicksuggest.rustEnabled")
? "rust"
: "remote-settings";
let source = "rust";
// Make sure `improve_suggest_experience_checked` is recorded correctly
// depending on the value of the related pref.
@@ -171,9 +169,7 @@ add_task(async function sponsored() {
// higher-placement sponsored, a.k.a sponsored priority, sponsored best match
add_task(async function sponsoredBestMatch() {
let match_type = "best-match";
let source = UrlbarPrefs.get("quicksuggest.rustEnabled")
? "rust"
: "remote-settings";
let source = "rust";
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.quicksuggest.sponsoredPriority", true]],
@@ -502,8 +502,6 @@ async function doOneShowLessFrequentlyTest({
* The order doesn't matter.
*/
async function doRustProvidersTests({ searchString, tests }) {
UrlbarPrefs.set("quicksuggest.rustEnabled", true);
for (let { prefs, expectedUrls } of tests) {
info(
"Starting Rust providers test: " + JSON.stringify({ prefs, expectedUrls })
@@ -531,8 +529,4 @@ async function doRustProvidersTests({ searchString, tests }) {
}
await QuickSuggestTestUtils.forceSync();
}
info("Clearing rustEnabled pref and forcing sync");
UrlbarPrefs.clear("quicksuggest.rustEnabled");
await QuickSuggestTestUtils.forceSync();
}
@@ -672,7 +672,7 @@ add_task(async function maxResults() {
await QuickSuggestTestUtils.forceSync();
let searchString = "maxresults";
let suggestions = await QuickSuggest.backend.query(searchString);
let suggestions = await QuickSuggest.rustBackend.query(searchString);
Assert.equal(
suggestions.length,
gMaxResultsSuggestionsCount,
@@ -784,7 +784,7 @@ async function doManySuggestResultsTest({
// Make sure many Suggest suggestions match the search string.
let searchString = "maxresults";
let suggestions = await QuickSuggest.backend.query(searchString);
let suggestions = await QuickSuggest.rustBackend.query(searchString);
Assert.equal(
suggestions.length,
gMaxResultsSuggestionsCount,
@@ -75,10 +75,9 @@ add_task(async function basic() {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
matches:
UrlbarPrefs.get("quickSuggestRustEnabled") && !fullKeyword.includes(" ")
? [QuickSuggestTestUtils.mdnResult(suggestion)]
: [],
matches: !fullKeyword.includes(" ")
? [QuickSuggestTestUtils.mdnResult(suggestion)]
: [],
});
}
});
@@ -479,17 +479,10 @@ function makeExpectedResult({
searchString,
fullKeyword = searchString,
suggestion = REMOTE_SETTINGS_DATA[0].attachment[0],
source = "remote-settings",
source = "rust",
isTopPick = false,
suggestedIndex,
} = {}) {
if (
source == "remote-settings" &&
UrlbarPrefs.get("quicksuggest.rustEnabled")
) {
source = "rust";
}
let provider;
let keywordSubstringNotTyped = fullKeyword.substring(searchString.length);
let description = suggestion.description;
@@ -277,6 +277,5 @@ add_task(async function manyExposureResults_hidden_manyMatched() {
});
function suggestResultType(typeWithoutSource) {
let source = UrlbarPrefs.get("quickSuggestRustEnabled") ? "rust" : "rs";
return `${source}_${typeWithoutSource}`;
return `rust_${typeWithoutSource}`;
}
@@ -467,12 +467,6 @@ urlbar:
quickSuggestRemoteSettingsDataType:
type: string
description: The `type` of the suggestions data in remote settings. If not specified, "data" is used.
quickSuggestRustEnabled:
type: boolean
fallbackPref: browser.urlbar.quicksuggest.rustEnabled
description: >-
Whether Firefox Suggest will use the new Rust backend instead of the
original JS backend.
quickSuggestScenario:
# IMPORTANT: This should not have a fallbackPref. See UrlbarPrefs.sys.mjs.
type: string