Bug 1839558 - Allow suggestion scores to be specified in Nimbus. r=daisuke

This adds a `quickSuggestScoreMap` Nimbus variable that lets experiments
override suggestion scores. It maps from telemetry types to score values. For
example:

```
"quickSuggestScoreMap": {
  "amo": 0.25,
  "adm_sponsored": 0.3
}
```

In this example, addon suggestions will always have a score of 0.25, and
sponsored suggestions will always have a score of 0.3. Of course, different
branches within an experiment and different experiments can set different
scores.

While working on this, I saw we have a bug when we try to look up the
`BaseFeature` for a result. To do the lookup, we look up the result's
`telemetryType` in `FEATURE_NAMES_BY_TELEMETRY_TYPE`. That's a problem for `adm`
suggestions because the `telemetryType` will be either `adm_sponsored` or
`adm_nonsponsored`, but neither of those is present in
`FEATURE_NAMES_BY_TELEMETRY_TYPE` -- only `adm` is.

To fix it, I added back the `provider` property to result payloads that I
previously removed, and I added `BaseFeature.merinoProvider` so each feature can
specify its Merino provider. Then, `QuickSuggest` can build a map from Merino
provider names to features, allowing us to look up features without needing to
hardcode something like `FEATURE_NAMES_BY_TELEMETRY_TYPE` or
`FEATURE_NAMES_BY_MERINO_PROVIDER`.

Since I added back the `provider` property, I had to update a lot of tests. (As
a follow up, it would be nice to centralize the creation of expected result
objects in the test helper.)

I also added `BaseFeature.getSuggestionTelemetryType()` to help implement the
score map and to better formalize the idea that telemetry types are an important
property that all quick suggest results should include.

Differential Revision: https://phabricator.services.mozilla.com/D181709
This commit is contained in:
Drew Willcoxon 2023-06-22 03:55:40 +00:00
Родитель 01cd7f07de
Коммит 0e465234f7
25 изменённых файлов: 1022 добавлений и 32 удалений

Просмотреть файл

