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: migrate magnite to ortb2 #9555

Merged
140 changes: 86 additions & 54 deletions modules/permutiveRtdProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
import {getGlobal} from '../src/prebidGlobal.js';
import {submodule} from '../src/hook.js';
import {getStorageManager} from '../src/storageManager.js';
import {deepAccess, deepSetValue, isFn, logError, mergeDeep, isPlainObject, safeJSONParse} from '../src/utils.js';
import {deepAccess, deepSetValue, isFn, logError, mergeDeep, isPlainObject, safeJSONParse, prefixLog} from '../src/utils.js';
import {includes} from '../src/polyfill.js';

const MODULE_NAME = 'permutive'

const logger = prefixLog('[PermutiveRTD]')

export const PERMUTIVE_SUBMODULE_CONFIG_KEY = 'permutive-prebid-rtd'
export const PERMUTIVE_STANDARD_KEYWORD = 'p_standard'
export const PERMUTIVE_CUSTOM_COHORTS_KEYWORD = 'permutive'
export const PERMUTIVE_STANDARD_AUD_KEYWORD = 'p_standard_aud'

export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME})
Expand All @@ -24,30 +28,6 @@ function init(moduleConfig, userConsent) {
return true
}

/**
* Set segment targeting from cache and then try to wait for Permutive
* to initialise to get realtime segment targeting
* @param {Object} reqBidsConfigObj
* @param {function} callback - Called when submodule is done
* @param {customModuleConfig} reqBidsConfigObj - Publisher config for module
*/
export function initSegments (reqBidsConfigObj, callback, customModuleConfig) {
const permutiveOnPage = isPermutiveOnPage()
const moduleConfig = getModuleConfig(customModuleConfig)
const segmentData = getSegments(moduleConfig.params.maxSegs)

setSegments(reqBidsConfigObj, moduleConfig, segmentData)

if (moduleConfig.waitForIt && permutiveOnPage) {
window.permutive.ready(function () {
setSegments(reqBidsConfigObj, moduleConfig, segmentData)
callback()
}, 'realtime')
} else {
callback()
}
}

