tor-browser

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

commit 596d8db2a1a0c7864ca3ada9eb810490d0d2babb
parent b8e2b3b04fd931a2ba0db57510b76a39ab030e2e
Author: James Teow <jteow@mozilla.com>
Date:   Tue, 11 Nov 2025 02:37:13 +0000

Bug 1986285 - Part 3: Replace fixed frecency thresholds with places API in New Tab code - r=mlplyler,home-newtab-reviewers,thecount

Originally, I wanted to have this patch land prior to frecency graduation but the issue
was setting a threshold that kept the exact same behaviour for old frecency and had
similar results for new frecency.

To minimize the impact to New Tab / Top Sites, I chose a threshold that prevents
certain first page visits (e.g. clicking a link on a website, the browser
automatically opening a URL on a new build) from passing the threshold.

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

Diffstat:
Mbrowser/components/topsites/TopSites.sys.mjs | 22++++++++++++++++++++--
Mbrowser/components/topsites/test/unit/test_top_sites.js | 3++-
Mbrowser/extensions/newtab/lib/TopSitesFeed.sys.mjs | 29+++++++++++++++++++++++++++--
Mbrowser/extensions/newtab/test/unit/unit-entry.js | 15+++++++++++++++
Mbrowser/extensions/newtab/test/xpcshell/test_TopSitesFeed.js | 3++-
Mtoolkit/modules/NewTabUtils.sys.mjs | 23+++++++++++++++++++++--
Mtoolkit/modules/tests/xpcshell/test_NewTabUtils.js | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
7 files changed, 156 insertions(+), 31 deletions(-)

