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: add support for new ssp standard cohorts #9236

Merged
merged 19 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion integrationExamples/gpt/permutiveRtdProvider_example.html
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
61 changes: 49 additions & 12 deletions modules/permutiveRtdProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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})

Expand Down Expand Up @@ -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;
})
}
Expand All @@ -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 = {
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Copy link
Collaborator

@robertrmartinez robertrmartinez Jan 10, 2023

Choose a reason for hiding this comment

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

@zshnr Hello!

I am seeing data.ssp.cohorts come back as undefined in some cases for our publishers prebid wrappers.

Here is an example:

image

image

This causes an exception to be thrown and the permutive makeSafe function catches it but probably does not do all of the necessary stuff!

So probably just need to add a safe check here for making sure cohorts is defined before accessing length

Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems like it is okay on the publisher site when the Perutive endpoint comes back with an actual data resonse:

image

So probably not a huge deal, but just in case might be a good idea to add a safe check!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah that is bizzare 🤔 . I was sure if the ssp property exists then it will have a cohorts property even if its empty.

Thanks for flagging I'll check internally. @robertrmartinez

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @robertrmartinez , I have merged a fix for this :)

To help us understand the impact of this, can you let me know which publisher websites you saw this on?

Thanks!

}
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)
Copy link
Collaborator

@patmmccann patmmccann Dec 8, 2022

Choose a reason for hiding this comment

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

you guys should consider yourself on notice to remove these special handlings, move them into their respective bid adapters, expect someone else moves them for you or simply deletes them, or expect not to be in the next version of prebid if the deletion or move proves challenging.

In other words, if you don't take care of this yourselves soon, it may be especially disruptive to your business in q2.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hi Patrick, thanks for the feedback. I'm already working on removing the appnexus special handling, I just wanted to do it as a separate PR in order to simplify reviewing.

For the other SSPs for which we have special handling - we're looking into them. I don't have the full context myself, but I understand it's not as simple as migrating to the ortb2.user.data field. I'll catch up internally with my team to ensure we don't break any existing targeting, as this might impact our customer's revenue!

Regardless, thanks for the heads-up that this method will stop working soon - we'll prioritise updating this to avoid any issues.

}
if (data.appnexus && data.appnexus.length) {
deepSetValue(bid, 'params.keywords.permutive', data.appnexus)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
113 changes: 105 additions & 8 deletions test/spec/modules/permutiveRtdProvider_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
}])
})
})

Expand All @@ -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)
}
}
})
})
Expand All @@ -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))
}
})
})
Expand All @@ -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))
}
})
})
Expand Down Expand Up @@ -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))
}
})
})
Expand All @@ -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))
}
})
})
Expand Down Expand Up @@ -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))
}
})
})
Expand Down Expand Up @@ -555,6 +650,7 @@ function transformedTargeting (data = getTargetingData()) {
appnexus: data._papns,
rubicon: data._prubicons,
gam: data._pdfps,
ssp: data._pssps,
}
}

Expand All @@ -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'] }
}
}

Expand Down