mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 11:25:00 +00:00
Bug 1834795 - Implement Share-of-Voice for sponsored tiles r=thecount,ttran
Differential Revision: https://phabricator.services.mozilla.com/D179149
This commit is contained in:
parent
745fb89c08
commit
da167427f9
@ -1533,6 +1533,13 @@ pref("browser.topsites.useRemoteSetting", true);
|
||||
pref("browser.topsites.contile.enabled", true);
|
||||
pref("browser.topsites.contile.endpoint", "https://contile.services.mozilla.com/v1/tiles");
|
||||
|
||||
// Whether to enable the Share-of-Voice feature for Sponsored Topsites via Contile.
|
||||
#if defined(EARLY_BETA_OR_EARLIER)
|
||||
pref("browser.topsites.contile.sov.enabled", true);
|
||||
#else
|
||||
pref("browser.topsites.contile.sov.enabled", false);
|
||||
#endif
|
||||
|
||||
// The base URL for the Quick Suggest anonymizing proxy. To make a request to
|
||||
// the proxy, include a campaign ID in the path.
|
||||
pref("browser.partnerlink.attributionURL", "https://topsites.services.mozilla.com/cid/");
|
||||
|
@ -78,7 +78,9 @@ export class ImpressionStats extends React.PureComponent {
|
||||
tile_id: card.id,
|
||||
source: "newtab",
|
||||
advertiser: card.advertiser,
|
||||
position: card.pos + 1, // positions are 1-based for telemetry
|
||||
// Keep the 0-based position, can be adjusted by the telemetry
|
||||
// sender if necessary.
|
||||
position: card.pos,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -7249,8 +7249,9 @@ class ImpressionStats_ImpressionStats extends (external_React_default()).PureCom
|
||||
tile_id: card.id,
|
||||
source: "newtab",
|
||||
advertiser: card.advertiser,
|
||||
position: card.pos + 1 // positions are 1-based for telemetry
|
||||
|
||||
// Keep the 0-based position, can be adjusted by the telemetry
|
||||
// sender if necessary.
|
||||
position: card.pos
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
Region: "resource://gre/modules/Region.sys.mjs",
|
||||
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
|
||||
PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
|
||||
Sampling: "resource://gre/modules/components-utils/Sampling.sys.mjs",
|
||||
});
|
||||
ChromeUtils.defineModuleGetter(
|
||||
lazy,
|
||||
@ -68,6 +69,17 @@ XPCOMUtils.defineLazyGetter(lazy, "log", () => {
|
||||
return new Logger("TopSitesFeed");
|
||||
});
|
||||
|
||||
// `contextId` is a unique identifier used by Contextual Services
|
||||
const CONTEXT_ID_PREF = "browser.contextual-services.contextId";
|
||||
XPCOMUtils.defineLazyGetter(lazy, "contextId", () => {
|
||||
let _contextId = Services.prefs.getStringPref(CONTEXT_ID_PREF, null);
|
||||
if (!_contextId) {
|
||||
_contextId = String(Services.uuid.generateUUID());
|
||||
Services.prefs.setStringPref(CONTEXT_ID_PREF, _contextId);
|
||||
}
|
||||
return _contextId;
|
||||
});
|
||||
|
||||
const DEFAULT_SITES_PREF = "default.sites";
|
||||
const SHOWN_ON_NEWTAB_PREF = "feeds.topsites";
|
||||
const DEFAULT_TOP_SITES = [];
|
||||
@ -93,6 +105,8 @@ const NIMBUS_VARIABLE_MAX_SPONSORED = "topSitesMaxSponsored";
|
||||
//considered for Top Sites.
|
||||
const NIMBUS_VARIABLE_ADDITIONAL_TILES =
|
||||
"topSitesUseAdditionalTilesFromContile";
|
||||
// Nimbus variable to enable the SOV feature for sponsored tiles.
|
||||
const NIMBUS_VARIABLE_CONTILE_SOV_ENABLED = "topSitesContileSovEnabled";
|
||||
|
||||
// Search experiment stuff
|
||||
const FILTER_DEFAULT_SEARCH_PREF = "improvesearch.noDefaultSearchTile";
|
||||
@ -123,6 +137,14 @@ const CONTILE_CACHE_PREF = "browser.topsites.contile.cachedTiles";
|
||||
const CONTILE_CACHE_VALID_FOR_PREF = "browser.topsites.contile.cacheValidFor";
|
||||
const CONTILE_CACHE_LAST_FETCH_PREF = "browser.topsites.contile.lastFetch";
|
||||
|
||||
// Partners of sponsored tiles.
|
||||
const SPONSORED_TILE_PARTNER_AMP = "amp";
|
||||
const SPONSORED_TILE_PARTNER_MOZ_SALES = "moz-sales";
|
||||
const SPONSORED_TILE_PARTNERS = new Set([
|
||||
SPONSORED_TILE_PARTNER_AMP,
|
||||
SPONSORED_TILE_PARTNER_MOZ_SALES,
|
||||
]);
|
||||
|
||||
function getShortURLForCurrentSearch() {
|
||||
const url = shortURL({ url: Services.search.defaultEngine.searchForm });
|
||||
return url;
|
||||
@ -133,12 +155,18 @@ class ContileIntegration {
|
||||
this._topSitesFeed = topSitesFeed;
|
||||
this._lastPeriodicUpdate = 0;
|
||||
this._sites = [];
|
||||
// The Share-of-Voice object managed by Shepherd and sent via Contile.
|
||||
this._sov = null;
|
||||
}
|
||||
|
||||
get sites() {
|
||||
return this._sites;
|
||||
}
|
||||
|
||||
get sov() {
|
||||
return this._sov;
|
||||
}
|
||||
|
||||
periodicUpdate() {
|
||||
let now = Date.now();
|
||||
if (now - this._lastPeriodicUpdate >= CONTILE_UPDATE_INTERVAL) {
|
||||
@ -266,6 +294,10 @@ class ContileIntegration {
|
||||
return false;
|
||||
}
|
||||
const body = await response.json();
|
||||
|
||||
if (body?.sov) {
|
||||
this._sov = JSON.parse(atob(body.sov));
|
||||
}
|
||||
if (body?.tiles && Array.isArray(body.tiles)) {
|
||||
const useAdditionalTiles = lazy.NimbusFeatures.newtab.getVariable(
|
||||
NIMBUS_VARIABLE_ADDITIONAL_TILES
|
||||
@ -458,6 +490,7 @@ class TopSitesFeed {
|
||||
sponsored_click_url: site.click_url,
|
||||
sponsored_impression_url: site.impression_url,
|
||||
sponsored_tile_id: site.id,
|
||||
partner: SPONSORED_TILE_PARTNER_AMP,
|
||||
};
|
||||
if (site.image_url && site.image_size >= MIN_FAVICON_SIZE) {
|
||||
// Only use the image from Contile if it's hi-res, otherwise, fallback
|
||||
@ -751,7 +784,13 @@ class TopSitesFeed {
|
||||
return false;
|
||||
}
|
||||
|
||||
insertDiscoveryStreamSpocs(sponsored) {
|
||||
/**
|
||||
* Fetch topsites spocs from the DiscoveryStream feed.
|
||||
*
|
||||
* @returns {Array} An array of sponsored tile objects.
|
||||
*/
|
||||
fetchDiscoveryStreamSpocs() {
|
||||
let sponsored = [];
|
||||
const { DiscoveryStream } = this.store.getState();
|
||||
if (DiscoveryStream) {
|
||||
const discoveryStreamSpocs =
|
||||
@ -814,11 +853,13 @@ class TopSitesFeed {
|
||||
sponsored_position: positionIndex + 1,
|
||||
// This is used for topsites deduping.
|
||||
hostname: shortURL({ url: spoc.url }),
|
||||
partner: SPONSORED_TILE_PARTNER_MOZ_SALES,
|
||||
};
|
||||
sponsored.push(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sponsored;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-statements
|
||||
@ -850,15 +891,8 @@ class TopSitesFeed {
|
||||
}
|
||||
|
||||
// Get defaults.
|
||||
let date = new Date();
|
||||
let pad = number => number.toString().padStart(2, "0");
|
||||
let yyyymmddhh =
|
||||
String(date.getFullYear()) +
|
||||
pad(date.getMonth() + 1) +
|
||||
pad(date.getDate()) +
|
||||
pad(date.getHours());
|
||||
let contileSponsored = [];
|
||||
let notBlockedDefaultSites = [];
|
||||
let sponsored = [];
|
||||
for (let link of DEFAULT_TOP_SITES) {
|
||||
// For sponsored Yandex links, default filtering is reversed: we only
|
||||
// show them if Yandex is the default search engine.
|
||||
@ -877,24 +911,6 @@ class TopSitesFeed {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// Process %YYYYMMDDHH% tag in the URL.
|
||||
let url_end;
|
||||
let url_start;
|
||||
if (this._useRemoteSetting) {
|
||||
[url_start, url_end] = link.url.split("%YYYYMMDDHH%");
|
||||
}
|
||||
if (typeof url_end === "string") {
|
||||
link = {
|
||||
...link,
|
||||
// Save original URL without %YYYYMMDDHH% replaced so it can be
|
||||
// blocked properly.
|
||||
original_url: link.url,
|
||||
url: url_start + yyyymmddhh + url_end,
|
||||
};
|
||||
if (link.url_urlbar) {
|
||||
link.url_urlbar = link.url_urlbar.replace("%YYYYMMDDHH%", yyyymmddhh);
|
||||
}
|
||||
}
|
||||
// If we've previously blocked a search shortcut, remove the default top site
|
||||
// that matches the hostname
|
||||
const searchProvider = getSearchProvider(shortURL(link));
|
||||
@ -908,7 +924,7 @@ class TopSitesFeed {
|
||||
if (!prefValues[SHOW_SPONSORED_PREF]) {
|
||||
continue;
|
||||
}
|
||||
sponsored[link.sponsored_position - 1] = link;
|
||||
contileSponsored[link.sponsored_position - 1] = link;
|
||||
|
||||
// Unpin search shortcut if present for the sponsored link to be shown
|
||||
// instead.
|
||||
@ -922,7 +938,12 @@ class TopSitesFeed {
|
||||
}
|
||||
}
|
||||
|
||||
this.insertDiscoveryStreamSpocs(sponsored);
|
||||
const discoverySponsored = this.fetchDiscoveryStreamSpocs();
|
||||
|
||||
const sponsored = await this._mergeSponsoredLinks({
|
||||
[SPONSORED_TILE_PARTNER_AMP]: contileSponsored,
|
||||
[SPONSORED_TILE_PARTNER_MOZ_SALES]: discoverySponsored,
|
||||
});
|
||||
|
||||
this._maybeCapSponsoredLinks(sponsored);
|
||||
|
||||
@ -1061,6 +1082,73 @@ class TopSitesFeed {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge sponsored links from all the partners using SOV if present.
|
||||
* For each tile position, the user is assigned to one partner via stable sampling.
|
||||
* If the chosen partner doesn't have a tile to serve, another tile from a different
|
||||
* partner is used as the replacement.
|
||||
*
|
||||
* @param {Object} sponsoredLinks An object with sponsored links from all the partners.
|
||||
* @returns {Array} An array of merged sponsored links.
|
||||
*/
|
||||
async _mergeSponsoredLinks(sponsoredLinks) {
|
||||
if (
|
||||
!this._contile.sov ||
|
||||
!lazy.NimbusFeatures.pocketNewtab.getVariable(
|
||||
NIMBUS_VARIABLE_CONTILE_SOV_ENABLED
|
||||
)
|
||||
) {
|
||||
return Object.values(sponsoredLinks).flat();
|
||||
}
|
||||
|
||||
const sampleInput = `${lazy.contextId}-${this._contile.sov.name}`;
|
||||
let sponsored = [];
|
||||
|
||||
for (const allocation of this._contile.sov.allocations) {
|
||||
let link = null;
|
||||
let chosenPartner = null;
|
||||
const ratios = allocation.allocation.map(alloc => alloc.percentage);
|
||||
if (ratios.length) {
|
||||
const index = await lazy.Sampling.ratioSample(sampleInput, ratios);
|
||||
chosenPartner = allocation.allocation[index].partner;
|
||||
// Unknown partners are allowed so that new parters can be added to Shepherd
|
||||
// sooner without waiting for client changes.
|
||||
link = sponsoredLinks[chosenPartner]?.shift();
|
||||
}
|
||||
|
||||
if (!link) {
|
||||
// If the chosen partner doesn't have a tile for this postion, choose any
|
||||
// one from another group. For simplicity, we do _not_ do resampling here
|
||||
// against the remaining partners.
|
||||
for (const partner of SPONSORED_TILE_PARTNERS) {
|
||||
if (
|
||||
partner === chosenPartner ||
|
||||
sponsoredLinks[partner].length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
link = sponsoredLinks[partner].shift();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!link) {
|
||||
// No more links to be added across all the partners, just return.
|
||||
return sponsored;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the position fields. Note that postion is also 1-based in SOV.
|
||||
link.sponsored_position = allocation.position;
|
||||
if (link.pos !== undefined) {
|
||||
// Pocket `pos` is 0-based.
|
||||
link.pos = allocation.position - 1;
|
||||
}
|
||||
sponsored.push(link);
|
||||
}
|
||||
|
||||
return sponsored;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach TippyTop icon to the given search shortcut
|
||||
*
|
||||
|
@ -177,7 +177,7 @@ describe("<ImpressionStats>", () => {
|
||||
tile_id: 1,
|
||||
source: "newtab",
|
||||
advertiser: "test advertiser",
|
||||
position: 2,
|
||||
position: 1,
|
||||
});
|
||||
});
|
||||
it("should send an impression when the wrapped item transiting from invisible to visible", () => {
|
||||
|
@ -62,6 +62,7 @@ describe("Top Sites Feed", () => {
|
||||
let fakePageThumbs;
|
||||
let fetchStub;
|
||||
let fakeNimbusFeatures;
|
||||
let fakeSampling;
|
||||
|
||||
beforeEach(() => {
|
||||
globals = new GlobalOverrider();
|
||||
@ -122,6 +123,9 @@ describe("Top Sites Feed", () => {
|
||||
getVariable: sinon.stub(),
|
||||
},
|
||||
};
|
||||
fakeSampling = {
|
||||
ratioSample: sinon.stub(),
|
||||
};
|
||||
globals.set({
|
||||
PageThumbs: fakePageThumbs,
|
||||
NewTabUtils: fakeNewTabUtils,
|
||||
@ -130,6 +134,7 @@ describe("Top Sites Feed", () => {
|
||||
LinksCache,
|
||||
FilterAdult: filterAdultStub,
|
||||
Screenshots: fakeScreenshot,
|
||||
Sampling: fakeSampling,
|
||||
});
|
||||
sandbox.spy(global.XPCOMUtils, "defineLazyGetter");
|
||||
FAKE_GLOBAL_PREFS.set("default.sites", "https://foo.com/");
|
||||
@ -186,13 +191,18 @@ describe("Top Sites Feed", () => {
|
||||
}
|
||||
|
||||
describe("#constructor", () => {
|
||||
it("should defineLazyGetter for log and _currentSearchHostname", () => {
|
||||
assert.calledTwice(global.XPCOMUtils.defineLazyGetter);
|
||||
it("should defineLazyGetter for log, contextId, and _currentSearchHostname", () => {
|
||||
assert.calledThrice(global.XPCOMUtils.defineLazyGetter);
|
||||
|
||||
let spyCall = global.XPCOMUtils.defineLazyGetter.getCall(0);
|
||||
assert.ok(spyCall.calledWith(sinon.match.any, "log", sinon.match.func));
|
||||
|
||||
spyCall = global.XPCOMUtils.defineLazyGetter.getCall(1);
|
||||
assert.ok(
|
||||
spyCall.calledWith(sinon.match.any, "contextId", sinon.match.func)
|
||||
);
|
||||
|
||||
spyCall = global.XPCOMUtils.defineLazyGetter.getCall(2);
|
||||
assert.ok(
|
||||
spyCall.calledWith(feed, "_currentSearchHostname", sinon.match.func)
|
||||
);
|
||||
@ -2187,6 +2197,73 @@ describe("Top Sites Feed", () => {
|
||||
assert.equal(feed._contile.sites.length, 2);
|
||||
});
|
||||
|
||||
it("should fetch SOV (Share-of-Voice) settings from Contile", async () => {
|
||||
const sov = {
|
||||
name: "SOV-20230518215316",
|
||||
allocations: [
|
||||
{
|
||||
position: 1,
|
||||
allocation: [
|
||||
{
|
||||
partner: "foo",
|
||||
percentage: 100,
|
||||
},
|
||||
{
|
||||
partner: "bar",
|
||||
percentage: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
position: 2,
|
||||
allocation: [
|
||||
{
|
||||
partner: "foo",
|
||||
percentage: 80,
|
||||
},
|
||||
{
|
||||
partner: "bar",
|
||||
percentage: 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: new Map([
|
||||
["cache-control", "private, max-age=859, stale-if-error=10463"],
|
||||
]),
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
sov: btoa(JSON.stringify(sov)),
|
||||
tiles: [
|
||||
{
|
||||
url: "https://www.test.com",
|
||||
image_url: "images/test-com.png",
|
||||
click_url: "https://www.test-click.com",
|
||||
impression_url: "https://www.test-impression.com",
|
||||
name: "test",
|
||||
},
|
||||
{
|
||||
url: "https://www.test1.com",
|
||||
image_url: "images/test1-com.png",
|
||||
click_url: "https://www.test1-click.com",
|
||||
impression_url: "https://www.test1-impression.com",
|
||||
name: "test1",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const fetched = await feed._contile._fetchSites();
|
||||
|
||||
assert.ok(fetched);
|
||||
assert.deepEqual(feed._contile.sov, sov);
|
||||
assert.equal(feed._contile.sites.length, 2);
|
||||
});
|
||||
|
||||
it("should not fetch from Contile if it's not enabled", async () => {
|
||||
fakeNimbusFeatures.newtab.getVariable.reset();
|
||||
fakeNimbusFeatures.newtab.getVariable.returns(false);
|
||||
@ -2564,6 +2641,140 @@ describe("Top Sites Feed", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#_mergeSponsoredLinks", () => {
|
||||
let fakeSponsoredLinks;
|
||||
let sov;
|
||||
beforeEach(() => {
|
||||
fakeSponsoredLinks = {
|
||||
amp: [
|
||||
{
|
||||
url: "https://www.test.com",
|
||||
image_url: "images/test-com.png",
|
||||
click_url: "https://www.test-click.com",
|
||||
impression_url: "https://www.test-impression.com",
|
||||
name: "test",
|
||||
partner: "amp",
|
||||
sponsored_position: 1,
|
||||
},
|
||||
{
|
||||
url: "https://www.test1.com",
|
||||
image_url: "images/test1-com.png",
|
||||
click_url: "https://www.test1-click.com",
|
||||
impression_url: "https://www.test1-impression.com",
|
||||
name: "test1",
|
||||
partner: "amp",
|
||||
sponsored_position: 2,
|
||||
},
|
||||
],
|
||||
"moz-sales": [
|
||||
{
|
||||
url: "https://foo.com",
|
||||
image_url: "images/foo-com.png",
|
||||
click_url: "https://www.foo-click.com",
|
||||
impression_url: "https://www.foo-impression.com",
|
||||
name: "foo",
|
||||
partner: "moz-sales",
|
||||
pos: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
sov = {
|
||||
name: "SOV-20230518215316",
|
||||
allocations: [
|
||||
{
|
||||
position: 1,
|
||||
allocation: [
|
||||
{
|
||||
partner: "amp",
|
||||
percentage: 100,
|
||||
},
|
||||
{
|
||||
partner: "moz-sales",
|
||||
percentage: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
position: 2,
|
||||
allocation: [
|
||||
{
|
||||
partner: "amp",
|
||||
percentage: 80,
|
||||
},
|
||||
{
|
||||
partner: "moz-sales",
|
||||
percentage: 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it("should join sponsored links if the sov object is absent", async () => {
|
||||
sandbox.stub(feed._contile, "sov").get(() => null);
|
||||
|
||||
const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);
|
||||
|
||||
assert.deepEqual(sponsored, Object.values(fakeSponsoredLinks).flat());
|
||||
});
|
||||
|
||||
it("should join sponosred links if the SOV Nimbus variable is disabled", async () => {
|
||||
fakeNimbusFeatures.pocketNewtab.getVariable.returns(false);
|
||||
const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);
|
||||
|
||||
assert.deepEqual(sponsored, Object.values(fakeSponsoredLinks).flat());
|
||||
});
|
||||
|
||||
it("should pick sponsored links based on sov configurations", async () => {
|
||||
sandbox.stub(feed._contile, "sov").get(() => sov);
|
||||
fakeNimbusFeatures.pocketNewtab.getVariable.returns(true);
|
||||
global.Sampling.ratioSample.onCall(0).resolves(0);
|
||||
global.Sampling.ratioSample.onCall(1).resolves(1);
|
||||
|
||||
const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);
|
||||
|
||||
assert.equal(sponsored.length, 2);
|
||||
assert.equal(sponsored[0].partner, "amp");
|
||||
assert.equal(sponsored[0].sponsored_position, 1);
|
||||
assert.equal(sponsored[1].partner, "moz-sales");
|
||||
assert.equal(sponsored[1].sponsored_position, 2);
|
||||
assert.equal(sponsored[1].pos, 1);
|
||||
});
|
||||
|
||||
it("should fall back to other partners if the chosen partner does not have any links", async () => {
|
||||
sandbox.stub(feed._contile, "sov").get(() => sov);
|
||||
fakeNimbusFeatures.pocketNewtab.getVariable.returns(true);
|
||||
global.Sampling.ratioSample.onCall(0).resolves(0);
|
||||
global.Sampling.ratioSample.onCall(1).resolves(0);
|
||||
|
||||
fakeSponsoredLinks.amp = [];
|
||||
const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);
|
||||
|
||||
assert.equal(sponsored.length, 1);
|
||||
assert.equal(sponsored[0].partner, "moz-sales");
|
||||
assert.equal(sponsored[0].sponsored_position, 1);
|
||||
assert.equal(sponsored[0].pos, 0);
|
||||
});
|
||||
|
||||
it("should return an empty array if none of the partners have links", async () => {
|
||||
sandbox.stub(feed._contile, "sov").get(() => sov);
|
||||
fakeNimbusFeatures.pocketNewtab.getVariable.returns(true);
|
||||
global.Sampling.ratioSample.onCall(0).resolves(0);
|
||||
global.Sampling.ratioSample.onCall(1).resolves(0);
|
||||
|
||||
fakeSponsoredLinks.amp = [];
|
||||
fakeSponsoredLinks["moz-sales"] = [];
|
||||
const sponsored = await feed._mergeSponsoredLinks(fakeSponsoredLinks);
|
||||
|
||||
assert.equal(sponsored.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#_readDefaults", () => {
|
||||
beforeEach(() => {
|
||||
// Turn on sponsored TopSites for testing
|
||||
|
@ -680,6 +680,12 @@ pocketNewtab:
|
||||
# Defined under `pocketNewtab` as it needs to be used along with other variables
|
||||
type: int
|
||||
description: The maximum number of sponsored Top Sites to be displayed
|
||||
topSitesContileSovEnabled:
|
||||
# Defined under `pocketNewtab` as it needs to be used along with other variables
|
||||
description: Enable the Share-of-Voice feature for Sponsored Topsites.
|
||||
type: boolean
|
||||
fallbackPref: >-
|
||||
browser.topsites.contile.sov.enabled
|
||||
|
||||
saveToPocket:
|
||||
description: The save to Pocket feature
|
||||
|
Loading…
Reference in New Issue
Block a user