Skip to content

Commit

Permalink
Permutive RTD module: support IAB Audience taxonomy
Browse files Browse the repository at this point in the history
Updates the Permutive RTD module to facilitate for segmentation by
the new IAB Audience taxonomy.

To achieve this, this change introduces the concept of "transformations"
on the ORT2B `user.data` object. There are two components to these
transformations: a new `transformations` property on the Prebid config,
to be set by the publisher, and logic in the module for the actual
behaviour of the transformation.

We plan to use the transformation logic in this PR, combined with
configuration we'll share with publishers, to send IAB Audience
taxonomy cohort IDs to bidders.
  • Loading branch information
desbo committed Mar 31, 2022
1 parent 15f7ec1 commit 000411a
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 34 deletions.
22 changes: 20 additions & 2 deletions integrationExamples/gpt/permutiveRtdProvider_example.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@
}
},
bids: [
{
bidder: 'ix',
params: {
siteId: '123456',
}
},
{
bidder: 'appnexus',
params: {
Expand Down Expand Up @@ -135,15 +141,27 @@
pbjs.que.push(function() {
pbjs.setConfig({
debug: true,
pageUrl: 'http://www.test.com/test.html',
realTimeData: {
auctionDelay: 80, // maximum time for RTD modules to respond
dataProviders: [
{
name: 'permutive',
waitForIt: true,
params: {
acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx'],
acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'],
maxSegs: 500,
transformations: [
{
id: 'iabAudienceTaxonomy11',
config: {
iabIds: {
1000001: '777777',
1000002: '888888'
}
}
}
],
overwrites: {
rubicon: function (bid, data, acEnabled, utils, defaultFn) {
if (defaultFn){
Expand All @@ -160,7 +178,7 @@
}
});
pbjs.setBidderConfig({
bidders: ['appnexus', 'rubicon'],
bidders: ['appnexus', 'rubicon', 'ix'],
config: {
ortb2: {
site: {
Expand Down
70 changes: 55 additions & 15 deletions modules/permutiveRtdProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@ export function setBidderRtb (auctionDetails, customModuleConfig) {
const moduleConfig = getModuleConfig(customModuleConfig)
const acBidders = deepAccess(moduleConfig, 'params.acBidders')
const maxSegs = deepAccess(moduleConfig, 'params.maxSegs')
const transformationConfigs = deepAccess(moduleConfig, 'params.transformations') || []
const segmentData = getSegments(maxSegs)

acBidders.forEach(function (bidder) {
const currConfig = bidderConfig[bidder] || {}
const nextConfig = mergeOrtbConfig(currConfig, segmentData)
const nextConfig = mergeOrtbConfig(currConfig, segmentData.ac, transformationConfigs) // ORTB2 uses the `ac` segment IDs

config.setBidderConfig({
bidders: [bidder],
Expand All @@ -84,23 +85,34 @@ export function setBidderRtb (auctionDetails, customModuleConfig) {
}

/**
* Merges segments into existing bidder config
* Merges segment data into existing bidder config
* Segments are retrieved from the `ac` property of `segmentData`
* @param {Object} currConfig - Current bidder config
* @param {Object} segmentData - Segment data
* @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 {string[]} segmentIDs - Permutive segment IDs
* @return {Object} Merged ortb2 object
*/
function mergeOrtbConfig (currConfig, segmentData) {
const segment = segmentData.ac.map(seg => {
return { id: seg }
})
function mergeOrtbConfig (currConfig, segmentIDs, transformationConfigs) {
const name = 'permutive.com'

const permutiveUserData = {
name,
segment: segmentIDs.map(segmentId => ({ id: segmentId })),
}

const transformedUserData = transformationConfigs
.filter(({ id }) => ortb2UserDataTransformations.hasOwnProperty(id))
.map(({ id, config }) => ortb2UserDataTransformations[id](permutiveUserData, config))

const ortbConfig = mergeDeep({}, currConfig)
const currSegments = deepAccess(ortbConfig, 'ortb2.user.data') || []
const userSegment = currSegments
const currentUserData = deepAccess(ortbConfig, 'ortb2.user.data') || []

const updatedUserData = currentUserData
.filter(el => el.name !== name)
.concat({ name, segment })
.concat(permutiveUserData, transformedUserData)

deepSetValue(ortbConfig, 'ortb2.user.data', userSegment)
deepSetValue(ortbConfig, 'ortb2.user.data', updatedUserData)

return ortbConfig
}
Expand Down Expand Up @@ -236,11 +248,11 @@ export function getSegments (maxSegs) {
ac: [..._pcrprs, ..._ppam, ...legacySegs],
rubicon: readSegments('_prubicons'),
appnexus: readSegments('_papns'),
gam: readSegments('_pdfps')
gam: readSegments('_pdfps'),
}

for (const type in segments) {
segments[type] = segments[type].slice(0, maxSegs)
for (const bidder in segments) {
segments[bidder] = segments[bidder].slice(0, maxSegs)
}

return segments
Expand All @@ -260,6 +272,34 @@ function readSegments (key) {
}
}

const unknownIabSegmentId = '_unknown_'

/**
* Functions to apply to ORT2B2 `user.data` objects.
* Each function should return an a new object containing a `name`, (optional) `ext` and `segment`
* properties. The result of the each transformation defined here will be appended to the array
* under `user.data` in the bid request.
*/
const ortb2UserDataTransformations = {
iabAudienceTaxonomy11: (userData, config) => ({
name: userData.name,
ext: { segtax: '4' },
segment: (userData.segment || [])
.map(segment => ({ id: iabSegmentId(segment.id, config.iabIds) }))
.filter(segment => segment.id !== unknownIabSegmentId)
})
}

/**
* Transform a Permutive segment ID into an IAB audience taxonomy ID.
* @param {string} permutiveSegmentId
* @param {Object} iabIds object of mappings between Permutive and IAB segment IDs (key: permutive ID, value: IAB ID)
* @return {string} IAB audience taxonomy ID associated with the Permutive segment ID
*/
function iabSegmentId(permutiveSegmentId, iabIds) {
return iabIds[permutiveSegmentId] || unknownIabSegmentId
}

/** @type {RtdSubmodule} */
export const permutiveSubmodule = {
name: MODULE_NAME,
Expand All @@ -272,7 +312,7 @@ export const permutiveSubmodule = {
onAuctionInitEvent: function (auctionDetails, customModuleConfig) {
makeSafe(function () {
// Route for bidders supporting ORTB2
setBidderRtb(auctionDetails, customModuleConfig)
setBidderRtb(auctionDetails, customModuleConfig, ortb2UserDataTransformations)
})
},
init: init
Expand Down
38 changes: 24 additions & 14 deletions modules/permutiveRtdProvider.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Permutive Real-time Data Submodule

This submodule reads cohorts from Permutive and attaches them as targeting keys to bid requests. Using this module will deliver best targeting results, leveraging Permutive's real-time segmentation and modelling capabilities.

## Usage

Compile the Permutive RTD module into your Prebid build:

```
gulp build --modules=rtdModule,permutiveRtdProvider
```
Expand All @@ -29,25 +32,32 @@ pbjs.setConfig({
```

## Supported Bidders

The Permutive RTD module sets Audience Connector cohorts as bidder-specific `ortb2.user.data` first-party data, following the Prebid `ortb2` convention, for any bidder included in `acBidders`. The module also supports bidder-specific data locations per ad unit (custom parameters) for the below bidders:

| Bidder | ID | Custom Cohorts | Audience Connector |
| ----------- | ---------- | -------------------- | ------------------ |
| Xandr | `appnexus` | Yes | Yes |
| Magnite | `rubicon` | Yes | No |
| Ozone | `ozone` | No | Yes |
| Bidder | ID | Custom Cohorts | Audience Connector |
| ------- | ---------- | -------------- | ------------------ |
| Xandr | `appnexus` | Yes | Yes |
| Magnite | `rubicon` | Yes | No |
| Ozone | `ozone` | No | Yes |

Key-values details for custom parameters:
* **Custom Cohorts:** 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.

* **Audience Connector:** You'll need to define which bidders should receive Audience Connector cohorts. You need to include the `ID` of any bidder in the `acBidders` array. Audience Connector cohorts will be sent in the `p_standard` key-value.
- **Custom Cohorts:** 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.

- **Audience Connector:** You'll need to define which bidders should receive Audience Connector cohorts. You need to include the `ID` of any bidder in the `acBidders` array. Audience Connector cohorts will be sent in the `p_standard` key-value.

## Parameters
| Name | Type | Description | Default |
| ----------------- | -------------------- | ------------------ | ------------------ |
| name | String | This should always be `permutive` | - |
| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` |
| params | Object | | - |
| params.acBidders | String[] | An array of bidders which should receive AC cohorts. | `[]` |
| params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` |

| Name | Type | Description | Default |
| ---------------------- | -------- | --------------------------------------------------------------------------------------------- | ------- |
| name | String | This should always be `permutive` | - |
| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` |
| params | Object | | - |
| params.acBidders | String[] | An array of bidders which should receive AC cohorts. | `[]` |
| params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` |
| params.transformations | Object[] | An array of configurations for ORTB2 user data transformations |

### The `transformations` parameter

This array contains configurations for transformations we'll apply to the Permutive object in the ORTB2 `user.data` array. The results of these transformations will be appended to the `user.data` array that's attached to ORTB2 bid requests.
57 changes: 54 additions & 3 deletions test/spec/modules/permutiveRtdProvider_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,45 @@ describe('permutiveRtdProvider', function () {
}])
})
})
it('should include ortb2 user data transformation for IAB audience taxonomy', function() {
const moduleConfig = getConfig()
const bidderConfig = config.getBidderConfig()
const acBidders = moduleConfig.params.acBidders
const expectedTargetingData = transformedTargeting().ac.map(seg => {
return { id: seg }
})

Object.assign(
moduleConfig.params,
{
transformations: [{
id: 'iabAudienceTaxonomy11',
config: {
iabIds: {
1000001: '9000009',
1000002: '9000008'
}
}
}]
}
)

setBidderRtb({}, moduleConfig)

acBidders.forEach(bidder => {
expect(bidderConfig[bidder].ortb2.user.data).to.deep.include.members([
{
name: 'permutive.com',
segment: expectedTargetingData
},
{
name: 'permutive.com',
ext: { segtax: '4' },
segment: [{ id: '9000009' }, { id: '9000008' }]
}
])
})
})
it('should not overwrite ortb2 config', function () {
const moduleConfig = getConfig()
const bidderConfig = config.getBidderConfig()
Expand Down Expand Up @@ -78,7 +117,15 @@ describe('permutiveRtdProvider', function () {
config: sampleOrtbConfig
})

setBidderRtb({}, moduleConfig)
const transformedUserData = {
name: 'transformation',
ext: { test: true },
segment: [1, 2, 3]
}

setBidderRtb({}, moduleConfig, {
testTransformation: userData => transformedUserData
})

acBidders.forEach(bidder => {
expect(bidderConfig[bidder].ortb2.site.name).to.equal(sampleOrtbConfig.ortb2.site.name)
Expand Down Expand Up @@ -293,6 +340,10 @@ describe('permutiveRtdProvider', function () {
expect(isAcEnabled({ params: { acBidders: ['ozone'] } }, 'ozone')).to.equal(true)
expect(isAcEnabled({ params: { acBidders: ['kjdvb'] } }, 'ozone')).to.equal(false)
})
it('checks if AC is enabled for Index', function () {
expect(isAcEnabled({ params: { acBidders: ['ix'] } }, 'ix')).to.equal(true)
expect(isAcEnabled({ params: { acBidders: ['kjdvb'] } }, 'ix')).to.equal(false)
})
})
})

Expand All @@ -313,7 +364,7 @@ function getConfig () {
name: 'permutive',
waitForIt: true,
params: {
acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx'],
acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'],
maxSegs: 500
}
}
Expand All @@ -326,7 +377,7 @@ function transformedTargeting () {
ac: [...data._pcrprs, ...data._ppam, ...data._psegs.filter(seg => seg >= 1000000)],
appnexus: data._papns,
rubicon: data._prubicons,
gam: data._pdfps
gam: data._pdfps,
}
}

Expand Down

0 comments on commit 000411a

Please sign in to comment.