diff --git a/integrationExamples/gpt/permutiveRtdProvider_example.html b/integrationExamples/gpt/permutiveRtdProvider_example.html index b6a22096c90..d4d97ccb733 100644 --- a/integrationExamples/gpt/permutiveRtdProvider_example.html +++ b/integrationExamples/gpt/permutiveRtdProvider_example.html @@ -45,6 +45,12 @@ } }, bids: [ + { + bidder: 'ix', + params: { + siteId: '123456', + } + }, { bidder: 'appnexus', params: { @@ -135,6 +141,7 @@ 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: [ @@ -142,8 +149,19 @@ 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){ @@ -160,7 +178,7 @@ } }); pbjs.setBidderConfig({ - bidders: ['appnexus', 'rubicon'], + bidders: ['appnexus', 'rubicon', 'ix'], config: { ortb2: { site: { diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index c9d22655a31..8f6a58e5bda 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -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], @@ -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 } @@ -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 @@ -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, @@ -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 diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md index 0acd42405d1..03b5db39a35 100644 --- a/modules/permutiveRtdProvider.md +++ b/modules/permutiveRtdProvider.md @@ -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 ``` @@ -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. diff --git a/test/spec/modules/permutiveRtdProvider_spec.js b/test/spec/modules/permutiveRtdProvider_spec.js index 7cf6b66f839..dccdac83d49 100644 --- a/test/spec/modules/permutiveRtdProvider_spec.js +++ b/test/spec/modules/permutiveRtdProvider_spec.js @@ -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() @@ -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) @@ -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) + }) }) }) @@ -313,7 +364,7 @@ function getConfig () { name: 'permutive', waitForIt: true, params: { - acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx'], + acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'], maxSegs: 500 } } @@ -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, } }