diff --git a/integrationExamples/gpt/permutiveRtdProvider_example.html b/integrationExamples/gpt/permutiveRtdProvider_example.html index 118cc678726..554f2081c6d 100644 --- a/integrationExamples/gpt/permutiveRtdProvider_example.html +++ b/integrationExamples/gpt/permutiveRtdProvider_example.html @@ -15,7 +15,8 @@ _papns: ['appnexus1', 'appnexus2'], _psegs: ['1234', '1000001', '1000002'], _ppam: ['ppam1', 'ppam2'], - _pcrprs: ['pcrprs1', 'pcrprs2'] + _pcrprs: ['pcrprs1', 'pcrprs2'], + _pssps: { ssps: ['appnexus', 'some other'], cohorts: ['abcd', 'efgh', 'ijkl'] }, } for (let key in data) { diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index c11d1c12436..d62834cfcfc 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -14,6 +14,7 @@ import {includes} from '../src/polyfill.js'; const MODULE_NAME = 'permutive' export const PERMUTIVE_SUBMODULE_CONFIG_KEY = 'permutive-prebid-rtd' +export const PERMUTIVE_STANDARD_AUD_KEYWORD = 'p_standard_aud' export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}) @@ -118,9 +119,21 @@ export function setBidderRtb (bidderOrtb2, customModuleConfig) { const transformationConfigs = deepAccess(moduleConfig, 'params.transformations') || [] const segmentData = getSegments(maxSegs) - acBidders.forEach(function (bidder) { + const ssps = segmentData?.ssp?.ssps ?? [] + const sspCohorts = segmentData?.ssp?.cohorts ?? [] + + const bidders = new Set([...acBidders, ...ssps]) + bidders.forEach(function (bidder) { const currConfig = { ortb2: bidderOrtb2[bidder] || {} } - const nextConfig = updateOrtbConfig(currConfig, segmentData.ac, transformationConfigs) // ORTB2 uses the `ac` segment IDs + + const isAcBidder = acBidders.indexOf(bidder) > -1 + const isSspBidder = ssps.indexOf(bidder) > -1 + + let cohorts = [] + if (isAcBidder) cohorts = segmentData.ac + if (isSspBidder) cohorts = [...new Set([...cohorts, ...sspCohorts])].slice(0, maxSegs) + + const nextConfig = updateOrtbConfig(currConfig, cohorts, sspCohorts, transformationConfigs) bidderOrtb2[bidder] = nextConfig.ortb2; }) } @@ -131,9 +144,10 @@ export function setBidderRtb (bidderOrtb2, customModuleConfig) { * @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 + * @param {string[]} sspSegmentIDs - Permutive SSP segment IDs * @return {Object} Merged ortb2 object */ -function updateOrtbConfig (currConfig, segmentIDs, transformationConfigs) { +function updateOrtbConfig (currConfig, segmentIDs, sspSegmentIDs, transformationConfigs) { const name = 'permutive.com' const permutiveUserData = { @@ -154,6 +168,12 @@ function updateOrtbConfig (currConfig, segmentIDs, transformationConfigs) { deepSetValue(ortbConfig, 'ortb2.user.data', updatedUserData) + // As of writing this, only used for AppNexus/Xandr in place of appnexusAuctionKeywords in config + const currentUserKeywords = deepAccess(ortbConfig, 'ortb2.user.keywords') || '' + const keywords = sspSegmentIDs.map(segment => `${PERMUTIVE_STANDARD_AUD_KEYWORD}=${segment}`).join(',') + const updatedUserKeywords = (currentUserKeywords === '') ? keywords : `${currentUserKeywords},${keywords}` + deepSetValue(ortbConfig, 'ortb2.user.keywords', updatedUserKeywords) + return ortbConfig } @@ -222,10 +242,19 @@ function getCustomBidderFn (moduleConfig, bidder) { * @return {Object} Bidder function */ function getDefaultBidderFn (bidder) { + const isPStandardTargetingEnabled = (data, acEnabled) => { + return (acEnabled && data.ac && data.ac.length) || (data.ssp && data.ssp.cohorts.length) + } + const pStandardTargeting = (data, acEnabled) => { + const ac = (acEnabled) ? (data.ac ?? []) : [] + const ssp = data?.ssp?.cohorts ?? [] + return [...new Set([...ac, ...ssp])] + } const bidderMap = { appnexus: function (bid, data, acEnabled) { - if (acEnabled && data.ac && data.ac.length) { - deepSetValue(bid, 'params.keywords.p_standard', data.ac) + if (isPStandardTargetingEnabled(data, acEnabled)) { + const segments = pStandardTargeting(data, acEnabled) + deepSetValue(bid, 'params.keywords.p_standard', segments) } if (data.appnexus && data.appnexus.length) { deepSetValue(bid, 'params.keywords.permutive', data.appnexus) @@ -234,19 +263,20 @@ function getDefaultBidderFn (bidder) { return bid }, rubicon: function (bid, data, acEnabled) { - if (acEnabled && data.ac && data.ac.length) { - deepSetValue(bid, 'params.visitor.p_standard', data.ac) + if (isPStandardTargetingEnabled(data, acEnabled)) { + const segments = pStandardTargeting(data, acEnabled) + deepSetValue(bid, 'params.visitor.p_standard', segments) } if (data.rubicon && data.rubicon.length) { - const rubiconCohorts = deepAccess(bid, 'params.video') ? data.rubicon.map(String) : data.rubicon - deepSetValue(bid, 'params.visitor.permutive', rubiconCohorts) + deepSetValue(bid, 'params.visitor.permutive', data.rubicon.map(String)) } return bid }, ozone: function (bid, data, acEnabled) { - if (acEnabled && data.ac && data.ac.length) { - deepSetValue(bid, 'params.customData.0.targeting.p_standard', data.ac) + if (isPStandardTargetingEnabled(data, acEnabled)) { + const segments = pStandardTargeting(data, acEnabled) + deepSetValue(bid, 'params.customData.0.targeting.p_standard', segments) } return bid @@ -290,10 +320,17 @@ export function getSegments (maxSegs) { rubicon: readSegments('_prubicons'), appnexus: readSegments('_papns'), gam: readSegments('_pdfps'), + ssp: readSegments('_pssps'), } for (const bidder in segments) { - segments[bidder] = segments[bidder].slice(0, maxSegs) + if (bidder === 'ssp') { + if (segments[bidder].cohorts && Array.isArray(segments[bidder].cohorts)) { + segments[bidder].cohorts = segments[bidder].cohorts.slice(0, maxSegs) + } + } else { + segments[bidder] = segments[bidder].slice(0, maxSegs) + } } return segments diff --git a/test/spec/modules/permutiveRtdProvider_spec.js b/test/spec/modules/permutiveRtdProvider_spec.js index 3f104ee1e2e..5030e662ea9 100644 --- a/test/spec/modules/permutiveRtdProvider_spec.js +++ b/test/spec/modules/permutiveRtdProvider_spec.js @@ -242,6 +242,42 @@ describe('permutiveRtdProvider', function () { }) }) it('should not overwrite ortb2 config', function () { + const moduleConfig = getConfig() + const acBidders = moduleConfig.params.acBidders + const sampleOrtbConfig = { + site: { + name: 'example' + }, + user: { + data: [ + { + name: 'www.dataprovider1.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ id: '687' }, { id: '123' }] + } + ] + } + } + + const bidderConfig = Object.fromEntries(acBidders.map(bidder => [bidder, sampleOrtbConfig])) + + const transformedUserData = { + name: 'transformation', + ext: { test: true }, + segment: [1, 2, 3] + } + + setBidderRtb(bidderConfig, moduleConfig, { + // TODO: this argument is unused, is the test still valid / needed? + testTransformation: userData => transformedUserData + }) + + acBidders.forEach(bidder => { + expect(bidderConfig[bidder].site.name).to.equal(sampleOrtbConfig.site.name) + expect(bidderConfig[bidder].user.data).to.deep.include.members([sampleOrtbConfig.user.data[0]]) + }) + }) + it('should update user.keywords and not override existing values', function () { const moduleConfig = getConfig() const acBidders = moduleConfig.params.acBidders const sampleOrtbConfig = { @@ -275,9 +311,64 @@ describe('permutiveRtdProvider', function () { acBidders.forEach(bidder => { expect(bidderConfig[bidder].site.name).to.equal(sampleOrtbConfig.site.name) - expect(bidderConfig[bidder].user.keywords).to.equal(sampleOrtbConfig.user.keywords) expect(bidderConfig[bidder].user.data).to.deep.include.members([sampleOrtbConfig.user.data[0]]) + expect(bidderConfig[bidder].user.keywords).to.deep.equal('a,b,p_standard_aud=123,p_standard_aud=abc') + }) + }) + it('should merge ortb2 correctly for ac and ssps', function () { + setLocalStorage({ + '_ppam': [], + '_psegs': [], + '_pcrprs': ['abc', 'def', 'xyz'], + '_pssps': { + ssps: ['foo', 'bar'], + cohorts: ['xyz', 'uvw'], + } }) + const moduleConfig = { + name: 'permutive', + waitForIt: true, + params: { + acBidders: ['foo', 'other'], + maxSegs: 30 + } + } + const bidderConfig = {}; + + setBidderRtb(bidderConfig, moduleConfig) + + // include both ac and ssp cohorts, as foo is both in ac bidders and ssps + const expectedFooTargetingData = [ + { id: 'abc' }, + { id: 'def' }, + { id: 'xyz' }, + { id: 'uvw' }, + ] + expect(bidderConfig['foo'].user.data).to.deep.include.members([{ + name: 'permutive.com', + segment: expectedFooTargetingData + }]) + + // don't include ac targeting as it's not in ac bidders + const expectedBarTargetingData = [ + { id: 'xyz' }, + { id: 'uvw' }, + ] + expect(bidderConfig['bar'].user.data).to.deep.include.members([{ + name: 'permutive.com', + segment: expectedBarTargetingData + }]) + + // only include ac targeting as this ssp is not in ssps list + const expectedOtherTargetingData = [ + { id: 'abc' }, + { id: 'def' }, + { id: 'xyz' }, + ] + expect(bidderConfig['other'].user.data).to.deep.include.members([{ + name: 'permutive.com', + segment: expectedOtherTargetingData + }]) }) }) @@ -291,7 +382,11 @@ describe('permutiveRtdProvider', function () { const segments = getSegments(max) for (const key in segments) { - expect(segments[key]).to.have.length(max) + if (key === 'ssp') { + expect(segments[key].cohorts).to.have.length(max) + } else { + expect(segments[key]).to.have.length(max) + } } }) }) @@ -311,7 +406,7 @@ describe('permutiveRtdProvider', function () { if (bidder === 'appnexus') { expect(deepAccess(params, 'keywords.permutive')).to.eql(data.appnexus) - expect(deepAccess(params, 'keywords.p_standard')).to.eql(data.ac) + expect(deepAccess(params, 'keywords.p_standard')).to.eql(data.ac.concat(data.ssp.cohorts)) } }) }) @@ -332,7 +427,7 @@ describe('permutiveRtdProvider', function () { if (bidder === 'rubicon') { expect(deepAccess(params, 'visitor.permutive')).to.eql(data.rubicon) - expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac) + expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac.concat(data.ssp.cohorts)) } }) }) @@ -363,7 +458,7 @@ describe('permutiveRtdProvider', function () { deepAccess(params, 'visitor.permutive'), 'Should map all targeting values to a string', ).to.eql(data.rubicon.map(String)) - expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac) + expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac.concat(data.ssp.cohorts)) } }) }) @@ -383,7 +478,7 @@ describe('permutiveRtdProvider', function () { const { bidder, params } = bid if (bidder === 'ozone') { - expect(deepAccess(params, 'customData.0.targeting.p_standard')).to.eql(data.ac) + expect(deepAccess(params, 'customData.0.targeting.p_standard')).to.eql(data.ac.concat(data.ssp.cohorts)) } }) }) @@ -417,7 +512,7 @@ describe('permutiveRtdProvider', function () { if (bidder === 'rubicon') { expect(deepAccess(params, 'visitor.permutive')).to.eql(data.gam) - expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac) + expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac.concat(data.ssp.cohorts)) } }) }) @@ -555,6 +650,7 @@ function transformedTargeting (data = getTargetingData()) { appnexus: data._papns, rubicon: data._prubicons, gam: data._pdfps, + ssp: data._pssps, } } @@ -565,7 +661,8 @@ function getTargetingData () { _papns: ['appnexus1', 'appnexus2'], _psegs: ['1234', '1000001', '1000002'], _ppam: ['ppam1', 'ppam2'], - _pcrprs: ['pcrprs1', 'pcrprs2'] + _pcrprs: ['pcrprs1', 'pcrprs2', 'dup'], + _pssps: { ssps: ['xyz', 'abc', 'dup'], cohorts: ['123', 'abc'] } } }