tor-browser

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

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:
Mbrowser/extensions/newtab/lib/PrefsFeed.sys.mjs | 36+++++++++++++++++++++++++++++++-----
Mbrowser/extensions/newtab/test/unit/lib/PrefsFeed.test.js | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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