From 399c45cd0a9e3a0887df94cee8da9bc755493b1f Mon Sep 17 00:00:00 2001 From: antoniogargaro Date: Mon, 30 Mar 2026 12:42:52 +0100 Subject: [PATCH 1/3] Permutive RTD: support msft bidder custom cohorts via configurable storage read Add a `bidders` config to the Permutive RTD module that allows configuring per-bidder custom cohort storage sources. This enables the msft bidder (Microsoft/Xandr) to read custom cohorts from the same localStorage key (`_papns`) used by appnexus, resolving an issue where publishers migrating from appnexus to msft lost custom cohort targeting. Refactors `updateOrtbConfig` to receive resolved `customCohortsData` directly instead of performing the lookup internally, keeping segment resolution centralized in `setBidderRtb`. Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/permutiveRtdProvider.js | 33 +++++++++-- test/spec/modules/permutiveCombined_spec.js | 65 +++++++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index 09697b6f906..ab9a561aed3 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -91,6 +91,11 @@ export function getModuleConfig(customModuleConfig) { acBidders: [], overwrites: {}, enforceVendorConsent: false, + bidders: { + msft: { + customCohorts: { source: 'ls', key: '_papns' } + }, + }, }, }, permutiveModuleConfig, @@ -108,6 +113,7 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { const acBidders = deepAccess(moduleConfig, 'params.acBidders') const maxSegs = deepAccess(moduleConfig, 'params.maxSegs') const transformationConfigs = deepAccess(moduleConfig, 'params.transformations') || [] + const biddersConfig = deepAccess(moduleConfig, 'params.bidders') || {} const ssps = segmentData?.ssp?.ssps ?? [] const sspCohorts = segmentData?.ssp?.cohorts ?? [] @@ -115,6 +121,7 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { const bidders = new Set([...acBidders, ...ssps]) bidders.forEach(function (bidder) { + const bidderConfig = biddersConfig[bidder] || {}; const currConfig = { ortb2: bidderOrtb2[bidder] || {} } let cohorts = [] @@ -129,11 +136,29 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { cohorts = [...new Set([...cohorts, ...sspCohorts])].slice(0, maxSegs) } - const nextConfig = updateOrtbConfig(bidder, currConfig, cohorts, sspCohorts, topics, transformationConfigs, segmentData) + const customCohortsData = getCustomCohortsData(bidderConfig, bidder, segmentData, maxSegs) + + const nextConfig = updateOrtbConfig(bidder, currConfig, cohorts, sspCohorts, topics, transformationConfigs, customCohortsData) bidderOrtb2[bidder] = nextConfig.ortb2 }) } +/** + * Resolves custom cohorts data for a bidder, reading from localStorage if configured. + * @param {Object} bidderCfg - Bidder-specific configuration from params.bidders + * @param {string} bidder - The bidder identifier + * @param {Object} segmentData - Segment data grouped by bidder or type + * @param {number} maxSegs - Maximum number of segments + * @return {string[]} Custom cohort IDs + */ +function getCustomCohortsData (bidderCfg, bidder, segmentData, maxSegs) { + const customCohorts = bidderCfg?.customCohorts + if (customCohorts?.source === 'ls' && customCohorts?.key) { + return makeSafe(() => readSegments(customCohorts.key, []).map(String).slice(0, maxSegs)) || [] + } + return deepAccess(segmentData, bidder) || [] +} + /** * Updates `user.data` object in existing bidder config with Permutive segments * @param {string} bidder - The bidder identifier @@ -143,14 +168,12 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { * @param {Object} topics - Privacy Sandbox Topics, keyed by IAB taxonomy version (600, 601, etc.) * @param {Object[]} transformationConfigs - array of objects with `id` and `config` properties, used to determine * the transformations on user data to include the ORTB2 object - * @param {Object} segmentData - The segments available for targeting + * @param {string[]} customCohortsData - Custom cohort IDs for this bidder * @return {Object} Merged ortb2 object */ -function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, topics, transformationConfigs, segmentData) { +function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, topics, transformationConfigs, customCohortsData) { logger.logInfo(`Current ortb2 config`, { bidder, config: currConfig }) - const customCohortsData = deepAccess(segmentData, bidder) || [] - const name = 'permutive.com' const permutiveUserData = { diff --git a/test/spec/modules/permutiveCombined_spec.js b/test/spec/modules/permutiveCombined_spec.js index 1870c2161d4..bb124260c42 100644 --- a/test/spec/modules/permutiveCombined_spec.js +++ b/test/spec/modules/permutiveCombined_spec.js @@ -128,6 +128,11 @@ describe('permutiveRtdProvider', function () { acBidders: [], overwrites: {}, enforceVendorConsent: false, + bidders: { + msft: { + customCohorts: { source: 'ls', key: '_papns' } + }, + }, }, }) @@ -689,6 +694,66 @@ describe('permutiveRtdProvider', function () { }) }) }) + + describe('bidders config with customCohorts', function () { + it('should read custom cohorts from localStorage for msft bidder using customCohorts config', function () { + const segmentsData = transformedTargeting() + const expectedAppnexusCohorts = segmentsData.appnexus + + const moduleConfig = { + name: 'permutive', + waitForIt: true, + params: { + acBidders: ['msft'], + maxSegs: 500, + bidders: { + msft: { + customCohorts: { source: 'ls', key: '_papns' } + } + } + } + } + const bidderConfig = {} + + setBidderRtb(bidderConfig, moduleConfig, segmentsData) + + expect(bidderConfig['msft'].user.data).to.deep.include.members([ + { + name: 'permutive', + segment: expectedAppnexusCohorts.map(id => ({ id })), + }, + ]) + + expectedAppnexusCohorts.forEach(id => { + expect(bidderConfig['msft'].user.keywords).to.include(`permutive=${id}`) + }) + }) + + it('should fall back to segmentData lookup when customCohorts is not configured', function () { + const segmentsData = transformedTargeting() + + const moduleConfig = { + name: 'permutive', + waitForIt: true, + params: { + acBidders: ['appnexus'], + maxSegs: 500, + bidders: {} + } + } + const bidderConfig = {} + + setBidderRtb(bidderConfig, moduleConfig, segmentsData) + + const expectedAppnexusCohorts = segmentsData.appnexus + expect(bidderConfig['appnexus'].user.data).to.deep.include.members([ + { + name: 'permutive', + segment: expectedAppnexusCohorts.map(id => ({ id })), + }, + ]) + }) + }) }) describe('Getting segments', function () { From bd0e822e9573b17a4a6bc17550d75d4ec76f7c2f Mon Sep 17 00:00:00 2001 From: antoniogargaro Date: Thu, 9 Apr 2026 16:12:50 +0100 Subject: [PATCH 2/3] docs: update permutiveRtdProvider.md for msft bidder custom cohorts Add msft to supported custom cohort bidders list, document the new params.bidders configuration, and note the appnexus-to-msft migration path. Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/permutiveRtdProvider.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md index 8ef632c75f5..e8094142bde 100644 --- a/modules/permutiveRtdProvider.md +++ b/modules/permutiveRtdProvider.md @@ -48,6 +48,10 @@ as well as enabling settings for specific use cases mentioned above (e.g. acbidd | params.acBidders | String[] | An array of bidder codes to share cohorts with in certain versions of Prebid, see below | `[]` | | params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` | | params.enforceVendorConsent | Boolean | When `true`, require TCF vendor consent for Permutive (vendor 361). See note below. | `false` | +| params.bidders | Object | Per-bidder configuration for custom cohort sources. Keys are bidder codes. | `{ msft: { customCohorts: { source: 'ls', key: '_papns' } } }` | +| params.bidders.\.customCohorts | Object | Custom cohorts source configuration for a specific bidder. | - | +| params.bidders.\.customCohorts.source | String | Storage type to read custom cohorts from. Currently only `'ls'` (localStorage) is supported. | - | +| params.bidders.\.customCohorts.key | String | The localStorage key to read custom cohorts from. | - | #### Consent @@ -81,11 +85,14 @@ Currently, bidders with known support for custom cohort targeting are: - Xandr - Magnite +- Microsoft (msft) When enabling the respective Activation for a cohort in Permutive, this module will automatically attach that cohort ID to the bid request. There is no need to enable individual bidders in the module configuration, it will automatically reflect which SSP integrations you have enabled in your Permutive dashboard. Permutive cohorts will be sent in the permutive key-value. +**Note:** Publishers migrating from the `appnexus` bidder to `msft` should add `msft` to the `acBidders` array (or via the Permutive platform). The module automatically reads custom cohorts for `msft` from the same localStorage key used by `appnexus`. + ### _Enabling Advertiser Cohorts_ From b2464da21d59bf99fa068a6c42e62515438153c2 Mon Sep 17 00:00:00 2001 From: antoniogargaro Date: Tue, 14 Apr 2026 17:18:53 +0100 Subject: [PATCH 3/3] remove hardcoded msft default, document publisher config instead Per review feedback, removes the default msft bidder config from getModuleConfig to avoid publishers upgrading solely for built-in compatibility. Publishers now need to explicitly configure the bidders param for msft custom cohorts. Updated docs with example config. Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/permutiveRtdProvider.js | 6 +----- modules/permutiveRtdProvider.md | 15 +++++++++++++-- test/spec/modules/permutiveCombined_spec.js | 6 +----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index ab9a561aed3..f5a37d934d6 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -91,11 +91,7 @@ export function getModuleConfig(customModuleConfig) { acBidders: [], overwrites: {}, enforceVendorConsent: false, - bidders: { - msft: { - customCohorts: { source: 'ls', key: '_papns' } - }, - }, + bidders: {}, }, }, permutiveModuleConfig, diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md index e8094142bde..fc13a5e6a4e 100644 --- a/modules/permutiveRtdProvider.md +++ b/modules/permutiveRtdProvider.md @@ -48,7 +48,7 @@ as well as enabling settings for specific use cases mentioned above (e.g. acbidd | params.acBidders | String[] | An array of bidder codes to share cohorts with in certain versions of Prebid, see below | `[]` | | params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` | | params.enforceVendorConsent | Boolean | When `true`, require TCF vendor consent for Permutive (vendor 361). See note below. | `false` | -| params.bidders | Object | Per-bidder configuration for custom cohort sources. Keys are bidder codes. | `{ msft: { customCohorts: { source: 'ls', key: '_papns' } } }` | +| params.bidders | Object | Per-bidder configuration for custom cohort sources. Keys are bidder codes. | `{}` | | params.bidders.\.customCohorts | Object | Custom cohorts source configuration for a specific bidder. | - | | params.bidders.\.customCohorts.source | String | Storage type to read custom cohorts from. Currently only `'ls'` (localStorage) is supported. | - | | params.bidders.\.customCohorts.key | String | The localStorage key to read custom cohorts from. | - | @@ -91,7 +91,18 @@ When enabling the respective Activation for a cohort in Permutive, this module w There is no need to enable individual bidders in the module configuration, it will automatically reflect which SSP integrations you have enabled in your Permutive dashboard. Permutive cohorts will be sent in the permutive key-value. -**Note:** Publishers migrating from the `appnexus` bidder to `msft` should add `msft` to the `acBidders` array (or via the Permutive platform). The module automatically reads custom cohorts for `msft` from the same localStorage key used by `appnexus`. +**Note:** Publishers migrating from the `appnexus` bidder to `msft` need to configure the `params.bidders` object so the module knows where to read custom cohorts from. The Permutive SDK writes `msft` custom cohorts to the same localStorage key used by `appnexus` (`_papns`), so publishers should add the following to their Permutive RTD config: + +```javascript +params: { + acBidders: ['msft'], + bidders: { + msft: { + customCohorts: { source: 'ls', key: '_papns' } + } + } +} +``` ### _Enabling Advertiser Cohorts_ diff --git a/test/spec/modules/permutiveCombined_spec.js b/test/spec/modules/permutiveCombined_spec.js index bb124260c42..60d3672b8d8 100644 --- a/test/spec/modules/permutiveCombined_spec.js +++ b/test/spec/modules/permutiveCombined_spec.js @@ -128,11 +128,7 @@ describe('permutiveRtdProvider', function () { acBidders: [], overwrites: {}, enforceVendorConsent: false, - bidders: { - msft: { - customCohorts: { source: 'ls', key: '_papns' } - }, - }, + bidders: {}, }, })