@ -141,6 +141,9 @@ class _QuickSuggest {
let { [name]: ctor } = ChromeUtils.importESModule(uri);
let feature = new ctor();
this.#features[name] = feature;
if (feature.merinoProvider) {
this.#featuresByMerinoProvider.set(feature.merinoProvider, feature);
}
// Update the map from enabling preferences to features.
let prefs = feature.enablingPreferences;
@ -173,6 +176,21 @@ class _QuickSuggest {
return this.#features[name];
}
/**
* Returns a quick suggest feature by the name of the Merino provider that
* serves its suggestions (as defined by `feature.merinoProvider`). Not all
* features correspond to a Merino provider.
*
* @param {string} provider
* The name of a Merino provider.
* @returns {BaseFeature}
* The feature object, an instance of a subclass of `BaseFeature`, or null
* if no feature corresponds to the Merino provider.
*/
getFeatureByMerinoProvider(provider) {
return this.#featuresByMerinoProvider.get(provider);
}
/**
* Called when a urlbar pref changes.
*
@ -471,6 +489,9 @@ class _QuickSuggest {
// Maps from quick suggest feature class names to feature instances.
#features = {};
// Maps from Merino provider names to quick suggest feature class names.
#featuresByMerinoProvider = new Map();
// Maps from preference names to the `Set` of feature instances they enable.
#featuresByEnablingPrefs = new Map();
}

Просмотреть файл

@ -458,6 +458,7 @@ const NIMBUS_DEFAULTS = {
experimentType: "",
isBestMatchExperiment: false,
quickSuggestRemoteSettingsDataType: "data",
quickSuggestScoreMap: null,
recordNavigationalSuggestionTelemetry: false,
weatherKeywords: null,
weatherKeywordsMinimumLength: 0,

Просмотреть файл

@ -21,12 +21,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
});
const FEATURE_NAMES_BY_TELEMETRY_TYPE = {
adm: "AdmWikipedia",
amo: "AddonSuggestions",
pocket: "PocketSuggestions",
};
const TELEMETRY_PREFIX = "contextual.services.quicksuggest";
const TELEMETRY_SCALARS = {
@ -161,7 +155,26 @@ class ProviderQuickSuggest extends UrlbarProvider {
return;
}
let suggestions = values.flat().sort((a, b) => b.score - a.score);
let suggestions = values.flat();
// Override suggestion scores with the ones defined in the Nimbus variable
// `quickSuggestScoreMap`. It maps telemetry types to scores.
let scoreMap = lazy.UrlbarPrefs.get("quickSuggestScoreMap");
if (scoreMap) {
for (let i = 0; i < suggestions.length; i++) {
let telemetryType = this.#getSuggestionTelemetryType(suggestions[i]);
if (scoreMap.hasOwnProperty(telemetryType)) {
let score = parseFloat(scoreMap[telemetryType]);
if (!isNaN(score)) {
// Don't modify the original suggestion object in case the feature
// that provided it returns the same object to all callers.
suggestions[i] = { ...suggestions[i], score };
}
}
}
}
suggestions.sort((a, b) => b.score - a.score);
// Add a result for the first suggestion that can be shown.
for (let suggestion of suggestions) {
@ -255,23 +268,57 @@ class ProviderQuickSuggest extends UrlbarProvider {
return this.#getFeatureByResult(result)?.getResultCommands?.(result);
}
/**
* Gets the `BaseFeature` instance that implements suggestions for a source
* and provider name. The source and provider name can be supplied from either
* a suggestion object or the payload of a `UrlbarResult` object.
*
* @param {object} options
* Options object.
* @param {string} options.source
* The suggestion source, one of: "remote-settings", "merino"
* @param {string} options.provider
* If the suggestion source is remote settings, this should be the name of
* the `BaseFeature` instance (`feature.name`) that manages the suggestion
* type. If the suggestion source is Merino, this should be the name of the
* Merino provider that serves the suggestion type.
* @returns {BaseFeature}
* The feature instance or null if no feature was found.
*/
#getFeature({ source, provider }) {
return source == "remote-settings"
? lazy.QuickSuggest.getFeature(provider)
: lazy.QuickSuggest.getFeatureByMerinoProvider(provider);
}
#getFeatureByResult(result) {
return lazy.QuickSuggest.getFeature(
FEATURE_NAMES_BY_TELEMETRY_TYPE[result.payload.telemetryType]
);
return this.#getFeature(result.payload);
}
/**
* Returns the telemetry type for a suggestion. A telemetry type uniquely
* identifies a type of suggestion as well as the kind of `UrlbarResult`
* instances created from it.
*
* @param {object} suggestion
* A suggestion from remote settings or Merino.
* @returns {string}
* The telemetry type. If the suggestion type is managed by a `BaseFeature`
* instance, the telemetry type is retrieved from it. Otherwise the
* suggestion type is assumed to come from Merino, and `suggestion.provider`
* (the Merino provider name) is returned.
*/
#getSuggestionTelemetryType(suggestion) {
let feature = this.#getFeature(suggestion);
if (feature) {
return feature.getSuggestionTelemetryType(suggestion);
}
return suggestion.provider;
}
async #makeResult(queryContext, suggestion) {
// For suggestions from remote settings, `suggestion.provider` will be the
// feature name. For suggestions from Merino, it will be the Merino provider
// name, which is also used as the `telemetryType`.
let feature =
lazy.QuickSuggest.getFeature(suggestion.provider) ||
lazy.QuickSuggest.getFeature(
FEATURE_NAMES_BY_TELEMETRY_TYPE[suggestion.provider]
);
let result;
let feature = this.#getFeature(suggestion);
if (!feature) {
result = this.#makeDefaultResult(queryContext, suggestion);
} else {
@ -286,6 +333,17 @@ class ProviderQuickSuggest extends UrlbarProvider {
}
}
// `source` will be one of: "remote-settings", "merino"
result.payload.source = suggestion.source;
// If the suggestion source is remote settings, `provider` will be the name
// of the `BaseFeature` instance (`feature.name`) that manages the
// suggestion type. If the source is Merino, it will be the name of the
// Merino provider that served the suggestion.
result.payload.provider = suggestion.provider;
result.payload.telemetryType = this.#getSuggestionTelemetryType(suggestion);
if (!result.hasSuggestedIndex) {
// When `bestMatchEnabled` is true, a "Top pick" checkbox appears in
// about:preferences. Show top pick suggestions as top picks only if that
@ -322,8 +380,6 @@ class ProviderQuickSuggest extends UrlbarProvider {
url: suggestion.url,
icon: suggestion.icon,
isSponsored: suggestion.is_sponsored,
source: suggestion.source,
telemetryType: suggestion.provider,
helpUrl: lazy.QuickSuggest.HELP_URL,
helpL10n: {
id: "urlbar-result-menu-learn-more-about-firefox-suggest",

Просмотреть файл

@ -256,7 +256,7 @@ class ProviderWeather extends UrlbarProvider {
},
requestId: suggestion.request_id,
source: suggestion.source,
merinoProvider: suggestion.provider,
provider: suggestion.provider,
dynamicType: WEATHER_DYNAMIC_TYPE,
city: suggestion.city_name,
temperatureUnit: unit,

Просмотреть файл

@ -1631,6 +1631,9 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
originalUrl: {
type: "string",
},
provider: {
type: "string",
},
qsSuggestion: {
type: "string",
},

Просмотреть файл

@ -133,6 +133,10 @@ export class AddonSuggestions extends BaseFeature {
return ["suggest.addons", "suggest.quicksuggest.nonsponsored"];
}
get merinoProvider() {
return "amo";
}
enable(enabled) {
if (enabled) {
lazy.QuickSuggestRemoteSettings.register(this);
@ -233,7 +237,6 @@ export class AddonSuggestions extends BaseFeature {
}
const payload = {
source: suggestion.source,
icon: suggestion.icon,
url: suggestion.url,
title: suggestion.title,
@ -243,7 +246,6 @@ export class AddonSuggestions extends BaseFeature {
helpUrl: lazy.QuickSuggest.HELP_URL,
shouldNavigate: true,
dynamicType: "addons",
telemetryType: "amo",
};
return Object.assign(

Просмотреть файл

@ -44,6 +44,14 @@ export class AdmWikipedia extends BaseFeature {
];
}
get merinoProvider() {
return "adm";
}
getSuggestionTelemetryType(suggestion) {
return suggestion.is_sponsored ? "adm_sponsored" : "adm_nonsponsored";
}
enable(enabled) {
if (enabled) {
lazy.QuickSuggestRemoteSettings.register(this);
@ -126,10 +134,6 @@ export class AdmWikipedia extends BaseFeature {
url: suggestion.url,
icon: suggestion.icon,
isSponsored: suggestion.is_sponsored,
source: suggestion.source,
telemetryType: suggestion.is_sponsored
? "adm_sponsored"
: "adm_nonsponsored",
requestId: suggestion.request_id,
urlTimestampIndex: suggestion.urlTimestampIndex,
sponsoredImpressionUrl: suggestion.impression_url,

Просмотреть файл

@ -63,6 +63,16 @@ export class BaseFeature {
return null;
}
/**
* @returns {string}
* If the feature manages suggestions served by Merino, the subclass should
* override this getter and return the name of the specific Merino provider
* that serves them.
*/
get merinoProvider() {
return "";
}
/**
* This method should initialize or uninitialize any state related to the
* feature.
@ -97,6 +107,22 @@ export class BaseFeature {
*/
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
* should override this method. It should return the telemetry type for the
* given suggestion. A telemetry type uniquely identifies a type of suggestion
* as well as the kind of `UrlbarResult` instances created from it.
*
* @param {object} suggestion
* A suggestion from either remote settings or Merino.
* @returns {string}
* The suggestion's telemetry type.
*/
getSuggestionTelemetryType(suggestion) {
return this.merinoProvider;
}
/**
* If the feature corresponds to a type of suggestion, the subclass should
* override this method. It should return a new `UrlbarResult` for a given

Просмотреть файл

@ -46,6 +46,10 @@ export class PocketSuggestions extends BaseFeature {
return ["suggest.pocket", "suggest.quicksuggest.nonsponsored"];
}
get merinoProvider() {
return "pocket";
}
get showLessFrequentlyCount() {
let count = lazy.UrlbarPrefs.get("pocket.showLessFrequentlyCount") || 0;
return Math.max(count, 0);
@ -118,8 +122,6 @@ export class PocketSuggestions extends BaseFeature {
title: [suggestion.title, lazy.UrlbarUtils.HIGHLIGHT.TYPED],
icon: "chrome://global/skin/icons/pocket.svg",
helpUrl: lazy.QuickSuggest.HELP_URL,
source: suggestion.source,
telemetryType: "pocket",
})
),
{ showFeedbackMenu: true }

Просмотреть файл

@ -106,6 +106,7 @@ const EXPECTED_SPONSORED_RESULT = {
},
displayUrl: "http://test.com/q=frabbits",
source: "remote-settings",
provider: "AdmWikipedia",
},
};
@ -140,6 +141,7 @@ const EXPECTED_NONSPONSORED_RESULT = {
},
displayUrl: "http://test.com/?q=nonsponsored",
source: "remote-settings",
provider: "AdmWikipedia",
},
};
@ -174,6 +176,7 @@ const EXPECTED_HTTP_RESULT = {
},
displayUrl: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL,
source: "remote-settings",
provider: "AdmWikipedia",
},
};
@ -208,6 +211,7 @@ const EXPECTED_HTTPS_RESULT = {
},
displayUrl: PREFIX_SUGGESTIONS_STRIPPED_URL,
source: "remote-settings",
provider: "AdmWikipedia",
},
};
@ -242,6 +246,31 @@ add_setup(async function init() {
});
});
add_task(async function telemetryType_sponsored() {
Assert.equal(
QuickSuggest.getFeature("AdmWikipedia").getSuggestionTelemetryType({
is_sponsored: true,
}),
"adm_sponsored",
"Telemetry type should be 'adm_sponsored'"
);
});
add_task(async function telemetryType_nonsponsored() {
Assert.equal(
QuickSuggest.getFeature("AdmWikipedia").getSuggestionTelemetryType({
is_sponsored: false,
}),
"adm_nonsponsored",
"Telemetry type should be 'adm_nonsponsored'"
);
Assert.equal(
QuickSuggest.getFeature("AdmWikipedia").getSuggestionTelemetryType({}),
"adm_nonsponsored",
"Telemetry type should be 'adm_nonsponsored' if `is_sponsored` not defined"
);
});
// Tests with only non-sponsored suggestions enabled with a matching search
// string.
add_task(async function nonsponsoredOnly_match() {
@ -1024,6 +1053,7 @@ add_task(async function dedupeAgainstURL_timestamps() {
: "firefox-suggest-urlbar-block",
},
source: "remote-settings",
provider: "AdmWikipedia",
},
};

Просмотреть файл

@ -84,6 +84,14 @@ add_setup(async function init() {
});
});
add_task(async function telemetryType() {
Assert.equal(
QuickSuggest.getFeature("AddonSuggestions").getSuggestionTelemetryType({}),
"amo",
"Telemetry type should be 'amo'"
);
});
// When non-sponsored suggestions are disabled, addon suggestions should be
// disabled.
add_task(async function nonsponsoredDisabled() {
@ -694,12 +702,15 @@ async function doShowLessFrequentlyTest({ tests, rs = {}, nimbus = {} }) {
}
function makeExpectedResult({ suggestion, source, isTopPick }) {
let provider;
let rating;
let number_of_ratings;
if (source === "remote-settings") {
provider = "AddonSuggestions";
rating = suggestion.rating;
number_of_ratings = suggestion.number_of_ratings;
} else {
provider = "amo";
rating = suggestion.custom_details.amo.rating;
number_of_ratings = suggestion.custom_details.amo.number_of_ratings;
}
@ -723,6 +734,7 @@ function makeExpectedResult({ suggestion, source, isTopPick }) {
shouldNavigate: true,
helpUrl: QuickSuggest.HELP_URL,
source,
provider,
},
};
}

Просмотреть файл

@ -88,6 +88,7 @@ const EXPECTED_BEST_MATCH_URLBAR_RESULT = {
},
displayUrl: "http://example.com",
source: "remote-settings",
provider: "AdmWikipedia",
},
};
@ -121,6 +122,7 @@ const EXPECTED_NON_BEST_MATCH_URLBAR_RESULT = {
},
displayUrl: "http://example.com",
source: "remote-settings",
provider: "AdmWikipedia",
},
};
@ -154,6 +156,7 @@ const EXPECTED_BEST_MATCH_POSITION_URLBAR_RESULT = {
},
displayUrl: "http://example.com/best-match-position",
source: "remote-settings",
provider: "AdmWikipedia",
},
};

Просмотреть файл

@ -82,6 +82,7 @@ function makeExpectedResult() {
icon: "icon",
qsSuggestion: "full_keyword",
source: "merino",
provider: "wikipedia",
helpUrl: QuickSuggest.HELP_URL,
helpL10n: {
id: "urlbar-result-menu-learn-more-about-firefox-suggest",

Просмотреть файл

@ -65,6 +65,7 @@ const EXPECTED_SPONSORED_URLBAR_RESULT = {
: "firefox-suggest-urlbar-block",
},
source: "remote-settings",
provider: "AdmWikipedia",
},
};
@ -99,6 +100,7 @@ const EXPECTED_NONSPONSORED_URLBAR_RESULT = {
: "firefox-suggest-urlbar-block",
},
source: "remote-settings",
provider: "AdmWikipedia",
},
};

Просмотреть файл

@ -55,6 +55,7 @@ const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = {
},
displayUrl: "http://test.com/q=frabbits",
source: "remote-settings",
provider: "AdmWikipedia",
},
};
@ -89,6 +90,7 @@ const EXPECTED_MERINO_URLBAR_RESULT = {
displayUrl: "url",
requestId: "request_id",
source: "merino",
provider: "adm",
},
};
@ -504,6 +506,7 @@ add_task(async function multipleMerinoSuggestions() {
displayUrl: "multipleMerinoSuggestions 1 url",
requestId: "request_id",
source: "merino",
provider: "adm",
},
},
],

Просмотреть файл

@ -204,6 +204,7 @@ add_task(async function () {
: "firefox-suggest-urlbar-block",
},
source: "remote-settings",
provider: "AdmWikipedia",
},
});
}

Просмотреть файл

@ -45,6 +45,14 @@ add_setup(async function init() {
});
});
add_task(async function telemetryType() {
Assert.equal(
QuickSuggest.getFeature("PocketSuggestions").getSuggestionTelemetryType({}),
"pocket",
"Telemetry type should be 'pocket'"
);
});
// When non-sponsored suggestions are disabled, Pocket suggestions should be
// disabled.
add_task(async function nonsponsoredDisabled() {
@ -256,6 +264,7 @@ function makeExpectedResult({
heuristic: false,
payload: {
source,
provider: source == "remote-settings" ? "PocketSuggestions" : "pocket",
telemetryType: "pocket",
title: suggestion.title,
url: suggestion.url,

Просмотреть файл

@ -171,6 +171,7 @@ function createExpectedQuickSuggestResult(suggest) {
},
displayUrl: suggest.url,
source: "remote-settings",
provider: "AdmWikipedia",
},
};
}

Просмотреть файл

@ -0,0 +1,798 @@
/* 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/. */
// Tests the `quickSuggestScoreMap` Nimbus variable that assigns scores to
// specified types of quick suggest suggestions. The scores in the map should
// override the scores in the individual suggestion objects so that experiments
// can fully control the relative ranking of suggestions.
"use strict";
const { DEFAULT_SUGGESTION_SCORE } = QuickSuggestRemoteSettings;
const REMOTE_SETTINGS_RECORDS = [
{
type: "data",
attachment: [
// sponsored without score
{
iab_category: "22 - Shopping",
keywords: [
"sponsored without score",
"sponsored without score, nonsponsored without score",
"sponsored without score, nonsponsored with score",
"sponsored without score, addon without score",
],
id: 1,
url: "https://example.com/sponsored-without-score",
title: "Sponsored without score",
click_url: "https://example.com/click",
impression_url: "https://example.com/impression",
advertiser: "TestAdvertiser",
icon: null,
},
// sponsored with score
{
iab_category: "22 - Shopping",
score: 2 * DEFAULT_SUGGESTION_SCORE,
keywords: [
"sponsored with score",
"sponsored with score, nonsponsored without score",
"sponsored with score, nonsponsored with score",
"sponsored with score, addon with score",
],
id: 2,
url: "https://example.com/sponsored-with-score",
title: "Sponsored with score",
click_url: "https://example.com/click",
impression_url: "https://example.com/impression",
advertiser: "TestAdvertiser",
icon: null,
},
// nonsponsored without score
{
iab_category: "5 - Education",
keywords: [
"nonsponsored without score",
"sponsored without score, nonsponsored without score",
"sponsored with score, nonsponsored without score",
],
id: 3,
url: "https://example.com/nonsponsored-without-score",
title: "Nonsponsored without score",
click_url: "https://example.com/click",
impression_url: "https://example.com/impression",
advertiser: "TestAdvertiser",
icon: null,
},
// nonsponsored with score
{
iab_category: "5 - Education",
score: 2 * DEFAULT_SUGGESTION_SCORE,
keywords: [
"nonsponsored with score",
"sponsored without score, nonsponsored with score",
"sponsored with score, nonsponsored with score",
],
id: 4,
url: "https://example.com/nonsponsored-with-score",
title: "Nonsponsored with score",
click_url: "https://example.com/click",
impression_url: "https://example.com/impression",
advertiser: "TestAdvertiser",
icon: null,
},
],
},
{
type: "amo-suggestions",
attachment: [
// addon without score
{
keywords: [
"addon without score",
"sponsored without score, addon without score",
],
url: "https://example.com/addon-without-score",
guid: "addon-without-score@example.com",
icon: "https://example.com/addon.svg",
title: "Addon without score",
rating: "4.7",
description: "Addon without score",
number_of_ratings: 1256,
is_top_pick: true,
},
// addon with score
{
score: 2 * DEFAULT_SUGGESTION_SCORE,
keywords: [
"addon with score",
"sponsored with score, addon with score",
],
url: "https://example.com/addon-with-score",
guid: "addon-with-score@example.com",
icon: "https://example.com/addon.svg",
title: "Addon with score",
rating: "4.7",
description: "Addon with score",
number_of_ratings: 1256,
is_top_pick: true,
},
],
},
];
const ADM_RECORD = REMOTE_SETTINGS_RECORDS[0];
const SPONSORED_WITHOUT_SCORE = ADM_RECORD.attachment[0];
const SPONSORED_WITH_SCORE = ADM_RECORD.attachment[1];
const NONSPONSORED_WITHOUT_SCORE = ADM_RECORD.attachment[2];
const NONSPONSORED_WITH_SCORE = ADM_RECORD.attachment[3];
const ADDON_RECORD = REMOTE_SETTINGS_RECORDS[1];
const ADDON_WITHOUT_SCORE = ADDON_RECORD.attachment[0];
const ADDON_WITH_SCORE = ADDON_RECORD.attachment[1];
const MERINO_SPONSORED_SUGGESTION = {
provider: "adm",
score: DEFAULT_SUGGESTION_SCORE,
iab_category: "22 - Shopping",
is_sponsored: true,
keywords: ["test"],
full_keyword: "test",
block_id: 1,
url: "https://example.com/merino-sponsored",
title: "Merino sponsored",
click_url: "https://example.com/click",
impression_url: "https://example.com/impression",
advertiser: "TestAdvertiser",
icon: null,
};
const MERINO_ADDON_SUGGESTION = {
provider: "amo",
score: DEFAULT_SUGGESTION_SCORE,
keywords: ["test"],
icon: "https://example.com/addon.svg",
url: "https://example.com/merino-addon",
title: "Merino addon",
description: "Merino addon",
is_top_pick: true,
custom_details: {
amo: {
guid: "merino-addon@example.com",
rating: "4.7",
number_of_ratings: "1256",
},
},
};
const MERINO_UNKNOWN_SUGGESTION = {
provider: "some_unknown_provider",
score: DEFAULT_SUGGESTION_SCORE,
keywords: ["test"],
url: "https://example.com/merino-unknown",
title: "Merino unknown",
};
add_setup(async function init() {
UrlbarPrefs.set("quicksuggest.enabled", true);
UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
UrlbarPrefs.set("addons.featureGate", true);
// Disable search suggestions so we don't hit the network.
Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
await QuickSuggestTestUtils.ensureQuickSuggestInit({
remoteSettingsResults: REMOTE_SETTINGS_RECORDS,
merinoSuggestions: [],
});
});
add_task(async function sponsoredWithout_nonsponsoredWithout_sponsoredWins() {
let keyword = "sponsored without score, nonsponsored without score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_sponsored: score,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: SPONSORED_WITHOUT_SCORE,
}),
});
});
add_task(
async function sponsoredWithout_nonsponsoredWithout_nonsponsoredWins() {
let keyword = "sponsored without score, nonsponsored without score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_nonsponsored: score,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: NONSPONSORED_WITHOUT_SCORE,
}),
});
}
);
add_task(
async function sponsoredWithout_nonsponsoredWithout_sponsoredWins_both() {
let keyword = "sponsored without score, nonsponsored without score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_sponsored: score,
adm_nonsponsored: score / 2,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: SPONSORED_WITHOUT_SCORE,
}),
});
}
);
add_task(
async function sponsoredWithout_nonsponsoredWithout_nonsponsoredWins_both() {
let keyword = "sponsored without score, nonsponsored without score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_nonsponsored: score,
adm_sponsored: score / 2,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: NONSPONSORED_WITHOUT_SCORE,
}),
});
}
);
add_task(async function sponsoredWith_nonsponsoredWith_sponsoredWins() {
let keyword = "sponsored with score, nonsponsored with score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_sponsored: score,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: SPONSORED_WITH_SCORE,
}),
});
});
add_task(async function sponsoredWith_nonsponsoredWith_nonsponsoredWins() {
let keyword = "sponsored with score, nonsponsored with score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_nonsponsored: score,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: NONSPONSORED_WITH_SCORE,
}),
});
});
add_task(async function sponsoredWith_nonsponsoredWith_sponsoredWins_both() {
let keyword = "sponsored with score, nonsponsored with score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_sponsored: score,
adm_nonsponsored: score / 2,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: SPONSORED_WITH_SCORE,
}),
});
});
add_task(async function sponsoredWith_nonsponsoredWith_nonsponsoredWins_both() {
let keyword = "sponsored with score, nonsponsored with score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_nonsponsored: score,
adm_sponsored: score / 2,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: NONSPONSORED_WITH_SCORE,
}),
});
});
add_task(async function sponsoredWithout_addonWithout_sponsoredWins() {
let keyword = "sponsored without score, addon without score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_sponsored: score,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: SPONSORED_WITHOUT_SCORE,
}),
});
});
add_task(async function sponsoredWithout_addonWithout_addonWins() {
let keyword = "sponsored without score, addon without score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
amo: score,
},
expectedFeatureName: "AddonSuggestions",
expectedScore: score,
expectedResult: makeExpectedAddonResult({
suggestion: ADDON_WITHOUT_SCORE,
}),
});
});
add_task(async function sponsoredWithout_addonWithout_sponsoredWins_both() {
let keyword = "sponsored without score, addon without score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_sponsored: score,
amo: score / 2,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: SPONSORED_WITHOUT_SCORE,
}),
});
});
add_task(async function sponsoredWithout_addonWithout_addonWins_both() {
let keyword = "sponsored without score, addon without score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
amo: score,
adm_sponsored: score / 2,
},
expectedFeatureName: "AddonSuggestions",
expectedScore: score,
expectedResult: makeExpectedAddonResult({
suggestion: ADDON_WITHOUT_SCORE,
}),
});
});
add_task(async function sponsoredWith_addonWith_sponsoredWins() {
let keyword = "sponsored with score, addon with score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_sponsored: score,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: SPONSORED_WITH_SCORE,
}),
});
});
add_task(async function sponsoredWith_addonWith_addonWins() {
let keyword = "sponsored with score, addon with score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
amo: score,
},
expectedFeatureName: "AddonSuggestions",
expectedScore: score,
expectedResult: makeExpectedAddonResult({
suggestion: ADDON_WITH_SCORE,
}),
});
});
add_task(async function sponsoredWith_addonWith_sponsoredWins_both() {
let keyword = "sponsored with score, addon with score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
adm_sponsored: score,
amo: score / 2,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: SPONSORED_WITH_SCORE,
}),
});
});
add_task(async function sponsoredWith_addonWith_addonWins_both() {
let keyword = "sponsored with score, addon with score";
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword,
scoreMap: {
amo: score,
adm_sponsored: score / 2,
},
expectedFeatureName: "AddonSuggestions",
expectedScore: score,
expectedResult: makeExpectedAddonResult({
suggestion: ADDON_WITH_SCORE,
}),
});
});
add_task(async function merino_sponsored_addon_sponsoredWins() {
UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", false);
MerinoTestUtils.server.response.body.suggestions = [
MERINO_SPONSORED_SUGGESTION,
MERINO_ADDON_SUGGESTION,
];
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword: "test",
scoreMap: {
adm_sponsored: score,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword: "test",
suggestion: MERINO_SPONSORED_SUGGESTION,
source: "merino",
}),
});
UrlbarPrefs.clear("quicksuggest.remoteSettings.enabled");
});
add_task(async function merino_sponsored_addon_addonWins() {
UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", false);
MerinoTestUtils.server.response.body.suggestions = [
MERINO_SPONSORED_SUGGESTION,
MERINO_ADDON_SUGGESTION,
];
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword: "test",
scoreMap: {
amo: score,
},
expectedFeatureName: "AddonSuggestions",
expectedScore: score,
expectedResult: makeExpectedAddonResult({
suggestion: MERINO_ADDON_SUGGESTION,
source: "merino",
}),
});
UrlbarPrefs.clear("quicksuggest.remoteSettings.enabled");
});
add_task(async function merino_sponsored_unknown_sponsoredWins() {
UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", false);
MerinoTestUtils.server.response.body.suggestions = [
MERINO_SPONSORED_SUGGESTION,
MERINO_UNKNOWN_SUGGESTION,
];
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword: "test",
scoreMap: {
adm_sponsored: score,
},
expectedFeatureName: "AdmWikipedia",
expectedScore: score,
expectedResult: makeExpectedAdmResult({
keyword: "test",
suggestion: MERINO_SPONSORED_SUGGESTION,
source: "merino",
}),
});
UrlbarPrefs.clear("quicksuggest.remoteSettings.enabled");
});
add_task(async function merino_sponsored_unknown_unknownWins() {
UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", false);
MerinoTestUtils.server.response.body.suggestions = [
MERINO_SPONSORED_SUGGESTION,
MERINO_UNKNOWN_SUGGESTION,
];
let score = 10 * DEFAULT_SUGGESTION_SCORE;
await doTest({
keyword: "test",
scoreMap: {
[MERINO_UNKNOWN_SUGGESTION.provider]: score,
},
expectedFeatureName: null,
expectedScore: score,
expectedResult: makeExpectedDefaultResult({
suggestion: MERINO_UNKNOWN_SUGGESTION,
}),
});
UrlbarPrefs.clear("quicksuggest.remoteSettings.enabled");
});
add_task(async function stringValue() {
let keyword = "sponsored with score, nonsponsored with score";
await doTest({
keyword,
scoreMap: {
adm_sponsored: "123.456",
},
expectedFeatureName: "AdmWikipedia",
expectedScore: 123.456,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: SPONSORED_WITH_SCORE,
}),
});
});
add_task(async function nanValue_sponsoredWins() {
let keyword = "sponsored with score, nonsponsored without score";
await doTest({
keyword,
scoreMap: {
adm_nonsponsored: "this is NaN",
},
expectedFeatureName: "AdmWikipedia",
expectedScore: 2 * DEFAULT_SUGGESTION_SCORE,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: SPONSORED_WITH_SCORE,
}),
});
});
add_task(async function nanValue_nonsponsoredWins() {
let keyword = "sponsored without score, nonsponsored with score";
await doTest({
keyword,
scoreMap: {
adm_sponsored: "this is NaN",
},
expectedFeatureName: "AdmWikipedia",
expectedScore: 2 * DEFAULT_SUGGESTION_SCORE,
expectedResult: makeExpectedAdmResult({
keyword,
suggestion: NONSPONSORED_WITH_SCORE,
}),
});
});
/**
* Sets up Nimbus with a `quickSuggestScoreMap` variable value, does a search,
* and makes sure the expected result is shown and the expected score is set on
* the suggestion.
*
* @param {object} options
* Options object.
* @param {string} options.keyword
* The search string. This should be equal to a keyword from one or more
* suggestions.
* @param {object} options.scoreMap
* The value to set for the `quickSuggestScoreMap` variable.
* @param {string} options.expectedFeatureName
* The name of the `BaseFeature` instance that is expected to create the
* `UrlbarResult` that's shown. If the suggestion is intentionally from an
* unknown Merino provider and therefore the quick suggest provider is
* expected to create a default result for it, set this to null.
* @param {UrlbarResultstring} options.expectedResult
* The `UrlbarResult` that's expected to be shown.
* @param {number} options.expectedScore
* The final `score` value that's expected to be defined on the suggestion
* object.
*/
async function doTest({
keyword,
scoreMap,
expectedFeatureName,
expectedResult,
expectedScore,
}) {
let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
quickSuggestScoreMap: scoreMap,
});
// Stub the expected feature's `makeResult()` so we can see the value of the
// passed-in suggestion's score. If the suggestion's type is in the score map,
// the provider will set its score before calling `makeResult()`.
let actualScore;
let sandbox;
if (expectedFeatureName) {
sandbox = sinon.createSandbox();
let feature = QuickSuggest.getFeature(expectedFeatureName);
let stub = sandbox
.stub(feature, "makeResult")
.callsFake((queryContext, suggestion, searchString) => {
actualScore = suggestion.score;
return stub.wrappedMethod.call(
feature,
queryContext,
suggestion,
searchString
);
});
}
await check_results({
context: createContext(keyword, {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
matches: [expectedResult],
});
if (expectedFeatureName) {
Assert.equal(
actualScore,
expectedScore,
"Suggestion score should be set correctly"
);
sandbox.restore();
}
await cleanUpNimbus();
}
function makeExpectedAdmResult({
suggestion,
keyword,
source = "remote-settings",
}) {
let isSponsored = suggestion.iab_category != "5 - Education";
let result = {
type: UrlbarUtils.RESULT_TYPE.URL,
source: UrlbarUtils.RESULT_SOURCE.SEARCH,
heuristic: false,
payload: {
source,
isSponsored,
provider: source == "remote-settings" ? "AdmWikipedia" : "adm",
telemetryType: isSponsored ? "adm_sponsored" : "adm_nonsponsored",
title: suggestion.title,
url: suggestion.url,
originalUrl: suggestion.url,
displayUrl: suggestion.url.replace(/^https:\/\//, ""),
icon: suggestion.icon,
sponsoredBlockId:
source == "remote-settings" ? suggestion.id : suggestion.block_id,
sponsoredImpressionUrl: suggestion.impression_url,
sponsoredClickUrl: suggestion.click_url,
sponsoredAdvertiser: suggestion.advertiser,
sponsoredIabCategory: suggestion.iab_category,
qsSuggestion: keyword,
helpUrl: QuickSuggest.HELP_URL,
helpL10n: {
id: "urlbar-result-menu-learn-more-about-firefox-suggest",
},
isBlockable: true,
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
},
},
};
if (source == "merino") {
result.payload.requestId = "request_id";
}
return result;
}
function makeExpectedAddonResult({ suggestion, source = "remote-settings" }) {
return {
type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
source: UrlbarUtils.RESULT_SOURCE.SEARCH,
heuristic: false,
payload: {
source,
provider: source == "remote-settings" ? "AddonSuggestions" : "amo",
telemetryType: "amo",
dynamicType: "addons",
title: suggestion.title,
url: suggestion.url,
displayUrl: suggestion.url.replace(/^https:\/\//, ""),
icon: suggestion.icon,
description: suggestion.description,
rating: Number(
source == "remote-settings"
? suggestion.rating
: suggestion.custom_details.amo.rating
),
reviews: Number(
source == "remote-settings"
? suggestion.number_of_ratings
: suggestion.custom_details.amo.number_of_ratings
),
shouldNavigate: true,
helpUrl: QuickSuggest.HELP_URL,
},
};
}
function makeExpectedDefaultResult({ suggestion }) {
return {
type: UrlbarUtils.RESULT_TYPE.URL,
source: UrlbarUtils.RESULT_SOURCE.SEARCH,
heuristic: false,
payload: {
source: "merino",
provider: suggestion.provider,
telemetryType: suggestion.provider,
isSponsored: suggestion.is_sponsored,
title: suggestion.title,
url: suggestion.url,
displayUrl: suggestion.url.replace(/^https:\/\//, ""),
icon: suggestion.icon,
helpUrl: QuickSuggest.HELP_URL,
helpL10n: {
id: "urlbar-result-menu-learn-more-about-firefox-suggest",
},
isBlockable: true,
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
},
},
};
}

Просмотреть файл

@ -267,6 +267,7 @@ function makeExpectedResult({
icon: "icon",
isSponsored: false,
source: "merino",
provider: telemetryType,
helpUrl: QuickSuggest.HELP_URL,
helpL10n: {
id: "urlbar-result-menu-learn-more-about-firefox-suggest",

Просмотреть файл

@ -1379,7 +1379,7 @@ function makeExpectedResult({
},
requestId: MerinoTestUtils.server.response.body.request_id,
source: "merino",
merinoProvider: "accuweather",
provider: "accuweather",
dynamicType: "weather",
city: WEATHER_SUGGESTION.city_name,
temperature:

Просмотреть файл

@ -883,6 +883,7 @@ async function doMatchingQuickSuggestTest(pref, isSponsored) {
: "firefox-suggest-urlbar-block",
},
source: "remote-settings",
provider: "AdmWikipedia",
},
},
],
@ -1364,7 +1365,7 @@ function makeExpectedResult({
},
requestId: MerinoTestUtils.server.response.body.request_id,
source: "merino",
merinoProvider: "accuweather",
provider: "accuweather",
dynamicType: "weather",
city: WEATHER_SUGGESTION.city_name,
temperature:

Просмотреть файл

@ -18,6 +18,7 @@ firefox-appdir = browser
[test_quicksuggest_offlineDefault.js]
[test_quicksuggest_pocket.js]
[test_quicksuggest_positionInSuggestions.js]
[test_quicksuggest_scoreMap.js]
[test_quicksuggest_topPicks.js]
[test_suggestionsMap.js]
[test_weather.js]

Просмотреть файл

@ -62,6 +62,7 @@ const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = {
},
displayUrl: "http://test.com/q=frabbits",
source: "remote-settings",
provider: "AdmWikipedia",
},
};
@ -96,6 +97,7 @@ const EXPECTED_NON_SPONSORED_REMOTE_SETTINGS_RESULT = {
},
displayUrl: "http://test.com/q=frabbits",
source: "remote-settings",
provider: "AdmWikipedia",
},
};

Просмотреть файл

@ -290,6 +290,16 @@ urlbar:
- history
- offline
- online
quickSuggestScoreMap:
type: json
description: >-
A JSON object that maps telemetry result types to suggestion scores. If
a telemetry result type is present in this map, the client will use the
corresponding score as the score for all suggestions of the type,
overriding all other sources of scores for the type. In other words,
the scores in this map will override scores that are set in remote
settings and Merino as well as scores that are hardcoded in the client.
Example entries: `"amo": 0.5`, `"adm_sponsored": 0.9`
quickSuggestShouldShowOnboardingDialog:
type: boolean
fallbackPref: browser.urlbar.quicksuggest.shouldShowOnboardingDialog