tor-browser

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

commit 50168e596be6f68f34fcb62c6f6ec27551a4856f
parent a0a1d82648223e9659299307eb4d8a438edf501f
Author: Mitchell Plyler <mlplyler@mozilla.com>
Date:   Fri, 31 Oct 2025 20:47:18 +0000

Bug 1996666 - add smart shortcuts telemetry r=rrando,home-newtab-reviewers,thecount

Adds telemetry for smart shortcuts scores and weights on clicks and impressions.

Adds fields "smart_scores" and "smart_weights". These are dictionaries mapping features about potential shortcut urls to features and weights respectively. These features are like frecency in that they focus on how and when a site is visited instead of the content of the site. The values in these dictionaries are lower precision (4).

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

Diffstat:
Mbrowser/components/newtab/metrics.yaml | 10++++++++++
Mbrowser/extensions/newtab/content-src/components/TopSites/TopSite.jsx | 4++++
Mbrowser/extensions/newtab/data/content/activity-stream.bundle.js | 8++++++--
Mbrowser/extensions/newtab/lib/SmartShortcutsRanker/RankShortcuts.mjs | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/extensions/newtab/lib/TelemetryFeed.sys.mjs | 14++++++++++++++
Mbrowser/extensions/newtab/test/xpcshell/test_ShortcutRanker.js | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/extensions/newtab/test/xpcshell/test_TelemetryFeed.js | 8++++++++
7 files changed, 142 insertions(+), 2 deletions(-)

