Skip to content

Commit 0677a84

Browse files
authored
PrebidServerBidAdapter CCPA (USP) Support (#4501)
* PrebidServerBidAdapter CCPA Support * making us privacy (ccpa) consent available to user syncs * code cleanup * fixed usp param name
1 parent 931e2d2 commit 0677a84

File tree

3 files changed

+132
-21
lines changed

3 files changed

+132
-21
lines changed

modules/prebidServerBidAdapter/index.js

+26-18
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export function resetSyncedStatus() {
120120
/**
121121
* @param {Array} bidderCodes list of bidders to request user syncs for.
122122
*/
123-
function queueSync(bidderCodes, gdprConsent) {
123+
function queueSync(bidderCodes, gdprConsent, uspConsent) {
124124
if (_synced) {
125125
return;
126126
}
@@ -147,6 +147,12 @@ function queueSync(bidderCodes, gdprConsent) {
147147
payload.gdpr_consent = gdprConsent.consentString;
148148
}
149149
}
150+
151+
// US Privace (CCPA) support
152+
if (uspConsent) {
153+
payload.us_privacy = uspConsent;
154+
}
155+
150156
const jsonPayload = JSON.stringify(payload);
151157
ajax(_s2sConfig.syncEndpoint,
152158
(response) => {
@@ -775,24 +781,21 @@ const OPEN_RTB_PROTOCOL = {
775781
}
776782
}
777783

778-
if (bidRequests && bidRequests[0].gdprConsent) {
779-
// note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module
780-
let gdprApplies;
781-
if (typeof bidRequests[0].gdprConsent.gdprApplies === 'boolean') {
782-
gdprApplies = bidRequests[0].gdprConsent.gdprApplies ? 1 : 0;
783-
}
784-
785-
if (request.regs) {
786-
if (request.regs.ext) {
787-
request.regs.ext.gdpr = gdprApplies;
788-
} else {
789-
request.regs.ext = { gdpr: gdprApplies };
784+
if (bidRequests) {
785+
if (bidRequests[0].gdprConsent) {
786+
// note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module
787+
let gdprApplies;
788+
if (typeof bidRequests[0].gdprConsent.gdprApplies === 'boolean') {
789+
gdprApplies = bidRequests[0].gdprConsent.gdprApplies ? 1 : 0;
790790
}
791-
} else {
792-
request.regs = { ext: { gdpr: gdprApplies } };
791+
utils.deepSetValue(request, 'regs.ext.gdpr', gdprApplies);
792+
utils.deepSetValue(request, 'user.ext.consent', bidRequests[0].gdprConsent.consentString);
793793
}
794794

795-
utils.deepSetValue(request, 'user.ext.consent', bidRequests[0].gdprConsent.consentString);
795+
// US Privacy (CCPA) support
796+
if (bidRequests[0].uspConsent) {
797+
utils.deepSetValue(request, 'regs.ext.us_privacy', bidRequests[0].uspConsent);
798+
}
796799
}
797800

798801
if (getConfig('coppa') === true) {
@@ -993,12 +996,17 @@ export function PrebidServer() {
993996
.filter(utils.uniques);
994997

995998
if (_s2sConfig && _s2sConfig.syncEndpoint) {
996-
let consent = (Array.isArray(bidRequests) && bidRequests.length > 0) ? bidRequests[0].gdprConsent : undefined;
999+
let gdprConsent, uspConsent;
1000+
if (Array.isArray(bidRequests) && bidRequests.length > 0) {
1001+
gdprConsent = bidRequests[0].gdprConsent;
1002+
uspConsent = bidRequests[0].uspConsent;
1003+
}
1004+
9971005
let syncBidders = _s2sConfig.bidders
9981006
.map(bidder => adapterManager.aliasRegistry[bidder] || bidder)
9991007
.filter((bidder, index, array) => (array.indexOf(bidder) === index));
10001008

1001-
queueSync(syncBidders, consent);
1009+
queueSync(syncBidders, gdprConsent, uspConsent);
10021010
}
10031011

10041012
const request = protocolAdapter().buildRequest(s2sBidRequest, bidRequests, validAdUnits);

src/adapters/bidderFactory.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ export function newBidder(spec) {
187187
function afterAllResponses() {
188188
done();
189189
events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest);
190-
registerSyncs(responses, bidderRequest.gdprConsent);
190+
registerSyncs(responses, bidderRequest.gdprConsent, bidderRequest.uspConsent);
191191
}
192192

193193
const validBidRequests = bidderRequest.bids.filter(filterAndWarn);
@@ -330,13 +330,13 @@ export function newBidder(spec) {
330330
}
331331
});
332332

333-
function registerSyncs(responses, gdprConsent) {
333+
function registerSyncs(responses, gdprConsent, uspConsent) {
334334
if (spec.getUserSyncs) {
335335
let filterConfig = config.getConfig('userSync.filterSettings');
336336
let syncs = spec.getUserSyncs({
337337
iframeEnabled: !!(config.getConfig('userSync.iframeEnabled') || (filterConfig && (filterConfig.iframe || filterConfig.all))),
338338
pixelEnabled: !!(config.getConfig('userSync.pixelEnabled') || (filterConfig && (filterConfig.image || filterConfig.all))),
339-
}, responses, gdprConsent);
339+
}, responses, gdprConsent, uspConsent);
340340
if (syncs) {
341341
if (!Array.isArray(syncs)) {
342342
syncs = [syncs];

test/spec/modules/prebidServerBidAdapter_spec.js

+103
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,109 @@ describe('S2S Adapter', function () {
702702
});
703703
});
704704

705+
describe('us_privacy (ccpa) consent data', function () {
706+
afterEach(function () {
707+
config.resetConfig();
708+
$$PREBID_GLOBAL$$.requestBids.removeAll();
709+
});
710+
711+
it('is added to ortb2 request when in bidRequest', function () {
712+
let ortb2Config = utils.deepClone(CONFIG);
713+
ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'
714+
config.setConfig({ s2sConfig: ortb2Config });
715+
716+
let uspBidRequest = utils.deepClone(BID_REQUESTS);
717+
uspBidRequest[0].uspConsent = '1NYN';
718+
719+
adapter.callBids(REQUEST, uspBidRequest, addBidResponse, done, ajax);
720+
let requestBid = JSON.parse(requests[0].requestBody);
721+
722+
expect(requestBid.regs.ext.us_privacy).is.equal('1NYN');
723+
724+
config.resetConfig();
725+
config.setConfig({ s2sConfig: CONFIG });
726+
727+
adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax);
728+
requestBid = JSON.parse(requests[1].requestBody);
729+
730+
expect(requestBid.regs).to.not.exist;
731+
});
732+
733+
it('is added to cookie_sync request when in bidRequest', function () {
734+
let cookieSyncConfig = utils.deepClone(CONFIG);
735+
cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync';
736+
config.setConfig({ s2sConfig: cookieSyncConfig });
737+
738+
let uspBidRequest = utils.deepClone(BID_REQUESTS);
739+
uspBidRequest[0].uspConsent = '1YNN';
740+
741+
adapter.callBids(REQUEST, uspBidRequest, addBidResponse, done, ajax);
742+
let requestBid = JSON.parse(requests[0].requestBody);
743+
744+
expect(requestBid.us_privacy).is.equal('1YNN');
745+
expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1);
746+
expect(requestBid.account).is.equal('1');
747+
});
748+
});
749+
750+
describe('gdpr and us_privacy (ccpa) consent data', function () {
751+
afterEach(function () {
752+
config.resetConfig();
753+
$$PREBID_GLOBAL$$.requestBids.removeAll();
754+
});
755+
756+
it('is added to ortb2 request when in bidRequest', function () {
757+
let ortb2Config = utils.deepClone(CONFIG);
758+
ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'
759+
config.setConfig({ s2sConfig: ortb2Config });
760+
761+
let consentBidRequest = utils.deepClone(BID_REQUESTS);
762+
consentBidRequest[0].uspConsent = '1NYN';
763+
consentBidRequest[0].gdprConsent = {
764+
consentString: 'abc123',
765+
gdprApplies: true
766+
};
767+
768+
adapter.callBids(REQUEST, consentBidRequest, addBidResponse, done, ajax);
769+
let requestBid = JSON.parse(requests[0].requestBody);
770+
771+
expect(requestBid.regs.ext.us_privacy).is.equal('1NYN');
772+
expect(requestBid.regs.ext.gdpr).is.equal(1);
773+
expect(requestBid.user.ext.consent).is.equal('abc123');
774+
775+
config.resetConfig();
776+
config.setConfig({ s2sConfig: CONFIG });
777+
778+
adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax);
779+
requestBid = JSON.parse(requests[1].requestBody);
780+
781+
expect(requestBid.regs).to.not.exist;
782+
expect(requestBid.user).to.not.exist;
783+
});
784+
785+
it('is added to cookie_sync request when in bidRequest', function () {
786+
let cookieSyncConfig = utils.deepClone(CONFIG);
787+
cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync';
788+
config.setConfig({ s2sConfig: cookieSyncConfig });
789+
790+
let consentBidRequest = utils.deepClone(BID_REQUESTS);
791+
consentBidRequest[0].uspConsent = '1YNN';
792+
consentBidRequest[0].gdprConsent = {
793+
consentString: 'abc123def',
794+
gdprApplies: true
795+
};
796+
797+
adapter.callBids(REQUEST, consentBidRequest, addBidResponse, done, ajax);
798+
let requestBid = JSON.parse(requests[0].requestBody);
799+
800+
expect(requestBid.us_privacy).is.equal('1YNN');
801+
expect(requestBid.gdpr).is.equal(1);
802+
expect(requestBid.gdpr_consent).is.equal('abc123def');
803+
expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1);
804+
expect(requestBid.account).is.equal('1');
805+
});
806+
});
807+
705808
it('sets invalid cacheMarkup value to 0', function () {
706809
const s2sConfig = Object.assign({}, CONFIG, {
707810
cacheMarkup: 999

0 commit comments

Comments
 (0)