Skip to content
29 changes: 24 additions & 5 deletions modules/permutiveRtdProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function getModuleConfig(customModuleConfig) {
acBidders: [],
overwrites: {},
enforceVendorConsent: false,
bidders: {},
},
},
permutiveModuleConfig,
Expand All @@ -108,13 +109,15 @@ 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 ?? []
const topics = segmentData?.topics ?? {}

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

let cohorts = []
Expand All @@ -129,11 +132,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
Expand All @@ -143,14 +164,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 = {
Expand Down
18 changes: 18 additions & 0 deletions modules/permutiveRtdProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. | `{}` |
| params.bidders.\<bidder\>.customCohorts | Object | Custom cohorts source configuration for a specific bidder. | - |
| params.bidders.\<bidder\>.customCohorts.source | String | Storage type to read custom cohorts from. Currently only `'ls'` (localStorage) is supported. | - |
| params.bidders.\<bidder\>.customCohorts.key | String | The localStorage key to read custom cohorts from. | - |

#### Consent

Expand Down Expand Up @@ -81,11 +85,25 @@ 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` 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:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about rubicon -> magnite


```javascript
params: {
acBidders: ['msft'],
bidders: {
msft: {
customCohorts: { source: 'ls', key: '_papns' }
}
}
}
```


### _Enabling Advertiser Cohorts_

Expand Down
61 changes: 61 additions & 0 deletions test/spec/modules/permutiveCombined_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ describe('permutiveRtdProvider', function () {
acBidders: [],
overwrites: {},
enforceVendorConsent: false,
bidders: {},
},
})

Expand Down Expand Up @@ -689,6 +690,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 () {
Expand Down
Loading