diff --git a/integrationExamples/gpt/permutiveRtdProvider_example.html b/integrationExamples/gpt/permutiveRtdProvider_example.html index dc7bb0d111a..b5cea5ed860 100644 --- a/integrationExamples/gpt/permutiveRtdProvider_example.html +++ b/integrationExamples/gpt/permutiveRtdProvider_example.html @@ -10,13 +10,21 @@ function setLocalStorageData () { const data = { + // AC Signals + _psegs: ['1234', '1000001', '1000002'], // Standard cohorts (>= 1000000) + _pcrprs: ['pcrprs1', 'pcrprs2'], // DCR cohorts + + // SSP Signals + _pssps: { ssps: ['appnexus', 'some other'], cohorts: ['abcd', 'efgh', 'ijkl'] }, + + // CC Signals - New unified custom cohorts model + _pprebid: ['custom1', 'custom2', 'custom3'], // Primary custom cohorts + + // CC Signals - Legacy keys (merged with _pprebid) _pdfps: ['gam1', 'gam2'], _prubicons: ['rubicon1', 'rubicon2'], _papns: ['appnexus1', 'appnexus2'], - _psegs: ['1234', '1000001', '1000002'], - _ppam: ['ppam1', 'ppam2'], - _pcrprs: ['pcrprs1', 'pcrprs2'], - _pssps: { ssps: ['appnexus', 'some other'], cohorts: ['abcd', 'efgh', 'ijkl'] }, + _pindexs: ['index1', 'index2'], } for (let key in data) { @@ -91,15 +99,6 @@ ], ozoneData: {} } - }, - { - bidder: 'trustx', - params: { - uid: 45, - keywords: { - test_kv: ['true'] - } - } } ] }, @@ -150,7 +149,8 @@ name: 'permutive', waitForIt: true, params: { - acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'], + acBidders: ['appnexus', 'rubicon', 'ozone', 'ix'], + ccBidders: ['ozone'], // Custom cohort bidders (ix, rubicon, appnexus, gam get them automatically) maxSegs: 500, transformations: [ { @@ -179,6 +179,7 @@ ] } }); + pbjs.setBidderConfig({ bidders: ['appnexus', 'rubicon', 'ix'], config: { diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index bb06d2d138e..ba919d5c32d 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -5,13 +5,35 @@ * @module modules/permutiveRtdProvider * @requires module:modules/realTimeData */ + +/** + * LOCAL STORAGE KEYS: + * - _psegs: Raw Permutive segments (filtered to >= 1000000 for Standard Cohorts) + * - _pcrprs: Data Clean Room (DCR) cohorts + * - _pssps: { ssps: [...], cohorts: [...] } - SSP signals and bidder codes + * - _pprebid: Custom cohorts + * - _papns, _prubicons, _pindexs, _pdfps: Legacy custom cohorts (merged with _pprebid) + * - _ppsts: Privacy Sandbox Topics by taxonomy version + * + * SIGNAL TYPES: + * - AC Signals: Standard Cohorts + DCR Cohorts → AC bidders (params.acBidders) + * - SSP Signals: Curation signals → SSP bidders (from _pssps.ssps) + * - CC Signals: Custom cohorts → CC bidders (params.ccBidders) + legacy bidders (ix, rubicon, appnexus, gam) + * - Topics: Privacy Sandbox Topics → All bidders + * + * ORTB2 LOCATIONS: + * - ortb2.user.data "permutive.com": AC/SSP Signals + * - ortb2.user.data "permutive": CC Signals + * - ortb2.user.keywords: p_standard, p_standard_aud, permutive + * - ortb2.user.ext.data: p_standard, permutive + */ + import {getGlobal} from '../src/prebidGlobal.js'; import {submodule} from '../src/hook.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; import {getStorageManager} from '../src/storageManager.js'; import {deepAccess, deepSetValue, isFn, logError, mergeDeep, isPlainObject, safeJSONParse, prefixLog} from '../src/utils.js'; -import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; - /** * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule */ @@ -85,6 +107,7 @@ export function getModuleConfig(customModuleConfig) { params: { maxSegs: 500, acBidders: [], + ccBidders: [], overwrites: {}, }, }, @@ -94,8 +117,8 @@ export function getModuleConfig(customModuleConfig) { } /** - * Sets ortb2 config for ac bidders - * @param {Object} bidderOrtb2 - The ortb2 object for the all bidders + * Sets ortb2 config for bidders with Permutive signals + * @param {Object} bidderOrtb2 - The ortb2 object for all bidders * @param {Object} moduleConfig - Publisher config for module * @param {Object} segmentData - Segment data grouped by bidder or type */ @@ -104,67 +127,72 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { const maxSegs = deepAccess(moduleConfig, 'params.maxSegs') const transformationConfigs = deepAccess(moduleConfig, 'params.transformations') || [] - const ssps = segmentData?.ssp?.ssps ?? [] - const sspCohorts = segmentData?.ssp?.cohorts ?? [] + const acSignals = segmentData?.ac ?? [] + const sspBidders = segmentData?.ssp?.ssps ?? [] + const sspSignals = segmentData?.ssp?.cohorts ?? [] const topics = segmentData?.topics ?? {} + const ccSignals = segmentData?.customCohorts ?? [] + + const ccBidders = deepAccess(moduleConfig, 'params.ccBidders') || [] + const legacyCcBidders = ['ix', 'rubicon', 'appnexus', 'gam'] + + const bidders = new Set([...acBidders, ...sspBidders, ...ccBidders, ...legacyCcBidders]) - const bidders = new Set([...acBidders, ...ssps]) bidders.forEach(function (bidder) { const currConfig = { ortb2: bidderOrtb2[bidder] || {} } - let cohorts = [] - const isAcBidder = acBidders.indexOf(bidder) > -1 - if (isAcBidder) { - cohorts = segmentData.ac - } - - const isSspBidder = ssps.indexOf(bidder) > -1 - if (isSspBidder) { - cohorts = [...new Set([...cohorts, ...sspCohorts])].slice(0, maxSegs) - } - - const nextConfig = updateOrtbConfig(bidder, currConfig, cohorts, sspCohorts, topics, transformationConfigs, segmentData) + const isSspBidder = sspBidders.indexOf(bidder) > -1 + const isCcBidder = ccBidders.indexOf(bidder) > -1 || legacyCcBidders.indexOf(bidder) > -1 + + const nextConfig = updateOrtbConfig( + bidder, + currConfig, + isAcBidder ? acSignals : [], + isSspBidder ? sspSignals : [], + isCcBidder ? ccSignals : [], + topics, + transformationConfigs, + maxSegs + ) bidderOrtb2[bidder] = nextConfig.ortb2 }) } /** - * Updates `user.data` object in existing bidder config with Permutive segments - * @param {string} bidder - The bidder identifier - * @param {Object} currConfig - Current bidder config - * @param {string[]} segmentIDs - Permutive segment IDs - * @param {string[]} sspSegmentIDs - Permutive SSP segment IDs - * @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 - * @return {Object} Merged ortb2 object + * Updates ORTB2 config for a bidder with Permutive signals + * @param {string} bidder + * @param {Object} currConfig + * @param {string[]} acSignals - AC Signals for this bidder + * @param {string[]} sspSignals - SSP Signals for this bidder + * @param {string[]} ccSignals - CC Signals for this bidder + * @param {Object} topics - Privacy Sandbox Topics + * @param {Object[]} transformationConfigs - IAB taxonomy transformations + * @param {number} maxSegs - Maximum segments per signal type + * @return {Object} Updated ortb2 config */ -function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, topics, transformationConfigs, segmentData) { +function updateOrtbConfig(bidder, currConfig, acSignals, sspSignals, ccSignals, topics, transformationConfigs, maxSegs) { logger.logInfo(`Current ortb2 config`, { bidder, config: currConfig }) - const customCohortsData = deepAccess(segmentData, bidder) || [] + // Merge AC + SSP signals for p_standard and permutive.com provider + const mergedSignals = [...new Set([...acSignals, ...sspSignals])].slice(0, maxSegs) const name = 'permutive.com' const permutiveUserData = { name, - segment: segmentIDs.map(segmentId => ({ id: segmentId })), + segment: mergedSignals.map(segmentId => ({ id: segmentId })), } const transformedUserData = transformationConfigs .filter(({ id }) => ortb2UserDataTransformations.hasOwnProperty(id)) .map(({ id, config }) => ortb2UserDataTransformations[id](permutiveUserData, config)) - const customCohortsUserData = { + const ccUserData = { name: PERMUTIVE_CUSTOM_COHORTS_KEYWORD, - segment: customCohortsData.map(cohortID => ({ id: cohortID })), + segment: ccSignals.map(cohortID => ({ id: cohortID })), } - const ortbConfig = mergeDeep({}, currConfig) - const currentUserData = deepAccess(ortbConfig, 'ortb2.user.data') || [] - const topicsUserData = [] for (const [k, value] of Object.entries(topics)) { topicsUserData.push({ @@ -176,24 +204,23 @@ function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, topics, }) } + const ortbConfig = mergeDeep({}, currConfig) + const currentUserData = deepAccess(ortbConfig, 'ortb2.user.data') || [] const updatedUserData = currentUserData - .filter(el => el.name !== permutiveUserData.name && el.name !== customCohortsUserData.name) - .concat(permutiveUserData, transformedUserData, customCohortsUserData) + .filter(el => el.name !== permutiveUserData.name && el.name !== ccUserData.name) + .concat(permutiveUserData, transformedUserData, ccUserData) .concat(topicsUserData) logger.logInfo(`Updating ortb2.user.data`, { bidder, user_data: updatedUserData }) deepSetValue(ortbConfig, 'ortb2.user.data', updatedUserData) - // Set ortb2.user.keywords const currentKeywords = deepAccess(ortbConfig, 'ortb2.user.keywords') const keywordGroups = { - [PERMUTIVE_STANDARD_KEYWORD]: segmentIDs, - [PERMUTIVE_STANDARD_AUD_KEYWORD]: sspSegmentIDs, - [PERMUTIVE_CUSTOM_COHORTS_KEYWORD]: customCohortsData, + [PERMUTIVE_STANDARD_KEYWORD]: mergedSignals, + [PERMUTIVE_STANDARD_AUD_KEYWORD]: sspSignals, + [PERMUTIVE_CUSTOM_COHORTS_KEYWORD]: ccSignals, } - // Transform groups of key-values into a single array of strings - // i.e { permutive: ['1', '2'], p_standard: ['3', '4'] } => ['permutive=1', 'permutive=2', 'p_standard=3',' p_standard=4'] const transformedKeywordGroups = Object.entries(keywordGroups) .flatMap(([keyword, ids]) => ids.map(id => `${keyword}=${id}`)) @@ -210,21 +237,19 @@ function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, topics, }) deepSetValue(ortbConfig, 'ortb2.user.keywords', keywords) - // Set user extensions - if (segmentIDs.length > 0) { - deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_STANDARD_KEYWORD}`, segmentIDs) - logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_STANDARD_KEYWORD}"`, segmentIDs) + if (mergedSignals.length > 0) { + deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_STANDARD_KEYWORD}`, mergedSignals) + logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_STANDARD_KEYWORD}"`, mergedSignals) } - if (customCohortsData.length > 0) { - deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}`, customCohortsData.map(String)) - logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}"`, customCohortsData) + if (ccSignals.length > 0) { + deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}`, ccSignals.map(String)) + logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}"`, ccSignals) } - // Set site extensions - if (segmentIDs.length > 0) { - deepSetValue(ortbConfig, `ortb2.site.ext.permutive.${PERMUTIVE_STANDARD_KEYWORD}`, segmentIDs) - logger.logInfo(`Extending ortb2.site.ext.permutive with "${PERMUTIVE_STANDARD_KEYWORD}"`, segmentIDs) + if (mergedSignals.length > 0) { + deepSetValue(ortbConfig, `ortb2.site.ext.permutive.${PERMUTIVE_STANDARD_KEYWORD}`, mergedSignals) + logger.logInfo(`Extending ortb2.site.ext.permutive with "${PERMUTIVE_STANDARD_KEYWORD}"`, mergedSignals) } logger.logInfo(`Updated ortb2 config`, { bidder, config: ortbConfig }) @@ -308,51 +333,50 @@ export function isPermutiveOnPage () { } /** - * Get all relevant segment IDs in an object - * @param {number} maxSegs - Maximum number of segments to be included - * @return {Object} + * Reads cohort data from local storage and returns organized by signal type + * @param {number} maxSegs - Maximum number of segments per signal type + * @return {Object} Segment data with AC, SSP, CC signals and topics */ export function getSegments(maxSegs) { const segments = { + // AC Signals ac: makeSafe(() => { - const legacySegs = + const standardCohorts = makeSafe(() => readSegments('_psegs', []) .map(Number) .filter((seg) => seg >= 1000000) .map(String), ) || []; - const _ppam = makeSafe(() => readSegments('_ppam', []).map(String)) || []; - const _pcrprs = makeSafe(() => readSegments('_pcrprs', []).map(String)) || []; - return [..._pcrprs, ..._ppam, ...legacySegs]; - }) || [], + const dcrCohorts = makeSafe(() => readSegments('_pcrprs', []).map(String)) || []; - ix: - makeSafe(() => { - const _pindexs = readSegments('_pindexs', []); - return _pindexs.map(String); + return [...dcrCohorts, ...standardCohorts].slice(0, maxSegs); }) || [], - rubicon: + // CC Signals + customCohorts: makeSafe(() => { - const _prubicons = readSegments('_prubicons', []); - return _prubicons.map(String); - }) || [], - - appnexus: - makeSafe(() => { - const _papns = readSegments('_papns', []); - return _papns.map(String); - }) || [], - - gam: - makeSafe(() => { - const _pdfps = readSegments('_pdfps', []); - return _pdfps.map(String); + const pprebid = makeSafe(() => + readSegments('_pprebid', []).map(String) + ) || []; + + const legacyAppnexus = makeSafe(() => readSegments('_papns', []).map(String)) || []; + const legacyRubicon = makeSafe(() => readSegments('_prubicons', []).map(String)) || []; + const legacyIndex = makeSafe(() => readSegments('_pindexs', []).map(String)) || []; + const legacyGam = makeSafe(() => readSegments('_pdfps', []).map(String)) || []; + + return [...new Set([ + ...pprebid, + ...legacyAppnexus, + ...legacyRubicon, + ...legacyIndex, + ...legacyGam + ])].slice(0, maxSegs); }) || [], + // SSP Signals ssp: makeSafe(() => { const _pssps = readSegments('_pssps', { cohorts: [], @@ -360,38 +384,25 @@ export function getSegments(maxSegs) { }); return { - cohorts: makeSafe(() => _pssps.cohorts.map(String)) || [], + cohorts: (makeSafe(() => _pssps.cohorts.map(String)) || []).slice(0, maxSegs), ssps: makeSafe(() => _pssps.ssps.map(String)) || [], }; }), + // Privacy Sandbox Topics topics: makeSafe(() => { const _ppsts = readSegments('_ppsts', {}); const topics = {}; for (const [k, value] of Object.entries(_ppsts)) { - topics[k] = makeSafe(() => value.map(String)) || []; + topics[k] = (makeSafe(() => value.map(String)) || []).slice(0, maxSegs); } return topics; }) || {}, }; - for (const bidder in segments) { - if (bidder === 'ssp') { - if (segments[bidder].cohorts && Array.isArray(segments[bidder].cohorts)) { - segments[bidder].cohorts = segments[bidder].cohorts.slice(0, maxSegs) - } - } else if (bidder === 'topics') { - for (const taxonomy in segments[bidder]) { - segments[bidder][taxonomy] = segments[bidder][taxonomy].slice(0, maxSegs) - } - } else { - segments[bidder] = segments[bidder].slice(0, maxSegs) - } - } - logger.logInfo(`Read segments`, segments) return segments; } @@ -415,9 +426,9 @@ function readSegments (key, defaultValue) { 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 + * Functions to apply to ORTB2 `user.data` objects. + * Each function should return a new object containing `name`, (optional) `ext` and `segment` + * properties. The result of each transformation defined here will be appended to the array * under `user.data` in the bid request. */ const ortb2UserDataTransformations = { @@ -442,9 +453,8 @@ function iabSegmentId(permutiveSegmentId, iabIds) { /** * Pull the latest configuration and cohort information and update accordingly. - * - * @param reqBidsConfigObj - Bidder provided config for request - * @param moduleConfig - Publisher provided config + * @param {Object} reqBidsConfigObj - Bidder provided config for request + * @param {Object} moduleConfig - Publisher provided config */ export function readAndSetCohorts(reqBidsConfigObj, moduleConfig) { const segmentData = getSegments(deepAccess(moduleConfig, 'params.maxSegs')) diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md index 3cf3ed2b367..34acf9c01d6 100644 --- a/modules/permutiveRtdProvider.md +++ b/modules/permutiveRtdProvider.md @@ -1,10 +1,25 @@ -## Prebid Config for Permutive RTD Module +# Permutive Real-time Data Submodule -This module reads cohorts from Permutive and attaches them as targeting keys to bid requests. +## Overview -### _Permutive Real-time Data Submodule_ + Module Name: Permutive Rtd Provider + Module Type: Rtd Provider + Maintainer: support@permutive.com + +## Description + +The Permutive real-time data module enables publishers to enrich bid requests with Permutive audience segments and targeting data. The module reads cohort data from local storage (set by the Permutive SDK) and attaches it to bid requests as first-party data following OpenRTB 2.x conventions. + +Supported cohort types include: +- **Standard Cohorts**: IAB-compliant audience segments (segment IDs ≥ 1000000) +- **Custom Cohorts**: Publisher-defined audiences created in the Permutive dashboard +- **DCR Cohorts**: Data Clean Room cohorts for privacy-safe audience activation +- **Curation Cohorts**: SSP-specific curation signals for supply-side optimization + +## Usage + +### Build -#### Usage Compile the Permutive RTD module into your Prebid build: ``` @@ -13,142 +28,163 @@ gulp build --modules=rtdModule,permutiveRtdProvider > Note that the global RTD module, `rtdModule`, is a prerequisite of the Permutive RTD module. -You then need to enable the Permutive RTD in your Prebid configuration. Below is an example of the format: +### Configuration + +Enable the Permutive RTD module in your Prebid configuration: ```javascript pbjs.setConfig({ - ..., realTimeData: { auctionDelay: 50, // optional auction delay dataProviders: [{ name: 'permutive', - waitForIt: true, // should be true if there's an `auctionDelay` + waitForIt: true, // should be true if there's an auctionDelay params: { - acBidders: ['appnexus'] + acBidders: ['appnexus', 'rubicon'], + ccBidders: ['ozone'] } }] - }, - ... + } }) ``` -#### Parameters - -The parameters below provide configurability for general behaviours of the RTD submodule, -as well as enabling settings for specific use cases mentioned above (e.g. acbidders). +### Parameters -## Parameters - -{: .table .table-bordered .table-striped } | 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 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` | +| name | String | Real-time data module name (always `permutive`) | - | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined | `false` | +| params | Object | Module configuration parameters | - | +| params.acBidders | String[] | Bidder codes to receive Standard Cohorts and DCR Cohorts (see Standard Cohorts section) | `[]` | +| params.ccBidders | String[] | Bidder codes to receive Custom Cohorts (see Custom Cohorts section) | `[]` | +| params.maxSegs | Integer | Maximum number of cohorts per cohort type | `500` | -#### Context +## GDPR and TCF Configuration -While Permutive is listed as a TCF vendor (ID: 361), Permutive does not obtain consent directly from the TCF. As we act as a processor on behalf of our publishers consent is given to the Permutive SDK by the publisher, not by the [GDPR Consent Management Module](https://prebid-docs.atre.net/dev-docs/modules/consentManagement.html). +While Permutive is listed as a TCF vendor (ID: 361), Permutive does not obtain consent directly from the TCF. As a data processor, consent is managed by the Permutive SDK on behalf of publishers, not by Prebid's [GDPR Consent Management Module](https://docs.prebid.org/dev-docs/modules/consentManagement.html). -This means that if GDPR enforcement is configured within the Permutive SDK _and_ the user consent isn’t given for Permutive to fire, no cohorts will populate. +If GDPR enforcement is configured within the Permutive SDK and user consent is not granted, no cohorts will be passed to bidders. -If you are also using the [TCF Control Module](https://docs.prebid.org/dev-docs/modules/tcfControl.html), in order to prevent Permutive from being blocked, it needs to be labeled within the Vendor Exceptions. +### TCF Control Module Configuration -#### Instructions +If you are using the [TCF Control Module](https://docs.prebid.org/dev-docs/modules/tcfControl.html), Permutive must be added as a vendor exception to prevent it from being blocked: -1. Publisher enables rules within Prebid.js configuration. -2. Label Permutive as an exception, as shown below. ```javascript -[ - { - purpose: 'storage', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: ["permutive"] - }, - { - purpose: 'basicAds', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] +pbjs.setConfig({ + consentManagement: { + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: ['permutive'] + }, { + purpose: 'basicAds', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + }] + } } -] +}) ``` -Before making any updates to this configuration, please ensure that this approach aligns with internal policies and current regulations regarding consent. - -## Cohort Activation with Permutive RTD Module +Before implementing this configuration, ensure it aligns with your organization's privacy policies and regulatory requirements. -**Note**: Publishers must be enabled on the above Permutive RTD Submodule to enable Standard Cohorts. +## Cohort Configuration -### _Enabling Publisher Cohorts_ +### Standard Cohorts -#### Standard Cohorts +Standard Cohorts are IAB-compliant audience segments that can be shared with demand partners. The module automatically includes DCR Cohorts (Data Clean Room) alongside Standard Cohorts when sharing with bidders. -The Permutive RTD module sets Standard Cohort IDs as bidder-specific ortb2.user.data first-party data, following the Prebid ortb2 convention. Cohorts will be sent in the `p_standard` key-value. +**Prebid.js Version Requirements:** +- **Version 7.29.0+**: Standard Cohorts are shared via OpenRTB 2.x first-party data. Configure eligible bidders in the Permutive dashboard (see Managing acBidders below). +- **Version 7.13.0 - 7.28.x**: Use `params.acBidders` to specify which bidders should receive Standard Cohorts. +- **Version < 7.13.0**: Limited support. Upgrade recommended. -For Prebid versions below 7.29.0, populate the acbidders config in the Permutive RTD with an array of bidder codes with whom you wish to share Standard Cohorts with. You also need to permission the bidders by communicating the bidder list to the Permutive team at strategicpartnershipops@permutive.com. +**Bidder-Specific Requirements:** +- **PubMatic or OpenX**: Prebid.js 7.13+ +- **Xandr**: Prebid.js 7.29+ +- **Equativ**: Prebid.js 7.26+ -For Prebid versions 7.29.0 and above, do not populate bidder codes in acbidders for the purpose of sharing Standard Cohorts (Note: there may be other business needs that require you to populate acbidders for Prebid versions 7.29.0+, see Advertiser Cohorts below). To share Standard Cohorts with bidders in Prebid versions 7.29.0 and above, communicate the bidder list to the Permutive team at strategicpartnershipops@permutive.com. +Standard Cohorts are sent to bidders in the `p_standard` keyword and as `ortb2.user.data` with provider name `permutive.com`. Curation Cohorts from SSPs are sent in the `p_standard_aud` keyword when applicable. -#### _Bidder Specific Requirements for Standard Cohorts_ -For PubMatic or OpenX: Please ensure you are using Prebid.js 7.13 (or later) -For Xandr: Please ensure you are using Prebid.js 7.29 (or later) -For Equativ: Please ensure you are using Prebid.js 7.26 (or later) +### Custom Cohorts -#### Custom Cohorts +Custom Cohorts are publisher-defined audience segments created in the Permutive dashboard, typically used for direct deals and private marketplace (PMP) setups. -The Permutive RTD module also supports passing any of the **Custom** Cohorts created in the dashboard to some SSP partners for targeting -e.g. setting up publisher deals. For these activations, cohort IDs are set in bidder-specific locations per ad unit (custom parameters). +To enable Custom Cohorts for specific bidders, add their bidder codes to the `ccBidders` parameter: -Currently, bidders with known support for custom cohort targeting are: +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'permutive', + params: { + ccBidders: ['appnexus', 'rubicon', 'ozone'] + } + }] + } +}) +``` -- Xandr -- Magnite +**Legacy Bidder Support:** -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. +The following bidders automatically receive Custom Cohorts for backwards compatibility, even if not included in `ccBidders`: +- Index Exchange (`ix`) +- Rubicon/Magnite (`rubicon`) +- AppNexus/Xandr (`appnexus`) +- Google Ad Manager (`gam`) +Custom Cohorts are read from the `_pprebid` local storage key (set by the Permutive SDK) and sent to target bidders in the `permutive` keyword and as `ortb2.user.data` with provider name `permutive`. -### _Enabling Advertiser Cohorts_ +### Advertiser Cohorts -If you are connecting to an Advertiser seat within Permutive to share Advertiser Cohorts, populate the acbidders config in the Permutive RTD with an array of bidder codes with whom you wish to share Advertiser Cohorts with. +If you are using Permutive's Advertiser product to share cohorts with demand partners, add the relevant bidder codes to `params.acBidders` to enable Advertiser Cohort sharing. -### _Managing acbidders_ +### Managing acBidders -If your business needs require you to populate acbidders with bidder codes based on the criteria above, there are **two** ways to manage it. +For Prebid.js version 7.13.0 and above, bidders can be managed directly in the Permutive Dashboard. -#### Option 1 - Automated +#### Dashboard Configuration -If you are using Prebid.js v7.13.0+, bidders may be added to or removed from the acbidders config directly within the Permutive Dashboard. +1. **Enable Prebid Integration**: Navigate to the integrations page in your Permutive dashboard settings and enable the Prebid integration. -**Permutive can do this on your behalf**. Simply contact your Permutive CSM with strategicpartnershipops@permutive.com on cc, -indicating which bidders you would like added. + > **Note on Revenue Insights:** The Prebid integration includes a Revenue Insights feature, which is optional and not required for cohort activation. See the [Revenue Insights documentation](https://support.permutive.com/hc/en-us/articles/360019044079-Revenue-Insights) for more details. -Or, a publisher may do this themselves within the Permutive Dashboard using the below instructions. +2. **Configure Bidders**: In the "Data Provider config" section, enter valid bidder codes to enable Standard Cohorts, DCR Cohorts, or Advertiser Cohorts for specific partners. Refer to the [Prebid bidder codes list](https://docs.prebid.org/dev-docs/bidders.html) for valid values. -##### Create Integration +3. **Manual Override**: Bidders configured via the dashboard will automatically populate `params.acBidders`. If you have manually defined bidders in your Prebid configuration, dashboard settings will not override them. -In order to manage acbidders via the Permutive dashboard, it is necessary to first enable the Prebid integration via the integrations page (settings). +#### Manual Configuration -**Note on Revenue Insights:** The prebid integration includes a feature for revenue insights, -which is not required for the purpose of updating acbidders config. -Please see [this document](https://support.permutive.com/hc/en-us/articles/360019044079-Revenue-Insights) for more information about revenue insights. +Alternatively, you can manually define bidders in your Prebid configuration: -##### Update acbidders +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'permutive', + params: { + acBidders: ['appnexus', 'rubicon', 'pubmatic'] + } + }] + } +}) +``` -The input for the “Data Provider config” is a multi-input free text. A valid “bidder code” needs to be entered in order to enable Standard or Advertiser Cohorts to be passed to the desired partner. The [prebid Bidders page](https://docs.prebid.org/dev-docs/bidders.html) contains instructions and a link to a list of possible bidder codes. +**Note:** Manually configured bidders must be removed manually if no longer needed, as dashboard settings will not override them. -Bidders can be added or removed from acbidders using this feature, however, this will not impact any bidders that have been applied using the manual method below. +## Testing -#### Option 2 - Manual +To view an example of the Permutive RTD module: -As a secondary option, bidders may be added manually. +``` +gulp serve --modules=rtdModule,permutiveRtdProvider,appnexusBidAdapter +``` -To do so, define which bidders should receive Standard or Advertiser Cohorts by -including the _bidder code_ of any bidder in the `acBidders` array. +Then navigate to: -**Note:** If you ever need to remove a manually-added bidder, the bidder will also need to be removed manually. +``` +http://localhost:9999/integrationExamples/gpt/permutiveRtdProvider_example.html +``` diff --git a/test/spec/modules/permutiveCombined_spec.js b/test/spec/modules/permutiveCombined_spec.js index dbf82d68fee..dee64c5c749 100644 --- a/test/spec/modules/permutiveCombined_spec.js +++ b/test/spec/modules/permutiveCombined_spec.js @@ -48,6 +48,7 @@ describe('permutiveRtdProvider', function () { params: { maxSegs: 500, acBidders: [], + ccBidders: [], overwrites: {}, }, }) @@ -200,7 +201,7 @@ describe('permutiveRtdProvider', function () { setBidderRtb(bidderConfig, moduleConfig, segmentsData) acBidders.forEach(bidder => { - const customCohorts = segmentsData[bidder] || [] + const customCohorts = segmentsData.customCohorts || [] expect(bidderConfig[bidder].user.data).to.deep.include.members([ { name: 'permutive.com', @@ -267,7 +268,7 @@ describe('permutiveRtdProvider', function () { setBidderRtb(bidderConfig, moduleConfig, segmentsData) acBidders.forEach(bidder => { - const customCohorts = segmentsData[bidder] || [] + const customCohorts = segmentsData.customCohorts || [] expect(bidderConfig[bidder].user.data).to.not.deep.include.members([...sampleOrtbConfig.user.data]) expect(bidderConfig[bidder].user.data).to.deep.include.members([ @@ -391,15 +392,15 @@ describe('permutiveRtdProvider', function () { setBidderRtb(bidderConfig, moduleConfig, segmentsData) acBidders.forEach(bidder => { - const customCohortsData = segmentsData[bidder] || [] + const customCohortsData = segmentsData.customCohorts || [] + const isSspBidder = segmentsData.ssp.ssps.includes(bidder) + const keywordGroups = { [PERMUTIVE_STANDARD_KEYWORD]: segmentsData.ac, - [PERMUTIVE_STANDARD_AUD_KEYWORD]: segmentsData.ssp.cohorts, + [PERMUTIVE_STANDARD_AUD_KEYWORD]: isSspBidder ? segmentsData.ssp.cohorts : [], [PERMUTIVE_CUSTOM_COHORTS_KEYWORD]: customCohortsData } - // Transform groups of key-values into a single array of strings - // i.e { permutive: ['1', '2'], p_standard: ['3', '4'] } => ['permutive=1', 'permutive=2', 'p_standard=3',' p_standard=4'] const transformedKeywordGroups = Object.entries(keywordGroups) .flatMap(([keyword, ids]) => ids.map(id => `${keyword}=${id}`)) @@ -443,7 +444,7 @@ describe('permutiveRtdProvider', function () { setBidderRtb(bidderConfig, moduleConfig, segmentsData) acBidders.forEach(bidder => { - const customCohortsData = segmentsData[bidder] || [] + const customCohortsData = segmentsData.customCohorts || [] const expectedKeywords = [ ...existingKeywords, @@ -459,7 +460,6 @@ describe('permutiveRtdProvider', function () { it('should merge ortb2 correctly for ac and ssps', function () { const customTargetingData = { ...getTargetingData(), - '_ppam': [], '_psegs': [], '_pcrprs': ['abc', 'def', 'xyz'], '_pssps': { @@ -529,7 +529,6 @@ describe('permutiveRtdProvider', function () { _prubicons: [], _papns: [], _psegs: [], - _ppam: [], _pcrprs: [], _pindexs: [], _pssps: { ssps: [], cohorts: [] }, @@ -558,7 +557,7 @@ describe('permutiveRtdProvider', function () { p_standard: segmentsData.ac, } - const customCohorts = segmentsData[bidder] || [] + const customCohorts = segmentsData.customCohorts || [] if (customCohorts.length > 0) { deepSetValue(userExtData, 'permutive', customCohorts) } @@ -574,10 +573,8 @@ describe('permutiveRtdProvider', function () { const bidderConfig = {} const segmentsData = transformedTargeting() - moduleConfig.params.acBidders.forEach((bidder) => { - // Remove custom cohorts - delete segmentsData[bidder] - }) + // Remove custom cohorts by setting to empty array + segmentsData.customCohorts = [] setBidderRtb(bidderConfig, moduleConfig, segmentsData) @@ -600,7 +597,7 @@ describe('permutiveRtdProvider', function () { setBidderRtb(bidderConfig, moduleConfig, segmentsData) moduleConfig.params.acBidders.forEach(bidder => { - const customCohorts = segmentsData[bidder] || [] + const customCohorts = segmentsData.customCohorts || [] if (customCohorts.length > 0) { expect(bidderConfig[bidder].user.ext.data).to.deep .eq({ permutive: customCohorts }) @@ -636,21 +633,35 @@ describe('permutiveRtdProvider', function () { }) it('should coerce numbers to strings', function () { - setLocalStorage({ _prubicons: [1, 2, 3], _pssps: { ssps: ['foo', 'bar'], cohorts: [4, 5, 6] } }) + setLocalStorage({ + _prubicons: [1, 2, 3], + _pssps: { ssps: ['foo', 'bar'], cohorts: [4, 5, 6] }, + // Clear other custom cohort keys + _pdfps: [], + _papns: [], + _pindexs: [] + }) const segments = getSegments(200) - expect(segments.rubicon).to.deep.equal(['1', '2', '3']) + expect(segments.customCohorts).to.deep.equal(['1', '2', '3']) expect(segments.ssp.ssps).to.deep.equal(['foo', 'bar']) expect(segments.ssp.cohorts).to.deep.equal(['4', '5', '6']) }) it('should return empty values on unexpected format', function () { - setLocalStorage({ _prubicons: 'a string instead?', _pssps: 123 }) + setLocalStorage({ + _prubicons: 'a string instead?', + _pssps: 123, + // Clear other custom cohort keys + _pdfps: [], + _papns: [], + _pindexs: [] + }) const segments = getSegments(200) - expect(segments.rubicon).to.deep.equal([]) + expect(segments.customCohorts).to.deep.equal([]) expect(segments.ssp.ssps).to.deep.equal([]) expect(segments.ssp.cohorts).to.deep.equal([]) }) @@ -767,6 +778,7 @@ function getConfig () { waitForIt: true, params: { acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'], + ccBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'], // Same as acBidders so tests receive custom cohorts maxSegs: 500 } } @@ -781,12 +793,19 @@ function transformedTargeting (data = getTargetingData()) { return topics })() + // Merge all custom cohorts (from _pprebid + legacy keys) + const pprebid = data._pprebid || [] + const customCohorts = [...new Set([ + ...pprebid, + ...data._papns, + ...data._prubicons, + ...data._pindexs, + ...data._pdfps + ])].map(String) + return { - ac: [...data._pcrprs, ...data._ppam, ...data._psegs.filter(seg => seg >= 1000000)].map(String), - appnexus: data._papns.map(String), - ix: data._pindexs.map(String), - rubicon: data._prubicons.map(String), - gam: data._pdfps.map(String), + ac: [...data._pcrprs, ...data._psegs.filter(seg => seg >= 1000000)].map(String), + customCohorts: customCohorts, ssp: { ssps: data._pssps.ssps.map(String), cohorts: data._pssps.cohorts.map(String) @@ -801,7 +820,6 @@ function getTargetingData () { _prubicons: ['rubicon1', 'rubicon2'], _papns: ['appnexus1', 'appnexus2'], _psegs: ['1234', '1000001', '1000002'], - _ppam: ['ppam1', 'ppam2'], _pindexs: ['pindex1', 'pindex2'], _pcrprs: ['pcrprs1', 'pcrprs2', 'dup'], _pssps: { ssps: ['xyz', 'abc', 'dup'], cohorts: ['123', 'abc'] },