diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 9371083c8d2..abad0284e9e 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -120,7 +120,7 @@ export function resetSyncedStatus() { /** * @param {Array} bidderCodes list of bidders to request user syncs for. */ -function queueSync(bidderCodes, gdprConsent) { +function queueSync(bidderCodes, gdprConsent, uspConsent) { if (_synced) { return; } @@ -147,6 +147,12 @@ function queueSync(bidderCodes, gdprConsent) { payload.gdpr_consent = gdprConsent.consentString; } } + + // US Privace (CCPA) support + if (uspConsent) { + payload.us_privacy = uspConsent; + } + const jsonPayload = JSON.stringify(payload); ajax(_s2sConfig.syncEndpoint, (response) => { @@ -766,24 +772,21 @@ const OPEN_RTB_PROTOCOL = { } } - if (bidRequests && bidRequests[0].gdprConsent) { - // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module - let gdprApplies; - if (typeof bidRequests[0].gdprConsent.gdprApplies === 'boolean') { - gdprApplies = bidRequests[0].gdprConsent.gdprApplies ? 1 : 0; - } - - if (request.regs) { - if (request.regs.ext) { - request.regs.ext.gdpr = gdprApplies; - } else { - request.regs.ext = { gdpr: gdprApplies }; + if (bidRequests) { + if (bidRequests[0].gdprConsent) { + // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module + let gdprApplies; + if (typeof bidRequests[0].gdprConsent.gdprApplies === 'boolean') { + gdprApplies = bidRequests[0].gdprConsent.gdprApplies ? 1 : 0; } - } else { - request.regs = { ext: { gdpr: gdprApplies } }; + utils.deepSetValue(request, 'regs.ext.gdpr', gdprApplies); + utils.deepSetValue(request, 'user.ext.consent', bidRequests[0].gdprConsent.consentString); } - utils.deepSetValue(request, 'user.ext.consent', bidRequests[0].gdprConsent.consentString); + // US Privacy (CCPA) support + if (bidRequests[0].uspConsent) { + utils.deepSetValue(request, 'regs.ext.us_privacy', bidRequests[0].uspConsent); + } } if (getConfig('coppa') === true) { @@ -984,12 +987,17 @@ export function PrebidServer() { .filter(utils.uniques); if (_s2sConfig && _s2sConfig.syncEndpoint) { - let consent = (Array.isArray(bidRequests) && bidRequests.length > 0) ? bidRequests[0].gdprConsent : undefined; + let gdprConsent, uspConsent; + if (Array.isArray(bidRequests) && bidRequests.length > 0) { + gdprConsent = bidRequests[0].gdprConsent; + uspConsent = bidRequests[0].uspConsent; + } + let syncBidders = _s2sConfig.bidders .map(bidder => adapterManager.aliasRegistry[bidder] || bidder) .filter((bidder, index, array) => (array.indexOf(bidder) === index)); - queueSync(syncBidders, consent); + queueSync(syncBidders, gdprConsent, uspConsent); } const request = protocolAdapter().buildRequest(s2sBidRequest, bidRequests, validAdUnits); diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 7ace0f786e7..18408e78d17 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -187,7 +187,7 @@ export function newBidder(spec) { function afterAllResponses() { done(); events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest); - registerSyncs(responses, bidderRequest.gdprConsent); + registerSyncs(responses, bidderRequest.gdprConsent, bidderRequest.uspConsent); } const validBidRequests = bidderRequest.bids.filter(filterAndWarn); @@ -330,13 +330,13 @@ export function newBidder(spec) { } }); - function registerSyncs(responses, gdprConsent) { + function registerSyncs(responses, gdprConsent, uspConsent) { if (spec.getUserSyncs) { let filterConfig = config.getConfig('userSync.filterSettings'); let syncs = spec.getUserSyncs({ iframeEnabled: !!(config.getConfig('userSync.iframeEnabled') || (filterConfig && (filterConfig.iframe || filterConfig.all))), pixelEnabled: !!(config.getConfig('userSync.pixelEnabled') || (filterConfig && (filterConfig.image || filterConfig.all))), - }, responses, gdprConsent); + }, responses, gdprConsent, uspConsent); if (syncs) { if (!Array.isArray(syncs)) { syncs = [syncs]; diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 5ee05ad401e..cdb674509cb 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -702,6 +702,109 @@ describe('S2S Adapter', function () { }); }); + describe('us_privacy (ccpa) consent data', function () { + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + }); + + it('is added to ortb2 request when in bidRequest', function () { + let ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + config.setConfig({ s2sConfig: ortb2Config }); + + let uspBidRequest = utils.deepClone(BID_REQUESTS); + uspBidRequest[0].uspConsent = '1NYN'; + + adapter.callBids(REQUEST, uspBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); + + expect(requestBid.regs.ext.us_privacy).is.equal('1NYN'); + + config.resetConfig(); + config.setConfig({ s2sConfig: CONFIG }); + + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + requestBid = JSON.parse(requests[1].requestBody); + + expect(requestBid.regs).to.not.exist; + }); + + it('is added to cookie_sync request when in bidRequest', function () { + let cookieSyncConfig = utils.deepClone(CONFIG); + cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + config.setConfig({ s2sConfig: cookieSyncConfig }); + + let uspBidRequest = utils.deepClone(BID_REQUESTS); + uspBidRequest[0].uspConsent = '1YNN'; + + adapter.callBids(REQUEST, uspBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); + + expect(requestBid.us_privacy).is.equal('1YNN'); + expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); + expect(requestBid.account).is.equal('1'); + }); + }); + + describe('gdpr and us_privacy (ccpa) consent data', function () { + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + }); + + it('is added to ortb2 request when in bidRequest', function () { + let ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + config.setConfig({ s2sConfig: ortb2Config }); + + let consentBidRequest = utils.deepClone(BID_REQUESTS); + consentBidRequest[0].uspConsent = '1NYN'; + consentBidRequest[0].gdprConsent = { + consentString: 'abc123', + gdprApplies: true + }; + + adapter.callBids(REQUEST, consentBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); + + expect(requestBid.regs.ext.us_privacy).is.equal('1NYN'); + expect(requestBid.regs.ext.gdpr).is.equal(1); + expect(requestBid.user.ext.consent).is.equal('abc123'); + + config.resetConfig(); + config.setConfig({ s2sConfig: CONFIG }); + + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + requestBid = JSON.parse(requests[1].requestBody); + + expect(requestBid.regs).to.not.exist; + expect(requestBid.user).to.not.exist; + }); + + it('is added to cookie_sync request when in bidRequest', function () { + let cookieSyncConfig = utils.deepClone(CONFIG); + cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + config.setConfig({ s2sConfig: cookieSyncConfig }); + + let consentBidRequest = utils.deepClone(BID_REQUESTS); + consentBidRequest[0].uspConsent = '1YNN'; + consentBidRequest[0].gdprConsent = { + consentString: 'abc123def', + gdprApplies: true + }; + + adapter.callBids(REQUEST, consentBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); + + expect(requestBid.us_privacy).is.equal('1YNN'); + expect(requestBid.gdpr).is.equal(1); + expect(requestBid.gdpr_consent).is.equal('abc123def'); + expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); + expect(requestBid.account).is.equal('1'); + }); + }); + it('sets invalid cacheMarkup value to 0', function () { const s2sConfig = Object.assign({}, CONFIG, { cacheMarkup: 999