diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index f499f5a0ae4..eb7ba3db9a1 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -96,36 +96,56 @@ function setS2sConfig(options) { } _s2sConfig = options; - if (options.syncEndpoint) { - queueSync(options.bidders); - } } getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig)); +/** + * resets the _synced variable back to false, primiarily used for testing purposes +*/ +export function resetSyncedStatus() { + _synced = false; +} + /** * @param {Array} bidderCodes list of bidders to request user syncs for. */ -function queueSync(bidderCodes) { +function queueSync(bidderCodes, gdprConsent) { if (_synced) { return; } _synced = true; - const payload = JSON.stringify({ + + const payload = { uuid: utils.generateUUID(), bidders: bidderCodes - }); - ajax(_s2sConfig.syncEndpoint, (response) => { - try { - response = JSON.parse(response); - response.bidder_status.forEach(bidder => doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder)); - } catch (e) { - utils.logError(e); + }; + + if (gdprConsent) { + // only populate gdpr field if we know CMP returned consent information (ie didn't timeout or have an error) + if (gdprConsent.consentString) { + payload.gdpr = (gdprConsent.gdprApplies) ? 1 : 0; } - }, - payload, { - contentType: 'text/plain', - withCredentials: true - }); + // attempt to populate gdpr_consent if we know gdprApplies or it may apply + if (gdprConsent.gdprApplies !== false) { + payload.gdpr_consent = gdprConsent.consentString; + } + } + const jsonPayload = JSON.stringify(payload); + + ajax(_s2sConfig.syncEndpoint, + (response) => { + try { + response = JSON.parse(response); + response.bidder_status.forEach(bidder => doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder)); + } catch (e) { + utils.logError(e); + } + }, + jsonPayload, + { + contentType: 'text/plain', + withCredentials: true + }); } /** @@ -348,9 +368,6 @@ const LEGACY_PROTOCOL = { if (result.status === 'OK' || result.status === 'no_cookie') { if (result.bidder_status) { result.bidder_status.forEach(bidder => { - if (bidder.no_cookie) { - doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder); - } if (bidder.error) { utils.logWarn(`Prebid Server returned error: '${bidder.error}' for ${bidder.bidder}`); } @@ -666,6 +683,11 @@ export function PrebidServer() { .reduce(utils.flatten) .filter(utils.uniques); + if (_s2sConfig && _s2sConfig.syncEndpoint) { + let consent = (Array.isArray(bidRequests) && bidRequests.length > 0) ? bidRequests[0].gdprConsent : undefined; + queueSync(_s2sConfig.bidders, consent); + } + const request = protocolAdapter().buildRequest(s2sBidRequest, bidRequests, adUnitsWithSizes); const requestJson = JSON.stringify(request); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index cdb3113c205..5cc9e6f3e58 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { PrebidServer as Adapter } from 'modules/prebidServerBidAdapter'; +import { PrebidServer as Adapter, resetSyncedStatus } from 'modules/prebidServerBidAdapter'; import adapterManager from 'src/adaptermanager'; import * as utils from 'src/utils'; import cookie from 'src/cookie'; @@ -375,6 +375,7 @@ describe('S2S Adapter', () => { requests = []; xhr.onCreate = request => requests.push(request); config.resetConfig(); + resetSyncedStatus(); }); afterEach(() => xhr.restore()); @@ -392,11 +393,11 @@ describe('S2S Adapter', () => { expect(requestBid.ad_units[0].bids[0].params.member).to.exist.and.to.be.a('string'); }); - it('adds gdpr consent information to ortb2 request depending on module use', () => { + it('adds gdpr consent information to ortb2 request depending on presence of module', () => { let ortb2Config = utils.deepClone(CONFIG); ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' - let consentConfig = { consentManagement: { cmp: 'iab' }, s2sConfig: ortb2Config }; + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: ortb2Config }; config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); @@ -424,6 +425,67 @@ describe('S2S Adapter', () => { $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); }); + it('check gdpr info gets added into cookie_sync request: have consent data', () => { + let cookieSyncConfig = utils.deepClone(CONFIG); + cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; + config.setConfig(consentConfig); + + let gdprBidRequest = utils.deepClone(BID_REQUESTS); + + gdprBidRequest[0].gdprConsent = { + consentString: 'abc123def', + gdprApplies: true + }; + + adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); + + expect(requestBid.gdpr).is.equal(1); + expect(requestBid.gdpr_consent).is.equal('abc123def'); + }); + + it('check gdpr info gets added into cookie_sync request: have consent data but gdprApplies is false', () => { + let cookieSyncConfig = utils.deepClone(CONFIG); + cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; + config.setConfig(consentConfig); + + let gdprBidRequest = utils.deepClone(BID_REQUESTS); + gdprBidRequest[0].gdprConsent = { + consentString: 'xyz789abcc', + gdprApplies: false + }; + + adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); + + expect(requestBid.gdpr).is.equal(0); + expect(requestBid.gdpr_consent).is.undefined; + }); + + it('checks gdpr info gets added to cookie_sync request: consent data unknown', () => { + let cookieSyncConfig = utils.deepClone(CONFIG); + cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; + config.setConfig(consentConfig); + + let gdprBidRequest = utils.deepClone(BID_REQUESTS); + gdprBidRequest[0].gdprConsent = { + consentString: undefined, + gdprApplies: undefined + }; + + adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); + + expect(requestBid.gdpr).is.undefined; + expect(requestBid.gdpr_consent).is.undefined; + }); + it('sets invalid cacheMarkup value to 0', () => { const s2sConfig = Object.assign({}, CONFIG, { cacheMarkup: 999 @@ -794,31 +856,6 @@ describe('S2S Adapter', () => { expect(bid_request_passed).to.have.property('adId', '123'); }); - it('does cookie sync when no_cookie response', () => { - server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); - - config.setConfig({s2sConfig: CONFIG}); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); - server.respond(); - - sinon.assert.calledOnce(utils.triggerPixel); - sinon.assert.calledWith(utils.triggerPixel, 'https://pixel.rubiconproject.com/exchange/sync.php?p=prebid'); - sinon.assert.calledOnce(utils.insertUserSyncIframe); - sinon.assert.calledWith(utils.insertUserSyncIframe, '//ads.pubmatic.com/AdServer/js/user_sync.html?predirect=https%3A%2F%2Fprebid.adnxs.com%2Fpbs%2Fv1%2Fsetuid%3Fbidder%3Dpubmatic%26uid%3D'); - }); - - it('logs error when no_cookie response is missing type or url', () => { - server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE_ERROR)); - - config.setConfig({s2sConfig: CONFIG}); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); - server.respond(); - - sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(utils.insertUserSyncIframe); - sinon.assert.calledTwice(utils.logError); - }); - it('does not call cookieSet cookie sync when no_cookie response && not opted in', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE));