@@ -106,6 +106,12 @@ const PREF_UNIFIED_ADS_COUNTS = "discoverystream.placements.tiles.counts";
106106const PREF_UNIFIED_ADS_BLOCKED_LIST = "unifiedAds.blockedAds" ;
107107const PREF_UNIFIED_ADS_ADSFEED_ENABLED = "unifiedAds.adsFeed.enabled" ;
108108
109+ const PREF_SOV_ENABLED = "sov.enabled" ;
110+ const PREF_SOV_NAME = "sov.name" ;
111+ const PREF_SOV_AMP_ALLOCATION = "sov.amp.allocation" ;
112+ const PREF_SOV_FRECENCY_ALLOCATION = "sov.frecency.allocation" ;
113+ const DEFAULT_SOV_SLOT_COUNT = 3 ;
114+
109115// Search experiment stuff
110116const FILTER_DEFAULT_SEARCH_PREF = "improvesearch.noDefaultSearchTile" ;
111117const SEARCH_FILTERS = [
@@ -139,9 +145,11 @@ const CONTILE_CACHE_VALID_FOR_FALLBACK = 3 * 60 * 60; // 3 hours in seconds
139145// Partners of sponsored tiles.
140146const SPONSORED_TILE_PARTNER_AMP = "amp" ;
141147const SPONSORED_TILE_PARTNER_MOZ_SALES = "moz-sales" ;
148+ const SPONSORED_TILE_PARTNER_FREC_BOOST = "frec-boost" ;
142149const SPONSORED_TILE_PARTNERS = new Set ( [
143150 SPONSORED_TILE_PARTNER_AMP ,
144151 SPONSORED_TILE_PARTNER_MOZ_SALES ,
152+ SPONSORED_TILE_PARTNER_FREC_BOOST ,
145153] ) ;
146154
147155const DISPLAY_FAIL_REASON_OVERSOLD = "oversold" ;
@@ -515,6 +523,64 @@ export class ContileIntegration {
515523 return { tiles : formattedTileData } ;
516524 }
517525
526+ sovEnabled ( ) {
527+ return this . _topSitesFeed . store . getState ( ) . Prefs . values [ PREF_SOV_ENABLED ] ;
528+ }
529+
530+ csvToInts ( val ) {
531+ if ( ! val ) {
532+ return [ ] ;
533+ }
534+
535+ return val
536+ . split ( "," )
537+ . map ( s => s . trim ( ) )
538+ . filter ( item => item )
539+ . map ( item => parseInt ( item , 10 ) ) ;
540+ }
541+
542+ /**
543+ * Builds a Share of Voice (SOV) config.
544+ *
545+ * @example input data from prefs/trainhopConfig
546+ * // name: "SOV-20251122215625"
547+ * // amp: "100, 100, 100"
548+ * // frec: "0, 0, 0"
549+ *
550+ * @returns {{
551+ * name: string,
552+ * allocations: Array<{
553+ * position: number,
554+ * allocation: Array<{
555+ * partner: string,
556+ * percentage: number,
557+ * }>,
558+ * }>,
559+ * }}
560+ */
561+ generateSov ( ) {
562+ const { values } = this . _topSitesFeed . store . getState ( ) . Prefs ;
563+ const name = values [ PREF_SOV_NAME ] ;
564+ const amp = this . csvToInts ( values [ PREF_SOV_AMP_ALLOCATION ] ) ;
565+ const frec = this . csvToInts ( values [ PREF_SOV_FRECENCY_ALLOCATION ] ) ;
566+
567+ const allocations = Array . from (
568+ { length : DEFAULT_SOV_SLOT_COUNT } ,
569+ ( val , i ) => ( {
570+ position : i + 1 , // 1-based
571+ allocation : [
572+ { partner : SPONSORED_TILE_PARTNER_AMP , percentage : amp [ i ] || 0 } ,
573+ {
574+ partner : SPONSORED_TILE_PARTNER_FREC_BOOST ,
575+ percentage : frec [ i ] || 0 ,
576+ } ,
577+ ] ,
578+ } )
579+ ) ;
580+
581+ return { name, allocations } ;
582+ }
583+
518584 // eslint-disable-next-line max-statements
519585 async _fetchSites ( ) {
520586 if (
@@ -687,6 +753,8 @@ export class ContileIntegration {
687753 // Logic below runs the same regardless of ad source
688754 if ( body ?. sov ) {
689755 this . _sov = JSON . parse ( atob ( body . sov ) ) ;
756+ } else if ( this . sovEnabled ( ) ) {
757+ this . _sov = this . generateSov ( ) ;
690758 }
691759
692760 if ( body ?. tiles && Array . isArray ( body . tiles ) ) {
@@ -1244,6 +1312,16 @@ export class TopSitesFeed {
12441312 return fetch ( ...args ) ;
12451313 }
12461314
1315+ /**
1316+ * Fetch topsites spocs that are frecency boosted.
1317+ *
1318+ * @returns {Array } An array of sponsored tile objects.
1319+ */
1320+ fetchFrecencyBoostedSpocs ( ) {
1321+ let sponsored = [ ] ;
1322+ return sponsored ;
1323+ }
1324+
12471325 /**
12481326 * Fetch topsites spocs from the DiscoveryStream feed.
12491327 *
@@ -1417,11 +1495,13 @@ export class TopSitesFeed {
14171495 ) ;
14181496
14191497 const discoverySponsored = this . fetchDiscoveryStreamSpocs ( ) ;
1498+ const frecencyBoostedSponsored = this . fetchFrecencyBoostedSpocs ( ) ;
14201499 this . _telemetryUtility . setTiles ( discoverySponsored ) ;
14211500
14221501 const sponsored = this . _mergeSponsoredLinks ( {
14231502 [ SPONSORED_TILE_PARTNER_AMP ] : contileSponsored ,
14241503 [ SPONSORED_TILE_PARTNER_MOZ_SALES ] : discoverySponsored ,
1504+ [ SPONSORED_TILE_PARTNER_FREC_BOOST ] : frecencyBoostedSponsored ,
14251505 } ) ;
14261506
14271507 this . _maybeCapSponsoredLinks ( sponsored ) ;
0 commit comments