diff --git a/browser/components/newtab/metrics.yaml b/browser/components/newtab/metrics.yaml @@ -1397,6 +1397,7 @@ topsites: - https://bugzilla.mozilla.org/show_bug.cgi?id=1820707#c3 - https://bugzilla.mozilla.org/show_bug.cgi?id=1821556#c3 - https://bugzilla.mozilla.org/show_bug.cgi?id=1824842#c7 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1996666#c7 data_sensitivity: - interaction notification_emails: @@ -1428,6 +1429,12 @@ topsites: visible_topsites: &visible_topsites description: The quantity of visible topsites when event occurred type: quantity + smart_scores: &smart_scores + description: stringified json storing scores for each feature during smart shortcut ranking + type: string + smart_weights: &smart_weights + description: stringified json storing weights for each feature during smart shortcut ranking + type: string send_in_pings: - newtab @@ -1449,6 +1456,7 @@ topsites: - https://bugzilla.mozilla.org/show_bug.cgi?id=1820707#c3 - https://bugzilla.mozilla.org/show_bug.cgi?id=1821556#c3 - https://bugzilla.mozilla.org/show_bug.cgi?id=1824842#c7 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1996666#c7 data_sensitivity: - interaction notification_emails: @@ -1466,6 +1474,8 @@ topsites: position: *topsite_position is_pinned: *is_pinned visible_topsites: *visible_topsites + smart_scores: *smart_scores + smart_weights: *smart_weights send_in_pings: - newtab diff --git a/browser/extensions/newtab/content-src/components/TopSites/TopSite.jsx b/browser/extensions/newtab/content-src/components/TopSites/TopSite.jsx @@ -355,6 +355,8 @@ export class TopSiteLink extends React.PureComponent { isPinned: this.props.link.isPinned, guid: this.props.link.guid, visible_topsites: visibleTopSites, + smartScores: this.props.link.scores, + smartWeights: this.props.link.weights, }} // For testing. IntersectionObserver={this.props.IntersectionObserver} @@ -598,6 +600,8 @@ export class TopSite extends React.PureComponent { isPinned: this.props.link.isPinned, guid: this.props.link.guid, visible_topsites: this.props.visibleTopSites, + smartScores: this.props.link.scores, + smartWeights: this.props.link.weights, }, }) ); diff --git a/browser/extensions/newtab/data/content/activity-stream.bundle.js b/browser/extensions/newtab/data/content/activity-stream.bundle.js @@ -9170,7 +9170,9 @@ class TopSiteLink extends (external_React_default()).PureComponent { source: NEWTAB_SOURCE, isPinned: this.props.link.isPinned, guid: this.props.link.guid, - visible_topsites: visibleTopSites + visible_topsites: visibleTopSites, + smartScores: this.props.link.scores, + smartWeights: this.props.link.weights } // For testing. , @@ -9384,7 +9386,9 @@ class TopSite extends (external_React_default()).PureComponent { source: NEWTAB_SOURCE, isPinned: this.props.link.isPinned, guid: this.props.link.guid, - visible_topsites: this.props.visibleTopSites + visible_topsites: this.props.visibleTopSites, + smartScores: this.props.link.scores, + smartWeights: this.props.link.weights } })); } diff --git a/browser/extensions/newtab/lib/SmartShortcutsRanker/RankShortcuts.mjs b/browser/extensions/newtab/lib/SmartShortcutsRanker/RankShortcuts.mjs @@ -60,6 +60,7 @@ const FEATURES = ["frec", "thom", "bias"]; const SHORTCUT_POSITIVE_PRIOR = 1; const SHORTCUT_NEGATIVE_PRIOR = 1000; const STICKY_NUMIMPS = 0; +const SMART_TELEM = false; const lazy = {}; @@ -73,6 +74,25 @@ ChromeUtils.defineESModuleGetters(lazy, { import { sortKeysValues } from "resource://newtab/lib/SmartShortcutsRanker/ThomSample.mjs"; +// helper for lowering precision of numbers, save space in telemetry +// longest string i can come up with out of this function: +// -0.000009999 which is 12 characters +export const roundNum = (x, sig = 4, eps = 1e-6) => { + if (typeof x !== "number" || !isFinite(x)) { + return x; + } + + // clip very small absolute values to zero + if (Math.abs(x) < eps) { + return 0; + } + + const n = Number(x.toPrecision(sig)); + + // normalize -0 to 0 + return Object.is(n, -0) ? 0 : n; +}; + /** * For each guid, look at its last 10 shortcut interactions and, if a click occurred, * return the position of the (most recent) click within those 10. @@ -1013,6 +1033,33 @@ export class RankShortcutsProvider { } // grab topsites without guid const combined = sortedSites.concat(withoutGuid); + + // tack weights and scores so they can pass through to telemetry + if (prefValues?.trainhopConfig?.smartShortcuts?.telem || SMART_TELEM) { + // store a version of weights that is rounded + const roundWeights = Object.fromEntries( + Object.entries(weights ?? {}).map(([key, v]) => [ + key, + typeof v === "number" && isFinite(v) ? roundNum(v) : (v ?? null), + ]) + ); + // do the tacking + combined.forEach(s => { + const raw = output?.score_map?.[s.guid]; + s.scores = + raw && typeof raw === "object" + ? Object.fromEntries( + Object.entries(raw).map(([k, v]) => [ + k, + typeof v === "number" && isFinite(v) + ? roundNum(v) + : (v ?? null), + ]) + ) + : null; + s.weights = roundWeights; + }); + } return combined; } } diff --git a/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs b/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs @@ -691,6 +691,13 @@ export class TelemetryFeed { position: action.data.position, is_pinned: !!action.data.isPinned, visible_topsites, + // @backward-compat { version 146 } This newtab train-hop compatibility + // shim can be removed once Firefox 146 makes it to the release channel. + ...(Services.vc.compare(AppConstants.MOZ_APP_VERSION, "146.0a1") >= + 0 && { + smart_scores: JSON.stringify(action.data.smartScores), + smart_weights: JSON.stringify(action.data.smartWeights), + }), }); break; @@ -701,6 +708,13 @@ export class TelemetryFeed { position: action.data.position, is_pinned: !!action.data.isPinned, visible_topsites, + // @backward-compat { version 146 } This newtab train-hop compatibility + // shim can be removed once Firefox 146 makes it to the release channel. + ...(Services.vc.compare(AppConstants.MOZ_APP_VERSION, "146.0a1") >= + 0 && { + smart_scores: JSON.stringify(action.data.smartScores), + smart_weights: JSON.stringify(action.data.smartWeights), + }), }); break; diff --git a/browser/extensions/newtab/test/xpcshell/test_ShortcutRanker.js b/browser/extensions/newtab/test/xpcshell/test_ShortcutRanker.js @@ -937,6 +937,7 @@ add_task(async function test_rankTopSites_sql_pipeline_happy_path() { positive_prior: 1, negative_prior: 1, fset: 8, + telem: true, }, }, }; @@ -948,6 +949,14 @@ add_task(async function test_rankTopSites_sql_pipeline_happy_path() { // assertions Assert.ok(Array.isArray(out), "returns an array"); + Assert.ok( + out.every( + site => + Object.prototype.hasOwnProperty.call(site, "scores") && + Object.prototype.hasOwnProperty.call(site, "weights") + ), + "Every shortcut should expose scores and weights (even when null) because telem=true" + ); Assert.equal( out[out.length - 1].url, "https://no-guid.com", @@ -1743,3 +1752,47 @@ add_task(async function test_rankTopSites_feature_matrix() { clock.restore(); sandbox.restore(); }); + +add_task(function test_roundNum_precision_and_edge_cases() { + const { roundNum } = ChromeUtils.importESModule( + "resource://newtab/lib/SmartShortcutsRanker/RankShortcuts.mjs" + ); + + Assert.equal( + roundNum(1.23456789), + 1.235, + "roundNum rounds to four significant digits by default" + ); + Assert.equal( + roundNum(987.65, 2), + 990, + "roundNum respects the requested precision" + ); + Assert.equal( + roundNum(5e-7), + 0, + "roundNum clamps magnitudes smaller than eps to zero" + ); + Assert.equal( + roundNum(-5e-7), + 0, + "roundNum clamps small negative magnitudes to zero" + ); + Assert.equal( + roundNum(5e-7, 4, 1e-9), + 5e-7, + "roundNum uses the provided eps override when keeping small values" + ); + Assert.strictEqual( + roundNum(Number.POSITIVE_INFINITY), + Number.POSITIVE_INFINITY, + "roundNum leaves non-finite numbers unchanged" + ); + Assert.ok(Number.isNaN(roundNum(NaN)), "roundNum returns NaN for NaN"); + const sentinel = { foo: "bar" }; + Assert.strictEqual( + roundNum(sentinel), + sentinel, + "roundNum returns non-number inputs unchanged" + ); +}); diff --git a/browser/extensions/newtab/test/xpcshell/test_TelemetryFeed.js b/browser/extensions/newtab/test/xpcshell/test_TelemetryFeed.js @@ -1820,6 +1820,8 @@ add_task( source: "newtab", position: 0, isPinned: false, + smartScores: { moo: 1 }, + smartWeights: { moo: 0 }, }; const SESSION_ID = "decafc0ffee"; sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); @@ -1833,6 +1835,8 @@ add_task( newtab_visit_id: SESSION_ID, is_sponsored: String(false), position: String(0), + smart_scores: JSON.stringify({ moo: 1 }), + smart_weights: JSON.stringify({ moo: 0 }), }); sandbox.restore(); @@ -1855,6 +1859,8 @@ add_task( source: "newtab", position: 0, isPinned: false, + smartScores: { moo: 1 }, + smartWeights: { moo: 0 }, }; const SESSION_ID = "decafc0ffee"; sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); @@ -1868,6 +1874,8 @@ add_task( is_sponsored: String(false), position: String(0), is_pinned: String(false), + smart_scores: JSON.stringify({ moo: 1 }), + smart_weights: JSON.stringify({ moo: 0 }), }); sandbox.restore();