Skip to content

Commit 70cd775

Browse files
authored
Prebid core: make GDPR/USP consent data available without requiring an auction (#8185)
* Load USP consent data on page load * Load GPDR consent data on page load * Update jsdoc
1 parent cbb9ee4 commit 70cd775

File tree

7 files changed

+241
-241
lines changed

7 files changed

+241
-241
lines changed

modules/consentManagement.js

Lines changed: 134 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,22 @@ const cmpCallMap = {
3434

3535
/**
3636
* This function reads the consent string from the config to obtain the consent information of the user.
37-
* @param {function(string)} cmpSuccess acts as a success callback when the value is read from config; pass along consentObject (string) from CMP
38-
* @param {function(string)} cmpError acts as an error callback while interacting with the config string; pass along an error message (string)
39-
* @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
37+
* @param {function({})} onSuccess acts as a success callback when the value is read from config; pass along consentObject from CMP
4038
*/
41-
function lookupStaticConsentData(cmpSuccess, cmpError, hookConfig) {
42-
cmpSuccess(staticConsentData, hookConfig);
39+
function lookupStaticConsentData({onSuccess}) {
40+
onSuccess(staticConsentData);
4341
}
4442

4543
/**
4644
* This function handles interacting with an IAB compliant CMP to obtain the consent information of the user.
4745
* Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function
4846
* based on the appropriate result.
49-
* @param {function(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP
50-
* @param {function(string)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string)
51-
* @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
47+
* @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP
48+
* @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)
49+
* @param width
50+
* @param height size info passed to the SafeFrame API (used only for TCFv1 when Prebid is running within a safeframe)
5251
*/
53-
function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
52+
function lookupIabConsent({onSuccess, onError, width, height}) {
5453
function findCMP() {
5554
let f = window;
5655
let cmpFrame;
@@ -100,10 +99,10 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
10099
logInfo('Received a response from CMP', tcfData);
101100
if (success) {
102101
if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') {
103-
cmpSuccess(tcfData, hookConfig);
102+
processCmpData(tcfData, {onSuccess, onError});
104103
}
105104
} else {
106-
cmpError('CMP unable to register callback function. Please check CMP setup.', hookConfig);
105+
onError('CMP unable to register callback function. Please check CMP setup.');
107106
}
108107
}
109108

@@ -113,7 +112,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
113112
function afterEach() {
114113
if (cmpResponse.getConsentData && cmpResponse.getVendorConsents) {
115114
logInfo('Received all requested responses from CMP', cmpResponse);
116-
cmpSuccess(cmpResponse, hookConfig);
115+
processCmpData(cmpResponse, {onSuccess, onError});
117116
}
118117
}
119118

@@ -134,7 +133,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
134133
let { cmpFrame, cmpFunction } = findCMP();
135134

136135
if (!cmpFrame) {
137-
return cmpError('CMP not found.', hookConfig);
136+
return onError('CMP not found.');
138137
}
139138
// to collect the consent information from the user, we perform two calls to the CMP in parallel:
140139
// first to collect the user's consent choices represented in an encoded string (via getConsentData)
@@ -181,16 +180,6 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
181180
}
182181
}
183182

184-
// find sizes from adUnits object
185-
let adUnits = hookConfig.adUnits;
186-
let width = 1;
187-
let height = 1;
188-
if (Array.isArray(adUnits) && adUnits.length > 0) {
189-
let sizes = getAdUnitSizes(adUnits[0]);
190-
width = sizes[0][0];
191-
height = sizes[0][1];
192-
}
193-
194183
window.$sf.ext.register(width, height, sfCallback);
195184
window.$sf.ext.cmp(commandName);
196185
}
@@ -259,6 +248,70 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
259248
}
260249
}
261250

