Skip to content

Commit 61e9fd4

Browse files
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) <noreply@anthropic.com>
1 parent 5ce10de commit 61e9fd4

2 files changed

Lines changed: 93 additions & 5 deletions

File tree

modules/permutiveRtdProvider.js

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ export function getModuleConfig(customModuleConfig) {
9191
acBidders: [],
9292
overwrites: {},
9393
enforceVendorConsent: false,
94+
bidders: {
95+
msft: {
96+
customCohortsFrom: { type: 'ls', key: '_papns' }
97+
},
98+
},
9499
},
95100
},
96101
permutiveModuleConfig,
@@ -108,13 +113,15 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) {
108113
const acBidders = deepAccess(moduleConfig, 'params.acBidders')
109114
const maxSegs = deepAccess(moduleConfig, 'params.maxSegs')
110115
const transformationConfigs = deepAccess(moduleConfig, 'params.transformations') || []
116+
const biddersConfig = deepAccess(moduleConfig, 'params.bidders') || {}
111117

112118
const ssps = segmentData?.ssp?.ssps ?? []
113119
const sspCohorts = segmentData?.ssp?.cohorts ?? []
114120
const topics = segmentData?.topics ?? {}
115121

116122
const bidders = new Set([...acBidders, ...ssps])
117123
bidders.forEach(function (bidder) {
124+
const bidderConfig = biddersConfig[bidder] || {};
118125
const currConfig = { ortb2: bidderOrtb2[bidder] || {} }
119126

120127
let cohorts = []
@@ -129,11 +136,29 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) {
129136
cohorts = [...new Set([...cohorts, ...sspCohorts])].slice(0, maxSegs)
130137
}
131138

132-
const nextConfig = updateOrtbConfig(bidder, currConfig, cohorts, sspCohorts, topics, transformationConfigs, segmentData)
139+
const customCohortsData = getCustomCohortsData(bidderConfig, bidder, segmentData, maxSegs)
140+
141+
const nextConfig = updateOrtbConfig(bidder, currConfig, cohorts, sspCohorts, topics, transformationConfigs, customCohortsData)
133142
bidderOrtb2[bidder] = nextConfig.ortb2
134143
})
135144
}
136145

146+
/**
147+
* Resolves custom cohorts data for a bidder, reading from localStorage if configured.
148+
* @param {Object} bidderCfg - Bidder-specific configuration from params.bidders
149+
* @param {string} bidder - The bidder identifier
150+
* @param {Object} segmentData - Segment data grouped by bidder or type
151+
* @param {number} maxSegs - Maximum number of segments
152+
* @return {string[]} Custom cohort IDs
153+
*/
154+
function getCustomCohortsData (bidderCfg, bidder, segmentData, maxSegs) {
155+
const customCohortsFrom = bidderCfg?.customCohortsFrom
156+
if (customCohortsFrom?.type === 'ls' && customCohortsFrom?.key) {
157+
return makeSafe(() => readSegments(customCohortsFrom.key, []).map(String).slice(0, maxSegs)) || []
158+
}
159+
return deepAccess(segmentData, bidder) || []
160+
}
161+
137162
/**
138163
* Updates `user.data` object in existing bidder config with Permutive segments
139164
* @param {string} bidder - The bidder identifier
@@ -143,14 +168,12 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) {
143168
* @param {Object} topics - Privacy Sandbox Topics, keyed by IAB taxonomy version (600, 601, etc.)
144169
* @param {Object[]} transformationConfigs - array of objects with `id` and `config` properties, used to determine
145170
* the transformations on user data to include the ORTB2 object
146-
* @param {Object} segmentData - The segments available for targeting
171+
* @param {string[]} customCohortsData - Custom cohort IDs for this bidder
147172
* @return {Object} Merged ortb2 object
148173
*/
149-
function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, topics, transformationConfigs, segmentData) {
174+
function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, topics, transformationConfigs, customCohortsData) {
150175
logger.logInfo(`Current ortb2 config`, { bidder, config: currConfig })
151176

152-
const customCohortsData = deepAccess(segmentData, bidder) || []
153-
154177
const name = 'permutive.com'
155178

156179
const permutiveUserData = {

test/spec/modules/permutiveCombined_spec.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ describe('permutiveRtdProvider', function () {
128128
acBidders: [],
129129
overwrites: {},
130130
enforceVendorConsent: false,
131+
bidders: {
132+
msft: {
133+
customCohortsFrom: { type: 'ls', key: '_papns' }
134+
},
135+
},
131136
},
132137
})
133138

@@ -689,6 +694,66 @@ describe('permutiveRtdProvider', function () {
689694
})
690695
})
691696
})
697+
698+
describe('bidders config with customCohortsFrom', function () {
699+
it('should read custom cohorts from localStorage for msft bidder using customCohortsFrom config', function () {
700+
const segmentsData = transformedTargeting()
701+
const expectedAppnexusCohorts = segmentsData.appnexus
702+
703+
const moduleConfig = {
704+
name: 'permutive',
705+
waitForIt: true,
706+
params: {
707+
acBidders: ['msft'],
708+
maxSegs: 500,
709+
bidders: {
710+
msft: {
711+
customCohortsFrom: { type: 'ls', key: '_papns' }
712+
}
713+
}
714+
}
715+
}
716+
const bidderConfig = {}
717+
718+
setBidderRtb(bidderConfig, moduleConfig, segmentsData)
719+
720+
expect(bidderConfig['msft'].user.data).to.deep.include.members([
721+
{
722+
name: 'permutive',
723+
segment: expectedAppnexusCohorts.map(id => ({ id })),
724+
},
725+
])
726+
727+
expectedAppnexusCohorts.forEach(id => {
728+
expect(bidderConfig['msft'].user.keywords).to.include(`permutive=${id}`)
729+
})
730+
})
731+
732+
it('should fall back to segmentData lookup when customCohortsFrom is not configured', function () {
733+
const segmentsData = transformedTargeting()
734+
735+
const moduleConfig = {
736+
name: 'permutive',
737+
waitForIt: true,
738+
params: {
739+
acBidders: ['appnexus'],
740+
maxSegs: 500,
741+
bidders: {}
742+
}
743+
}
744+
const bidderConfig = {}
745+
746+
setBidderRtb(bidderConfig, moduleConfig, segmentsData)
747+
748+
const expectedAppnexusCohorts = segmentsData.appnexus
749+
expect(bidderConfig['appnexus'].user.data).to.deep.include.members([
750+
{
751+
name: 'permutive',
752+
segment: expectedAppnexusCohorts.map(id => ({ id })),
753+
},
754+
])
755+
})
756+
})
692757
})
693758

694759
describe('Getting segments', function () {

0 commit comments

Comments
 (0)