Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Permutive RTD module: support IAB Audience taxonomy #8242

Merged
merged 11 commits into from
Apr 11, 2022
29 changes: 22 additions & 7 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,28 @@
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: 'iab',
config: {
segtax: 4,
iabIds: {
1000001: '777777',
1000002: '888888'
}
}
}
],
overwrites: {
rubicon: function (bid, data, acEnabled, utils, defaultFn) {
if (defaultFn){
Expand All @@ -160,7 +179,7 @@
}
});
pbjs.setBidderConfig({
bidders: ['appnexus', 'rubicon'],
bidders: ['appnexus', 'rubicon', 'ix'],
patmmccann marked this conversation as resolved.
Show resolved Hide resolved
config: {
ortb2: {
site: {
Expand All @@ -180,13 +199,9 @@
gender: 'm',
keywords: 'a,b',
data: [
{
name: 'www.dataprovider1.com',
ext: { taxonomyname: 'iab_audience_taxonomy' },
segment: [{ id: '687' }, { id: '123' }]
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

two things left: circleci needs to run; add a segtax here. Can be the permutive contextual segtax or the iab contextual (don't use the audience bc that is user.data)

{
name: 'permutive.com',
ext: { segtax: 6 },
segment: [{ id: '1' }]
}
]
Expand Down
67 changes: 53 additions & 14 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 = updateOrtbConfig(currConfig, segmentData.ac, transformationConfigs) // ORTB2 uses the `ac` segment IDs

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

/**
* Merges segments into existing bidder config
* Updates `user.data` object in existing bidder config with Permutive segments
* @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 updateOrtbConfig (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 +247,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 +271,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 = {
iab: (userData, config) => ({
name: userData.name,
ext: { segtax: config.segtax },
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 Down
44 changes: 30 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,38 @@ 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.

#### Supported transformations

| Name | ID | Config structure | Description |
| -------------- | --- | ------------------------------------------------- | ------------------------------------------------------------------------------------ |
| IAB taxonomies | iab | { segtax: number, iabIds: Object<number, number>} | Transform segment IDs from Permutive to IAB (note: alpha version, subject to change) |
58 changes: 55 additions & 3 deletions test/spec/modules/permutiveRtdProvider_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,46 @@ 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: 'iab',
config: {
segtax: 4,
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',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it really permutive that decided the audience member belongs in that iab segment if permutive fails to provide the transformations? The publisher could easily transform any permutive segment to any iab segment; so I'm not sure this remains correct.

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 +118,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 +341,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 +365,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 +378,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