tor-browser

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

commit 81f7f765376fa7dbf278cc9edec9d8f4a06bd6ad
parent 730809f3983bb5d9ecaed7c305fea480f3f1e575
Author: Irene Ni <ini@mozilla.com>
Date:   Thu,  8 Jan 2026 21:56:22 +0000

Bug 2008733 - Use random tile as fallback if no frecency match is found. r=nbarrett,home-newtab-reviewers

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

Diffstat:
Mbrowser/extensions/newtab/lib/TopSitesFeed.sys.mjs | 22++++++++++++++++------
Mbrowser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyDeduping.js | 5+++++
Mbrowser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyRanking.js | 44++++++++++++++++++++++++++++++++++++++------
3 files changed, 59 insertions(+), 12 deletions(-)

diff --git a/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs b/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs @@ -1391,13 +1391,23 @@ export class TopSitesFeed { ) { const { values } = this.store.getState().Prefs; const numItems = values?.trainhopConfig?.sov?.numItems; + const randomSponsorEnabled = values?.trainhopConfig?.sov?.random_sponsor; + + if (!randomSponsorEnabled) { + candidates = await this.frecencyBoostProvider.fetch(numItems); + // If we have a matched set of candidates, + // we can check if it's an exposure event. + if (candidates.length) { + this.frecencyBoostedSpocsExposureEvent(); + } + } - candidates = await this.frecencyBoostProvider.fetch(numItems); - - // If we have a matched set of candidates, - // we can check if it's an exposure event. - if (candidates.length) { - this.frecencyBoostedSpocsExposureEvent(); + if (!candidates.length) { + const randomTile = + await this.frecencyBoostProvider.retrieveRandomFrecencyTile(); + if (randomTile) { + candidates = [randomTile]; + } } } return candidates; diff --git a/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyDeduping.js b/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyDeduping.js @@ -85,6 +85,11 @@ async function getTopSitesFeedForTest( .stub(feed.frecencyBoostProvider, "_frecencyBoostedSponsors") .value(frecencyBoostedSponsors); + // Stub random fallback to return null for testing deduping logic. + sandbox + .stub(feed.frecencyBoostProvider, "retrieveRandomFrecencyTile") + .returns(null); + // We need to refresh, because TopSitesFeed's // DEFAULT_TOP_SITES acts like a singleton. DEFAULT_TOP_SITES.length = 0; diff --git a/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyRanking.js b/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyRanking.js @@ -109,12 +109,13 @@ add_task(async function test_frecency_sponsored_topsites() { { info( "TopSitesFeed.fetchFrecencyBoostedSpocs - " + - "Should return an empty array with no history" + "Should return random fallback tile with no history" ); const feed = await getTopSitesFeedForTest(sandbox); const frecencyBoostedSpocs = await feed.fetchFrecencyBoostedSpocs(); - Assert.equal(frecencyBoostedSpocs.length, 0); + Assert.equal(frecencyBoostedSpocs.length, 1); + Assert.equal(frecencyBoostedSpocs[0].type, "frecency-boost-random"); sandbox.restore(); } @@ -190,6 +191,7 @@ add_task(async function test_frecency_sponsored_topsites() { const frecencyBoostedSpocs = await feed.fetchFrecencyBoostedSpocs(); Assert.equal(frecencyBoostedSpocs.length, 1); Assert.equal(frecencyBoostedSpocs[0].hostname, "domain1"); + Assert.equal(frecencyBoostedSpocs[0].type, "frecency-boost"); sandbox.restore(); } @@ -210,13 +212,14 @@ add_task(async function test_frecency_sponsored_topsites() { const frecencyBoostedSpocs = await feed.fetchFrecencyBoostedSpocs(); Assert.equal(frecencyBoostedSpocs.length, 1); Assert.equal(frecencyBoostedSpocs[0].hostname, "domain1"); + Assert.equal(frecencyBoostedSpocs[0].type, "frecency-boost"); sandbox.restore(); } { info( "TopSitesFeed.fetchFrecencyBoostedSpocs - " + - "Should not return a match with a different subdomain" + "Should not return a match with a different subdomain (returns random fallback)" ); const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ @@ -228,7 +231,8 @@ add_task(async function test_frecency_sponsored_topsites() { }); const frecencyBoostedSpocs = await feed.fetchFrecencyBoostedSpocs(); - Assert.equal(frecencyBoostedSpocs.length, 0); + Assert.equal(frecencyBoostedSpocs.length, 1); + Assert.equal(frecencyBoostedSpocs[0].type, "frecency-boost-random"); sandbox.restore(); } @@ -249,13 +253,14 @@ add_task(async function test_frecency_sponsored_topsites() { const frecencyBoostedSpocs = await feed.fetchFrecencyBoostedSpocs(); Assert.equal(frecencyBoostedSpocs.length, 1); Assert.equal(frecencyBoostedSpocs[0].hostname, "sub.domain1"); + Assert.equal(frecencyBoostedSpocs[0].type, "frecency-boost"); sandbox.restore(); } { info( "TopSitesFeed.fetchFrecencyBoostedSpocs - " + - "Should not match a partial domain" + "Should not match a partial domain (returns random fallback)" ); const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ @@ -267,7 +272,34 @@ add_task(async function test_frecency_sponsored_topsites() { }); const frecencyBoostedSpocs = await feed.fetchFrecencyBoostedSpocs(); - Assert.equal(frecencyBoostedSpocs.length, 0); + Assert.equal(frecencyBoostedSpocs.length, 1); + Assert.equal(frecencyBoostedSpocs[0].type, "frecency-boost-random"); + + sandbox.restore(); + } + { + info( + "TopSitesFeed.fetchFrecencyBoostedSpocs - " + + "Control group always returns random tile (ignores frecency matches)" + ); + const feed = await getTopSitesFeedForTest(sandbox, { + frecent: [ + { + url: "https://domain1.com", + frecency: 1234, + }, + ], + }); + + feed.store.state.Prefs.values.trainhopConfig = { + sov: { + random_sponsor: true, + }, + }; + + const frecencyBoostedSpocs = await feed.fetchFrecencyBoostedSpocs(); + Assert.equal(frecencyBoostedSpocs.length, 1); + Assert.equal(frecencyBoostedSpocs[0].type, "frecency-boost-random"); sandbox.restore(); }