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