function liftIntoParams(params) {
return isPlainObject(params) ? { params } : {}
}
Expand Down Expand Up @@ -109,15 +89,13 @@ export function getModuleConfig(customModuleConfig) {

/**
* Sets ortb2 config for ac bidders
* @param {Object} bidderOrtb2
* @param {Object} bidderOrtb2 - The ortb2 object for the all bidders
* @param {Object} customModuleConfig - Publisher config for module
*/
export function setBidderRtb (bidderOrtb2, customModuleConfig) {
const moduleConfig = getModuleConfig(customModuleConfig)
export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) {
const acBidders = deepAccess(moduleConfig, 'params.acBidders')
const maxSegs = deepAccess(moduleConfig, 'params.maxSegs')
const transformationConfigs = deepAccess(moduleConfig, 'params.transformations') || []
const segmentData = getSegments(maxSegs)

const ssps = segmentData?.ssp?.ssps ?? []
const sspCohorts = segmentData?.ssp?.cohorts ?? []
Expand All @@ -126,28 +104,35 @@ export function setBidderRtb (bidderOrtb2, customModuleConfig) {
bidders.forEach(function (bidder) {
const currConfig = { ortb2: bidderOrtb2[bidder] || {} }

let cohorts = []

const isAcBidder = acBidders.indexOf(bidder) > -1
const isSspBidder = ssps.indexOf(bidder) > -1
if (isAcBidder) {
cohorts = segmentData.ac
}

let cohorts = []
if (isAcBidder) cohorts = segmentData.ac
if (isSspBidder) cohorts = [...new Set([...cohorts, ...sspCohorts])].slice(0, maxSegs)
const isSspBidder = ssps.indexOf(bidder) > -1
if (isSspBidder) {
cohorts = [...new Set([...cohorts, ...sspCohorts])].slice(0, maxSegs)
}

const nextConfig = updateOrtbConfig(currConfig, cohorts, sspCohorts, transformationConfigs)
bidderOrtb2[bidder] = nextConfig.ortb2;
const nextConfig = updateOrtbConfig(bidder, currConfig, cohorts, sspCohorts, transformationConfigs, segmentData)
bidderOrtb2[bidder] = nextConfig.ortb2
})
}

/**
* Updates `user.data` object in existing bidder config with Permutive segments
* @param string bidder - The bidder
* @param {Object} currConfig - Current bidder config
* @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
* @param {Object} segmentData - The segments available for targeting
* @return {Object} Merged ortb2 object
*/
function updateOrtbConfig (currConfig, segmentIDs, sspSegmentIDs, transformationConfigs) {
function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, transformationConfigs, segmentData) {
const name = 'permutive.com'

const permutiveUserData = {
Expand All @@ -174,6 +159,21 @@ function updateOrtbConfig (currConfig, segmentIDs, sspSegmentIDs, transformation
const updatedUserKeywords = (currentUserKeywords === '') ? keywords : `${currentUserKeywords},${keywords}`
deepSetValue(ortbConfig, 'ortb2.user.keywords', updatedUserKeywords)

// Set bidder specific extensions
if (bidder === 'rubicon') {
if (segmentIDs.length > 0) {
deepSetValue(ortbConfig, 'ortb2.user.ext.data.' + PERMUTIVE_STANDARD_KEYWORD, segmentIDs)
Copy link
Collaborator

Choose a reason for hiding this comment

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

this may be a dumb question - why is this set only for rubicon? with my naivete I'd expect it to be standardized and useful for everyone, or superfluous and the responsibility of rubicon to deduce from user.data.

Copy link
Contributor Author

@AntonioGargaro AntonioGargaro Mar 8, 2023

Choose a reason for hiding this comment

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

It's specific for Rubicon because this is how they read from the ortb2 object and it falls under ortb2.user.ext.data, where the .ext being they key part of the path. Following the OpenRTB 2.5 specification, we must place bidder-specific extensions in .ext properties. Since it's bidder specific, it doesn't make sense to add it for anyone else as their contracts with the ortb2 object may be different under the .ext paths.

I hope that makes sense 😬

Copy link
Contributor Author

@AntonioGargaro AntonioGargaro Mar 8, 2023

Choose a reason for hiding this comment

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

It's also worth pointing out, this data already exists under ortb2.user.data. We're just adding it into additional places in the ortb2 object for a bidder. There are follow up PRs coming that will be placing data in more places in ortb2 that will be standardised and available to all specified bidders.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The goal is, the word 'rubicon' should not appear in your code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks both, I updated it to be available for all bidders in this case.

}

if (segmentData?.rubicon?.length > 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

same question - why isn't this such that each bidder picks segmentData[bidder]?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the above comment should answer this, it's a specific extension for Rubicon.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a rules violation; you are not allowed to have a "specific extension for Rubicon."

deepSetValue(ortbConfig, 'ortb2.user.ext.data.' + PERMUTIVE_CUSTOM_COHORTS_KEYWORD, segmentData.rubicon.map(String))
}

logger.logInfo(`Extending ortb2.user.ext.data for ${bidder}`, deepAccess(ortbConfig, 'ortb2.user.ext.data'))
}

logger.logInfo(`Updating ortb2 config for ${bidder}`, ortbConfig)

return ortbConfig
}

Expand Down Expand Up @@ -262,17 +262,6 @@ function getDefaultBidderFn (bidder) {

return bid
},
rubicon: function (bid, data, acEnabled) {
if (isPStandardTargetingEnabled(data, acEnabled)) {
const segments = pStandardTargeting(data, acEnabled)
deepSetValue(bid, 'params.visitor.p_standard', segments)
}
if (data.rubicon && data.rubicon.length) {
deepSetValue(bid, 'params.visitor.permutive', data.rubicon.map(String))
}

return bid
},
Comment on lines -265 to -275
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the first special handling removed. The others are planned in follow up PRs.

Copy link
Collaborator

Choose a reason for hiding this comment

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

awesome ty!

ozone: function (bid, data, acEnabled) {
if (isPStandardTargetingEnabled(data, acEnabled)) {
const segments = pStandardTargeting(data, acEnabled)
Expand All @@ -283,7 +272,12 @@ function getDefaultBidderFn (bidder) {
}
}

return bidderMap[bidder]
// On no default bidder just return the same bid as passed in
function bidIdentity(bid) {
return bid
}

return bidderMap[bidder] || bidIdentity

Choose a reason for hiding this comment

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

Why does this need to change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So we don't break the public API of our module. rubicon did return a default bidder but since it was removed it would now return undefined. On line 208, we would then pass the default handler to any publisher defined custom handler. Ideally, they would check there is a default handler that exists first (i.e if (defaultFn) { defaultFn() }) but we can't guarantee any custom script on a publisher's page actually does this check. Hence if the invoke an undefined, it will throw an error. So we now provide a fallback which does nothing to the bid.

}

/**
Expand Down Expand Up @@ -383,17 +377,55 @@ function iabSegmentId(permutiveSegmentId, iabIds) {
return iabIds[permutiveSegmentId] || unknownIabSegmentId
}

/**
* Pull the latest configuration and cohort information and update accordingly.
*
* @param reqBidsConfigObj - Bidder provided config for request
* @param customModuleConfig - Publisher provide config
*/
export function readAndSetCohorts(reqBidsConfigObj, moduleConfig) {
const segmentData = getSegments(deepAccess(moduleConfig, 'params.maxSegs'))

makeSafe(function () {
// Legacy route with custom parameters
// ACK policy violation, in process of removing
setSegments(reqBidsConfigObj, moduleConfig, segmentData)
});

makeSafe(function () {
// Route for bidders supporting ORTB2
setBidderRtb(reqBidsConfigObj.ortb2Fragments?.bidder, moduleConfig, segmentData)
})
}

let permutiveSDKInRealTime = false

/** @type {RtdSubmodule} */
export const permutiveSubmodule = {
name: MODULE_NAME,
getBidRequestData: function (reqBidsConfigObj, callback, customModuleConfig) {
const completeBidRequestData = () => {
logger.logInfo(`Request data updated`)
callback()
}

const moduleConfig = getModuleConfig(customModuleConfig)

readAndSetCohorts(reqBidsConfigObj, moduleConfig)

makeSafe(function () {
// Legacy route with custom parameters
initSegments(reqBidsConfigObj, callback, customModuleConfig)
});
makeSafe(function () {
// Route for bidders supporting ORTB2
setBidderRtb(reqBidsConfigObj.ortb2Fragments?.bidder, customModuleConfig)
if (permutiveSDKInRealTime || !(moduleConfig.waitForIt && isPermutiveOnPage())) {
return completeBidRequestData()
}

window.permutive.ready(function () {
logger.logInfo(`SDK is realtime, updating cohorts`)
permutiveSDKInRealTime = true
readAndSetCohorts(reqBidsConfigObj, getModuleConfig(customModuleConfig))
completeBidRequestData()
}, 'realtime')

logger.logInfo(`Registered cohort update when SDK is realtime`)
})
},
init: init
Expand Down
Loading