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:
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();
}