From 2f8f0a20fbedc0ca6c28fd94898d6b70284e2859 Mon Sep 17 00:00:00 2001 From: Jason Snellbaker Date: Wed, 7 Dec 2022 08:19:11 -0500 Subject: [PATCH 1/6] GPP consent module phase 1 --- modules/appnexusBidAdapter.js | 20 +- modules/bidViewability.js | 7 +- modules/consentManagement.js | 8 +- modules/consentManagementGpp.js | 430 +++++++++++++ modules/dfpAdServerVideo.js | 7 +- modules/prebidServerBidAdapter/index.js | 22 +- modules/rtdModule/index.js | 3 +- src/adapterManager.js | 9 +- src/adapters/bidderFactory.js | 10 +- src/consentHandler.js | 14 + src/prebid.js | 3 +- .../spec/modules/consentManagementGpp_spec.js | 581 ++++++++++++++++++ 12 files changed, 1090 insertions(+), 24 deletions(-) create mode 100644 modules/consentManagementGpp.js create mode 100644 test/spec/modules/consentManagementGpp_spec.js diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 77ffe0f6b94..1105448f874 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -304,6 +304,13 @@ export const spec = { payload.us_privacy = bidderRequest.uspConsent; } + if (bidderRequest?.gppConsent) { + payload.privacy = { + gpp: bidderRequest.gppConsent.gppString, + gpp_sid: bidderRequest.gppConsent.applicableSections + } + } + if (bidderRequest && bidderRequest.refererInfo) { let refererinfo = { // TODO: are these the correct referer values? @@ -424,8 +431,17 @@ export const spec = { } }, - getUserSyncs: function (syncOptions, responses, gdprConsent) { - if (syncOptions.iframeEnabled && hasPurpose1Consent(gdprConsent)) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { + function checkGppStatus(gppConsent) { + // this is a temporary measure to supress usersync in US-based GPP regions + // this logic will be revised when proper signals (akin to purpose1 from TCF2) can be determined for US GPP + if (gppConsent && Array.isArray(gppConsent.applicableSections)) { + return gppConsent.applicableSections.every(sec => typeof sec === 'number' && sec <= 5); + } + return true; + } + + if (syncOptions.iframeEnabled && hasPurpose1Consent(gdprConsent) && checkGppStatus(gppConsent)) { return [{ type: 'iframe', url: 'https://acdn.adnxs.com/dmp/async_usersync.html' diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 362401e6d1c..a5cab99b1a7 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -7,7 +7,7 @@ import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import {isFn, logWarn, triggerPixel} from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import adapterManager, {gdprDataHandler, uspDataHandler} from '../src/adapterManager.js'; +import adapterManager, {gdprDataHandler, uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; import {find} from '../src/polyfill.js'; const MODULE_NAME = 'bidViewability'; @@ -44,6 +44,11 @@ export let fireViewabilityPixels = (globalModuleConfig, bid) => { const uspConsent = uspDataHandler.getConsentData(); if (uspConsent) { queryParams.us_privacy = uspConsent; } + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + // TODO - need to know what to set here for queryParams... + } + bid[BID_VURL_ARRAY].forEach(url => { // add '?' if not present in URL if (Object.keys(queryParams).length > 0 && url.indexOf('?') === -1) { diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 95d622e55e4..37677b0154d 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -93,7 +93,7 @@ function lookupIabConsent({onSuccess, onError}) { const { cmpFrame, cmpFunction } = findCMP(); if (!cmpFrame) { - return onError('CMP not found.'); + return onError('TCF2 CMP not found.'); } // to collect the consent information from the user, we perform two calls to the CMP in parallel: // first to collect the user's consent choices represented in an encoded string (via getConsentData) @@ -313,11 +313,11 @@ export function resetConsentData() { * @param {{cmp:string, timeout:number, allowAuctionWithoutConsent:boolean, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ export function setConsentConfig(config) { - // if `config.gdpr` or `config.usp` exist, assume new config format. + // if `config.gdpr`, `config.usp` or `config.gpp` exist, assume new config format. // else for backward compatability, just use `config` - config = config && (config.gdpr || config.usp ? config.gdpr : config); + config = config && (config.gdpr || config.usp || config.gpp ? config.gdpr : config); if (!config || typeof config !== 'object') { - logWarn('consentManagement config not defined, exiting consent manager'); + logWarn('consentManagement (gdpr) config not defined, exiting consent manager'); return; } if (isStr(config.cmpApi)) { diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js new file mode 100644 index 00000000000..ecf8064de6e --- /dev/null +++ b/modules/consentManagementGpp.js @@ -0,0 +1,430 @@ +/** + * This module adds GPP consentManagement support to prebid.js. It interacts with + * supported CMPs (Consent Management Platforms) to grab the user's consent information + * and make it available for any GPP supported adapters to read/pass this information to + * their system and for various other features/modules in Prebid.js. + */ +import {deepSetValue, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {gppDataHandler} from '../src/adapterManager.js'; +import {includes} from '../src/polyfill.js'; +import {timedAuctionHook} from '../src/utils/perfMetrics.js'; +import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; + +const DEFAULT_CMP = 'iab'; +const DEFAULT_CONSENT_TIMEOUT = 10000; +const CMP_VERSION = 1; + +export let userCMP; +export let consentTimeout; +export let staticConsentData; + +let consentData; +let addedConsentHook = false; +let provisionalConsent; + +// add new CMPs here, with their dedicated lookup function +const cmpCallMap = { + 'iab': lookupIabConsent, + 'static': lookupStaticConsentData +}; + +/** + * This function reads the consent string from the config to obtain the consent information of the user. + * @param {function({})} onSuccess acts as a success callback when the value is read from config; pass along consentObject from CMP + */ +function lookupStaticConsentData({onSuccess, onError}) { + processCmpData(staticConsentData, {onSuccess, onError}); +} + +/** + * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. + * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function + * based on the appropriate result. + * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP + * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) + */ +function lookupIabConsent({onSuccess, onError}) { + const cmpApiName = '__gpp'; + let registeredPostMessageResponseListener = false; + + function findCMP() { + let f = window; + let cmpFrame; + let cmpDirectAccess = false; + while (true) { + try { + if (typeof f[cmpApiName] === 'function') { + cmpFrame = f; + cmpDirectAccess = true; + break; + } + } catch (e) {} + + // need separate try/catch blocks due to the exception errors thrown when trying to check for a frame that doesn't exist in 3rd party env + try { + if (f.frames['__gppLocator']) { + cmpFrame = f; + break; + } + } catch (e) {} + + if (f === window.top) break; + f = f.parent; + } + + return { + cmpFrame, + cmpDirectAccess + }; + } + + // remove me later when revised v1 is fully adapted by all GPP CMPs + function checkApplicableSectionIsReady(gppData) { + return gppData && Array.isArray(gppData.applicableSection) && gppData.applicableSection.length > 0 && gppData.applicableSection[0] !== 0; + } + + function checkApplicableSectionsIsReady(gppData) { + return gppData && Array.isArray(gppData.applicableSections) && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== 0; + } + + const cmpCallbacks = {}; + + const {cmpFrame, cmpDirectAccess} = findCMP(); + + if (!cmpFrame) { + return onError('GPP CMP not found.'); + } + + // use cmpDirectAccess to avoid an error checking on a cross-frame window to see if CMP function exists + if (cmpDirectAccess && typeof cmpFrame[cmpApiName] === 'function') { + logInfo('Detected GPP CMP API is directly accessible, calling it now...'); + cmpFrame[cmpApiName]('addEventListener', cmpDirectResponseCallback); + } else { + logInfo('Detected GPP CMP is outside the current iframe where Prebid.js is located, calling it now...'); + callCmpWhileInIframe('addEventListener', cmpFrame, cmpPostResponseEventCallback); + } + + function cmpDirectResponseCallback(evt) { + if (evt) { + logInfo('Received a response from GPP CMP for event', evt); + if ( + evt.eventName === 'sectionChange' || // should be new consent data + ( + // cmp is loaded and not/no longer visible, a gpp string should be available + evt.pingData.cmpStatus === 'loaded' && + evt.pingData.cmpDisplayStatus !== 'visible' + ) + ) { + let gppData = cmpFrame[cmpApiName]('getGPPData'); + + logInfo('Received a response from GPP CMP for getGPPData', gppData); + processCmpData(gppData, {onSuccess, onError}); + } else if (evt.pingData.cmpStatus === 'error') { + onError('CMP returned with a cmpStatus:error response. Please check CMP setup.'); + } else if (!provisionalConsent) { + const provGppData = cmpFrame[cmpApiName]('getGPPData'); + logInfo('Called GPP CMP early to determine the value of applicableSections; value returned from CMP: ', provGppData); + if (checkApplicableSectionsIsReady(provGppData)) { + provisionalConsent = provGppData; + } else if (checkApplicableSectionIsReady(provGppData)) { + provisionalConsent = provGppData; + // conform naming convention from old version of the spec to new version + provisionalConsent.applicableSections = provGppData.applicableSection; + delete provisionalConsent.applicableSection; + } + } + } + } + + function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback, cmpCallParam) { + // changing the name away from the standard, to properly handle multiple calls to CMP, + // otherwise follow-up calls made to fetch getGPPData routed here will complain later that there's no callback + const cmpApiPMName = '__gpp-pm'; + const callName = `${cmpApiName}Call`; + + /* Setup up a __cmp function to do the postMessage and stash the callback. + This function behaves (from the caller's perspective identically to the in-frame __gpp call */ + window[cmpApiPMName] = function (cmd, callback, param) { + const callId = Math.random().toString(); + const msg = { + [callName]: { + command: cmd, + parameter: param, + version: 1, + callId: callId + } + }; + + // TODO? - add logic to check if random was already used in the same session, and roll another if so? + cmpCallbacks[callId] = callback; + cmpFrame.postMessage(msg, '*'); + } + + // when we get the return message, call the stashed callback; + // register the listener only once per session + if (!registeredPostMessageResponseListener) { + window.addEventListener('message', readPostMessageResponse, false); + registeredPostMessageResponseListener = true; + } + + // call CMP + window[cmpApiPMName](commandName, moduleCallback, cmpCallParam); + + function readPostMessageResponse(event) { + const cmpDataPkgName = `${cmpApiName}Return`; + const json = (typeof event.data === 'string' && event.data.includes(cmpDataPkgName)) ? JSON.parse(event.data) : event.data; + if (json[cmpDataPkgName] && json[cmpDataPkgName].callId) { + const payload = json[cmpDataPkgName]; + + if (cmpCallbacks.hasOwnProperty(payload.callId)) { + cmpCallbacks[payload.callId](payload.returnValue); + } + } + } + } + + function cmpPostResponseEventCallback(evt) { + if (evt) { + logInfo('Received a postmsg response from GPP CMP for event', evt); + if ( + evt.eventName === 'sectionChange' || // should be new consent data + ( + // cmp is loaded and not/no longer visible, a gpp string should be available + evt.pingData.cmpStatus === 'loaded' && + evt.pingData.cmpDisplayStatus !== 'visible' + ) + ) { + callCmpWhileInIframe('getGPPData', cmpFrame, cmpPostResponseGetGPPCallback); + } else if (evt.pingData.cmpStatus === 'error') { + onError('CMP returned with a cmpStatus:error response. Please check CMP setup.'); + } else if (!provisionalConsent) { + callCmpWhileInIframe('getGPPData', cmpFrame, cmpPostResponseProvGetGPPCallback); + } + } + } + + function cmpPostResponseGetGPPCallback(gppData) { + logInfo('Received a postmsg response from GPP CMP for getGPPData', gppData); + processCmpData(gppData, {onSuccess, onError}); + } + + function cmpPostResponseProvGetGPPCallback(provGppData) { + logInfo('Called GPP CMP early to determine the value of applicableSections; postmsg response returned from CMP: ', provGppData); + if (checkApplicableSectionsIsReady(provGppData)) { + provisionalConsent = provGppData; + } else if (checkApplicableSectionIsReady(provGppData)) { + provisionalConsent = provGppData; + // conform naming convention from old version of the spec to new version + provisionalConsent.applicableSections = provGppData.applicableSection; + delete provisionalConsent.applicableSection; + } + } +} + +/** + * Look up consent data and store it in the `consentData` global as well as `adapterManager.js`' gdprDataHandler. + * + * @param cb A callback that takes: a boolean that is true if the auction should be canceled; an error message and extra + * error arguments that will be undefined if there's no error. + */ +function loadConsentData(cb) { + let isDone = false; + let timer = null; + + function done(consentData, shouldCancelAuction, errMsg, ...extraArgs) { + if (timer != null) { + clearTimeout(timer); + } + isDone = true; + gppDataHandler.setConsentData(consentData); + if (typeof cb === 'function') { + cb(shouldCancelAuction, errMsg, ...extraArgs); + } + } + + if (!includes(Object.keys(cmpCallMap), userCMP)) { + done(null, false, `GPP CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + return; + } + + const callbacks = { + onSuccess: (data) => done(data, false), + onError: function (msg, ...extraArgs) { + done(null, true, msg, ...extraArgs); + } + } + cmpCallMap[userCMP](callbacks); + + if (!isDone) { + const onTimeout = () => { + const continueToAuction = (data) => { + done(data, false, 'GPP CMP did not load, continuing auction...'); + } + processCmpData(provisionalConsent, { + onSuccess: continueToAuction, + onError: () => continueToAuction(storeConsentData(undefined)) + }) + } + if (consentTimeout === 0) { + onTimeout(); + } else { + timer = setTimeout(onTimeout, consentTimeout); + } + } +} + +/** + * Like `loadConsentData`, but cache and re-use previously loaded data. + * @param cb + */ +function loadIfMissing(cb) { + if (consentData) { + logInfo('User consent information already known. Pulling internally stored information...'); + // eslint-disable-next-line standard/no-callback-literal + cb(false); + } else { + loadConsentData(cb); + } +} + +/** + * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the + * user's encoded consent string from the supported CMP. Once obtained, the module will store this + * data as part of a gppConsent object which gets transferred to adapterManager's gppDataHandler object. + * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. + * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. + * @param {function} fn required; The next function in the chain, used by hook.js + */ +export const requestBidsHook = timedAuctionHook('gpp', function requestBidsHook(fn, reqBidsConfigObj) { + loadIfMissing(function (shouldCancelAuction, errMsg, ...extraArgs) { + if (errMsg) { + let log = logWarn; + if (shouldCancelAuction) { + log = logError; + errMsg = `${errMsg} Canceling auction as per consentManagement config.`; + } + log(errMsg, ...extraArgs); + } + + if (shouldCancelAuction) { + fn.stopTiming(); + if (typeof reqBidsConfigObj.bidsBackHandler === 'function') { + reqBidsConfigObj.bidsBackHandler(); + } else { + logError('Error executing bidsBackHandler'); + } + } else { + fn.call(this, reqBidsConfigObj); + } + }); +}); + +/** + * This function checks the consent data provided by CMP to ensure it's in an expected state. + * If it's bad, we call `onError` + * If it's good, then we store the value and call `onSuccess` + */ +function processCmpData(consentObject, {onSuccess, onError}) { + function checkData() { + const gppString = consentObject && consentObject.gppString; + const gppSection = (consentObject && consentObject.applicableSections) ? consentObject.applicableSections + : (consentObject && consentObject.applicableSection) ? consentObject.applicableSection + : (provisionalConsent && provisionalConsent.applicableSections) ? provisionalConsent.applicableSections : []; + + return !!( + (!Array.isArray(gppSection)) || + (Array.isArray(gppSection) && (!gppString || !isStr(gppString))) + ); + } + + if (checkData()) { + onError(`CMP returned unexpected value during lookup process.`, consentObject); + } else { + onSuccess(storeConsentData(consentObject)); + } +} + +/** + * Stores CMP data locally in module to make information available in adaptermanager.js for later in the auction + * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) + */ +function storeConsentData(cmpConsentObject) { + consentData = { + gppString: (cmpConsentObject) ? cmpConsentObject.gppString : undefined, + fullGppData: (cmpConsentObject) || undefined, + }; + consentData.applicableSections = (cmpConsentObject && cmpConsentObject.applicableSections) ? cmpConsentObject.applicableSections + : (cmpConsentObject && cmpConsentObject.applicableSection) ? cmpConsentObject.applicableSection + : (provisionalConsent && provisionalConsent.applicableSections) ? provisionalConsent.applicableSections : []; + consentData.apiVersion = CMP_VERSION; + return consentData; +} + +/** + * Simply resets the module's consentData variable back to undefined, mainly for testing purposes + */ +export function resetConsentData() { + consentData = undefined; + userCMP = undefined; + consentTimeout = undefined; + gppDataHandler.reset(); +} + +/** + * A configuration function that initializes some module variables, as well as add a hook into the requestBids function + * @param {{cmp:string, timeout:number, allowAuctionWithoutConsent:boolean, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) + */ +export function setConsentConfig(config) { + config = config && config.gpp; + if (!config || typeof config !== 'object') { + logWarn('consentManagement.gpp config not defined, exiting consent manager module'); + return; + } + + if (isStr(config.cmpApi)) { + userCMP = config.cmpApi; + } else { + userCMP = DEFAULT_CMP; + logInfo(`consentManagement.gpp config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`); + } + + if (isNumber(config.timeout)) { + consentTimeout = config.timeout; + } else { + consentTimeout = DEFAULT_CONSENT_TIMEOUT; + logInfo(`consentManagement.gpp config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); + } + + if (userCMP === 'static') { + if (isPlainObject(config.consentData)) { + staticConsentData = config.consentData; + consentTimeout = 0; + } else { + logError(`consentManagement.gpp config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`); + } + } + + logInfo('consentManagement.gpp module has been activated...'); + + if (!addedConsentHook) { + $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); + } + addedConsentHook = true; + gppDataHandler.enable(); + loadConsentData(); // immediately look up consent data to make it available without requiring an auction +} +config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); + +// TODO this function will likely change a bit once PR #9205 is merged to master +export function setOrtbGpp(ortbRequest, bidderRequest) { + const consent = bidderRequest.gppConsent; + if (consent) { + if (Array.isArray(consent.applicableSections)) { + deepSetValue(ortbRequest, 'regs.gpp_sid', consent.applicableSections); + } + deepSetValue(ortbRequest, 'regs.gpp', consent.gppString); + } +} + +registerOrtbProcessor({type: REQUEST, name: 'gpp', fn: setOrtbGpp}); diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 072715b4bd6..71c9c7aa341 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -8,7 +8,7 @@ import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, bui import { config } from '../src/config.js'; import { getHook, submodule } from '../src/hook.js'; import { auctionManager } from '../src/auctionManager.js'; -import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import {getPPID} from '../src/adserver.js'; @@ -119,6 +119,11 @@ export function buildDfpVideoUrl(options) { const uspConsent = uspDataHandler.getConsentData(); if (uspConsent) { queryParams.us_privacy = uspConsent; } + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + // TODO - need to know what to set here for queryParams... + } + if (!queryParams.ppid) { const ppid = getPPID(); if (ppid != null) { diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 072c280aecf..9430a8768c7 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -216,7 +216,7 @@ export function resetSyncedStatus() { /** * @param {Array} bidderCodes list of bidders to request user syncs for. */ -function queueSync(bidderCodes, gdprConsent, uspConsent, s2sConfig) { +function queueSync(bidderCodes, gdprConsent, uspConsent, gppConsent, s2sConfig) { if (_s2sConfigs.length === _syncCount) { return; } @@ -246,6 +246,10 @@ function queueSync(bidderCodes, gdprConsent, uspConsent, s2sConfig) { payload.us_privacy = uspConsent; } + if (gppConsent) { + // TODO - need feedback on how to pass gpp values to payload for PBS usersync endpoints... + } + if (typeof s2sConfig.coopSync === 'boolean') { payload.coopSync = s2sConfig.coopSync; } @@ -330,7 +334,7 @@ function doBidderSync(type, url, bidder, done, timeout) { * * @param {Array} bidders a list of bidder names */ -function doClientSideSyncs(bidders, gdprConsent, uspConsent) { +function doClientSideSyncs(bidders, gdprConsent, uspConsent, gppConsent) { bidders.forEach(bidder => { let clientAdapter = adapterManager.getBidAdapter(bidder); if (clientAdapter && clientAdapter.registerSyncs) { @@ -341,7 +345,8 @@ function doClientSideSyncs(bidders, gdprConsent, uspConsent) { clientAdapter, [], gdprConsent, - uspConsent + uspConsent, + gppConsent ) ); } @@ -411,12 +416,13 @@ function getMatchingConsentUrl(urlProp, gdprConsent) { } function getConsentData(bidRequests) { - let gdprConsent, uspConsent; + let gdprConsent, uspConsent, gppConsent; if (Array.isArray(bidRequests) && bidRequests.length > 0) { gdprConsent = bidRequests[0].gdprConsent; uspConsent = bidRequests[0].uspConsent; + gppConsent = bidRequests[0].gppConsent; } - return { gdprConsent, uspConsent }; + return { gdprConsent, uspConsent, gppConsent }; } /** @@ -433,7 +439,7 @@ export function PrebidServer() { done = adapterMetrics.startTiming('total').stopBefore(done); bidRequests.forEach(req => useMetrics(req.metrics).join(adapterMetrics, {continuePropagation: false})); - let { gdprConsent, uspConsent } = getConsentData(bidRequests); + let { gdprConsent, uspConsent, gppConsent } = getConsentData(bidRequests); if (Array.isArray(_s2sConfigs)) { if (s2sBidRequest.s2sConfig && s2sBidRequest.s2sConfig.syncEndpoint && getMatchingConsentUrl(s2sBidRequest.s2sConfig.syncEndpoint, gdprConsent)) { @@ -441,7 +447,7 @@ export function PrebidServer() { .map(bidder => adapterManager.aliasRegistry[bidder] || bidder) .filter((bidder, index, array) => (array.indexOf(bidder) === index)); - queueSync(syncBidders, gdprConsent, uspConsent, s2sBidRequest.s2sConfig); + queueSync(syncBidders, gdprConsent, uspConsent, gppConsent, s2sBidRequest.s2sConfig); } processPBSRequest(s2sBidRequest, bidRequests, ajax, { @@ -450,7 +456,7 @@ export function PrebidServer() { bidRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest)); } done(); - doClientSideSyncs(requestedBidders, gdprConsent, uspConsent); + doClientSideSyncs(requestedBidders, gdprConsent, uspConsent, gppConsent); }, onError: done, onBid: function ({adUnit, bid}) { diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 876da8dda37..05594c63132 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -163,7 +163,7 @@ import {getHook, module} from '../../src/hook.js'; import {logError, logInfo, logWarn} from '../../src/utils.js'; import * as events from '../../src/events.js'; import CONSTANTS from '../../src/constants.json'; -import adapterManager, {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; +import adapterManager, {gdprDataHandler, uspDataHandler, gppDataHandler} from '../../src/adapterManager.js'; import {find} from '../../src/polyfill.js'; import {timedAuctionHook} from '../../src/utils/perfMetrics.js'; @@ -246,6 +246,7 @@ function getConsentData() { return { gdpr: gdprDataHandler.getConsentData(), usp: uspDataHandler.getConsentData(), + gpp: gppDataHandler.getConsentData(), coppa: !!(config.getConfig('coppa')) } } diff --git a/src/adapterManager.js b/src/adapterManager.js index eb6a74a82a4..28de07ffe84 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -31,7 +31,7 @@ import {hook} from './hook.js'; import {find, includes} from './polyfill.js'; import {adunitCounter} from './adUnits.js'; import {getRefererInfo} from './refererDetection.js'; -import {GdprConsentHandler, UspConsentHandler} from './consentHandler.js'; +import {GdprConsentHandler, UspConsentHandler, GppConsentHandler} from './consentHandler.js'; import * as events from './events.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; @@ -161,6 +161,7 @@ function getAdUnitCopyForClientAdapters(adUnits) { export let gdprDataHandler = new GdprConsentHandler(); export let uspDataHandler = new UspConsentHandler(); +export let gppDataHandler = new GppConsentHandler(); export let coppaDataHandler = { getCoppa: function() { @@ -321,6 +322,12 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a }); } + if (gppDataHandler.getConsentData()) { + bidRequests.forEach(bidRequest => { + bidRequest['gppConsent'] = gppDataHandler.getConsentData(); + }); + } + bidRequests.forEach(bidRequest => { config.runWithBidder(bidRequest.bidderCode, () => { const fledgeEnabledFromConfig = config.getConfig('fledgeEnabled'); diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 7e66b849783..55c84e57062 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -214,7 +214,7 @@ export function newBidder(spec) { done(); config.runWithBidder(spec.code, () => { events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest); - registerSyncs(responses, bidderRequest.gdprConsent, bidderRequest.uspConsent); + registerSyncs(responses, bidderRequest.gdprConsent, bidderRequest.uspConsent, bidderRequest.gppConsent); }); } @@ -295,8 +295,8 @@ export function newBidder(spec) { return false; } - function registerSyncs(responses, gdprConsent, uspConsent) { - registerSyncInner(spec, responses, gdprConsent, uspConsent); + function registerSyncs(responses, gdprConsent, uspConsent, gppConsent) { + registerSyncInner(spec, responses, gdprConsent, uspConsent, gppConsent); } function filterAndWarn(bid) { @@ -448,14 +448,14 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe }) }, 'processBidderRequests') -export const registerSyncInner = hook('async', function(spec, responses, gdprConsent, uspConsent) { +export const registerSyncInner = hook('async', function(spec, responses, gdprConsent, uspConsent, gppConsent) { const aliasSyncEnabled = config.getConfig('userSync.aliasSyncEnabled'); if (spec.getUserSyncs && (aliasSyncEnabled || !adapterManager.aliasRegistry[spec.code])) { let filterConfig = config.getConfig('userSync.filterSettings'); let syncs = spec.getUserSyncs({ iframeEnabled: !!(filterConfig && (filterConfig.iframe || filterConfig.all)), pixelEnabled: !!(filterConfig && (filterConfig.image || filterConfig.all)), - }, responses, gdprConsent, uspConsent); + }, responses, gdprConsent, uspConsent, gppConsent); if (syncs) { if (!Array.isArray(syncs)) { syncs = [syncs]; diff --git a/src/consentHandler.js b/src/consentHandler.js index 01470a4b38c..31f6e6e1515 100644 --- a/src/consentHandler.js +++ b/src/consentHandler.js @@ -107,3 +107,17 @@ export class GdprConsentHandler extends ConsentHandler { } } } + +export class GppConsentHandler extends ConsentHandler { + getConsentMeta() { + const consentData = this.getConsentData(); + if (consentData && this.generatedTime) { + return { + // TODO - is more needed here? Are the properties below fine? + apiVersion: consentData.apiVersion, + generatedAt: this.generatedTime, + gpp: consentData, + } + } + } +} diff --git a/src/prebid.js b/src/prebid.js index 06429b13a72..cbea0bd5d35 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -45,7 +45,7 @@ import {executeRenderer, isRendererRequired} from './Renderer.js'; import {createBid} from './bidfactory.js'; import {storageCallbacks} from './storageManager.js'; import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js'; -import {default as adapterManager, gdprDataHandler, getS2SBidderSet, uspDataHandler} from './adapterManager.js'; +import {default as adapterManager, gdprDataHandler, getS2SBidderSet, gppDataHandler, uspDataHandler} from './adapterManager.js'; import CONSTANTS from './constants.json'; import * as events from './events.js'; import {newMetrics, useMetrics} from './utils/perfMetrics.js'; @@ -335,6 +335,7 @@ function getConsentMetadata() { return { gdpr: gdprDataHandler.getConsentMeta(), usp: uspDataHandler.getConsentMeta(), + gpp: gppDataHandler.getConsentMeta(), coppa: !!(config.getConfig('coppa')) } } diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js new file mode 100644 index 00000000000..786b4f9fba0 --- /dev/null +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -0,0 +1,581 @@ +import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, staticConsentData } from 'modules/consentManagementGpp.js'; +import { gppDataHandler } from 'src/adapterManager.js'; +import * as utils from 'src/utils.js'; +import { config } from 'src/config.js'; +import 'src/prebid.js'; + +let expect = require('chai').expect; + +describe('consentManagementGpp', function () { + describe('setConsentConfig tests:', function () { + describe('empty setConsentConfig value', function () { + beforeEach(function () { + sinon.stub(utils, 'logInfo'); + sinon.stub(utils, 'logWarn'); + }); + + afterEach(function () { + utils.logInfo.restore(); + utils.logWarn.restore(); + config.resetConfig(); + resetConsentData(); + }); + + it('should use system default values', function () { + setConsentConfig({ + gpp: {} + }); + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(10000); + sinon.assert.callCount(utils.logInfo, 3); + }); + + it('should exit consent manager if config is not an object', function () { + setConsentConfig(''); + expect(userCMP).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }); + + it('should exit consentManagement module if config is "undefined"', function () { + setConsentConfig(undefined); + expect(userCMP).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }); + + it('should not produce any consent metadata', function () { + setConsentConfig(undefined) + let consentMetadata = gppDataHandler.getConsentMeta(); + expect(consentMetadata).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }) + + it('should immediately look up consent data', () => { + setConsentConfig({ + gpp: { + cmpApi: 'invalid' + } + }); + expect(gppDataHandler.ready).to.be.true; + }) + }); + + describe('valid setConsentConfig value', function () { + afterEach(function () { + config.resetConfig(); + }); + + it('results in all user settings overriding system defaults', function () { + let allConfig = { + gpp: { + cmpApi: 'iab', + timeout: 7500 + } + }; + + setConsentConfig(allConfig); + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(7500); + }); + + it('should recognize config.gpp, with default cmpApi and timeout', function () { + setConsentConfig({ + gpp: {} + }); + + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(10000); + }); + + it('should enable gppDataHandler', () => { + setConsentConfig({ + gpp: {} + }); + expect(gppDataHandler.enabled).to.be.true; + }); + }); + + describe('static consent string setConsentConfig value', () => { + afterEach(() => { + config.resetConfig(); + }); + + it('results in user settings overriding system defaults for v2 spec', () => { + let staticConfig = { + gpp: { + cmpApi: 'static', + timeout: 7500, + consentData: { + applicableSections: [7], + gppString: 'ABCDEFG1234', + gppVersion: 1, + sectionId: 3, + sectionList: [] + } + } + }; + + setConsentConfig(staticConfig); + expect(userCMP).to.be.equal('static'); + expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used + const consent = gppDataHandler.getConsentData(); + expect(consent.gppString).to.eql(staticConfig.gpp.consentData.gppString); + expect(consent.fullGppData).to.eql(staticConfig.gpp.consentData); + expect(staticConsentData).to.be.equal(staticConfig.gpp.consentData); + }); + }); + }); + + describe('requestBidsHook tests:', function () { + let goodConfig = { + gpp: { + cmpApi: 'iab', + timeout: 7500, + } + }; + + const staticConfig = { + gpp: { + cmpApi: 'static', + timeout: 7500, + consentData: { + gppString: 'abc12345', + applicableSections: [7] + } + } + } + + let didHookReturn; + + beforeEach(resetConsentData); + after(resetConsentData); + + describe('error checks:', function () { + beforeEach(function () { + didHookReturn = false; + sinon.stub(utils, 'logWarn'); + sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + utils.logWarn.restore(); + utils.logError.restore(); + config.resetConfig(); + }); + + it('should throw a warning and return to hooked function when an unknown CMP framework ID is used', function () { + let badCMPConfig = { + gpp: { + cmpApi: 'bad' + } + }; + setConsentConfig(badCMPConfig); + expect(userCMP).to.be.equal(badCMPConfig.gpp.cmpApi); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consent = gppDataHandler.getConsentData(); + + sinon.assert.calledOnce(utils.logWarn); + expect(didHookReturn).to.be.true; + expect(consent).to.be.null; + }); + + it('should call gppDataHandler.setConsentData() when unknown CMP api is used', () => { + setConsentConfig({ + gpp: { + cmpApi: 'invalid' + } + }); + let hookRan = false; + requestBidsHook(() => { + hookRan = true; + }, {}); + expect(hookRan).to.be.true; + expect(gppDataHandler.ready).to.be.true; + }) + + it('should throw proper errors when CMP is not found', function () { + setConsentConfig(goodConfig); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consent = gppDataHandler.getConsentData(); + // throw 2 errors; one for no bidsBackHandler and for CMP not being found (this is an error due to gdpr config) + sinon.assert.calledTwice(utils.logError); + expect(didHookReturn).to.be.false; + expect(consent).to.be.null; + expect(gppDataHandler.ready).to.be.true; + }); + + it('should not trip when adUnits have no size', () => { + setConsentConfig(staticConfig); + let ran = false; + requestBidsHook(() => { + ran = true; + }, { + adUnits: [{ + code: 'test', + mediaTypes: { + video: {} + } + }] + }); + return gppDataHandler.promise.then(() => { + expect(ran).to.be.true; + }); + }); + + it('should continue the auction immediately, without consent data, if timeout is 0', (done) => { + setConsentConfig({ + gpp: { + cmpApi: 'iab', + timeout: 0 + } + }); + window.__gpp = function () {}; + try { + requestBidsHook(() => { + const consent = gppDataHandler.getConsentData(); + expect(consent.applicableSections).to.deep.equal([]); + expect(consent.gppString).to.be.undefined; + done(); + }, {}) + } finally { + delete window.__gpp; + } + }); + }); + + describe('already known consentData:', function () { + let cmpStub = sinon.stub(); + + function mockCMP(cmpResponse) { + return function (...args) { + if (args[0] === 'addEventListener') { + args[1](({ + eventName: 'sectionChange' + })); + } else if (args[0] === 'getGPPData') { + return cmpResponse; + } + } + } + + beforeEach(function () { + didHookReturn = false; + window.__gpp = function () {}; + }); + + afterEach(function () { + config.resetConfig(); + cmpStub.restore(); + delete window.__gpp; + resetConsentData(); + }); + + it('should bypass CMP and simply use previously stored consentData', function () { + let testConsentData = { + applicableSections: [7], + gppString: 'xyz', + }; + + cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP(testConsentData)); + setConsentConfig(goodConfig); + requestBidsHook(() => {}, {}); + cmpStub.reset(); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consent = gppDataHandler.getConsentData(); + + expect(didHookReturn).to.be.true; + expect(consent.gppString).to.equal(testConsentData.gppString); + expect(consent.applicableSections).to.deep.equal(testConsentData.applicableSections); + sinon.assert.notCalled(cmpStub); + }); + }); + + describe('iframe tests', function () { + let cmpPostMessageCb = () => {}; + let stringifyResponse; + + function createIFrameMarker(frameName) { + let ifr = document.createElement('iframe'); + ifr.width = 0; + ifr.height = 0; + ifr.name = frameName; + document.body.appendChild(ifr); + return ifr; + } + + function creatCmpMessageHandler(prefix, returnEvtValue, returnGPPValue) { + return function (event) { + if (event && event.data) { + let data = event.data; + if (data[`${prefix}Call`]) { + let callId = data[`${prefix}Call`].callId; + let response; + if (data[`${prefix}Call`].command === 'addEventListener') { + response = { + [`${prefix}Return`]: { + callId, + returnValue: returnEvtValue, + success: true + } + } + } else if (data[`${prefix}Call`].command === 'getGPPData') { + response = { + [`${prefix}Return`]: { + callId, + returnValue: returnGPPValue, + success: true + } + } + } + event.source.postMessage(stringifyResponse ? JSON.stringify(response) : response, '*'); + } + } + } + } + + function testIFramedPage(testName, messageFormatString, tarConsentString, tarSections) { + it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { + stringifyResponse = messageFormatString; + setConsentConfig(goodConfig); + requestBidsHook(() => { + let consent = gppDataHandler.getConsentData(); + sinon.assert.notCalled(utils.logError); + expect(consent.gppString).to.equal(tarConsentString); + expect(consent.applicableSections).to.deep.equal(tarSections); + done(); + }, {}); + }); + } + + beforeEach(function () { + sinon.stub(utils, 'logError'); + sinon.stub(utils, 'logWarn'); + }); + + afterEach(function () { + utils.logError.restore(); + utils.logWarn.restore(); + config.resetConfig(); + resetConsentData(); + }); + + describe('v2 CMP workflow for iframe pages:', function () { + stringifyResponse = false; + let ifr2 = null; + + beforeEach(function () { + ifr2 = createIFrameMarker('__gppLocator'); + cmpPostMessageCb = creatCmpMessageHandler('__gpp', { + eventName: 'sectionChange' + }, { + gppString: 'abc12345234', + applicableSections: [7] + }); + window.addEventListener('message', cmpPostMessageCb, false); + }); + + afterEach(function () { + delete window.__gpp; // deletes the local copy made by the postMessage CMP call function + document.body.removeChild(ifr2); + window.removeEventListener('message', cmpPostMessageCb); + }); + + testIFramedPage('with/JSON response', false, 'abc12345234', [7]); + testIFramedPage('with/String response', true, 'abc12345234', [7]); + }); + }); + + describe('direct calls to CMP API tests', function () { + let cmpStub = sinon.stub(); + + beforeEach(function () { + didHookReturn = false; + sinon.stub(utils, 'logError'); + sinon.stub(utils, 'logWarn'); + }); + + afterEach(function () { + config.resetConfig(); + cmpStub.restore(); + utils.logError.restore(); + utils.logWarn.restore(); + resetConsentData(); + }); + + describe('v2 CMP workflow for normal pages:', function () { + beforeEach(function () { + window.__gpp = function () {}; + }); + + afterEach(function () { + delete window.__gpp; + }); + + it('performs lookup check and stores consentData for a valid existing user', function () { + let testConsentData = { + gppString: 'abc12345234', + applicableSections: [7] + }; + cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { + if (args[0] === 'addEventListener') { + args[1]({ + eventName: 'sectionChange' + }); + } else if (args[0] === 'getGPPData') { + return testConsentData; + } + }); + + setConsentConfig(goodConfig); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consent = gppDataHandler.getConsentData(); + sinon.assert.notCalled(utils.logError); + expect(didHookReturn).to.be.true; + expect(consent.gppString).to.equal(testConsentData.gppString); + expect(consent.applicableSections).to.deep.equal(testConsentData.applicableSections); + }); + + it('produces gdpr metadata', function () { + let testConsentData = { + gppString: 'abc12345234', + applicableSections: [7] + }; + cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { + if (args[0] === 'addEventListener') { + args[1]({ + eventName: 'sectionChange' + }); + } else if (args[0] === 'getGPPData') { + return testConsentData; + } + }); + + setConsentConfig(goodConfig); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consentMeta = gppDataHandler.getConsentMeta(); + sinon.assert.notCalled(utils.logError); + expect(consentMeta.gpp.gppString).to.equal(testConsentData.gppString); + expect(consentMeta.gpp.applicableSections).to.deep.equal(testConsentData.applicableSections); + expect(consentMeta.apiVersion).to.equal(1); + expect(consentMeta.generatedAt).to.be.above(1644367751709); + }); + + it('throws an error when processCmpData check fails + does not call requestBids callback', function () { + let testConsentData = {}; + let bidsBackHandlerReturn = false; + + cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { + if (args[0] === 'addEventListener') { + args[1]({ + eventName: 'sectionChange' + }); + } else if (args[0] === 'getGPPData') { + return testConsentData; + } + }); + + setConsentConfig(goodConfig); + + sinon.assert.notCalled(utils.logWarn); + sinon.assert.notCalled(utils.logError); + + [utils.logWarn, utils.logError].forEach((stub) => stub.reset()); + + requestBidsHook(() => { + didHookReturn = true; + }, { + bidsBackHandler: () => bidsBackHandlerReturn = true + }); + let consent = gppDataHandler.getConsentData(); + + sinon.assert.calledOnce(utils.logError); + sinon.assert.notCalled(utils.logWarn); + expect(didHookReturn).to.be.false; + expect(bidsBackHandlerReturn).to.be.true; + expect(consent).to.be.null; + expect(gppDataHandler.ready).to.be.true; + }); + + describe('when proper consent is not available', () => { + let gppStub; + + function runAuction() { + setConsentConfig({ + gpp: { + cmpApi: 'iab', + timeout: 10, + } + }); + return new Promise((resolve, reject) => { + requestBidsHook(() => { + didHookReturn = true; + }, {}); + setTimeout(() => didHookReturn ? resolve() : reject(new Error('Auction did not run')), 20); + }) + } + + function mockGppCmp(gppdata) { + gppStub.callsFake((api, cb) => { + if (api === 'addEventListener') { + // eslint-disable-next-line standard/no-callback-literal + cb({ + pingData: { + cmpStatus: 'loaded', + cmpDisplayStatus: 'hidden' + } + }, true); + } + if (api === 'getGPPData') { + return gppdata; + } + }); + } + + beforeEach(() => { + gppStub = sinon.stub(window, '__gpp'); + }); + + afterEach(() => { + gppStub.restore(); + }) + + it('should continue auction with null consent when CMP is unresponsive', () => { + return runAuction().then(() => { + const consent = gppDataHandler.getConsentData(); + expect(consent.applicableSections).to.deep.equal([]); + expect(consent.gppString).to.be.undefined; + expect(gppDataHandler.ready).to.be.true; + }); + }); + + it('should use consent provided by events other than tcloaded', () => { + mockGppCmp({ + gppString: 'mock-consent-string', + applicableSections: [7] + }); + return runAuction().then(() => { + const consent = gppDataHandler.getConsentData(); + expect(consent.applicableSections).to.deep.equal([7]); + expect(consent.gppString).to.equal('mock-consent-string'); + expect(gppDataHandler.ready).to.be.true; + }); + }); + }); + }); + }); + }); +}); From 4aff5e20a6fece6fdb6103f576315d301764180e Mon Sep 17 00:00:00 2001 From: Jason Snellbaker Date: Thu, 8 Dec 2022 10:47:07 -0500 Subject: [PATCH 2/6] various updates and added test pages --- .../gpt/gpp_us_hello_world.html | 124 ++++++++++++++++ .../gpt/gpp_us_hello_world_iframe.html | 12 ++ .../gpp_us_hello_world_iframe_subpage.html | 140 ++++++++++++++++++ modules/appnexusBidAdapter.js | 5 + modules/consentManagementGpp.js | 46 ++++-- modules/prebidServerBidAdapter/index.js | 8 +- src/adapterManager.js | 22 +-- test/spec/modules/appnexusBidAdapter_spec.js | 46 ++++++ 8 files changed, 373 insertions(+), 30 deletions(-) create mode 100644 integrationExamples/gpt/gpp_us_hello_world.html create mode 100644 integrationExamples/gpt/gpp_us_hello_world_iframe.html create mode 100644 integrationExamples/gpt/gpp_us_hello_world_iframe_subpage.html diff --git a/integrationExamples/gpt/gpp_us_hello_world.html b/integrationExamples/gpt/gpp_us_hello_world.html new file mode 100644 index 00000000000..28be86127fc --- /dev/null +++ b/integrationExamples/gpt/gpp_us_hello_world.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+ +
+ +
+ + + \ No newline at end of file diff --git a/integrationExamples/gpt/gpp_us_hello_world_iframe.html b/integrationExamples/gpt/gpp_us_hello_world_iframe.html new file mode 100644 index 00000000000..c0a62f9d72e --- /dev/null +++ b/integrationExamples/gpt/gpp_us_hello_world_iframe.html @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/integrationExamples/gpt/gpp_us_hello_world_iframe_subpage.html b/integrationExamples/gpt/gpp_us_hello_world_iframe_subpage.html new file mode 100644 index 00000000000..8c2096d614d --- /dev/null +++ b/integrationExamples/gpt/gpp_us_hello_world_iframe_subpage.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+ +
+ +
+ + + \ No newline at end of file diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 1105448f874..919831b8515 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -309,6 +309,11 @@ export const spec = { gpp: bidderRequest.gppConsent.gppString, gpp_sid: bidderRequest.gppConsent.applicableSections } + } else if (bidderRequest?.ortb2?.regs?.gpp) { + payload.privacy = { + gpp: bidderRequest.ortb2.regs.gpp, + gpp_sid: bidderRequest.ortb2.regs.gpp_sid + } } if (bidderRequest && bidderRequest.refererInfo) { diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js index ecf8064de6e..9fe718a8a88 100644 --- a/modules/consentManagementGpp.js +++ b/modules/consentManagementGpp.js @@ -29,6 +29,30 @@ const cmpCallMap = { 'static': lookupStaticConsentData }; +/** + * This function checks the state of the IAB gppData's applicableSection field (to ensure it's populated and has a valid value). + * section === 0 represents a CMP's default value when CMP is loading, it shoud not be used a real user's section. + * + * TODO --- The initial version of the GPP CMP API spec used this naming convention, but it was later changed as an update to the spec. + * CMPs should adjust their logic to use the new format (applicableSecctions), but that may not be the case with the initial release. + * Added support just in case for this transition period, can likely be removed at a later date... + * @param gppData represents the IAB gppData object + * @returns true|false + */ +function checkApplicableSectionIsReady(gppData) { + return gppData && Array.isArray(gppData.applicableSection) && gppData.applicableSection.length > 0 && gppData.applicableSection[0] !== 0; +} + +/** + * This function checks the state of the IAB gppData's applicableSections field (to ensure it's populated and has a valid value). + * section === 0 represents a CMP's default value when CMP is loading, it shoud not be used a real user's section. + * @param gppData represents the IAB gppData object + * @returns true|false + */ +function checkApplicableSectionsIsReady(gppData) { + return gppData && Array.isArray(gppData.applicableSections) && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== 0; +} + /** * This function reads the consent string from the config to obtain the consent information of the user. * @param {function({})} onSuccess acts as a success callback when the value is read from config; pass along consentObject from CMP @@ -79,15 +103,6 @@ function lookupIabConsent({onSuccess, onError}) { }; } - // remove me later when revised v1 is fully adapted by all GPP CMPs - function checkApplicableSectionIsReady(gppData) { - return gppData && Array.isArray(gppData.applicableSection) && gppData.applicableSection.length > 0 && gppData.applicableSection[0] !== 0; - } - - function checkApplicableSectionsIsReady(gppData) { - return gppData && Array.isArray(gppData.applicableSections) && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== 0; - } - const cmpCallbacks = {}; const {cmpFrame, cmpDirectAccess} = findCMP(); @@ -328,8 +343,8 @@ export const requestBidsHook = timedAuctionHook('gpp', function requestBidsHook( function processCmpData(consentObject, {onSuccess, onError}) { function checkData() { const gppString = consentObject && consentObject.gppString; - const gppSection = (consentObject && consentObject.applicableSections) ? consentObject.applicableSections - : (consentObject && consentObject.applicableSection) ? consentObject.applicableSection + const gppSection = (checkApplicableSectionsIsReady(consentObject)) ? consentObject.applicableSections + : (checkApplicableSectionIsReady(consentObject)) ? consentObject.applicableSection : (provisionalConsent && provisionalConsent.applicableSections) ? provisionalConsent.applicableSections : []; return !!( @@ -351,12 +366,13 @@ function processCmpData(consentObject, {onSuccess, onError}) { */ function storeConsentData(cmpConsentObject) { consentData = { - gppString: (cmpConsentObject) ? cmpConsentObject.gppString : undefined, + gppString: (cmpConsentObject) ? cmpConsentObject.gppString + : (provisionalConsent && isStr(provisionalConsent.gppString) && provisionalConsent.gppString !== '') ? provisionalConsent.gppString : undefined, fullGppData: (cmpConsentObject) || undefined, }; - consentData.applicableSections = (cmpConsentObject && cmpConsentObject.applicableSections) ? cmpConsentObject.applicableSections - : (cmpConsentObject && cmpConsentObject.applicableSection) ? cmpConsentObject.applicableSection - : (provisionalConsent && provisionalConsent.applicableSections) ? provisionalConsent.applicableSections : []; + consentData.applicableSections = (checkApplicableSectionsIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSections + : (checkApplicableSectionIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSection + : (provisionalConsent?.applicableSections) ? provisionalConsent.applicableSections : []; consentData.apiVersion = CMP_VERSION; return consentData; } diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 9430a8768c7..0183b310be1 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -247,7 +247,13 @@ function queueSync(bidderCodes, gdprConsent, uspConsent, gppConsent, s2sConfig) } if (gppConsent) { - // TODO - need feedback on how to pass gpp values to payload for PBS usersync endpoints... + // proposing the following formatting, can adjust if needed... + + // transform array of numbers to comma separated string + payload.gpp_sid = gppConsent.applicableSections.join(); + // should we add check if applicableSections was not equal to -1 (where user was out of scope)? + // this would be similar to what was done above for TCF + payload.gpp = gppConsent.gppString; } if (typeof s2sConfig.coopSync === 'boolean') { diff --git a/src/adapterManager.js b/src/adapterManager.js index 28de07ffe84..f4f9e59fb84 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -310,23 +310,17 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a } }); - if (gdprDataHandler.getConsentData()) { - bidRequests.forEach(bidRequest => { + bidRequests.forEach(bidRequest => { + if (gdprDataHandler.getConsentData()) { bidRequest['gdprConsent'] = gdprDataHandler.getConsentData(); - }); - } - - if (uspDataHandler.getConsentData()) { - bidRequests.forEach(bidRequest => { + } + if (uspDataHandler.getConsentData()) { bidRequest['uspConsent'] = uspDataHandler.getConsentData(); - }); - } - - if (gppDataHandler.getConsentData()) { - bidRequests.forEach(bidRequest => { + } + if (gppDataHandler.getConsentData()) { bidRequest['gppConsent'] = gppDataHandler.getConsentData(); - }); - } + } + }); bidRequests.forEach(bidRequest => { config.runWithBidder(bidRequest.bidderCode, () => { diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 5cd189da9a1..1ab8feceaeb 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -985,6 +985,52 @@ describe('AppNexusAdapter', function () { expect(payload.us_privacy).to.exist.and.to.equal(consentString); }); + it('should add gpp information to the request via bidderRequest.gppConsent', function () { + let consentString = 'abc1234'; + let bidderRequest = { + 'bidderCode': 'appnexus', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': consentString, + 'applicableSections': [8] + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.privacy).to.exist; + expect(payload.privacy.gpp).to.equal(consentString); + expect(payload.privacy.gpp_sid).to.deep.equal([8]); + }); + + it('should add gpp information to the request via bidderRequest.ortb2.regs', function () { + let consentString = 'abc1234'; + let bidderRequest = { + 'bidderCode': 'appnexus', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'ortb2': { + 'regs': { + 'gpp': consentString, + 'gpp_sid': [7] + } + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.privacy).to.exist; + expect(payload.privacy.gpp).to.equal(consentString); + expect(payload.privacy.gpp_sid).to.deep.equal([7]); + }); + it('supports sending hybrid mobile app parameters', function () { let appRequest = Object.assign({}, bidRequests[0], From a55c085837d2e86250483612b6b6a44c539233a2 Mon Sep 17 00:00:00 2001 From: Jason Snellbaker Date: Mon, 12 Dec 2022 16:00:19 -0500 Subject: [PATCH 3/6] revise calling CMP, remove provisionalConsent, remove cmpDisplayStatus check, update pbs usersync --- modules/consentManagementGpp.js | 154 ++++++------------ modules/prebidServerBidAdapter/index.js | 5 +- .../spec/modules/consentManagementGpp_spec.js | 5 +- 3 files changed, 50 insertions(+), 114 deletions(-) diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js index 9fe718a8a88..ebd9cd89261 100644 --- a/modules/consentManagementGpp.js +++ b/modules/consentManagementGpp.js @@ -21,7 +21,6 @@ export let staticConsentData; let consentData; let addedConsentHook = false; -let provisionalConsent; // add new CMPs here, with their dedicated lookup function const cmpCallMap = { @@ -70,6 +69,7 @@ function lookupStaticConsentData({onSuccess, onError}) { */ function lookupIabConsent({onSuccess, onError}) { const cmpApiName = '__gpp'; + const cmpCallbacks = {}; let registeredPostMessageResponseListener = false; function findCMP() { @@ -103,88 +103,44 @@ function lookupIabConsent({onSuccess, onError}) { }; } - const cmpCallbacks = {}; - const {cmpFrame, cmpDirectAccess} = findCMP(); if (!cmpFrame) { return onError('GPP CMP not found.'); } - // use cmpDirectAccess to avoid an error checking on a cross-frame window to see if CMP function exists - if (cmpDirectAccess && typeof cmpFrame[cmpApiName] === 'function') { - logInfo('Detected GPP CMP API is directly accessible, calling it now...'); - cmpFrame[cmpApiName]('addEventListener', cmpDirectResponseCallback); - } else { - logInfo('Detected GPP CMP is outside the current iframe where Prebid.js is located, calling it now...'); - callCmpWhileInIframe('addEventListener', cmpFrame, cmpPostResponseEventCallback); - } + const invokeCMP = (cmpDirectAccess) ? invokeCMPDirect : invokeCMPFrame; - function cmpDirectResponseCallback(evt) { - if (evt) { - logInfo('Received a response from GPP CMP for event', evt); - if ( - evt.eventName === 'sectionChange' || // should be new consent data - ( - // cmp is loaded and not/no longer visible, a gpp string should be available - evt.pingData.cmpStatus === 'loaded' && - evt.pingData.cmpDisplayStatus !== 'visible' - ) - ) { - let gppData = cmpFrame[cmpApiName]('getGPPData'); - - logInfo('Received a response from GPP CMP for getGPPData', gppData); - processCmpData(gppData, {onSuccess, onError}); - } else if (evt.pingData.cmpStatus === 'error') { - onError('CMP returned with a cmpStatus:error response. Please check CMP setup.'); - } else if (!provisionalConsent) { - const provGppData = cmpFrame[cmpApiName]('getGPPData'); - logInfo('Called GPP CMP early to determine the value of applicableSections; value returned from CMP: ', provGppData); - if (checkApplicableSectionsIsReady(provGppData)) { - provisionalConsent = provGppData; - } else if (checkApplicableSectionIsReady(provGppData)) { - provisionalConsent = provGppData; - // conform naming convention from old version of the spec to new version - provisionalConsent.applicableSections = provGppData.applicableSection; - delete provisionalConsent.applicableSection; - } - } + function invokeCMPDirect({command, callback, parameter, version = CMP_VERSION}, resultCb) { + if (resultCb != null) { + resultCb(cmpFrame[cmpApiName](command, callback, parameter, version)); + } else { + cmpFrame[cmpApiName](command, callback, parameter, version); } } - function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback, cmpCallParam) { - // changing the name away from the standard, to properly handle multiple calls to CMP, - // otherwise follow-up calls made to fetch getGPPData routed here will complain later that there's no callback - const cmpApiPMName = '__gpp-pm'; + function invokeCMPFrame({command, callback, parameter, version = CMP_VERSION}, resultCb) { const callName = `${cmpApiName}Call`; - - /* Setup up a __cmp function to do the postMessage and stash the callback. - This function behaves (from the caller's perspective identically to the in-frame __gpp call */ - window[cmpApiPMName] = function (cmd, callback, param) { - const callId = Math.random().toString(); - const msg = { - [callName]: { - command: cmd, - parameter: param, - version: 1, - callId: callId - } - }; - - // TODO? - add logic to check if random was already used in the same session, and roll another if so? - cmpCallbacks[callId] = callback; - cmpFrame.postMessage(msg, '*'); - } - - // when we get the return message, call the stashed callback; - // register the listener only once per session if (!registeredPostMessageResponseListener) { + // when we get the return message, call the stashed callback; window.addEventListener('message', readPostMessageResponse, false); registeredPostMessageResponseListener = true; } - // call CMP - window[cmpApiPMName](commandName, moduleCallback, cmpCallParam); + // call CMP via postMessage + const callId = Math.random().toString(); + const msg = { + [callName]: { + command: command, + parameter, + version, + callId: callId + } + }; + + // TODO? - add logic to check if random was already used in the same session, and roll another if so? + cmpCallbacks[callId] = (callback != null) ? callback : resultCb; + cmpFrame.postMessage(msg, '*'); function readPostMessageResponse(event) { const cmpDataPkgName = `${cmpApiName}Return`; @@ -199,42 +155,26 @@ function lookupIabConsent({onSuccess, onError}) { } } - function cmpPostResponseEventCallback(evt) { - if (evt) { - logInfo('Received a postmsg response from GPP CMP for event', evt); - if ( - evt.eventName === 'sectionChange' || // should be new consent data - ( - // cmp is loaded and not/no longer visible, a gpp string should be available - evt.pingData.cmpStatus === 'loaded' && - evt.pingData.cmpDisplayStatus !== 'visible' - ) - ) { - callCmpWhileInIframe('getGPPData', cmpFrame, cmpPostResponseGetGPPCallback); - } else if (evt.pingData.cmpStatus === 'error') { - onError('CMP returned with a cmpStatus:error response. Please check CMP setup.'); - } else if (!provisionalConsent) { - callCmpWhileInIframe('getGPPData', cmpFrame, cmpPostResponseProvGetGPPCallback); + const startupMsg = (cmpDirectAccess) ? 'Detected GPP CMP API is directly accessible, calling it now...' + : 'Detected GPP CMP is outside the current iframe where Prebid.js is located, calling it now...'; + logInfo(startupMsg); + + invokeCMP({ + command: 'addEventListener', + callback: function (evt) { + if (evt) { + logInfo(`Received a ${(cmpDirectAccess ? 'direct' : 'postmsg')} response from GPP CMP for event`, evt); + if (evt.eventName === 'sectionChange' || evt.pingData.cmpStatus === 'loaded') { + invokeCMP({command: 'getGPPData'}, function (gppData) { + logInfo(`Received a ${cmpDirectAccess ? 'direct' : 'postmsg'} response from GPP CMP for getGPPData`, gppData); + processCmpData(gppData, {onSuccess, onError}); + }); + } else if (evt.pingData.cmpStatus === 'error') { + onError('CMP returned with a cmpStatus:error response. Please check CMP setup.'); + } } } - } - - function cmpPostResponseGetGPPCallback(gppData) { - logInfo('Received a postmsg response from GPP CMP for getGPPData', gppData); - processCmpData(gppData, {onSuccess, onError}); - } - - function cmpPostResponseProvGetGPPCallback(provGppData) { - logInfo('Called GPP CMP early to determine the value of applicableSections; postmsg response returned from CMP: ', provGppData); - if (checkApplicableSectionsIsReady(provGppData)) { - provisionalConsent = provGppData; - } else if (checkApplicableSectionIsReady(provGppData)) { - provisionalConsent = provGppData; - // conform naming convention from old version of the spec to new version - provisionalConsent.applicableSections = provGppData.applicableSection; - delete provisionalConsent.applicableSection; - } - } + }); } /** @@ -276,7 +216,7 @@ function loadConsentData(cb) { const continueToAuction = (data) => { done(data, false, 'GPP CMP did not load, continuing auction...'); } - processCmpData(provisionalConsent, { + processCmpData(consentData, { onSuccess: continueToAuction, onError: () => continueToAuction(storeConsentData(undefined)) }) @@ -344,8 +284,7 @@ function processCmpData(consentObject, {onSuccess, onError}) { function checkData() { const gppString = consentObject && consentObject.gppString; const gppSection = (checkApplicableSectionsIsReady(consentObject)) ? consentObject.applicableSections - : (checkApplicableSectionIsReady(consentObject)) ? consentObject.applicableSection - : (provisionalConsent && provisionalConsent.applicableSections) ? provisionalConsent.applicableSections : []; + : (checkApplicableSectionIsReady(consentObject)) ? consentObject.applicableSection : []; return !!( (!Array.isArray(gppSection)) || @@ -366,13 +305,12 @@ function processCmpData(consentObject, {onSuccess, onError}) { */ function storeConsentData(cmpConsentObject) { consentData = { - gppString: (cmpConsentObject) ? cmpConsentObject.gppString - : (provisionalConsent && isStr(provisionalConsent.gppString) && provisionalConsent.gppString !== '') ? provisionalConsent.gppString : undefined, + gppString: (cmpConsentObject) ? cmpConsentObject.gppString : undefined, + fullGppData: (cmpConsentObject) || undefined, }; consentData.applicableSections = (checkApplicableSectionsIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSections - : (checkApplicableSectionIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSection - : (provisionalConsent?.applicableSections) ? provisionalConsent.applicableSections : []; + : (checkApplicableSectionIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSection : []; consentData.apiVersion = CMP_VERSION; return consentData; } diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 0183b310be1..b609d1a54ec 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -248,9 +248,8 @@ function queueSync(bidderCodes, gdprConsent, uspConsent, gppConsent, s2sConfig) if (gppConsent) { // proposing the following formatting, can adjust if needed... - - // transform array of numbers to comma separated string - payload.gpp_sid = gppConsent.applicableSections.join(); + // update - leaving this param as an array, since it's part of a POST payload where the [] characters shouldn't matter too much + payload.gpp_sid = gppConsent.applicableSections // should we add check if applicableSections was not equal to -1 (where user was out of scope)? // this would be similar to what was done above for TCF payload.gpp = gppConsent.gppString; diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js index 786b4f9fba0..3c940f26831 100644 --- a/test/spec/modules/consentManagementGpp_spec.js +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -534,8 +534,7 @@ describe('consentManagementGpp', function () { // eslint-disable-next-line standard/no-callback-literal cb({ pingData: { - cmpStatus: 'loaded', - cmpDisplayStatus: 'hidden' + cmpStatus: 'loaded' } }, true); } @@ -562,7 +561,7 @@ describe('consentManagementGpp', function () { }); }); - it('should use consent provided by events other than tcloaded', () => { + it('should use consent provided by events other than sectionChange', () => { mockGppCmp({ gppString: 'mock-consent-string', applicableSections: [7] From 765efc14ccc7074d2e871dc97c3b8ecb1b9bf5e2 Mon Sep 17 00:00:00 2001 From: Jason Snellbaker Date: Tue, 13 Dec 2022 08:15:40 -0500 Subject: [PATCH 4/6] change callback check to be more strict --- modules/consentManagementGpp.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js index ebd9cd89261..42b82a4cd3d 100644 --- a/modules/consentManagementGpp.js +++ b/modules/consentManagementGpp.js @@ -112,7 +112,7 @@ function lookupIabConsent({onSuccess, onError}) { const invokeCMP = (cmpDirectAccess) ? invokeCMPDirect : invokeCMPFrame; function invokeCMPDirect({command, callback, parameter, version = CMP_VERSION}, resultCb) { - if (resultCb != null) { + if (typeof resultCb === 'function') { resultCb(cmpFrame[cmpApiName](command, callback, parameter, version)); } else { cmpFrame[cmpApiName](command, callback, parameter, version); @@ -139,7 +139,7 @@ function lookupIabConsent({onSuccess, onError}) { }; // TODO? - add logic to check if random was already used in the same session, and roll another if so? - cmpCallbacks[callId] = (callback != null) ? callback : resultCb; + cmpCallbacks[callId] = (typeof callback === 'function') ? callback : resultCb; cmpFrame.postMessage(msg, '*'); function readPostMessageResponse(event) { From c96bcdd9736beeed28d05d6ad0080c6bb1292778 Mon Sep 17 00:00:00 2001 From: Jason Snellbaker Date: Tue, 13 Dec 2022 15:42:52 -0500 Subject: [PATCH 5/6] update logic on adding gpp data to ortb2 --- modules/consentManagementGpp.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js index 42b82a4cd3d..8a9c3f999f0 100644 --- a/modules/consentManagementGpp.js +++ b/modules/consentManagementGpp.js @@ -9,7 +9,7 @@ import {config} from '../src/config.js'; import {gppDataHandler} from '../src/adapterManager.js'; import {includes} from '../src/polyfill.js'; import {timedAuctionHook} from '../src/utils/perfMetrics.js'; -import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; +import { enrichFPD } from '../src/fpd/enrichment.js'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; @@ -370,15 +370,17 @@ export function setConsentConfig(config) { } config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); -// TODO this function will likely change a bit once PR #9205 is merged to master -export function setOrtbGpp(ortbRequest, bidderRequest) { - const consent = bidderRequest.gppConsent; - if (consent) { - if (Array.isArray(consent.applicableSections)) { - deepSetValue(ortbRequest, 'regs.gpp_sid', consent.applicableSections); +export function enrichFPDHook(next, fpd) { + return next(fpd.then(ortb2 => { + const consent = gppDataHandler.getConsentData(); + if (consent) { + if (Array.isArray(consent.applicableSections)) { + deepSetValue(ortb2, 'regs.gpp_sid', consent.applicableSections); + } + deepSetValue(ortb2, 'regs.gpp', consent.gppString); } - deepSetValue(ortbRequest, 'regs.gpp', consent.gppString); - } + return ortb2; + })); } -registerOrtbProcessor({type: REQUEST, name: 'gpp', fn: setOrtbGpp}); +enrichFPD.before(enrichFPDHook); From d63b02094c6a6912a0f4f6467d225c06e20cfcbe Mon Sep 17 00:00:00 2001 From: Jason Snellbaker Date: Thu, 15 Dec 2022 15:00:55 -0500 Subject: [PATCH 6/6] update gpp metadata --- src/consentHandler.js | 3 --- test/spec/modules/consentManagementGpp_spec.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/consentHandler.js b/src/consentHandler.js index 31f6e6e1515..b1b2a04c043 100644 --- a/src/consentHandler.js +++ b/src/consentHandler.js @@ -113,10 +113,7 @@ export class GppConsentHandler extends ConsentHandler { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { return { - // TODO - is more needed here? Are the properties below fine? - apiVersion: consentData.apiVersion, generatedAt: this.generatedTime, - gpp: consentData, } } } diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js index 3c940f26831..1170f418caf 100644 --- a/test/spec/modules/consentManagementGpp_spec.js +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -468,9 +468,6 @@ describe('consentManagementGpp', function () { }, {}); let consentMeta = gppDataHandler.getConsentMeta(); sinon.assert.notCalled(utils.logError); - expect(consentMeta.gpp.gppString).to.equal(testConsentData.gppString); - expect(consentMeta.gpp.applicableSections).to.deep.equal(testConsentData.applicableSections); - expect(consentMeta.apiVersion).to.equal(1); expect(consentMeta.generatedAt).to.be.above(1644367751709); });