tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 42c441f64dc73cc11a61a7530ee73eb8b0343d92
parent fba50614bc3690f892513791c3f891b0e786234a
Author: scottdowne <sdowne@mozilla.com>
Date:   Wed, 26 Nov 2025 19:56:12 +0000

Bug 2002078 - Newtab frecency based tile sorting SOV r=nbarrett,home-newtab-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D273902

Diffstat:
Mbrowser/extensions/newtab/lib/ActivityStream.sys.mjs | 29+++++++++++++++++++++++++++++
Mbrowser/extensions/newtab/lib/TopSitesFeed.sys.mjs | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 109 insertions(+), 0 deletions(-)

diff --git a/browser/extensions/newtab/lib/ActivityStream.sys.mjs b/browser/extensions/newtab/lib/ActivityStream.sys.mjs @@ -956,6 +956,35 @@ export const PREFS_CONFIG = new Map([ }, ], [ + "sov.enabled", + { + title: "Enables share of voice (SOV)", + value: false, + }, + ], + [ + "sov.name", + { + title: + "A unique id, usually this is a timestamp for the day it was generated", + value: "SOV-20251122215625", + }, + ], + [ + "sov.amp.allocation", + { + title: "How many positions can be filled from amp", + value: "100, 100, 100", + }, + ], + [ + "sov.frecency.allocation", + { + title: "How many positions can be filled by frecency", + value: "0, 0, 0", + }, + ], + [ "widgets.system.enabled", { title: "Enables visibility of all widgets and controls to enable them", diff --git a/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs b/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs @@ -106,6 +106,12 @@ const PREF_UNIFIED_ADS_COUNTS = "discoverystream.placements.tiles.counts"; const PREF_UNIFIED_ADS_BLOCKED_LIST = "unifiedAds.blockedAds"; const PREF_UNIFIED_ADS_ADSFEED_ENABLED = "unifiedAds.adsFeed.enabled"; +const PREF_SOV_ENABLED = "sov.enabled"; +const PREF_SOV_NAME = "sov.name"; +const PREF_SOV_AMP_ALLOCATION = "sov.amp.allocation"; +const PREF_SOV_FRECENCY_ALLOCATION = "sov.frecency.allocation"; +const DEFAULT_SOV_SLOT_COUNT = 3; + // Search experiment stuff const FILTER_DEFAULT_SEARCH_PREF = "improvesearch.noDefaultSearchTile"; const SEARCH_FILTERS = [ @@ -139,9 +145,11 @@ const CONTILE_CACHE_VALID_FOR_FALLBACK = 3 * 60 * 60; // 3 hours in seconds // Partners of sponsored tiles. const SPONSORED_TILE_PARTNER_AMP = "amp"; const SPONSORED_TILE_PARTNER_MOZ_SALES = "moz-sales"; +const SPONSORED_TILE_PARTNER_FREC_BOOST = "frec-boost"; const SPONSORED_TILE_PARTNERS = new Set([ SPONSORED_TILE_PARTNER_AMP, SPONSORED_TILE_PARTNER_MOZ_SALES, + SPONSORED_TILE_PARTNER_FREC_BOOST, ]); const DISPLAY_FAIL_REASON_OVERSOLD = "oversold"; @@ -515,6 +523,64 @@ export class ContileIntegration { return { tiles: formattedTileData }; } + sovEnabled() { + return this._topSitesFeed.store.getState().Prefs.values[PREF_SOV_ENABLED]; + } + + csvToInts(val) { + if (!val) { + return []; + } + + return val + .split(",") + .map(s => s.trim()) + .filter(item => item) + .map(item => parseInt(item, 10)); + } + + /** + * Builds a Share of Voice (SOV) config. + * + * @example input data from prefs/trainhopConfig + * // name: "SOV-20251122215625" + * // amp: "100, 100, 100" + * // frec: "0, 0, 0" + * + * @returns {{ + * name: string, + * allocations: Array<{ + * position: number, + * allocation: Array<{ + * partner: string, + * percentage: number, + * }>, + * }>, + * }} + */ + generateSov() { + const { values } = this._topSitesFeed.store.getState().Prefs; + const name = values[PREF_SOV_NAME]; + const amp = this.csvToInts(values[PREF_SOV_AMP_ALLOCATION]); + const frec = this.csvToInts(values[PREF_SOV_FRECENCY_ALLOCATION]); + + const allocations = Array.from( + { length: DEFAULT_SOV_SLOT_COUNT }, + (val, i) => ({ + position: i + 1, // 1-based + allocation: [ + { partner: SPONSORED_TILE_PARTNER_AMP, percentage: amp[i] || 0 }, + { + partner: SPONSORED_TILE_PARTNER_FREC_BOOST, + percentage: frec[i] || 0, + }, + ], + }) + ); + + return { name, allocations }; + } + // eslint-disable-next-line max-statements async _fetchSites() { if ( @@ -687,6 +753,8 @@ export class ContileIntegration { // Logic below runs the same regardless of ad source if (body?.sov) { this._sov = JSON.parse(atob(body.sov)); + } else if (this.sovEnabled()) { + this._sov = this.generateSov(); } if (body?.tiles && Array.isArray(body.tiles)) { @@ -1245,6 +1313,16 @@ export class TopSitesFeed { } /** + * Fetch topsites spocs that are frecency boosted. + * + * @returns {Array} An array of sponsored tile objects. + */ + fetchFrecencyBoostedSpocs() { + let sponsored = []; + return sponsored; + } + + /** * Fetch topsites spocs from the DiscoveryStream feed. * * @returns {Array} An array of sponsored tile objects. @@ -1417,11 +1495,13 @@ export class TopSitesFeed { ); const discoverySponsored = this.fetchDiscoveryStreamSpocs(); + const frecencyBoostedSponsored = this.fetchFrecencyBoostedSpocs(); this._telemetryUtility.setTiles(discoverySponsored); const sponsored = this._mergeSponsoredLinks({ [SPONSORED_TILE_PARTNER_AMP]: contileSponsored, [SPONSORED_TILE_PARTNER_MOZ_SALES]: discoverySponsored, + [SPONSORED_TILE_PARTNER_FREC_BOOST]: frecencyBoostedSponsored, }); this._maybeCapSponsoredLinks(sponsored);