commit 88a1cd9922b66621a6f5df1902f89344e24655f0
parent bb4770a2e59324982ed5862018edd07dc809b4bd
Author: Nathan Barrett <nbarrett@mozilla.com>
Date: Fri, 5 Dec 2025 21:31:22 +0000
Bug 2004068 - Allow trainhopConfig to use multi-payload type r=mconley,home-newtab-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D275083
Diffstat:
2 files changed, 169 insertions(+), 5 deletions(-)
diff --git a/browser/extensions/newtab/lib/PrefsFeed.sys.mjs b/browser/extensions/newtab/lib/PrefsFeed.sys.mjs
@@ -102,19 +102,46 @@ export class PrefsFeed {
/**
* Handler for when experiment data updates.
+ * Supports two formats:
+ * - Single payload: { type: "feature", payload: { "enabled": true, ... }}
+ * - Multi-payload: { type: "multi-payload", payload: [{ type: "feature", payload: { "enabled": true, ... }}] }
+ * Both formats output the same structure: { "feature": { "enabled": true, ... }}
*/
onTrainhopExperimentUpdated() {
const allEnrollments =
lazy.NimbusFeatures.newtabTrainhop.getAllEnrollments() || [];
+ let enrollmentsToProcess = [];
+
+ allEnrollments.forEach(enrollment => {
+ if (
+ enrollment?.value?.type === "multi-payload" &&
+ Array.isArray(enrollment?.value?.payload)
+ ) {
+ enrollment.value.payload.forEach(item => {
+ if (item?.type && item?.payload) {
+ enrollmentsToProcess.push({
+ value: {
+ type: item.type,
+ payload: item.payload,
+ },
+ meta: enrollment.meta,
+ });
+ }
+ });
+ } else if (enrollment?.value?.type) {
+ enrollmentsToProcess.push(enrollment);
+ }
+ });
+
// Combine all trainhop experiments keyed by type.
// Rules for duplicates:
// - Experiments take precedence over rollouts (this is expected).
// - If multiple experiments or multiple rollouts exist for the same type,
// only the first is kept. This is nondeterministic and considered an error;
// those experiments/rollouts should be relaunched.
- const value = {};
- allEnrollments.reduce((accumulator, currentValue) => {
+ const valueObj = {};
+ enrollmentsToProcess.reduce((accumulator, currentValue) => {
if (currentValue?.value?.type) {
if (
!accumulator[currentValue.value.type] ||
@@ -122,8 +149,7 @@ export class PrefsFeed {
!currentValue.meta.isRollout)
) {
accumulator[currentValue.value.type] = currentValue;
- // Shorten the data chain.
- value[currentValue.value.type] = currentValue.value.payload;
+ valueObj[currentValue.value.type] = currentValue.value.payload;
}
}
return accumulator;
@@ -134,7 +160,7 @@ export class PrefsFeed {
type: at.PREF_CHANGED,
data: {
name: "trainhopConfig",
- value,
+ value: valueObj,
},
})
);
diff --git a/browser/extensions/newtab/test/unit/lib/PrefsFeed.test.js b/browser/extensions/newtab/test/unit/lib/PrefsFeed.test.js
@@ -285,6 +285,144 @@ describe("PrefsFeed", () => {
})
);
});
+ it("should handle multi-payload format with single enrollment", () => {
+ const testObject = {
+ meta: {
+ isRollout: false,
+ },
+ value: {
+ type: "multi-payload",
+ payload: [
+ {
+ type: "testExperiment",
+ payload: {
+ enabled: true,
+ name: "test-name",
+ },
+ },
+ ],
+ },
+ };
+ sandbox
+ .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments")
+ .returns([testObject]);
+ feed.onTrainhopExperimentUpdated();
+ assert.calledWith(
+ feed.store.dispatch,
+ ac.BroadcastToContent({
+ type: at.PREF_CHANGED,
+ data: {
+ name: "trainhopConfig",
+ value: {
+ testExperiment: {
+ enabled: true,
+ name: "test-name",
+ },
+ },
+ },
+ })
+ );
+ });
+ it("should handle multi-payload format with multiple items in single enrollment", () => {
+ const testObject = {
+ meta: {
+ isRollout: false,
+ },
+ value: {
+ type: "multi-payload",
+ payload: [
+ {
+ type: "testExperiment1",
+ payload: {
+ enabled: true,
+ },
+ },
+ {
+ type: "testExperiment2",
+ payload: {
+ enabled: false,
+ },
+ },
+ ],
+ },
+ };
+ sandbox
+ .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments")
+ .returns([testObject]);
+ feed.onTrainhopExperimentUpdated();
+ assert.calledWith(
+ feed.store.dispatch,
+ ac.BroadcastToContent({
+ type: at.PREF_CHANGED,
+ data: {
+ name: "trainhopConfig",
+ value: {
+ testExperiment1: {
+ enabled: true,
+ },
+ testExperiment2: {
+ enabled: false,
+ },
+ },
+ },
+ })
+ );
+ });
+ it("should dedupe multi-payload format with experiment taking precedence over rollout", () => {
+ const rollout = {
+ meta: {
+ isRollout: true,
+ },
+ value: {
+ type: "multi-payload",
+ payload: [
+ {
+ type: "testExperiment",
+ payload: {
+ enabled: false,
+ name: "rollout-name",
+ },
+ },
+ ],
+ },
+ };
+ const experiment = {
+ meta: {
+ isRollout: false,
+ },
+ value: {
+ type: "multi-payload",
+ payload: [
+ {
+ type: "testExperiment",
+ payload: {
+ enabled: true,
+ name: "experiment-name",
+ },
+ },
+ ],
+ },
+ };
+ sandbox
+ .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments")
+ .returns([rollout, experiment]);
+ feed.onTrainhopExperimentUpdated();
+ assert.calledWith(
+ feed.store.dispatch,
+ ac.BroadcastToContent({
+ type: at.PREF_CHANGED,
+ data: {
+ name: "trainhopConfig",
+ value: {
+ testExperiment: {
+ enabled: true,
+ name: "experiment-name",
+ },
+ },
+ },
+ })
+ );
+ });
});
it("should dispatch PREF_CHANGED when onWidgetsUpdated is called", () => {
sandbox