251+
/**
252+
* Look up consent data and store it in the `consentData` global as well as `adapterManager.js`' gdprDataHandler.
253+
*
254+
* @param cb A callback that takes: a boolean that is true if the auction should be canceled; an error message and extra
255+
* error arguments that will be undefined if there's no error.
256+
* @param width if we are running in an iframe, the TCFv1 spec requires us to use the SafeFrame API to find the CMP - which
257+
* in turn requires width and height.
258+
* @param height see width above
259+
*/
260+
function loadConsentData(cb, width = 1, height = 1) {
261+
let isDone = false;
262+
let timer = null;
263+
264+
function done(consentData, shouldCancelAuction, errMsg, ...extraArgs) {
265+
if (timer != null) {
266+
clearTimeout(timer);
267+
}
268+
isDone = true;
269+
gdprDataHandler.setConsentData(consentData);
270+
if (cb != null) {
271+
cb(shouldCancelAuction, errMsg, ...extraArgs);
272+
}
273+
}
274+
275+
if (!includes(Object.keys(cmpCallMap), userCMP)) {
276+
done(null, false, `CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`);
277+
return;
278+
}
279+
280+
const callbacks = {
281+
onSuccess: (data) => done(data, false),
282+
onError: function (msg, ...extraArgs) {
283+
let consentData = null;
284+
let shouldCancelAuction = true;
285+
if (allowAuction.value && cmpVersion === 1) {
286+
// still set the consentData to undefined when there is a problem as per config options
287+
consentData = storeConsentData(undefined);
288+
shouldCancelAuction = false;
289+
}
290+
done(consentData, shouldCancelAuction, msg, ...extraArgs);
291+
}
292+
}
293+
cmpCallMap[userCMP]({
294+
width,
295+
height,
296+
...callbacks
297+
});
298+
299+
if (!isDone) {
300+
if (consentTimeout === 0) {
301+
processCmpData(undefined, callbacks);
302+
} else {
303+
timer = setTimeout(function () {
304+
if (cmpVersion === 2) {
305+
// for TCFv2, we allow the auction to continue on timeout
306+
done(storeConsentData(undefined), false, `No response from CMP, continuing auction...`)
307+
} else {
308+
callbacks.onError('CMP workflow exceeded timeout threshold.');
309+
}
310+
}, consentTimeout);
311+
}
312+
}
313+
}
314+
262315
/**
263316
* If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the
264317
* user's encoded consent string from the supported CMP. Once obtained, the module will store this
@@ -268,49 +321,60 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) {
268321
* @param {function} fn required; The next function in the chain, used by hook.js
269322
*/
270323
export function requestBidsHook(fn, reqBidsConfigObj) {
271-
// preserves all module related variables for the current auction instance (used primiarily for concurrent auctions)
272-
const hookConfig = {
273-
context: this,
274-
args: [reqBidsConfigObj],
275-
nextFn: fn,
276-
adUnits: reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits,
277-
bidsBackHandler: reqBidsConfigObj.bidsBackHandler,
278-
haveExited: false,
279-
timer: null
280-
};
281-
282-
// in case we already have consent (eg during bid refresh)
283-
if (consentData) {
284-
logInfo('User consent information already known. Pulling internally stored information...');
285-
return exitModule(null, hookConfig);
286-
}
324+
const load = (() => {
325+
if (consentData) {
326+
logInfo('User consent information already known. Pulling internally stored information...');
327+
return function (cb) {
328+
// eslint-disable-next-line standard/no-callback-literal
329+
cb(false);
330+
}
331+
} else {
332+
// find sizes from adUnits object
333+
let adUnits = reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits;
334+
let width = 1;
335+
let height = 1;
336+
if (Array.isArray(adUnits) && adUnits.length > 0) {
337+
let sizes = getAdUnitSizes(adUnits[0]);
338+
width = sizes[0][0];
339+
height = sizes[0][1];
340+
}
287341

288-
if (!includes(Object.keys(cmpCallMap), userCMP)) {
289-
logWarn(`CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`);
290-
gdprDataHandler.setConsentData(null);
291-
return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args);
292-
}
342+
return function (cb) {
343+
loadConsentData(cb, width, height);
344+
}
345+
}
346+
})();
293347