diff --git a/browser/components/topsites/TopSites.sys.mjs b/browser/components/topsites/TopSites.sys.mjs @@ -1,6 +1,10 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// eslint-disable-next-line mozilla/use-static-import +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); import { getDomain, @@ -33,9 +37,23 @@ ChromeUtils.defineLazyGetter(lazy, "log", () => { return new Logger("TopSites"); }); +ChromeUtils.defineLazyGetter(lazy, "pageFrecencyThreshold", () => { + // @backward-compat { version 147 } + // Frecency was changed in 147 Nightly. This is a pre-cautionary measure + // for train-hopping. + if (Services.vc.compare(AppConstants.MOZ_APP_VERSION, "147.0a1") >= 0) { + // 30 days ago, 5 visits. The threshold avoids one non-typed visit from + // immediately being included in recent history to mimic the original + // threshold which aimed to prevent first-run visits from being included in + // Top Sites. + return lazy.PlacesUtils.history.pageFrecencyThreshold(30, 5, false); + } + // The old threshold used for classic frecency: Slightly over one visit. + return 101; +}); + export const DEFAULT_TOP_SITES = []; -const FRECENCY_THRESHOLD = 100 + 1; // 1 visit (skip first-run/one-time pages) const MIN_FAVICON_SIZE = 96; const PINNED_FAVICON_PROPS_TO_MIGRATE = [ "favicon", @@ -619,7 +637,7 @@ class _TopSites { cache = await this.frecentCache.request({ // We need to overquery due to the top 5 alexa search + default search possibly being removed numItems: numItems + SEARCH_FILTERS.length + 1, - topsiteFrecency: FRECENCY_THRESHOLD, + topsiteFrecency: lazy.pageFrecencyThreshold, }); } catch (ex) { cache = []; diff --git a/browser/components/topsites/test/unit/test_top_sites.js b/browser/components/topsites/test/unit/test_top_sites.js @@ -21,7 +21,8 @@ ChromeUtils.defineESModuleGetters(this, { const FAKE_FAVICON = "data987"; const FAKE_FAVICON_SIZE = 128; -const FAKE_FRECENCY = 200; +// Two visits on the same day. +const FAKE_FRECENCY = PlacesUtils.history.pageFrecencyThreshold(0, 2, false); const FAKE_LINKS = new Array(2 * TOP_SITES_MAX_SITES_PER_ROW) .fill(null) .map((v, i) => ({ diff --git a/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs b/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs @@ -2,6 +2,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// AppConstants, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + import { actionCreators as ac, actionTypes as at, @@ -31,6 +42,7 @@ ChromeUtils.defineESModuleGetters(lazy, { ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs", PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs", PersistentCache: "resource://newtab/lib/PersistentCache.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", Region: "resource://gre/modules/Region.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", Sampling: "resource://gre/modules/components-utils/Sampling.sys.mjs", @@ -44,10 +56,23 @@ ChromeUtils.defineLazyGetter(lazy, "log", () => { return new Logger("TopSitesFeed"); }); +ChromeUtils.defineLazyGetter(lazy, "pageFrecencyThreshold", () => { + // @backward-compat { version 147 } + // Frecency was changed in 147 Nightly. + if (Services.vc.compare(AppConstants.MOZ_APP_VERSION, "147.0a1") >= 0) { + // 30 days ago, 5 visits. The threshold avoids one non-typed visit from + // immediately being included in recent history to mimic the original + // threshold which aimed to prevent first-run visits from being included in + // Top Sites. + return lazy.PlacesUtils.history.pageFrecencyThreshold(30, 5, false); + } + // The old threshold used for classic frecency: Slightly over one visit. + return 101; +}); + const DEFAULT_SITES_PREF = "default.sites"; const SHOWN_ON_NEWTAB_PREF = "feeds.topsites"; export const DEFAULT_TOP_SITES = []; -const FRECENCY_THRESHOLD = 100 + 1; // 1 visit (skip first-run/one-time pages) const MIN_FAVICON_SIZE = 96; const CACHED_LINK_PROPS_TO_MIGRATE = ["screenshot", "customScreenshot"]; const PINNED_FAVICON_PROPS_TO_MIGRATE = [ @@ -1328,7 +1353,7 @@ export class TopSitesFeed { const cache = await this.frecentCache.request({ // We need to overquery due to the top 5 alexa search + default search possibly being removed numItems: numFetch + SEARCH_FILTERS.length + 1, - topsiteFrecency: FRECENCY_THRESHOLD, + topsiteFrecency: lazy.pageFrecencyThreshold, }); for (let link of cache) { const hostname = lazy.NewTabUtils.shortURL(link); diff --git a/browser/extensions/newtab/test/unit/unit-entry.js b/browser/extensions/newtab/test/unit/unit-entry.js @@ -198,6 +198,7 @@ const TEST_GLOBAL = { insert() {}, markPageAsTyped() {}, removeObserver() {}, + pageFrecencyThreshold() {}, }, "@mozilla.org/io/string-input-stream;1": { createInstance() { @@ -445,6 +446,20 @@ const TEST_GLOBAL = { createNullPrincipal() {}, getSystemPrincipal() {}, }, + vc: { + compare(a, b) { + // Rather than re-write Services.vc.compare completely, do + // a simple comparison of the major version. + // This means this function will give the wrong output for differences + // in minor versions, but should be sufficient for unit tests. + let majorA = parseInt(a, 10); + let majorB = parseInt(b, 10); + if (majorA === majorB) { + return 0; + } + return majorA > majorB ? 1 : -1; + }, + }, wm: { getMostRecentWindow: () => window, getMostRecentBrowserWindow: () => window, diff --git a/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed.js b/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed.js @@ -12,6 +12,7 @@ ChromeUtils.defineESModuleGetters(this, { NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", sinon: "resource://testing-common/Sinon.sys.mjs", Screenshots: "resource://newtab/lib/Screenshots.sys.mjs", Sampling: "resource://gre/modules/components-utils/Sampling.sys.mjs", @@ -23,7 +24,7 @@ ChromeUtils.defineESModuleGetters(this, { const FAKE_FAVICON = "data987"; const FAKE_FAVICON_SIZE = 128; -const FAKE_FRECENCY = 200; +const FAKE_FRECENCY = PlacesUtils.history.pageFrecencyThreshold(0, 2, false); const FAKE_LINKS = new Array(2 * TOP_SITES_MAX_SITES_PER_ROW) .fill(null) .map((v, i) => ({ diff --git a/toolkit/modules/NewTabUtils.sys.mjs b/toolkit/modules/NewTabUtils.sys.mjs @@ -8,6 +8,11 @@ import { SEARCH_SHORTCUTS_EXPERIMENT, } from "moz-src:///toolkit/components/search/SearchShortcuts.sys.mjs"; +// eslint-disable-next-line mozilla/use-static-import +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -48,7 +53,18 @@ const LINKS_GET_LINKS_LIMIT = 100; const TOPIC_GATHER_TELEMETRY = "gather-telemetry"; // Some default frecency threshold for Activity Stream requests -const ACTIVITY_STREAM_DEFAULT_FRECENCY = 150; +ChromeUtils.defineLazyGetter(lazy, "pageFrecencyThreshold", () => { + // @backward-compat { version 147 } + // Frecency was graduated in 147 Nightly. + if (Services.vc.compare(AppConstants.MOZ_APP_VERSION, "147.0a1") >= 0) { + // 30 days ago, 7 visits. The threshold avoids one non-typed visit from + // immediately being included in recent history and is slightly higher than + // Top Sites to mimic how this had a higher threshold. + return lazy.PlacesUtils.history.pageFrecencyThreshold(30, 7, false); + } + // The old threshold used for classic frecency. + return 150; +}); // Some default query limit for Activity Stream requests const ACTIVITY_STREAM_DEFAULT_LIMIT = 12; @@ -999,7 +1015,7 @@ var ActivityStreamProvider = { { ignoreBlocked: false, numItems: ACTIVITY_STREAM_DEFAULT_LIMIT, - topsiteFrecency: ACTIVITY_STREAM_DEFAULT_FRECENCY, + topsiteFrecency: lazy.pageFrecencyThreshold, onePerDomain: true, includeFavicon: true, hideWithSearchParam: Services.prefs.getCharPref( @@ -1127,6 +1143,9 @@ var ActivityStreamProvider = { url => url.match(/:\/\/(?:www\.)?([^\/]+)/), // Combine frecencies when deduping these links (targetLink, otherLink) => { + // TODO: Experiment with max() vs sum() for frecency combination. + // Current additive approach may bias toward sites requiring + // deduplication. targetLink.frecency = link.frecency + otherLink.frecency; } ); diff --git a/toolkit/modules/tests/xpcshell/test_NewTabUtils.js b/toolkit/modules/tests/xpcshell/test_NewTabUtils.js @@ -13,6 +13,14 @@ const { PlacesUtils } = ChromeUtils.importESModule( "resource://gre/modules/PlacesUtils.sys.mjs" ); +// 0 days ago, 1 visit. This is to mimic the expectation of the original test, +// which is that a single visit should not be filtered when running tests. +const FRECENCY_THRESHOLD = PlacesUtils.history.pageFrecencyThreshold( + 0, + 1, + false +); + // const SEARCH_SHORTCUTS_EXPERIMENT_PREF = // "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts"; // Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, false); @@ -93,6 +101,22 @@ function getHistorySize() { ); } +async function getFrecencyForUrl(url) { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.executeCached( + ` + SELECT frecency + FROM moz_places + WHERE url = :url + `, + { + url, + } + ); + Assert.equal(rows?.length, 1, "Should have found a result."); + return rows[0].getResultByName("frecency"); +} + add_task(async function validCacheMidPopulation() { let expectedLinks = makeLinks(0, 3, 1); @@ -720,7 +744,9 @@ add_task(async function getTopFrecentSites() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; - let links = await provider.getTopSites({ topsiteFrecency: 100 }); + let links = await provider.getTopSites({ + topsiteFrecency: FRECENCY_THRESHOLD, + }); Assert.equal(links.length, 0, "empty history yields empty links"); // add a visit @@ -734,7 +760,7 @@ add_task(async function getTopFrecentSites() { "adding a single visit doesn't exceed default threshold" ); - links = await provider.getTopSites({ topsiteFrecency: 100 }); + links = await provider.getTopSites({ topsiteFrecency: FRECENCY_THRESHOLD }); Assert.equal(links.length, 1, "adding a visit yields a link"); Assert.equal(links[0].url, testURI, "added visit corresponds to added url"); }); @@ -757,7 +783,9 @@ add_task( await PlacesTestUtils.addVisits(testURI); let provider = NewTabUtils.activityStreamLinks; - let links = await provider.getTopSites({ topsiteFrecency: 100 }); + let links = await provider.getTopSites({ + topsiteFrecency: FRECENCY_THRESHOLD, + }); Assert.equal( links.length, 1, @@ -777,7 +805,9 @@ add_task(async function getTopFrecentSites_no_dedup() { await setUpActivityStreamTest(); let provider = NewTabUtils.activityStreamLinks; - let links = await provider.getTopSites({ topsiteFrecency: 100 }); + let links = await provider.getTopSites({ + topsiteFrecency: FRECENCY_THRESHOLD, + }); Assert.equal(links.length, 0, "empty history yields empty links"); // Add a visits in reverse order they will be returned in when not deduped. @@ -794,7 +824,7 @@ add_task(async function getTopFrecentSites_no_dedup() { "adding a single visit doesn't exceed default threshold" ); - links = await provider.getTopSites({ topsiteFrecency: 100 }); + links = await provider.getTopSites({ topsiteFrecency: FRECENCY_THRESHOLD }); Assert.equal(links.length, 1, "adding a visit yields a link"); // Plain domain is returned when deduped. Assert.equal( @@ -804,7 +834,7 @@ add_task(async function getTopFrecentSites_no_dedup() { ); links = await provider.getTopSites({ - topsiteFrecency: 100, + topsiteFrecency: FRECENCY_THRESHOLD, onePerDomain: false, }); Assert.equal(links.length, 2, "adding a visit yields a link"); @@ -825,55 +855,67 @@ add_task(async function getTopFrecentSites_dedupeWWW() { let provider = NewTabUtils.activityStreamLinks; - let links = await provider.getTopSites({ topsiteFrecency: 100 }); + let links = await provider.getTopSites({ + topsiteFrecency: FRECENCY_THRESHOLD, + }); Assert.equal(links.length, 0, "empty history yields empty links"); // add a visit without www - let testURI = "http://mozilla.com"; + let testURI = "http://mozilla.com/"; await PlacesTestUtils.addVisits(testURI); + let frecency1 = await getFrecencyForUrl(testURI); // add a visit with www - testURI = "http://www.mozilla.com"; + testURI = "http://www.mozilla.com/"; await PlacesTestUtils.addVisits(testURI); + let frecency2 = await getFrecencyForUrl(testURI); // Test combined frecency score - links = await provider.getTopSites({ topsiteFrecency: 100 }); + links = await provider.getTopSites({ topsiteFrecency: FRECENCY_THRESHOLD }); Assert.equal(links.length, 1, "adding both www. and no-www. yields one link"); - Assert.equal(links[0].frecency, 200, "frecency scores are combined"); + Assert.equal( + links[0].frecency, + frecency1 + frecency2, + "frecency scores are combined" + ); // add another page visit with www and without www let noWWW = "http://mozilla.com/page"; await PlacesTestUtils.addVisits(noWWW); + let noWWWFrecency = await getFrecencyForUrl(noWWW); let withWWW = "http://www.mozilla.com/page"; await PlacesTestUtils.addVisits(withWWW); - links = await provider.getTopSites({ topsiteFrecency: 100 }); + let withWWWFrecency = await getFrecencyForUrl(withWWW); + links = await provider.getTopSites({ topsiteFrecency: FRECENCY_THRESHOLD }); Assert.equal(links.length, 1, "adding both www. and no-www. yields one link"); Assert.equal( links[0].frecency, - 200, + noWWWFrecency + withWWWFrecency, "frecency scores are combined ignoring extra pages" ); // add another visit with www await PlacesTestUtils.addVisits(withWWW); - links = await provider.getTopSites({ topsiteFrecency: 100 }); + withWWWFrecency = await getFrecencyForUrl(withWWW); + links = await provider.getTopSites({ topsiteFrecency: FRECENCY_THRESHOLD }); Assert.equal(links.length, 1, "still yields one link"); Assert.equal(links[0].url, withWWW, "more frecent www link is used"); Assert.equal( links[0].frecency, - 300, + noWWWFrecency + withWWWFrecency, "frecency scores are combined ignoring extra pages" ); // add a couple more visits to the no-www page await PlacesTestUtils.addVisits(noWWW); await PlacesTestUtils.addVisits(noWWW); - links = await provider.getTopSites({ topsiteFrecency: 100 }); + noWWWFrecency = await getFrecencyForUrl(noWWW); + links = await provider.getTopSites({ topsiteFrecency: FRECENCY_THRESHOLD }); Assert.equal(links.length, 1, "still yields one link"); Assert.equal(links[0].url, noWWW, "now more frecent no-www link is used"); Assert.equal( links[0].frecency, - 500, + noWWWFrecency + withWWWFrecency, "frecency scores are combined ignoring extra pages" ); }); @@ -890,7 +932,9 @@ add_task(async function getTopFrencentSites_maxLimit() { await PlacesTestUtils.addVisits(testURI); } - let links = await provider.getTopSites({ topsiteFrecency: 100 }); + let links = await provider.getTopSites({ + topsiteFrecency: FRECENCY_THRESHOLD, + }); Assert.less( links.length, MANY_LINKS, @@ -908,21 +952,23 @@ add_task(async function getTopFrencentSites_allowedProtocols() { let testURI = "file:///some/file/path.png"; await PlacesTestUtils.addVisits(testURI); - let links = await provider.getTopSites({ topsiteFrecency: 100 }); + let links = await provider.getTopSites({ + topsiteFrecency: FRECENCY_THRESHOLD, + }); Assert.equal(links.length, 0, "don't get sites with the file:// protocol"); // now add a site with an allowed protocol testURI = "http://www.mozilla.com"; await PlacesTestUtils.addVisits(testURI); - links = await provider.getTopSites({ topsiteFrecency: 100 }); + links = await provider.getTopSites({ topsiteFrecency: FRECENCY_THRESHOLD }); Assert.equal(links.length, 1, "http:// is an allowed protocol"); // and just to be sure, add a visit to a site with ftp:// protocol testURI = "ftp://bad/example"; await PlacesTestUtils.addVisits(testURI); - links = await provider.getTopSites({ topsiteFrecency: 100 }); + links = await provider.getTopSites({ topsiteFrecency: FRECENCY_THRESHOLD }); Assert.equal( links.length, 1, @@ -933,7 +979,7 @@ add_task(async function getTopFrencentSites_allowedProtocols() { testURI = "https://https"; await PlacesTestUtils.addVisits(testURI); - links = await provider.getTopSites({ topsiteFrecency: 100 }); + links = await provider.getTopSites({ topsiteFrecency: FRECENCY_THRESHOLD }); Assert.equal( links.length, 2, @@ -1115,7 +1161,7 @@ add_task(async function getTopFrecentSites_hideWithSearchParam() { JSON.stringify(hideWithSearchParam) ); - let options = { topsiteFrecency: 100 }; + let options = { topsiteFrecency: FRECENCY_THRESHOLD }; if (hideWithSearchParam !== undefined) { options = { ...options, hideWithSearchParam }; }