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 77ffe0f6b94..919831b8515 100644
--- a/modules/appnexusBidAdapter.js
+++ b/modules/appnexusBidAdapter.js
@@ -304,6 +304,18 @@ export const spec = {
payload.us_privacy = bidderRequest.uspConsent;
}
+ if (bidderRequest?.gppConsent) {
+ payload.privacy = {
+ 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) {
let refererinfo = {
// TODO: are these the correct referer values?
@@ -424,8 +436,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 6ca12010c74..217ceecb1c4 100644
--- a/modules/consentManagement.js
+++ b/modules/consentManagement.js
@@ -94,7 +94,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)
@@ -314,11 +314,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..8a9c3f999f0
--- /dev/null
+++ b/modules/consentManagementGpp.js
@@ -0,0 +1,386 @@
+/**
+ * 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 { enrichFPD } from '../src/fpd/enrichment.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;
+
+// add new CMPs here, with their dedicated lookup function
+const cmpCallMap = {
+ 'iab': lookupIabConsent,
+ '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
+ */
+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';
+ const cmpCallbacks = {};
+ 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
+ };
+ }
+
+ const {cmpFrame, cmpDirectAccess} = findCMP();
+
+ if (!cmpFrame) {
+ return onError('GPP CMP not found.');
+ }
+
+ const invokeCMP = (cmpDirectAccess) ? invokeCMPDirect : invokeCMPFrame;
+
+ function invokeCMPDirect({command, callback, parameter, version = CMP_VERSION}, resultCb) {
+ if (typeof resultCb === 'function') {
+ resultCb(cmpFrame[cmpApiName](command, callback, parameter, version));
+ } else {
+ cmpFrame[cmpApiName](command, callback, parameter, version);
+ }
+ }
+
+ function invokeCMPFrame({command, callback, parameter, version = CMP_VERSION}, resultCb) {
+ const callName = `${cmpApiName}Call`;
+ if (!registeredPostMessageResponseListener) {
+ // when we get the return message, call the stashed callback;
+ window.addEventListener('message', readPostMessageResponse, false);
+ registeredPostMessageResponseListener = true;
+ }
+
+ // 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] = (typeof callback === 'function') ? callback : resultCb;
+ cmpFrame.postMessage(msg, '*');
+
+ 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);
+ }
+ }
+ }
+ }
+
+ 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.');
+ }
+ }
+ }
+ });
+}
+
+/**
+ * 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(consentData, {
+ 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 = (checkApplicableSectionsIsReady(consentObject)) ? consentObject.applicableSections
+ : (checkApplicableSectionIsReady(consentObject)) ? consentObject.applicableSection : [];
+
+ 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 = (checkApplicableSectionsIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSections
+ : (checkApplicableSectionIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSection : [];
+ 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));
+
+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);
+ }
+ return ortb2;
+ }));
+}
+
+enrichFPD.before(enrichFPDHook);
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..b609d1a54ec 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,15 @@ function queueSync(bidderCodes, gdprConsent, uspConsent, s2sConfig) {
payload.us_privacy = uspConsent;
}
+ if (gppConsent) {
+ // proposing the following formatting, can adjust if needed...
+ // 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;
+ }
+
if (typeof s2sConfig.coopSync === 'boolean') {
payload.coopSync = s2sConfig.coopSync;
}
@@ -330,7 +339,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 +350,8 @@ function doClientSideSyncs(bidders, gdprConsent, uspConsent) {
clientAdapter,
[],
gdprConsent,
- uspConsent
+ uspConsent,
+ gppConsent
)
);
}
@@ -411,12 +421,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 +444,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 +452,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 +461,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..f4f9e59fb84 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() {
@@ -309,17 +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()) {
+ bidRequest['gppConsent'] = gppDataHandler.getConsentData();
+ }
+ });
bidRequests.forEach(bidRequest => {
config.runWithBidder(bidRequest.bidderCode, () => {
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 6fb642be525..25c8e9df10f 100644
--- a/src/consentHandler.js
+++ b/src/consentHandler.js
@@ -97,3 +97,14 @@ export class GdprConsentHandler extends ConsentHandler {
}
}
}
+
+export class GppConsentHandler extends ConsentHandler {
+ getConsentMeta() {
+ const consentData = this.getConsentData();
+ if (consentData && this.generatedTime) {
+ return {
+ generatedAt: this.generatedTime,
+ }
+ }
+ }
+}
diff --git a/src/prebid.js b/src/prebid.js
index f30b4ea9bbe..a4abceb01d5 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';
@@ -336,6 +336,7 @@ function getConsentMetadata() {
return {
gdpr: gdprDataHandler.getConsentMeta(),
usp: uspDataHandler.getConsentMeta(),
+ gpp: gppDataHandler.getConsentMeta(),
coppa: !!(config.getConfig('coppa'))
}
}
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],
diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js
new file mode 100644
index 00000000000..1170f418caf
--- /dev/null
+++ b/test/spec/modules/consentManagementGpp_spec.js
@@ -0,0 +1,577 @@
+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.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'
+ }
+ }, 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 sectionChange', () => {
+ 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;
+ });
+ });
+ });
+ });
+ });
+ });
+});