294-
cmpCallMap[userCMP].call(this, processCmpData, cmpFailed, hookConfig);
348+
load(function (shouldCancelAuction, errMsg, ...extraArgs) {
349+
if (errMsg) {
350+
let log = logWarn;
351+
if (cmpVersion === 1 && !shouldCancelAuction) {
352+
errMsg = `${errMsg} 'allowAuctionWithoutConsent' activated.`;
353+
} else if (shouldCancelAuction) {
354+
log = logError;
355+
errMsg = `${errMsg} Canceling auction as per consentManagement config.`;
356+
}
357+
log(errMsg, ...extraArgs);
358+
}
295359

296-
// only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished)
297-
if (!hookConfig.haveExited) {
298-
if (consentTimeout === 0) {
299-
processCmpData(undefined, hookConfig);
360+
if (shouldCancelAuction) {
361+
if (typeof reqBidsConfigObj.bidsBackHandler === 'function') {
362+
reqBidsConfigObj.bidsBackHandler();
363+
} else {
364+
logError('Error executing bidsBackHandler');
365+
}
300366
} else {
301-
hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), consentTimeout);
367+
fn.call(this, reqBidsConfigObj);
302368
}
303-
}
369+
});
304370
}
305371

306372
/**
307373
* This function checks the consent data provided by CMP to ensure it's in an expected state.
308-
* If it's bad, we exit the module depending on config settings.
309-
* If it's good, then we store the value and exits the module.
310-
* @param {object} consentObject required; object returned by CMP that contains user's consent choices
311-
* @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
374+
* If it's bad, we call `onError`
375+
* If it's good, then we store the value and call `onSuccess`
312376
*/
313-
function processCmpData(consentObject, hookConfig) {
377+
function processCmpData(consentObject, {onSuccess, onError}) {
314378
function checkV1Data(consentObject) {
315379
let gdprApplies = consentObject && consentObject.getConsentData && consentObject.getConsentData.gdprApplies;
316380
return !!(
@@ -346,57 +410,19 @@ function processCmpData(consentObject, hookConfig) {
346410
// determine which set of checks to run based on cmpVersion
347411
let checkFn = (cmpVersion === 1) ? checkV1Data : (cmpVersion === 2) ? checkV2Data : null;
348412

349-
// Raise deprecation warning if 'allowAuctionWithoutConsent' is used with TCF 2.
350-
if (allowAuction.definedInConfig && cmpVersion === 2) {
351-
logWarn(`'allowAuctionWithoutConsent' ignored for TCF 2`);
352-
} else if (!allowAuction.definedInConfig && cmpVersion === 1) {
353-
logInfo(`'allowAuctionWithoutConsent' using system default: (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`);
354-
}
355-
356413
if (isFn(checkFn)) {
357414
if (checkFn(consentObject)) {
358-
cmpFailed(`CMP returned unexpected value during lookup process.`, hookConfig, consentObject);
415+
onError(`CMP returned unexpected value during lookup process.`, consentObject);
359416
} else {
360-
clearTimeout(hookConfig.timer);
361-
storeConsentData(consentObject);
362-
exitModule(null, hookConfig);
417+
onSuccess(storeConsentData(consentObject));
363418
}
364419
} else {
365-
cmpFailed('Unable to derive CMP version to process data. Consent object does not conform to TCF v1 or v2 specs.', hookConfig, consentObject);
420+
onError('Unable to derive CMP version to process data. Consent object does not conform to TCF v1 or v2 specs.', consentObject);
366421
}
367422
}
368423

369424
/**
370-
* General timeout callback when interacting with CMP takes too long.
371-
*/
372-
function cmpTimedOut(hookConfig) {
373-
if (cmpVersion === 2) {
374-
logWarn(`No response from CMP, continuing auction...`)
375-
storeConsentData(undefined);
376-
exitModule(null, hookConfig)
377-
} else {
378-
cmpFailed('CMP workflow exceeded timeout threshold.', hookConfig);
379-
}
380-
}
381-
382-
/**
383-
* This function contains the controlled steps to perform when there's a problem with CMP.
384-
* @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened.
385-
* @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
386-
* @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging
387-
*/
388-
function cmpFailed(errMsg, hookConfig, extraArgs) {
389-
clearTimeout(hookConfig.timer);
390-
391-
// still set the consentData to undefined when there is a problem as per config options
392-
if (allowAuction.value && cmpVersion === 1) {
393-
storeConsentData(undefined);
394-
}
395-
exitModule(errMsg, hookConfig, extraArgs);
396-
}
397-
398-
/**
399-
* Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanager.js for later in the auction
425+
* Stores CMP data locally in module to make information available in adaptermanager.js for later in the auction
400426
* @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only)
401427
*/
402428
function storeConsentData(cmpConsentObject) {
@@ -417,51 +443,7 @@ function storeConsentData(cmpConsentObject) {
417443
};
418444
}
419445
consentData.apiVersion = cmpVersion;
420-
gdprDataHandler.setConsentData(consentData);
421-
}
422-
423-
/**
424-
* This function handles the exit logic for the module.
425-
* While there are several paths in the module's logic to call this function, we only allow 1 of the 3 potential exits to happen before suppressing others.
426-
*
427-
* We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios.
428-
* One scenario could be auction was canceled due to timeout with CMP being reached.
429-
* While the timeout is the accepted exit and runs first, the CMP's callback still tries to process the user's data (which normally leads to a good exit).
430-
* In this case, the good exit will be suppressed since we already decided to cancel the auction.
431-
*
432-
* Three exit paths are:
433-
* 1. good exit where auction runs (CMP data is processed normally).
434-
* 2. bad exit but auction still continues (warning message is logged, CMP data is undefined and still passed along).
435-
* 3. bad exit with auction canceled (error message is logged).
436-
* @param {string} errMsg optional; only to be used when there was a 'bad' exit. String is a descriptive message for the failure/issue encountered.
437-
* @param {object} hookConfig contains module related variables (see comment in requestBidsHook function)
438-
* @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging
439-
*/
440-
function exitModule(errMsg, hookConfig, extraArgs) {
441-
if (hookConfig.haveExited === false) {
442-
hookConfig.haveExited = true;
443-
444-
let context = hookConfig.context;
445-
let args = hookConfig.args;
446-
let nextFn = hookConfig.nextFn;
447-
448-
if (errMsg) {
449-
if (allowAuction.value && cmpVersion === 1) {
450-
logWarn(errMsg + ` 'allowAuctionWithoutConsent' activated.`, extraArgs);
451-
nextFn.apply(context, args);
452-
} else {
453-
logError(errMsg + ' Canceling auction as per consentManagement config.', extraArgs);
454-
gdprDataHandler.setConsentData(null);
455-
if (typeof hookConfig.bidsBackHandler === 'function') {
456-
hookConfig.bidsBackHandler();
457-
} else {
458-
logError('Error executing bidsBackHandler');
459-
}
460-
}
461-
} else {
462-
nextFn.apply(context, args);
463-
}
464-
}
446+
return consentData;
465447
}
466448

467449
/**
@@ -509,7 +491,6 @@ export function setConsentConfig(config) {
509491
gdprScope = config.defaultGdprScope === true;
510492

511493
logInfo('consentManagement module has been activated...');
512-
gdprDataHandler.enable();
513494

514495
if (userCMP === 'static') {
515496
if (isPlainObject(config.consentData)) {
@@ -523,5 +504,14 @@ export function setConsentConfig(config) {
523504
$$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50);
524505
}
525506
addedConsentHook = true;
507+
gdprDataHandler.enable();
508+
loadConsentData(); // immediately look up consent data to make it available without requiring an auction
509+
510+
// Raise deprecation warning if 'allowAuctionWithoutConsent' is used with TCF 2.
511+
if (allowAuction.definedInConfig && cmpVersion === 2) {
512+
logWarn(`'allowAuctionWithoutConsent' ignored for TCF 2`);
513+
} else if (!allowAuction.definedInConfig && cmpVersion === 1) {
514+
logInfo(`'allowAuctionWithoutConsent' using system default: (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`);
515+
}
526516
}
527517
config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement));

0 commit comments

Comments
 (0)