diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index cc039354878..94f50094a91 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -10,8 +10,9 @@ import {loadExternalScript} from '../src/adloader.js'; const BIDDER_CODE = 'improvedigital'; const CREATIVE_TTL = 300; -const AD_SERVER_URL = 'https://ad.360yield.com/pb'; -const BASIC_ADS_URL = 'https://ad.360yield-basic.com/pb'; +const AD_SERVER_BASE_URL = 'https://ad.360yield.com'; +const BASIC_ADS_BASE_URL = 'https://ad.360yield-basic.com'; +const PB_ENDPOINT = 'pb'; const EXTEND_URL = 'https://pbs.360yield.com/openrtb2/auction'; const IFRAME_SYNC_URL = 'https://hb.360yield.com/prebid-universal-creative/load-cookie.html'; @@ -79,6 +80,13 @@ export const spec = { const syncs = []; if ((this.syncStore.extendMode || !syncOptions.pixelEnabled) && syncOptions.iframeEnabled) { const { gdprApplies, consentString } = gdprConsent || {}; + const bidders = new Set(); + if (this.syncStore.extendMode && serverResponses) { + serverResponses.forEach(response => { + if (!response?.body?.ext?.responsetimemillis) return; + Object.keys(response.body.ext.responsetimemillis).forEach(b => bidders.add(b)) + }) + } syncs.push({ type: 'iframe', url: IFRAME_SYNC_URL + @@ -86,7 +94,8 @@ export const spec = { (this.syncStore.extendMode ? '&pbs=1' : '') + (typeof gdprApplies === 'boolean' ? `&gdpr=${Number(gdprApplies)}` : '') + (consentString ? `&gdpr_consent=${consentString}` : '') + - (uspConsent ? `&us_privacy=${encodeURIComponent(uspConsent)}` : '') + (uspConsent ? `&us_privacy=${encodeURIComponent(uspConsent)}` : '') + + (bidders.size ? `&bidders=${[...bidders].join(',')}` : '') }); } else if (syncOptions.pixelEnabled) { serverResponses.forEach(response => { @@ -220,8 +229,14 @@ export const CONVERTER = ortbConverter({ }, request: { gdprAddtlConsent(setAddtlConsent, ortbRequest, bidderRequest) { - // override attlConsent processor to do some additional parsing, and use a different destination - const additionalConsent = deepAccess(bidderRequest, 'gdprConsent.addtlConsent'); + const additionalConsent = bidderRequest?.gdprConsent?.addtlConsent; + if (!additionalConsent) { + return; + } + if (spec.syncStore.extendMode) { + setAddtlConsent(ortbRequest, bidderRequest); + return; + } if (additionalConsent && additionalConsent.indexOf('~') !== -1) { // Google Ad Tech Provider IDs const atpIds = additionalConsent.substring(additionalConsent.indexOf('~') + 1); @@ -247,23 +262,43 @@ const ID_REQUEST = { const extendBids = []; const adServerBids = []; - function formatRequest(bidRequests, extendMode) { + function adServerUrl(extendMode, publisherId) { + if (extendMode) { + return EXTEND_URL; + } + const urlSegments = []; + urlSegments.push(hasPurpose1Consent(bidderRequest?.gdprConsent) ? AD_SERVER_BASE_URL : BASIC_ADS_BASE_URL) + if (publisherId) { + urlSegments.push(publisherId) + } + urlSegments.push(PB_ENDPOINT) + return urlSegments.join('/'); + } + + function formatRequest(bidRequests, publisherId, extendMode) { const ortbRequest = CONVERTER.toORTB({bidRequests, bidderRequest, context: {extendMode}}); - const adServerUrl = hasPurpose1Consent(bidderRequest?.gdprConsent) ? AD_SERVER_URL : BASIC_ADS_URL; return { method: 'POST', - url: extendMode ? EXTEND_URL : adServerUrl, + url: adServerUrl(extendMode, publisherId), data: JSON.stringify(ortbRequest), - ortbRequest + ortbRequest, + bidderRequest } } + let publisherId = null; bidRequests.map((bidRequest) => { + const bidParamsPublisherId = bidRequest.params.publisherId; const extendModeEnabled = this.isExtendModeEnabled(globalExtendMode, bidRequest.params); if (singleRequestMode) { + if (!publisherId) { + publisherId = bidParamsPublisherId; + } else if (bidParamsPublisherId && publisherId !== bidParamsPublisherId) { + throw new Error(`All Improve Digital placements in a single call must have the same publisherId. Please check your 'params.publisherId' or turn off the single request mode.`); + } extendModeEnabled ? extendBids.push(bidRequest) : adServerBids.push(bidRequest); } else { - requests.push(formatRequest([bidRequest], extendModeEnabled)); + requests.push(formatRequest([bidRequest], bidParamsPublisherId, extendModeEnabled)); } }); @@ -272,10 +307,10 @@ const ID_REQUEST = { } // In the single request mode, split imps between those going to the ad server and those going to extend server if (extendBids.length) { - requests.push(formatRequest(extendBids, true)); + requests.push(formatRequest(extendBids, publisherId, true)); } if (adServerBids.length) { - requests.push(formatRequest(adServerBids, false)); + requests.push(formatRequest(adServerBids, publisherId, false)); } return requests; diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 9190c008a9f..b3b01dc93b5 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -18,8 +18,11 @@ import {createEidsArray} from '../../../modules/userId/eids.js'; describe('Improve Digital Adapter Tests', function () { const METHOD = 'POST'; - const AD_SERVER_URL = 'https://ad.360yield.com/pb'; - const BASIC_ADS_URL = 'https://ad.360yield-basic.com/pb'; + const AD_SERVER_BASE_URL = 'https://ad.360yield.com'; + const BASIC_ADS_BASE_URL = 'https://ad.360yield-basic.com'; + const PB_ENDPOINT = 'pb'; + const AD_SERVER_URL = `${AD_SERVER_BASE_URL}/${PB_ENDPOINT}`; + const BASIC_ADS_URL = `${BASIC_ADS_BASE_URL}/${PB_ENDPOINT}`; const EXTEND_URL = 'https://pbs.360yield.com/openrtb2/auction'; const IFRAME_SYNC_URL = 'https://hb.360yield.com/prebid-universal-creative/load-cookie.html'; const INSTREAM_TYPE = 1; @@ -390,6 +393,7 @@ describe('Improve Digital Adapter Tests', function () { const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequestGdpr)[0].data); expect(payload.regs.ext.gdpr).to.exist.and.to.equal(1); expect(payload.user.ext.consent).to.equal('CONSENT'); + expect(payload.user.ext.ConsentedProvidersSettings).to.not.exist; expect(payload.user.ext.consented_providers_settings.consented_providers).to.exist.and.to.deep.equal([1, 35, 41, 101]); }); @@ -401,6 +405,15 @@ describe('Improve Digital Adapter Tests', function () { expect(payload.user.ext.consented_providers_settings).to.not.exist; }); + it('should add ConsentedProvidersSettings when extend mode enabled', function () { + const bidRequest = deepClone(extendBidRequest); + const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequestGdpr)[0].data); + expect(payload.regs.ext.gdpr).to.exist.and.to.equal(1); + expect(payload.user.ext.consent).to.equal('CONSENT'); + expect(payload.user.ext.ConsentedProvidersSettings.consented_providers).to.exist.and.to.equal('1~1.35.41.101'); + expect(payload.user.ext.consented_providers_settings).to.not.exist; + }); + it('should add CCPA consent string', function () { const bidRequest = Object.assign({}, simpleBidRequest); const request = spec.buildRequests([bidRequest], {...bidderRequest, ...{ uspConsent: '1YYY' }}); @@ -753,6 +766,64 @@ describe('Improve Digital Adapter Tests', function () { expect(requests[0].url).to.equal(AD_SERVER_URL); expect(requests[1].url).to.equal(EXTEND_URL); }); + + it('should add publisherId to request URL when available in request params', function() { + function formatPublisherUrl(baseUrl, publisherId) { + return `${baseUrl}/${publisherId}/${PB_ENDPOINT}`; + } + const bidRequest = deepClone(simpleBidRequest); + bidRequest.params.publisherId = 1000; + let request = spec.buildRequests([bidRequest], bidderRequest)[0]; + expect(request).to.be.an('object'); + sinon.assert.match(request, { + method: METHOD, + url: formatPublisherUrl(AD_SERVER_BASE_URL, 1000), + bidderRequest + }); + + const bidRequest2 = deepClone(simpleBidRequest) + bidRequest2.params.publisherId = 1002; + + const bidRequest3 = deepClone(extendBidRequest) + bidRequest3.params.publisherId = 1002; + + const request1 = spec.buildRequests([bidRequest, bidRequest2], bidderRequest)[0]; + expect(request1.url).to.equal(formatPublisherUrl(AD_SERVER_BASE_URL, 1000)); + const request2 = spec.buildRequests([bidRequest, bidRequest2], bidderRequest)[1]; + expect(request2.url).to.equal(formatPublisherUrl(AD_SERVER_BASE_URL, 1002)); + const request3 = spec.buildRequests([bidRequest, bidRequest3], bidderRequest)[1]; + expect(request3.url).to.equal(EXTEND_URL); + + // Enable single request mode + getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('improvedigital.singleRequest').returns(true); + try { + spec.buildRequests([bidRequest, bidRequest2], bidderRequest)[0]; + } catch (e) { + expect(e.name).to.exist.equal('Error') + expect(e.message).to.exist.equal(`All Improve Digital placements in a single call must have the same publisherId. Please check your 'params.publisherId' or turn off the single request mode.`) + } + + bidRequest2.params.publisherId = null; + request = spec.buildRequests([bidRequest, bidRequest2], bidderRequest)[0]; + expect(request.url).to.equal(formatPublisherUrl(AD_SERVER_BASE_URL, 1000)); + + const consent = deepClone(gdprConsent); + deepSetValue(consent, 'vendorData.purpose.consents.1', false); + const bidderRequestWithConsent = deepClone(bidderRequest); + bidderRequestWithConsent.gdprConsent = consent; + request = spec.buildRequests([bidRequest], bidderRequestWithConsent)[0]; + expect(request.url).to.equal(formatPublisherUrl(BASIC_ADS_BASE_URL, 1000)); + + deepSetValue(consent, 'vendorData.purpose.consents.1', true); + bidderRequestWithConsent.gdprConsent = consent; + request = spec.buildRequests([bidRequest], bidderRequestWithConsent)[0]; + expect(request.url).to.equal(formatPublisherUrl(AD_SERVER_BASE_URL, 1000)); + + delete bidRequest.params.publisherId; + request = spec.buildRequests([bidRequest], bidderRequestWithConsent)[0]; + expect(request.url).to.equal(AD_SERVER_URL); + }); }); const serverResponse = { @@ -1285,5 +1356,16 @@ describe('Improve Digital Adapter Tests', function () { const syncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, serverResponses); expect(syncs).to.deep.equal([{ type: 'iframe', url: basicIframeSyncUrl + '&pbs=1' }]); }); + + it('should add bidders to iframe user sync url', function () { + getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('improvedigital.extend').returns(true); + spec.buildRequests([simpleBidRequest], {}); + const rawResponse = deepClone(serverResponse) + deepSetValue(rawResponse, 'body.ext.responsetimemillis', {a: 1, b: 1, c: 1, d: 1, e: 1}) + let syncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, [rawResponse]); + let url = basicIframeSyncUrl + '&pbs=1' + '&bidders=a,b,c,d,e' + expect(syncs).to.deep.equal([{ type: 'iframe', url }]); + }); }); });