-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GDPR consentManagement module #2213
Changes from 25 commits
bcf27d8
cda55fe
77de2e5
ebe1ee8
3cda142
6337a2e
78f0e9c
2412b4f
b8c1da6
173d945
6f57a89
83a4ed9
2a587eb
2018378
0081610
a46ca14
6739fd0
63d0d21
effc19c
e6d8068
326e712
112a61b
91b6d83
a425228
f273018
2e465fd
540b4b5
4de3df0
7f78734
d6a4807
8d23307
2ccfedf
a96b129
b7811f8
9a1f09b
73d02f0
5ae5eff
78fcc64
c39b12d
213f2fa
405a6f2
fc41a5a
296e9ca
750797a
effa5b5
a3ca63f
4bc0f9b
4a6e273
2f32574
f042d7c
56f6df4
1a35235
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
/** | ||
* This module adds GDPR 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 GDPR supported adapters to read/pass this information to | ||
* their system. | ||
*/ | ||
import * as utils from 'src/utils'; | ||
import { config } from 'src/config'; | ||
import { gdprDataHandler } from 'src/adaptermanager'; | ||
|
||
const DEFAULT_CMP = 'appnexus'; | ||
const DEFAULT_CONSENT_TIMEOUT = 10000; | ||
const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true; | ||
|
||
export let userCMP; | ||
export let consentTimeout; | ||
export let allowAuction; | ||
|
||
let consentData; | ||
|
||
let context; | ||
let args; | ||
let nextFn; | ||
|
||
let timer; | ||
let haveExited; | ||
|
||
// add new CMPs here, with their dedicated lookup function that passes the consentString to postLookup() | ||
const cmpCallMap = { | ||
'appnexus': lookupAppNexusConsent | ||
}; | ||
|
||
/** | ||
* This function handles interacting with the AppNexus CMP to obtain the consentString value of the user. | ||
* Given the asynch 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(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentString (string) from CMP | ||
* @param {function(string)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This API is much better 👍 |
||
function lookupAppNexusConsent(cmpSuccess, cmpError) { | ||
if (!window.__cmp) { | ||
return cmpError('AppNexus CMP not detected.'); | ||
} | ||
|
||
// first lookup - to determine if new or existing user | ||
// if new user, then wait for user to make a choice and then run postLookup method | ||
// if existing user, then skip to postLookup method | ||
window.__cmp('getConsentData', 'vendorConsents', function(consentString) { | ||
if (consentString == null) { | ||
window.__cmp('addEventListener', 'onSubmit', function() { | ||
// redo lookup to find new string based on user's choices | ||
window.__cmp('getConsentData', 'vendorConsents', cmpSuccess); | ||
}); | ||
} else { | ||
cmpSuccess(consentString); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* 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 gdprConsent object and transferred to adaptermanager's gdprDataHandler object. | ||
* This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. | ||
* @param {object} config This is the same param that's used in pbjs.requestBids. The config.adunits will be updated. | ||
* @param {function} fn The next function in the chain, used by hook.js | ||
*/ | ||
export function requestBidsHook(config, fn) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would like to see JSdocs notation on exported functions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added some more descriptive information to the exported functions. |
||
context = this; | ||
args = arguments; | ||
nextFn = fn; | ||
haveExited = false; | ||
|
||
// in case we already have consent (eg during bid refresh) | ||
if (consentData) { | ||
return nextFn.apply(context, args); | ||
} | ||
|
||
if (!Object.keys(cmpCallMap).includes(userCMP)) { | ||
utils.logWarn(`CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); | ||
return nextFn.apply(context, args); | ||
} | ||
|
||
// lookup times and user interaction with CMP prompts can greatly vary, so enforcing a timeout on the CMP process | ||
timer = setTimeout(cmpTimedOut, consentTimeout); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please skip the setTimeout if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mkendall07 talked with me about this change this morning. I've got the changes put together and will include them in the next commit. As a confirmation question - if the publisher were to have the setting to cancel the auction enabled, the combination of Any subsequent page loads for that new user would go through the normal workflow and should be fine (even if the publisher had the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First time cancelation is fine -- it's a pretty straightforward implication of the publisher's wishes in this scenario, IMO. |
||
|
||
cmpCallMap[userCMP].call(this, processCmpData, exitFailedCmp); | ||
} | ||
|
||
// after we have grabbed ideal ID from CMP, apply the data to adUnits object and finish up the module | ||
function processCmpData(consentString) { | ||
if (typeof consentString !== 'string' || consentString === '') { | ||
exitFailedCmp(`CMP returned unexpected value during lookup process; returned value was (${consentString}).`); | ||
} | ||
clearTimeout(timer); | ||
|
||
// to stop the auction from running if we chose to cancel and timeout was reached | ||
if (haveExited === false) { | ||
storeConsentData(consentString); | ||
nextFn.apply(context, args); | ||
} | ||
} | ||
|
||
// store CMP string in module and invoke gdprDataHandler.setConsentData() to make information available in adaptermanger.js | ||
function storeConsentData(cmpConsentString) { | ||
consentData = { | ||
consentString: cmpConsentString, | ||
consentRequired: true | ||
}; | ||
gdprDataHandler.setConsentData(consentData); | ||
} | ||
|
||
function cmpTimedOut() { | ||
exitFailedCmp('CMP workflow exceeded timeout threshold.'); | ||
} | ||
|
||
// controls the exit of the module based on consentManagement config; either we'll resume the auction or cancel the auction | ||
function exitFailedCmp(message) { | ||
clearTimeout(timer); | ||
haveExited = true; | ||
if (allowAuction) { | ||
utils.logWarn(message + ' Resuming auction without consent data as per consentManagement config.'); | ||
storeConsentData(undefined); | ||
|
||
nextFn.apply(context, args); | ||
} else { | ||
utils.logError(message + ' Canceling auction as per consentManagement config.'); | ||
} | ||
} | ||
|
||
/** Simply resets the module's consentData variable back to undefined */ | ||
export function resetConsentData() { | ||
consentData = undefined; | ||
} | ||
|
||
/** | ||
* A configuration function that initializes some module variables, as well as add a hook into the requestBids function | ||
* @param {object} config consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) | ||
*/ | ||
export function setConfig(config) { | ||
if (typeof config.cmp === 'string') { | ||
userCMP = config.cmp; | ||
} else { | ||
userCMP = DEFAULT_CMP; | ||
utils.logInfo(`consentManagement config did not specify cmp. Using system default setting (${userCMP}).`); | ||
} | ||
|
||
if (typeof config.timeout === 'number') { | ||
consentTimeout = config.timeout; | ||
} else { | ||
consentTimeout = DEFAULT_CONSENT_TIMEOUT; | ||
utils.logInfo(`consentManagement config did not specify timeout. Using system default setting (${consentTimeout}).`); | ||
} | ||
|
||
if (typeof config.allowAuctionWithoutConsent !== 'undefined') { | ||
allowAuction = config.allowAuctionWithoutConsent; | ||
} else { | ||
allowAuction = DEFAULT_ALLOW_AUCTION_WO_CONSENT; | ||
utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent. Using system default setting (${allowAuction}).`); | ||
} | ||
|
||
$$PREBID_GLOBAL$$.requestBids.addHook(requestBidsHook, 50); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I always forget what the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See comment above for the change in the |
||
} | ||
config.getConfig('consentManagement', config => setConfig(config.consentManagement)); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -124,7 +124,7 @@ pbjs.requestBids.addHook((config, next = config) => { | |
} else { | ||
logWarn(`${MODULE_NAME} module: concurrency has been disabled and "$$PREBID_GLOBAL$$.requestBids" call was queued`); | ||
} | ||
}, 100); | ||
}, 5); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the impact of this change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The number (also relates to the other comment below) acts as a priority level for the hooked functions (when there are multiple hooked functions on the same hook). The Having the The 5 specifically is lower than the default priority that's set for any hooked function (which is 10), but still higher than the base function |
||
|
||
Object.keys(auctionPropMap).forEach(prop => { | ||
if (prop === 'allBidsAvailable') { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -147,6 +147,16 @@ function getAdUnitCopyForClientAdapters(adUnits) { | |
return adUnitsClientCopy; | ||
} | ||
|
||
exports.gdprDataHandler = { | ||
consentData: null, | ||
setConsentData: function(consentInfo) { | ||
this.consentData = consentInfo; | ||
}, | ||
getConsentData: function() { | ||
return this.consentData; | ||
} | ||
}; | ||
|
||
exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, labels) { | ||
let bidRequests = []; | ||
|
||
|
@@ -211,6 +221,12 @@ exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, | |
bidRequests.push(bidderRequest); | ||
} | ||
}); | ||
|
||
if (exports.gdprDataHandler.getConsentData()) { | ||
bidRequests.forEach(bidRequest => { | ||
bidRequest['gdprConsent'] = exports.gdprDataHandler.getConsentData(); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if there's a better way to do this.... |
||
} | ||
return bidRequests; | ||
}; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think these vars need to be exported?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm exporting these vars so I can read them in the
consentManagement_spec.js
test file for thesetConfig
unit tests.