diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 9de2e2b2d32..a0453466b87 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -9,9 +9,12 @@ import {Renderer} from '../src/Renderer.js'; import {createEidsArray} from './userId/eids.js'; const BIDDER_CODE = 'improvedigital'; -const REQUEST_URL = 'https://ad.360yield.com/pb'; const CREATIVE_TTL = 300; +const AD_SERVER_URL = 'https://ad.360yield.com/pb'; +const EXTEND_URL = 'https://pbs.360yield.com/openrtb2/auction'; +const IFRAME_SYNC_URL = 'https://hb.360yield.com/prebid-universal-creative/load-cookie.html'; + const VIDEO_PARAMS = { DEFAULT_MIMES: ['video/mp4'], SUPPORTED_PROPERTIES: ['mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', @@ -57,6 +60,7 @@ export const spec = { gvlid: 253, aliases: ['id'], supportedMediaTypes: [BANNER, NATIVE, VIDEO], + syncStore: { extendMode: false, placementId: null }, /** * Determines whether or not the given bid request is valid. @@ -77,7 +81,6 @@ export const spec = { */ buildRequests(bidRequests, bidderRequest) { const request = { - id: getUniqueIdentifierStr(), cur: [config.getConfig('currency.adServerCurrency') || 'USD'], ext: { improvedigital: { @@ -100,7 +103,7 @@ export const spec = { // Coppa const coppa = config.getConfig('coppa'); if (typeof coppa === 'boolean') { - deepSetValue(request, 'regs.coppa', ID_UTIL.toBit(coppa)); + deepSetValue(request, 'regs.coppa', Number(coppa)); } if (bidderRequest) { @@ -108,7 +111,7 @@ export const spec = { const gdprConsent = deepAccess(bidderRequest, 'gdprConsent') if (gdprConsent) { if (typeof gdprConsent.gdprApplies === 'boolean') { - deepSetValue(request, 'regs.ext.gdpr', ID_UTIL.toBit(gdprConsent.gdprApplies)); + deepSetValue(request, 'regs.ext.gdpr', Number(gdprConsent.gdprApplies)); } deepSetValue(request, 'user.ext.consent', gdprConsent.consentString); @@ -144,6 +147,9 @@ export const spec = { deepSetValue(request, 'source.ext.schain', bidRequest0.schain); deepSetValue(request, 'source.tid', bidRequest0.transactionId); + // Save a placement id to send it to the ad server when fetching the user syncs + this.syncStore.placementId = this.syncStore.placementId || bidRequest0.params.placementId; + if (bidRequest0.userId) { const eids = createEidsArray(bidRequest0.userId); deepSetValue(request, 'user.ext.eids', eids.length ? eids : undefined); @@ -174,7 +180,7 @@ export const spec = { return; } const bidRequest = getBidRequest(bidObject.impid, [bidderRequest]); - const idExt = deepAccess(bidObject, `ext.${BIDDER_CODE}`); + const idExt = deepAccess(bidObject, `ext.${BIDDER_CODE}`, {}); const bid = { requestId: bidObject.impid, @@ -210,57 +216,100 @@ export const spec = { * @param {ServerResponse[]} serverResponses List of server's responses. * @return {UserSync[]} The user syncs which should be dropped. */ - getUserSyncs(syncOptions, serverResponses) { - if (syncOptions.pixelEnabled) { - const syncs = []; + getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (config.getConfig('coppa') === true || !ID_UTIL.hasPurpose1Consent(gdprConsent)) { + return []; + } + + const syncs = []; + if ((this.syncStore.extendMode || !syncOptions.pixelEnabled) && syncOptions.iframeEnabled) { + const { gdprApplies, consentString } = gdprConsent || {}; + syncs.push({ + type: 'iframe', + url: IFRAME_SYNC_URL + + `?placement_id=${this.syncStore.placementId}` + + (this.syncStore.extendMode ? '&pbs=1' : '') + + (typeof gdprApplies === 'boolean' ? `&gdpr=${Number(gdprApplies)}` : '') + + (consentString ? `&gdpr_consent=${consentString}` : '') + + (uspConsent ? `&us_privacy=${encodeURIComponent(uspConsent)}` : '') + }); + } else if (syncOptions.pixelEnabled) { serverResponses.forEach(response => { const syncArr = deepAccess(response, `body.ext.${BIDDER_CODE}.sync`, []); - syncArr.forEach(syncElement => { - if (syncs.indexOf(syncElement) === -1) { - syncs.push(syncElement); + syncArr.forEach(url => { + if (!syncs.some(sync => sync.url === url)) { + syncs.push({ type: 'image', url }); } }); }); - return syncs.map(sync => ({ type: 'image', url: sync })); } - return []; + + return syncs; } }; registerBidder(spec); const ID_REQUEST = { - buildServerRequests(requestObject, bidRequests, bidderRequest) { + buildServerRequests(basicRequest, bidRequests, bidderRequest) { + const globalExtendMode = config.getConfig('improvedigital.extend') === true; const requests = []; - if (config.getConfig('improvedigital.singleRequest') === true) { - requestObject.imp = bidRequests.map((bidRequest) => this.buildImp(bidRequest)); - requests[0] = this.formatRequest(requestObject, bidderRequest); - } else { - bidRequests.map((bidRequest) => { - const request = deepClone(requestObject); - request.id = bidRequest.bidId || getUniqueIdentifierStr(); - request.imp = [this.buildImp(bidRequest)]; - deepSetValue(request, 'source.tid', bidRequest.transactionId); - requests.push(this.formatRequest(request, bidderRequest)); - }); + const singleRequestMode = config.getConfig('improvedigital.singleRequest') === true; + + const extendImps = []; + const adServerImps = []; + + function formatRequest(imps, transactionId, extendMode) { + const request = deepClone(basicRequest); + request.imp = imps; + request.id = getUniqueIdentifierStr(); + if (transactionId) { + deepSetValue(request, 'source.tid', transactionId); + } + return { + method: 'POST', + url: extendMode ? EXTEND_URL : AD_SERVER_URL, + data: JSON.stringify(request), + bidderRequest + } + }; + + bidRequests.map((bidRequest) => { + const extendModeEnabled = this.isExtendModeEnabled(globalExtendMode, bidRequest.params); + const imp = this.buildImp(bidRequest, extendModeEnabled); + if (singleRequestMode) { + extendModeEnabled ? extendImps.push(imp) : adServerImps.push(imp); + } else { + requests.push(formatRequest([imp], bidRequest.transactionId, extendModeEnabled)); + } + }); + + if (!singleRequestMode) { + return requests; + } + // In the single request mode, split imps between those going to the ad server and those going to extend server + if (extendImps.length) { + requests.push(formatRequest(extendImps, null, true)); + } + if (adServerImps.length) { + requests.push(formatRequest(adServerImps, null, false)); } return requests; }, - formatRequest(request, bidderRequest) { - return { - method: 'POST', - url: REQUEST_URL, - data: JSON.stringify(request), - bidderRequest + isExtendModeEnabled(globalExtendMode, bidParams) { + const extendMode = typeof bidParams.extend === 'boolean' ? bidParams.extend : globalExtendMode; + if (extendMode && !spec.syncStore.extendMode) { + spec.syncStore.extendMode = true; } + return extendMode; }, - buildImp(bidRequest) { + buildImp(bidRequest, extendMode) { const imp = { id: getBidIdParameter('bidId', bidRequest) || getUniqueIdentifierStr(), - secure: ID_UTIL.toBit(window.location.protocol === 'https:'), + secure: Number(window.location.protocol === 'https:'), }; // Floor @@ -271,15 +320,19 @@ const ID_REQUEST = { deepSetValue(imp, 'bidfloorcur', bidFloorCur ? bidFloorCur.toUpperCase() : undefined); } + const bidderParamsPath = extendMode ? 'ext.prebid.bidder.improvedigital' : 'ext.bidder'; const placementId = getBidIdParameter('placementId', bidRequest.params); if (placementId) { - deepSetValue(imp, 'ext.bidder.placementId', placementId); + deepSetValue(imp, `${bidderParamsPath}.placementId`, placementId); + if (extendMode) { + deepSetValue(imp, 'ext.prebid.storedrequest.id', '' + placementId); + } } else { - deepSetValue(imp, 'ext.bidder.publisherId', getBidIdParameter('publisherId', bidRequest.params)); - deepSetValue(imp, 'ext.bidder.placementKey', getBidIdParameter('placementKey', bidRequest.params)); + deepSetValue(imp, `${bidderParamsPath}.publisherId`, getBidIdParameter('publisherId', bidRequest.params)); + deepSetValue(imp, `${bidderParamsPath}.placementKey`, getBidIdParameter('placementKey', bidRequest.params)); } - deepSetValue(imp, 'ext.bidder.keyValues', getBidIdParameter('keyValues', bidRequest.params) || undefined); + deepSetValue(imp, `${bidderParamsPath}.keyValues`, getBidIdParameter('keyValues', bidRequest.params) || undefined); // Adding GPID const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || @@ -374,7 +427,7 @@ const ID_REQUEST = { const assetParams = nativeParams[i]; const asset = { id: assetOrtbParams.id, - required: ID_UTIL.toBit(assetParams.required), + required: Number(assetParams.required), }; switch (assetOrtbParams.assetType) { case NATIVE_DATA.ASSET_TYPES.TITLE: @@ -458,9 +511,10 @@ const ID_RESPONSE = { this.buildNativeAd(bid, bidRequest, bidResponse) } } else { - if (bidResponse.adm.search(/^ { - const bidRequest = JSON.parse(JSON.stringify(instreamBidRequest)); + const bidRequest = deepClone(instreamBidRequest); bidRequest.params.video = { w: 1024, h: 640 } @@ -422,7 +485,7 @@ describe('Improve Digital Adapter Tests', function () { }); it('should set skip params only if skip=1', function() { - const bidRequest = JSON.parse(JSON.stringify(instreamBidRequest)); + const bidRequest = deepClone(instreamBidRequest); // 1 const videoTest = { skip: 1, @@ -456,7 +519,7 @@ describe('Improve Digital Adapter Tests', function () { }); it('should ignore invalid/unexpected video params', function() { - const bidRequest = JSON.parse(JSON.stringify(instreamBidRequest)); + const bidRequest = deepClone(instreamBidRequest); // 1 const videoTest = { skip: 1, @@ -472,7 +535,7 @@ describe('Improve Digital Adapter Tests', function () { }); it('should set video params for outstream', function() { - const bidRequest = JSON.parse(JSON.stringify(outstreamBidRequest)); + const bidRequest = deepClone(outstreamBidRequest); bidRequest.params.video = videoParams; const request = spec.buildRequests([bidRequest])[0]; const payload = JSON.parse(request.data); @@ -486,7 +549,7 @@ describe('Improve Digital Adapter Tests', function () { }); // it('should set video params for multi-format', function() { - const bidRequest = JSON.parse(JSON.stringify(multiFormatBidRequest)); + const bidRequest = deepClone(multiFormatBidRequest); bidRequest.params.video = videoParams; const request = spec.buildRequests([bidRequest])[0]; const payload = JSON.parse(request.data); @@ -536,19 +599,34 @@ describe('Improve Digital Adapter Tests', function () { }); it('should return one request in a single request mode', function () { - const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('improvedigital.singleRequest').returns(true); - const requests = spec.buildRequests([ - simpleBidRequest, - simpleSmartTagBidRequest - ], bidderRequest); + const requests = spec.buildRequests([ simpleBidRequest, instreamBidRequest ], bidderRequest); expect(requests).to.be.an('array'); expect(requests.length).to.equal(1); - getConfigStub.restore(); + expect(requests[0].url).to.equal(AD_SERVER_URL); + const request = JSON.parse(requests[0].data); + expect(request.imp.length).to.equal(2); + expect(request.imp[0].banner).to.exist; + expect(request.imp[1].video).to.exist; + }); + + it('should create one request per endpoint in a single request mode', function () { + getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('improvedigital.singleRequest').returns(true); + const requests = spec.buildRequests([ extendBidRequest, simpleBidRequest, instreamBidRequest ], bidderRequest); + expect(requests).to.be.an('array'); + expect(requests.length).to.equal(2); + expect(requests[0].url).to.equal(EXTEND_URL); + expect(requests[1].url).to.equal(AD_SERVER_URL); + const adServerRequest = JSON.parse(requests[1].data); + expect(adServerRequest.imp.length).to.equal(2); + expect(adServerRequest.imp[0].banner).to.exist; + expect(adServerRequest.imp[1].video).to.exist; }); it('should set Prebid sizes in bid request', function () { - const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); const request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; const payload = JSON.parse(request.data); @@ -558,11 +636,10 @@ describe('Improve Digital Adapter Tests', function () { { w: 160, h: 600 } ] }); - getConfigStub.restore(); }); it('should not add single size filter when using Prebid sizes', function () { - const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); const bidRequest = Object.assign({}, simpleBidRequest); const size = { @@ -578,7 +655,6 @@ describe('Improve Digital Adapter Tests', function () { { w: 160, h: 600 } ] }); - getConfigStub.restore(); }); it('should set GPID and Instl Signal', function () { @@ -620,29 +696,27 @@ describe('Improve Digital Adapter Tests', function () { }); it('should not set site when app is defined in FPD', function () { - const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('ortb2.app').returns({ content: 'XYZ' }); let request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; let payload = JSON.parse(request.data); expect(payload.site).does.not.exist; expect(payload.app).does.exist; expect(payload.app.content).does.exist.and.equal('XYZ'); - getConfigStub.restore(); }); it('should not set site when app is defined in CONFIG', function () { - const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('app').returns({ content: 'XYZ' }); let request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; let payload = JSON.parse(request.data); expect(payload.site).does.not.exist; expect(payload.app).does.exist; expect(payload.app.content).does.exist.and.equal('XYZ'); - getConfigStub.restore(); }); it('should set correct site params', function () { - let getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('site').returns({ content: 'XYZ', page: 'https://improveditigal.com/', @@ -669,11 +743,10 @@ describe('Improve Digital Adapter Tests', function () { expect(payload.site.content).does.exist.and.equal('ZZZ'); expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); expect(payload.site.domain).does.exist.and.equal('blah.com'); - getConfigStub.restore(); }); it('should set pageUrl as site param', function () { - let getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('pageUrl').returns('https://improvidigital.com/test-page'); let request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; let payload = JSON.parse(request.data); @@ -686,17 +759,64 @@ describe('Improve Digital Adapter Tests', function () { payload = JSON.parse(request.data); expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); expect(payload.site.domain).does.exist.and.equal('blah.com'); - getConfigStub.restore(); }); it('should set site when app not available', function () { - const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('app').returns(undefined); let request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; let payload = JSON.parse(request.data); expect(payload.site).does.exist; expect(payload.app).does.not.exist; - getConfigStub.restore(); + }); + + it('should set extend params when extend mode enabled from global configuration', function () { + getConfigStub = sinon.stub(config, 'getConfig'); + const bannerRequest = deepClone(simpleBidRequest); + const keyValues = { testKey: [ 'testValue' ] }; + bannerRequest.params.keyValues = keyValues; + + getConfigStub.withArgs('improvedigital.extend').returns(true); + const requests = spec.buildRequests([bannerRequest, instreamBidRequest], bidderRequest); + expect(requests[0].method).to.equal(METHOD); + expect(requests[0].url).to.equal(EXTEND_URL); + expect(requests[1].url).to.equal(EXTEND_URL); + // banner + let payload = JSON.parse(requests[0].data); + expect(payload.imp[0].ext.bidder).to.not.exist; + expect(payload.imp[0].ext.prebid.bidder.improvedigital).to.deep.equal({ + placementId: 1053688, + keyValues + }); + expect(payload.imp[0].ext.prebid.storedrequest.id).to.equal('1053688'); + // video + payload = JSON.parse(requests[1].data); + expect(payload.imp[0].ext.bidder).to.not.exist; + expect(payload.imp[0].ext.prebid.bidder.improvedigital.placementId).to.equal(123456); + expect(payload.imp[0].ext.prebid.storedrequest.id).to.equal('123456'); + }); + + it('should set extend url when extend mode enabled in adunit params', function () { + const bidRequest = deepClone(extendBidRequest); + let request = spec.buildRequests([bidRequest], { bids: [bidRequest] })[0]; + expect(request.url).to.equal(EXTEND_URL); + + getConfigStub = sinon.stub(config, 'getConfig'); + + // adunit param takes precedence over the global config + getConfigStub.withArgs('improvedigital.extend').returns(false); + request = spec.buildRequests([bidRequest], { bids: [bidRequest] })[0]; + expect(request.url).to.equal(EXTEND_URL); + + bidRequest.params.extend = false; + getConfigStub.withArgs('improvedigital.extend').returns(true); + request = spec.buildRequests([bidRequest], { bids: [bidRequest] })[0]; + expect(request.url).to.equal(AD_SERVER_URL); + + const requests = spec.buildRequests([bidRequest, instreamBidRequest], { bids: [bidRequest, instreamBidRequest] }); + expect(requests.length).to.equal(2); + expect(requests[0].url).to.equal(AD_SERVER_URL); + expect(requests[1].url).to.equal(EXTEND_URL); }); }); @@ -730,7 +850,16 @@ describe('Improve Digital Adapter Tests', function () { ], 'seat': 'improvedigital' } - ] + ], + ext: { + improvedigital: { + sync: [ + 'https://link1', + 'https://link2', + 'https://link3', + ] + } + } } }; @@ -790,7 +919,7 @@ describe('Improve Digital Adapter Tests', function () { sync: [ 'https://link1', 'https://link2', - 'https://link3', + 'https://link4', ] } } @@ -989,7 +1118,7 @@ describe('Improve Digital Adapter Tests', function () { }); it('should set dealId correctly', function () { - const response = JSON.parse(JSON.stringify(serverResponse)); + const response = deepClone(serverResponse); let bids; delete response.body.seatbid[0].bid[0].ext.improvedigital.line_item_id; @@ -1019,14 +1148,14 @@ describe('Improve Digital Adapter Tests', function () { }); it('should set currency', function () { - const response = JSON.parse(JSON.stringify(serverResponse)); + const response = deepClone(serverResponse); response.body.cur = 'eur'; const bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].currency).to.equal('EUR'); }); it('should return empty array for bad response or no price', function () { - let response = JSON.parse(JSON.stringify(serverResponse)); + let response = deepClone(serverResponse); let bids; // Price missing or 0 @@ -1041,13 +1170,13 @@ describe('Improve Digital Adapter Tests', function () { expect(bids).to.deep.equal([]); // errorCode present - response = JSON.parse(JSON.stringify(serverResponse)); + response = deepClone(serverResponse); response.body.seatbid[0].bid[0].errorCode = undefined; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); // adm and native missing - response = JSON.parse(JSON.stringify(serverResponse)); + response = deepClone(serverResponse); delete response.body.seatbid[0].bid[0].adm; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); @@ -1057,7 +1186,7 @@ describe('Improve Digital Adapter Tests', function () { }); it('should set netRevenue', function () { - const response = JSON.parse(JSON.stringify(serverResponse)); + const response = deepClone(serverResponse); response.body.seatbid[0].bid[0].ext.improvedigital.is_net = true; const bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].netRevenue).to.equal(true); @@ -1065,7 +1194,7 @@ describe('Improve Digital Adapter Tests', function () { it('should set advertiserDomains', function () { const adomain = ['domain.com']; - const response = JSON.parse(JSON.stringify(serverResponse)); + const response = deepClone(serverResponse); response.body.seatbid[0].bid[0].adomain = adomain; const bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].meta.advertiserDomains).to.equal(adomain); @@ -1108,10 +1237,19 @@ describe('Improve Digital Adapter Tests', function () { }); it('should return a well-formed outstream video bid for multi-format ad unit', function () { - const bids = spec.interpretResponse(serverResponseVideo, {bidderRequest: multiFormatBidderRequest}); + const videoResponse = deepClone(serverResponseVideo); + let bids = spec.interpretResponse(videoResponse, {bidderRequest: multiFormatBidderRequest}); expect(bids[0].renderer).to.exist; delete (bids[0].renderer); expect(bids).to.deep.equal(expectedBidOutstreamVideo); + + videoResponse.body.seatbid[0].bid[0].adm = '