From 879e5c49628f5a4b3ad11f75a0359414742d9db1 Mon Sep 17 00:00:00 2001 From: Jason Quaccia Date: Fri, 5 May 2023 08:33:34 -0700 Subject: [PATCH 01/13] Prebid Core: Functionality to Optionally Defer Billing for an Ad (#9640) * logic for billing deferrals * refactored and addressed feedback * refactored triggerBilling func * addressed feedback * rebased on top of master * reverted some unneeded changes * refactored triggerBilling and addWinningBid funcs * reverted changes to example html file * addressed changes from feedback --- src/adapterManager.js | 4 +++ src/auction.js | 2 ++ src/prebid.js | 48 +++++++++++++++++++------- test/spec/unit/pbjs_api_spec.js | 60 ++++++++++++++++++++++++++++++++- 4 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/adapterManager.js b/src/adapterManager.js index 45438f59b55..8fcf04c7b41 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -612,6 +612,10 @@ adapterManager.callBidWonBidder = function(bidder, bid, adUnits) { tryCallBidderMethod(bidder, 'onBidWon', bid); }; +adapterManager.callBidBillableBidder = function(bid) { + tryCallBidderMethod(bid.bidder, 'onBidBillable', bid); +}; + adapterManager.callSetTargetingBidder = function(bidder, bid) { tryCallBidderMethod(bidder, 'onSetTargeting', bid); }; diff --git a/src/auction.js b/src/auction.js index 60892e5d7a2..736bc804ac5 100644 --- a/src/auction.js +++ b/src/auction.js @@ -367,8 +367,10 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a } function addWinningBid(winningBid) { + const winningAd = adUnits.find(adUnit => adUnit.transactionId === winningBid.transactionId); _winningBids = _winningBids.concat(winningBid); adapterManager.callBidWonBidder(winningBid.adapterCode || winningBid.bidder, winningBid, adUnits); + if (winningAd && !winningAd.deferBilling) adapterManager.callBidBillableBidder(winningBid); } function setBidTargeting(bid) { diff --git a/src/prebid.js b/src/prebid.js index 5070b6b42a4..c9848b5800c 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -1001,18 +1001,7 @@ if (FEATURES.VIDEO) { * @alias module:pbjs.markWinningBidAsUsed */ pbjsInstance.markWinningBidAsUsed = function (markBidRequest) { - let bids = []; - - if (markBidRequest.adUnitCode && markBidRequest.adId) { - bids = auctionManager.getBidsReceived() - .filter(bid => bid.adId === markBidRequest.adId && bid.adUnitCode === markBidRequest.adUnitCode); - } else if (markBidRequest.adUnitCode) { - bids = targeting.getWinningBids(markBidRequest.adUnitCode); - } else if (markBidRequest.adId) { - bids = auctionManager.getBidsReceived().filter(bid => bid.adId === markBidRequest.adId); - } else { - logWarn('Improper use of markWinningBidAsUsed. It needs an adUnitCode or an adId to function.'); - } + const bids = fetchReceivedBids(markBidRequest, 'Improper use of markWinningBidAsUsed. It needs an adUnitCode or an adId to function.'); if (bids.length > 0) { bids[0].status = CONSTANTS.BID_STATUS.RENDERED; @@ -1020,6 +1009,23 @@ if (FEATURES.VIDEO) { } } +const fetchReceivedBids = (bidRequest, warningMessage) => { + let bids = []; + + if (bidRequest.adUnitCode && bidRequest.adId) { + bids = auctionManager.getBidsReceived() + .filter(bid => bid.adId === bidRequest.adId && bid.adUnitCode === bidRequest.adUnitCode); + } else if (bidRequest.adUnitCode) { + bids = targeting.getWinningBids(bidRequest.adUnitCode); + } else if (bidRequest.adId) { + bids = auctionManager.getBidsReceived().filter(bid => bid.adId === bidRequest.adId); + } else { + logWarn(warningMessage); + } + + return bids; +}; + /** * Get Prebid config options * @param {Object} options @@ -1097,4 +1103,22 @@ pbjsInstance.processQueue = function () { processQueue(pbjsInstance.cmd); }; +/** + * @alias module:pbjs.triggerBilling + */ +pbjsInstance.triggerBilling = (winningBid) => { + const bids = fetchReceivedBids(winningBid, 'Improper use of triggerBilling. It requires a bid with at least an adUnitCode or an adId to function.'); + const triggerBillingBid = bids.find(bid => bid.requestId === winningBid.requestId) || bids[0]; + + if (bids.length > 0 && triggerBillingBid) { + try { + adapterManager.callBidBillableBidder(triggerBillingBid); + } catch (e) { + logError('Error when triggering billing :', e); + } + } else { + logWarn('The bid provided to triggerBilling did not match any bids received.'); + } +}; + export default pbjsInstance; diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 820d87ef49c..58b90d38ddb 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -2501,7 +2501,6 @@ describe('Unit: Prebid Module', function () { }]; let adUnitCodes = ['adUnit-code']; let auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: timeout}); - adUnits[0]['mediaTypes'] = { native: {} }; adUnitCodes = ['adUnit-code']; let auction1 = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: timeout}); @@ -3546,4 +3545,63 @@ describe('Unit: Prebid Module', function () { expect(bids[0].adId).to.equal('adid-1'); }); }); + + describe('deferred billing', function () { + const sandbox = sinon.createSandbox(); + + let adUnits = [ + { + code: 'adUnit-code-1', + mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, + transactionId: '1234567890', + bids: [ + { bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-1' } + ] + }, + { + code: 'adUnit-code-2', + deferBilling: true, + mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, + transactionId: '0987654321', + bids: [ + { bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-2' } + ] + } + ]; + + let winningBid1 = { adapterCode: 'pubmatic', bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-1', transactionId: '1234567890', adId: 'abcdefg' } + let winningBid2 = { adapterCode: 'pubmatic', bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-2', transactionId: '0987654321' } + let adUnitCodes = ['adUnit-code-1', 'adUnit-code-2']; + let auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: 2000}); + + beforeEach(function () { + sandbox.spy(adapterManager, 'callBidWonBidder'); + sandbox.spy(adapterManager, 'callBidBillableBidder'); + sandbox.stub(auctionManager, 'getBidsReceived').returns([winningBid1]); + }); + + afterEach(function () { + sandbox.resetHistory(); + sandbox.restore(); + }); + + it('should by default invoke callBidWonBidder and callBidBillableBidder', function () { + auction.addWinningBid(winningBid1); + sinon.assert.calledOnce(adapterManager.callBidWonBidder); + sinon.assert.calledOnce(adapterManager.callBidBillableBidder); + }); + + it('should only invoke callBidWonBidder and NOT callBidBillableBidder if deferBilling is present and true within the winning adUnit object', function () { + auction.addWinningBid(winningBid2); + sinon.assert.calledOnce(adapterManager.callBidWonBidder); + sinon.assert.notCalled(adapterManager.callBidBillableBidder); + }); + + it('should invoke callBidBillableBidder when pbjs.triggerBilling is invoked', function () { + $$PREBID_GLOBAL$$.triggerBilling(winningBid1); + sinon.assert.calledOnce(auctionManager.getBidsReceived); + sinon.assert.notCalled(adapterManager.callBidWonBidder); + sinon.assert.calledOnce(adapterManager.callBidBillableBidder); + }); + }); }); From 71a5e217a5721880857eccdce97ef22d96a62323 Mon Sep 17 00:00:00 2001 From: Gaudeamus Date: Sat, 6 May 2023 14:01:04 +0300 Subject: [PATCH 02/13] MgidAdapter: change user sync get parameters (#9891) * change user sync get parameters * change user sync get parameters --------- Co-authored-by: gaudeamus --- modules/mgidBidAdapter.js | 17 +++++++++-------- test/spec/modules/mgidBidAdapter_spec.js | 18 +++++++++--------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 0079936d803..8e889261e52 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -380,15 +380,15 @@ export const spec = { const syncs = []; const query = []; - query.push('cbuster=' + Math.round(new Date().getTime())); - query.push('consentData=' + encodeURIComponent(isPlainObject(gdprConsent) && isStr(gdprConsent?.consentString) ? gdprConsent.consentString : '')); + query.push('cbuster={cbuster}'); + query.push('gdpr_consent=' + encodeURIComponent(isPlainObject(gdprConsent) && isStr(gdprConsent?.consentString) ? gdprConsent.consentString : '')); if (isPlainObject(gdprConsent) && typeof gdprConsent?.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { - query.push('gdprApplies=1'); + query.push('gdpr=1'); } else { - query.push('gdprApplies=0'); + query.push('gdpr=0'); } if (isPlainObject(uspConsent) && uspConsent?.consentString) { - query.push(`uspString=${encodeURIComponent(uspConsent?.consentString)}`); + query.push(`us_privacy=${encodeURIComponent(uspConsent?.consentString)}`); } if (isPlainObject(gppConsent) && gppConsent?.gppString) { query.push(`gppString=${encodeURIComponent(gppConsent?.gppString)}`); @@ -396,24 +396,25 @@ export const spec = { if (config.getConfig('coppa')) { query.push('coppa=1') } + const q = query.join('&') if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: 'https://cm.mgid.com/i.html?' + query.join('&') + url: 'https://cm.mgid.com/i.html?' + q.replace('{cbuster}', Math.round(new Date().getTime())) }); } else if (syncOptions.pixelEnabled) { if (pixels.length === 0) { for (let i = 0; i < spb; i++) { syncs.push({ type: 'image', - url: 'https://cm.mgid.com/i.gif?' + query.join('&') // randomly selects partner if sync required + url: 'https://cm.mgid.com/i.gif?' + q.replace('{cbuster}', Math.round(new Date().getTime())) // randomly selects partner if sync required }); } } else { for (let i = 0; i < spb && i < pixels.length; i++) { syncs.push({ type: 'image', - url: pixels[i] + (pixels[i].indexOf('?') > 0 ? '&' : '?') + query.join('&') + url: pixels[i] + (pixels[i].indexOf('?') > 0 ? '&' : '?') + q.replace('{cbuster}', Math.round(new Date().getTime())) }); } } diff --git a/test/spec/modules/mgidBidAdapter_spec.js b/test/spec/modules/mgidBidAdapter_spec.js index 28530c4c4b4..f9bb1fb91e1 100644 --- a/test/spec/modules/mgidBidAdapter_spec.js +++ b/test/spec/modules/mgidBidAdapter_spec.js @@ -824,19 +824,19 @@ describe('Mgid bid adapter', function () { const sync = spec.getUserSyncs({iframeEnabled: true}) expect(sync).to.have.length(1) expect(sync[0]).to.have.property('type', 'iframe') - expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&consentData=&gdprApplies=0/) + expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&gdpr_consent=&gdpr=0/) }); it('should return frame object with gdpr consent', function () { const sync = spec.getUserSyncs({iframeEnabled: true}, undefined, {consentString: 'consent', gdprApplies: true}) expect(sync).to.have.length(1) expect(sync[0]).to.have.property('type', 'iframe') - expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&consentData=consent&gdprApplies=1/) + expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&gdpr_consent=consent&gdpr=1/) }); it('should return frame object with gdpr + usp', function () { const sync = spec.getUserSyncs({iframeEnabled: true}, undefined, {consentString: 'consent1', gdprApplies: true}, {'consentString': 'consent2'}) expect(sync).to.have.length(1) expect(sync[0]).to.have.property('type', 'iframe') - expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2/) + expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&gdpr_consent=consent1&gdpr=1&us_privacy=consent2/) }); it('should return img object with gdpr + usp', function () { config.setConfig({userSync: {syncsPerBidder: undefined}}); @@ -844,23 +844,23 @@ describe('Mgid bid adapter', function () { expect(sync).to.have.length(USERSYNC_DEFAULT_CONFIG.syncsPerBidder) for (let i = 0; i < USERSYNC_DEFAULT_CONFIG.syncsPerBidder; i++) { expect(sync[i]).to.have.property('type', 'image') - expect(sync[i]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.gif\?cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2/) + expect(sync[i]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.gif\?cbuster=\d+&gdpr_consent=consent1&gdpr=1&us_privacy=consent2/) } }); it('should return frame object with gdpr + usp', function () { const sync = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, undefined, {consentString: 'consent1', gdprApplies: true}, {'consentString': 'consent2'}) expect(sync).to.have.length(1) expect(sync[0]).to.have.property('type', 'iframe') - expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2/) + expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&gdpr_consent=consent1&gdpr=1&us_privacy=consent2/) }); it('should return img (pixels) objects with gdpr + usp', function () { const response = [{body: {ext: {cm: ['http://cm.mgid.com/i.gif?cdsp=1111', 'http://cm.mgid.com/i.gif']}}}] const sync = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, response, {consentString: 'consent1', gdprApplies: true}, {'consentString': 'consent2'}) expect(sync).to.have.length(2) expect(sync[0]).to.have.property('type', 'image') - expect(sync[0]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cdsp=1111&cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2/) + expect(sync[0]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cdsp=1111&cbuster=\d+&gdpr_consent=consent1&gdpr=1&us_privacy=consent2/) expect(sync[1]).to.have.property('type', 'image') - expect(sync[1]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2/) + expect(sync[1]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cbuster=\d+&gdpr_consent=consent1&gdpr=1&us_privacy=consent2/) }); }); @@ -874,9 +874,9 @@ describe('Mgid bid adapter', function () { const sync = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, response, {consentString: 'consent1', gdprApplies: true}, {'consentString': 'consent2'}, {gppString: 'gpp'}) expect(sync).to.have.length(2) expect(sync[0]).to.have.property('type', 'image') - expect(sync[0]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cdsp=1111&cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2&gppString=gpp&coppa=1/) + expect(sync[0]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cdsp=1111&cbuster=\d+&gdpr_consent=consent1&gdpr=1&us_privacy=consent2&gppString=gpp&coppa=1/) expect(sync[1]).to.have.property('type', 'image') - expect(sync[1]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2&gppString=gpp&coppa=1/) + expect(sync[1]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cbuster=\d+&gdpr_consent=consent1&gdpr=1&us_privacy=consent2&gppString=gpp&coppa=1/) }); }); From 88095cad669961dffe441a88eea0e1ce6b9e0ba4 Mon Sep 17 00:00:00 2001 From: YakirLavi <73641910+YakirLavi@users.noreply.github.com> Date: Sat, 6 May 2023 14:15:28 +0300 Subject: [PATCH 03/13] Rise Bid Adapter: support Coppa param (#9837) * add Rise adapter * fixes * change param isOrg to org * Rise adapter * change email for rise * fix circle failed * bump * bump * bump * remove space * Upgrade Rise adapter to 5.0 * added isWrapper param * addes is_wrapper parameter to documentation * added is_wrapper to test * removed isWrapper * Rise Bid Adapter: support Coppa param (#24) * MinuteMedia Bid Adapter: support Coppa param (#25) * Revert "MinuteMedia Bid Adapter: support Coppa param (#25)" (#26) This reverts commit 66c4e7b46121afc5331c8bca6e2fc972fc55f090. * bump * update coppa fetch * setting coppa param update * update Coppa tests * update test naming --------- Co-authored-by: Noam Tzuberi Co-authored-by: noamtzu Co-authored-by: Noam Tzuberi Co-authored-by: Laslo Chechur Co-authored-by: OronW <41260031+OronW@users.noreply.github.com> Co-authored-by: lasloche <62240785+lasloche@users.noreply.github.com> Co-authored-by: inna Co-authored-by: YakirLavi --- modules/riseBidAdapter.js | 6 ++++++ test/spec/modules/riseBidAdapter_spec.js | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index 89e4e85c627..e0f196fb072 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -292,6 +292,7 @@ function generateBidParameters(bid, bidderRequest) { bidderRequestId: getBidIdParameter('bidderRequestId', bid), loop: getBidIdParameter('bidderRequestsCount', bid), transactionId: getBidIdParameter('transactionId', bid), + coppa: 0 }; const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); @@ -364,6 +365,11 @@ function generateBidParameters(bid, bidderRequest) { if (protocols) { bidObject.protocols = protocols; } + + const coppa = deepAccess(bid, 'ortb2.regs.coppa') + if (coppa) { + bidObject.coppa = 1; + } } return bidObject; diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index 4fa4ff354ec..d22cbc01d39 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -341,6 +341,24 @@ describe('riseAdapter', function () { expect(request.data.bids[0]).to.be.an('object'); expect(request.data.bids[0]).to.have.property('floorPrice', 1.5); }); + + describe('COPPA Param', function() { + it('should set coppa equal 0 in bid request if coppa is set to false', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].coppa).to.be.equal(0); + }); + + it('should set coppa equal 1 in bid request if coppa is set to true', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.ortb2 = { + 'regs': { + 'coppa': true, + } + }; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].coppa).to.be.equal(1); + }); + }); }); describe('interpretResponse', function () { From d5bfdee42d7b905fcaa5be8e60090ed9b1d5558f Mon Sep 17 00:00:00 2001 From: Jason Quaccia Date: Sat, 6 May 2023 04:20:17 -0700 Subject: [PATCH 04/13] Bidder Settings: Support for option to apply adapter bid adjustment to unknown bidder codes (#9609) * support to default on adapter bidderSetting if config option is true * addressed review feedback * changed var name to adjustAlternateBids and refactored isInvalidAlternateBidder func * adjusted logic around adapter and bidder code * improvements * consolidate tests in single spec file * fix lint --------- Co-authored-by: Demetrio Girardi --- src/adapters/bidderFactory.js | 1 + src/utils/cpm.js | 5 +- test/spec/unit/core/bidderFactory_spec.js | 18 +++---- test/spec/unit/utils/cpm_spec.js | 57 ++++++++++++++++++++++- 4 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 53d2a4d3ca6..8b0aaec57d5 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -306,6 +306,7 @@ export function newBidder(spec) { return true; } } + return false; } diff --git a/src/utils/cpm.js b/src/utils/cpm.js index 07113e7c944..7601d0643fd 100644 --- a/src/utils/cpm.js +++ b/src/utils/cpm.js @@ -4,7 +4,10 @@ import {logError} from '../utils.js'; export function adjustCpm(cpm, bidResponse, bidRequest, {index = auctionManager.index, bs = bidderSettings} = {}) { bidRequest = bidRequest || index.getBidRequest(bidResponse); - const bidCpmAdjustment = bs.get(bidResponse?.bidderCode || bidRequest?.bidder, 'bidCpmAdjustment'); + const adapterCode = bidResponse?.adapterCode; + const bidderCode = bidResponse?.bidderCode || bidRequest?.bidder; + const adjustAlternateBids = bs.get(bidResponse?.adapterCode, 'adjustAlternateBids'); + const bidCpmAdjustment = bs.getOwn(bidderCode, 'bidCpmAdjustment') || bs.get(adjustAlternateBids ? adapterCode : bidderCode, 'bidCpmAdjustment'); if (bidCpmAdjustment && typeof bidCpmAdjustment === 'function') { try { diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index c0e48089b52..568c3bfdd09 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -1,24 +1,24 @@ import { + addComponentAuction, + isValid, newBidder, - registerBidder, preloadBidderMappingFile, - storage, - isValid, - addComponentAuction + registerBidder, + storage } from 'src/adapters/bidderFactory.js'; import adapterManager from 'src/adapterManager.js'; import * as ajax from 'src/ajax.js'; -import { expect } from 'chai'; -import { userSync } from 'src/userSync.js' +import {expect} from 'chai'; +import {userSync} from 'src/userSync.js'; import * as utils from 'src/utils.js'; -import { config } from 'src/config.js'; -import { server } from 'test/mocks/xhr.js'; +import {config} from 'src/config.js'; +import {server} from 'test/mocks/xhr.js'; import CONSTANTS from 'src/constants.json'; import * as events from 'src/events.js'; import {hook} from '../../../../src/hook.js'; import {auctionManager} from '../../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../../helpers/indexStub.js'; -import { bidderSettings } from '../../../../src/bidderSettings.js'; +import {bidderSettings} from '../../../../src/bidderSettings.js'; import {decorateAdUnitsWithNativeParams} from '../../../../src/native.js'; const CODE = 'sampleBidder'; diff --git a/test/spec/unit/utils/cpm_spec.js b/test/spec/unit/utils/cpm_spec.js index 9d104b04d09..7d63c53525e 100644 --- a/test/spec/unit/utils/cpm_spec.js +++ b/test/spec/unit/utils/cpm_spec.js @@ -1,11 +1,14 @@ import {adjustCpm} from '../../../../src/utils/cpm.js'; +import {ScopedSettings} from '../../../../src/bidderSettings.js'; +import {expect} from 'chai/index.js'; describe('adjustCpm', () => { const bidderCode = 'mockBidder'; let adjustmentFn, bs, index; beforeEach(() => { bs = { - get: sinon.stub() + get: sinon.stub(), + getOwn: sinon.stub() } index = { getBidRequest: sinon.stub() @@ -62,3 +65,55 @@ describe('adjustCpm', () => { }); }) }); + +describe('adjustAlternateBids', () => { + let bs; + afterEach(() => { + bs = null; + }); + + function runAdjustment(cpm, bidderCode, adapterCode) { + return adjustCpm(cpm, {bidderCode, adapterCode}, null, {bs: new ScopedSettings(() => bs)}); + } + + it('should fall back to the adapter adjustment fn when adjustAlternateBids is true', () => { + bs = { + adapter: { + adjustAlternateBids: true, + bidCpmAdjustment: function (cpm) { + return cpm * 2; + } + }, + bidder: {} + }; + expect(runAdjustment(1, 'bidder', 'adapter')).to.eql(2); + }); + + it('should NOT fall back to the adapter adjustment fn when adjustAlternateBids is not true', () => { + bs = { + adapter: { + bidCpmAdjustment(cpm) { + return cpm * 2 + } + } + } + expect(runAdjustment(1, 'bidder', 'adapter')).to.eql(1); + }); + + it('should prioritize bidder adjustment fn', () => { + bs = { + adapter: { + adjustAlternateBids: true, + bidCpmAdjustment(cpm) { + return cpm * 2 + } + }, + bidder: { + bidCpmAdjustment(cpm) { + return cpm * 3 + } + } + } + expect(runAdjustment(1, 'bidder', 'adapter')).to.eql(3); + }); +}); From 45867d44da53f6b3c4a7981d963b92b76adafd22 Mon Sep 17 00:00:00 2001 From: Ryan Schweitzer <50628828+r-schweitzer@users.noreply.github.com> Date: Sat, 6 May 2023 07:23:28 -0400 Subject: [PATCH 05/13] PBjs Core : filter bidders from config when not server-side (#9632) * filter bidders from config when not server-side * fixed prebid server spec * added filter for allowUnknownBidderCodes * added unit test --- .../prebidServerBidAdapter/ortbConverter.js | 6 +- .../modules/prebidServerBidAdapter_spec.js | 97 ++++++++++++++++++- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index 820c34c66fd..85c0e67a829 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -167,7 +167,11 @@ const PBS_CONVERTER = ortbConverter({ deepSetValue(ortbRequest, 'ext.prebid', mergeDeep(ortbRequest.ext?.prebid || {}, context.s2sBidRequest.s2sConfig.extPrebid)); } - const fpdConfigs = Object.entries(context.s2sBidRequest.ortb2Fragments?.bidder || {}).map(([bidder, ortb2]) => ({ + const fpdConfigs = Object.entries(context.s2sBidRequest.ortb2Fragments?.bidder || {}).filter(([bidder]) => { + const bidders = context.s2sBidRequest.s2sConfig.bidders; + const allowUnknownBidderCodes = context.s2sBidRequest.s2sConfig.allowUnknownBidderCodes; + return allowUnknownBidderCodes || (bidders && bidders.includes(bidder)); + }).map(([bidder, ortb2]) => ({ bidders: [bidder], config: {ortb2} })); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 9516f0402c1..9bdc39e3c76 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -2463,7 +2463,7 @@ describe('S2S Adapter', function () { }; const bcat = ['IAB25', 'IAB7-39']; const badv = ['blockedAdv-1.com', 'blockedAdv-2.com']; - const allowedBidders = ['rubicon', 'appnexus']; + const allowedBidders = ['appnexus']; const expected = allowedBidders.map(bidder => ({ bidders: [bidder], @@ -2516,6 +2516,101 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.bcat).to.deep.equal(bcat); }); + it('passes first party data in request for unknown when allowUnknownBidderCodes is true', () => { + const cfg = { ...CONFIG, allowUnknownBidderCodes: true }; + config.setConfig({ s2sConfig: cfg }); + + const clonedReq = {...REQUEST, s2sConfig: cfg} + const s2sBidRequest = utils.deepClone(clonedReq); + const bidRequests = utils.deepClone(BID_REQUESTS); + + const commonSite = { + keywords: ['power tools'], + search: 'drill' + }; + const commonUser = { + keywords: ['a', 'b'], + gender: 'M' + }; + + const site = { + content: {userrating: 4}, + ext: { + data: { + pageType: 'article', + category: 'tools' + } + } + }; + const user = { + yob: '1984', + geo: { country: 'ca' }, + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + }; + const bcat = ['IAB25', 'IAB7-39']; + const badv = ['blockedAdv-1.com', 'blockedAdv-2.com']; + const allowedBidders = ['appnexus', 'unknown']; + + const expected = allowedBidders.map(bidder => ({ + bidders: [bidder], + config: { + ortb2: { + site: { + content: { userrating: 4 }, + ext: { + data: { + pageType: 'article', + category: 'tools' + } + } + }, + user: { + yob: '1984', + geo: { country: 'ca' }, + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + }, + bcat: ['IAB25', 'IAB7-39'], + badv: ['blockedAdv-1.com', 'blockedAdv-2.com'] + } + } + })); + const commonContextExpected = utils.mergeDeep({ + 'page': 'http://mytestpage.com', + 'domain': 'mytestpage.com', + 'publisher': { + 'id': '1', + 'domain': 'mytestpage.com' + } + }, commonSite); + + const ortb2Fragments = { + global: {site: commonSite, user: commonUser, badv, bcat}, + bidder: Object.fromEntries(allowedBidders.map(bidder => [bidder, {site, user, bcat, badv}])) + }; + + // adapter.callBids({ ...REQUEST, s2sConfig: cfg }, BID_REQUESTS, addBidResponse, done, ajax); + + adapter.callBids(addFpdEnrichmentsToS2SRequest({...s2sBidRequest, ortb2Fragments}, bidRequests, cfg), bidRequests, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + // eslint-disable-next-line no-console + console.log(parsedRequestBody); + expect(parsedRequestBody.ext.prebid.bidderconfig).to.deep.equal(expected); + expect(parsedRequestBody.site).to.deep.equal(commonContextExpected); + expect(parsedRequestBody.user).to.deep.equal(commonUser); + expect(parsedRequestBody.badv).to.deep.equal(badv); + expect(parsedRequestBody.bcat).to.deep.equal(bcat); + }); + describe('pbAdSlot config', function () { it('should not send \"imp.ext.data.pbadslot\" if \"ortb2Imp.ext\" is undefined', function () { const consentConfig = { s2sConfig: CONFIG }; From ea889fe2e0b2e70017c4c4e4f48fd524f664f809 Mon Sep 17 00:00:00 2001 From: YakirLavi <73641910+YakirLavi@users.noreply.github.com> Date: Sat, 6 May 2023 14:26:56 +0300 Subject: [PATCH 06/13] MinuteMedia Bid Adapter: Support Coppa param (#9838) * add Rise adapter * fixes * change param isOrg to org * Rise adapter * change email for rise * fix circle failed * bump * bump * bump * remove space * Upgrade Rise adapter to 5.0 * added isWrapper param * addes is_wrapper parameter to documentation * added is_wrapper to test * removed isWrapper * add coppa tests * Update coppa tests * update coppa param fetch * update Coppa tests --------- Co-authored-by: Noam Tzuberi Co-authored-by: noamtzu Co-authored-by: Noam Tzuberi Co-authored-by: Laslo Chechur Co-authored-by: OronW <41260031+OronW@users.noreply.github.com> Co-authored-by: lasloche <62240785+lasloche@users.noreply.github.com> Co-authored-by: inna Co-authored-by: YakirLavi --- modules/minutemediaBidAdapter.js | 6 ++++++ .../spec/modules/minutemediaBidAdapter_spec.js | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/modules/minutemediaBidAdapter.js b/modules/minutemediaBidAdapter.js index a80a37f5ead..c19ebbda311 100644 --- a/modules/minutemediaBidAdapter.js +++ b/modules/minutemediaBidAdapter.js @@ -289,6 +289,7 @@ function generateBidParameters(bid, bidderRequest) { loop: getBidIdParameter('bidderRequestsCount', bid), bidderRequestId: getBidIdParameter('bidderRequestId', bid), transactionId: getBidIdParameter('transactionId', bid), + coppa: 0 }; const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); @@ -346,6 +347,11 @@ function generateBidParameters(bid, bidderRequest) { if (linearity) { bidObject.linearity = linearity; } + + const coppa = deepAccess(bid, `ortb2.regs.coppa`); + if (coppa) { + bidObject.coppa = 1; + } } return bidObject; diff --git a/test/spec/modules/minutemediaBidAdapter_spec.js b/test/spec/modules/minutemediaBidAdapter_spec.js index bbd23918031..d2b9bc68fa6 100644 --- a/test/spec/modules/minutemediaBidAdapter_spec.js +++ b/test/spec/modules/minutemediaBidAdapter_spec.js @@ -293,6 +293,24 @@ describe('minutemediaAdapter', function () { expect(request.data.bids[0]).to.be.an('object'); expect(request.data.bids[0]).to.have.property('floorPrice', 1.5); }); + + describe('COPPA Param', function() { + it('should set coppa equal 0 in bid request if coppa is set to false', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].coppa).to.be.equal(0); + }); + + it('should set coppa equal 1 in bid request if coppa is set to true', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.ortb2 = { + 'regs': { + 'coppa': true, + } + }; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].coppa).to.be.equal(1); + }); + }); }); describe('interpretResponse', function () { From 386c95a8579e70633f6fdf122d20e333f3412637 Mon Sep 17 00:00:00 2001 From: khang-vu-ttd <109103626+khang-vu-ttd@users.noreply.github.com> Date: Sat, 6 May 2023 04:43:49 -0700 Subject: [PATCH 07/13] ttd bid adapter: pass on all of user/app/ortb2imp for first party data support, default imp.secure (#9892) * pass on user, app, imp * cosmetic changes * get secure from the right place * don't need to import mergeDeep * add pmp and device first party data --- modules/ttdBidAdapter.js | 45 ++++---- test/spec/modules/ttdBidAdapter_spec.js | 131 +++++++++++++++++++++++- 2 files changed, 152 insertions(+), 24 deletions(-) diff --git a/modules/ttdBidAdapter.js b/modules/ttdBidAdapter.js index ce803aa72ad..48fda2abc8e 100644 --- a/modules/ttdBidAdapter.js +++ b/modules/ttdBidAdapter.js @@ -2,6 +2,7 @@ import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {isNumber} from '../src/utils.js'; const BIDADAPTERVERSION = 'TTD-PREBID-2022.06.28'; const BIDDER_CODE = 'ttd'; @@ -75,7 +76,7 @@ function getSource(validBidRequests) { return source; } -function getDevice() { +function getDevice(firstPartyData) { const language = navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage; let device = { ua: navigator.userAgent, @@ -84,6 +85,8 @@ function getDevice() { connectiontype: getConnectionType() }; + utils.mergeDeep(device, firstPartyData.device) + return device; }; @@ -114,7 +117,7 @@ function getConnectionType() { } } -function getUser(bidderRequest) { +function getUser(bidderRequest, firstPartyData) { let user = {}; if (bidderRequest.gdprConsent) { utils.deepSetValue(user, 'ext.consent', bidderRequest.gdprConsent.consentString); @@ -129,13 +132,8 @@ function getUser(bidderRequest) { utils.deepSetValue(user, 'ext.eids', eids); } - // gather user.data - const ortb2UserData = utils.deepAccess(bidderRequest, 'ortb2.user.data'); - if (ortb2UserData && ortb2UserData.length) { - user = utils.mergeDeep(user, { - data: [...ortb2UserData] - }); - }; + utils.mergeDeep(user, firstPartyData.user) + return user; } @@ -163,16 +161,6 @@ function getImpression(bidRequest) { }; const gpid = utils.deepAccess(bidRequest, 'ortb2Imp.ext.gpid'); - const tid = utils.deepAccess(bidRequest, 'ortb2Imp.ext.tid'); - const rwdd = utils.deepAccess(bidRequest, 'ortb2Imp.rwdd'); - if (gpid || tid) { - impression.ext = {} - if (gpid) { impression.ext.gpid = gpid } - if (tid) { impression.ext.tid = tid } - } - if (rwdd) { - impression.rwdd = rwdd; - } const tagid = gpid || bidRequest.params.placementId; if (tagid) { impression.tagid = tagid; @@ -197,6 +185,11 @@ function getImpression(bidRequest) { impression.bidfloorcur = 'USD'; } + const secure = utils.deepAccess(bidRequest, 'ortb2Imp.secure'); + impression.secure = isNumber(secure) ? secure : 1 + + utils.mergeDeep(impression, bidRequest.ortb2Imp) + return impression; } @@ -416,8 +409,8 @@ export const spec = { id: bidderRequest.auctionId, imp: validBidRequests.map(bidRequest => getImpression(bidRequest)), site: getSite(bidderRequest, firstPartyData), - device: getDevice(), - user: getUser(bidderRequest), + device: getDevice(firstPartyData), + user: getUser(bidderRequest, firstPartyData), at: 1, cur: ['USD'], regs: getRegs(bidderRequest), @@ -433,6 +426,14 @@ export const spec = { topLevel.badv = firstPartyData.badv; } + if (firstPartyData && firstPartyData.app) { + topLevel.app = firstPartyData.app + } + + if (firstPartyData && firstPartyData.pmp) { + topLevel.pmp = firstPartyData.pmp + } + let url = BIDDER_ENDPOINT + bidderRequest.bids[0].params.supplySourceId; let serverRequest = { @@ -468,7 +469,7 @@ export const spec = { * @param {ttdResponseObj} bidResponse A successful response from ttd. * @param {ServerRequest} serverRequest The result of buildRequests() that lead to this response. * @return {Bid[]} An array of formatted bids. - */ + */ interpretResponse: function (response, serverRequest) { let seatBidsInResponse = utils.deepAccess(response, 'body.seatbid'); const currency = utils.deepAccess(response, 'body.cur'); diff --git a/test/spec/modules/ttdBidAdapter_spec.js b/test/spec/modules/ttdBidAdapter_spec.js index 37d1bac40ee..5e472543a01 100644 --- a/test/spec/modules/ttdBidAdapter_spec.js +++ b/test/spec/modules/ttdBidAdapter_spec.js @@ -232,6 +232,25 @@ describe('ttdBidAdapter', function () { 'doneCbCallCount': 0 }; + const extFirstPartyDataValues = ['value', 'value2']; + const extFirstPartyData = { + data: { + firstPartyKey: 'firstPartyValue', + firstPartyKey2: extFirstPartyDataValues + }, + custom: 'custom_data', + custom_kvp: { + customKey: 'customValue' + } + } + + function validateExtFirstPartyData(ext) { + expect(ext.data.firstPartyKey).to.equal('firstPartyValue'); + expect(ext.data.firstPartyKey2).to.eql(extFirstPartyDataValues); + expect(ext.custom).to.equal('custom_data'); + expect(ext.custom_kvp.customKey).to.equal('customValue'); + } + it('sends bid request to our endpoint that makes sense', function () { const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest); expect(request.method).to.equal('POST'); @@ -572,6 +591,115 @@ describe('ttdBidAdapter', function () { config.resetConfig(); expect(requestBody.imp[0].bidfloor).to.equal(bidfloor); }); + + it('adds default value for secure if not set to request', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].secure).to.equal(1); + }); + + it('adds secure to request', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + clonedBannerRequests[0].ortb2Imp.secure = 0; + + let requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(0).to.equal(requestBody.imp[0].secure); + + clonedBannerRequests[0].ortb2Imp.secure = 1; + requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(1).to.equal(requestBody.imp[0].secure); + }); + + it('adds all of site first party data to request', function() { + const ortb2 = { + site: { + ext: extFirstPartyData, + search: 'test search' + } + }; + + let clonedBidderRequest = {...deepClone(baseBidderRequest), ortb2}; + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + + validateExtFirstPartyData(requestBody.site.ext) + expect(requestBody.site.search).to.equal('test search') + }); + + it('adds all of user first party data to request', function() { + const ortb2 = { + user: { + ext: extFirstPartyData, + yob: 1998 + } + }; + + let clonedBidderRequest = {...deepClone(baseBidderRequest), ortb2}; + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + + validateExtFirstPartyData(requestBody.user.ext) + expect(requestBody.user.yob).to.equal(1998) + }); + + it('adds all of imp first party data to request', function() { + const metric = { type: 'viewability', value: 0.8 }; + let clonedBannerRequests = deepClone(baseBannerBidRequests); + clonedBannerRequests[0].ortb2Imp = { + ext: extFirstPartyData, + metric: [metric], + clickbrowser: 1 + }; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + + validateExtFirstPartyData(requestBody.imp[0].ext) + expect(requestBody.imp[0].tagid).to.equal('1gaa015'); + expect(requestBody.imp[0].metric[0]).to.deep.equal(metric); + expect(requestBody.imp[0].clickbrowser).to.equal(1) + }); + + it('adds all of app first party data to request', function() { + const ortb2 = { + app: { + ext: extFirstPartyData, + ver: 'v1.0' + } + }; + + let clonedBidderRequest = {...deepClone(baseBidderRequest), ortb2}; + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + + validateExtFirstPartyData(requestBody.app.ext) + expect(requestBody.app.ver).to.equal('v1.0') + }); + + it('adds all of device first party data to request', function() { + const ortb2 = { + device: { + ext: extFirstPartyData, + os: 'iPhone' + } + }; + + let clonedBidderRequest = {...deepClone(baseBidderRequest), ortb2}; + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + + validateExtFirstPartyData(requestBody.device.ext) + expect(requestBody.device.os).to.equal('iPhone') + }); + + it('adds all of pmp first party data to request', function() { + const ortb2 = { + pmp: { + ext: extFirstPartyData, + private_auction: 1 + } + }; + + let clonedBidderRequest = {...deepClone(baseBidderRequest), ortb2}; + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + + validateExtFirstPartyData(requestBody.pmp.ext) + expect(requestBody.pmp.private_auction).to.equal(1) + }); }); describe('buildRequests-banner-multiple', function () { @@ -1295,8 +1423,7 @@ describe('ttdBidAdapter', function () { } }; - const expectedBid = - { + const expectedBid = { 'requestId': '2eabb87dfbcae4', 'cpm': 13.6, 'creativeId': 'mokivv6m', From c43a0e43a3ab1a6fb78bff3707b8894bc03c567d Mon Sep 17 00:00:00 2001 From: Andrea Tumbarello Date: Sat, 6 May 2023 13:49:58 +0200 Subject: [PATCH 08/13] AIDEM Bidder Adapter: added arbitrary ext field to win notice (#9906) * AIDEM Bid Adapter * Added _spec.js * update * Fix Navigator in _spec.js * Removed timeout handler. * Added publisherId as required bidder params * moved publisherId into site publisher object * Added wpar to environment * Added placementId parameter * added unit tests for the wpar environment object * PlacementId is now a required parameter Added optional rateLimit parameter Added publisherId, siteId, placementId in win notice payload Added unit tests * Revert to optional placementId parameter Added missing semicolons * Extended win notice * Added arbitrary ext field to win notice --------- Co-authored-by: Giovanni Sollazzo Co-authored-by: darkstar Co-authored-by: AndreaC <67786179+darkstarac@users.noreply.github.com> --- modules/aidemBidAdapter.js | 4 +++- test/spec/modules/aidemBidAdapter_spec.js | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/aidemBidAdapter.js b/modules/aidemBidAdapter.js index 2f8732a8ec4..d43c07aeece 100644 --- a/modules/aidemBidAdapter.js +++ b/modules/aidemBidAdapter.js @@ -153,6 +153,7 @@ function getPageUrl(bidderRequest) { function buildWinNotice(bid) { const params = bid.params[0]; const app = deepAccess(bid, 'meta.ext.app') + const winNoticeExt = deepAccess(bid, 'meta.ext.win_notice_ext') return { publisherId: params.publisherId, siteId: params.siteId, @@ -170,7 +171,8 @@ function buildWinNotice(bid) { responseTimestamp: bid.responseTimestamp, mediatype: bid.mediaType, environment: app ? 'app' : 'web', - ...app + ...app, + ext: winNoticeExt, }; } diff --git a/test/spec/modules/aidemBidAdapter_spec.js b/test/spec/modules/aidemBidAdapter_spec.js index 6a875feb2a9..8268efde2a1 100644 --- a/test/spec/modules/aidemBidAdapter_spec.js +++ b/test/spec/modules/aidemBidAdapter_spec.js @@ -359,6 +359,9 @@ const WIN_NOTICE_APP = { 'app_name': '{{APP_NAME}}', 'app_store_url': '{{APP_STORE_URL}}', 'inventory_source': '{{INVENTORY_SOURCE}}' + }, + 'win_notice_ext': { + 'seatid': '{{SEAT_ID}}' } } }, From ce01d89806580858d8b311c8bb945e407b161bff Mon Sep 17 00:00:00 2001 From: nkloeber <100145701+nkloeber@users.noreply.github.com> Date: Sat, 6 May 2023 13:52:15 +0200 Subject: [PATCH 09/13] YieldlabBidAdapter updated native asset mapping (#9895) * YieldlabBidAdapter added asset mapping for native ad server responses and mapped native assets based on property names instead of IDs to account for dynamic assets. * YieldlabBidAdapter update main image asset mapping --- modules/yieldlabBidAdapter.js | 32 +++++++++++++++++--- test/spec/modules/yieldlabBidAdapter_spec.js | 9 +++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index cadeb9c1300..31e9b35f178 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -184,13 +184,12 @@ export const spec = { if (isNative(bidRequest, adType)) { // there may be publishers still rely on it - const url = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}${pvId}`; - bidResponse.adUrl = url; + bidResponse.adUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}${pvId}`; bidResponse.mediaType = NATIVE; - const nativeImageAssetObj = find(matchedBid.native.assets, e => e.id === 2); + const nativeImageAssetObj = find(matchedBid.native.assets, asset => isMainImage(asset)); const nativeImageAsset = nativeImageAssetObj ? nativeImageAssetObj.img : { url: '', w: 0, h: 0 }; - const nativeTitleAsset = find(matchedBid.native.assets, e => e.id === 1); - const nativeBodyAsset = find(matchedBid.native.assets, e => e.id === 3); + const nativeTitleAsset = find(matchedBid.native.assets, asset => hasValidProperty(asset, 'title')); + const nativeBodyAsset = find(matchedBid.native.assets, asset => hasValidProperty(asset, 'data')); bidResponse.native = { title: nativeTitleAsset ? nativeTitleAsset.title.text : '', body: nativeBodyAsset ? nativeBodyAsset.data.value : '', @@ -201,6 +200,7 @@ export const spec = { }, clickUrl: matchedBid.native.link.url, impressionTrackers: matchedBid.native.imptrackers, + assets: matchedBid.native.assets, }; } @@ -505,4 +505,26 @@ function getBidFloor(bid, sizes) { return undefined; } +/** + * Checks if an object has a property with a given name and the property value is not null or undefined. + * + * @param {Object} obj - The object to check. + * @param {string} propName - The name of the property to check. + * @returns {boolean} Returns true if the object has a property with the given name and the property value is not null or undefined, otherwise false. + */ +function hasValidProperty(obj, propName) { + return obj.hasOwnProperty(propName) && obj[propName] != null; +} + +/** + * Checks if an asset object is a main image. + * A main image is defined as an image asset whose type value is 3. + * + * @param {Object} asset - The asset object to check. + * @returns {boolean} Returns true if the object has a property img.type with a value of 3, otherwise false. + */ +function isMainImage(asset) { + return asset?.img?.type === 3 +} + registerBidder(spec); diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index e5151cf789c..80537facd41 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -191,6 +191,7 @@ const NATIVE_RESPONSE = Object.assign({}, RESPONSE, { url: 'https://localhost:8080/yl-logo100x100.jpg', w: 100, h: 100, + type: 3, }, }, { @@ -557,7 +558,6 @@ describe('yieldlabBidAdapter', () => { it('should add adUrl and native assets when type is Native', () => { const result = spec.interpretResponse({body: [NATIVE_RESPONSE]}, {validBidRequests: [NATIVE_REQUEST()], queryParams: REQPARAMS}); - expect(result[0].requestId).to.equal('2d925f27f5079f'); expect(result[0].cpm).to.equal(0.01); expect(result[0].mediaType).to.equal('native'); @@ -569,6 +569,13 @@ describe('yieldlabBidAdapter', () => { expect(result[0].native.image.height).to.equal(100); expect(result[0].native.clickUrl).to.equal('https://www.yieldlab.de'); expect(result[0].native.impressionTrackers.length).to.equal(3); + expect(result[0].native.assets.length).to.equal(3); + const titleAsset = result[0].native.assets.find(asset => 'title' in asset); + const imageAsset = result[0].native.assets.find(asset => 'img' in asset); + const bodyAsset = result[0].native.assets.find(asset => 'data' in asset); + expect(titleAsset).to.exist.and.to.have.nested.property('id', 1) + expect(imageAsset).to.exist.and.to.have.nested.property('id', 2) + expect(bodyAsset).to.exist.and.to.have.nested.property('id', 3) }); it('should add adUrl and default native assets when type is Native', () => { From 122f67d0f2cbf4d033713067497dacc8ac2030e5 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Mon, 8 May 2023 10:17:01 -0400 Subject: [PATCH 10/13] Update index.js (#9913) --- modules/prebidServerBidAdapter/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index ffb02204ed0..59af37b0686 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -260,9 +260,7 @@ function queueSync(bidderCodes, gdprConsent, uspConsent, gppConsent, s2sConfig) } if (gppConsent) { - // proposing the following formatting, can adjust if needed... - // update - leaving this param as an array, since it's part of a POST payload where the [] characters shouldn't matter too much - payload.gpp_sid = gppConsent.applicableSections + payload.gpp_sid = gppConsent.applicableSections.join(); // should we add check if applicableSections was not equal to -1 (where user was out of scope)? // this would be similar to what was done above for TCF payload.gpp = gppConsent.gppString; From b6d949f2dbd7edb34850bcf8a86ec492e44c5cc8 Mon Sep 17 00:00:00 2001 From: "Takaaki.Kojima" Date: Mon, 8 May 2023 23:25:16 +0900 Subject: [PATCH 11/13] fix for #8421, Update ad generation adapter 1.5.0 (#9911) * fix for #8421 * update adapter version * update test spec --- modules/adgenerationBidAdapter.js | 39 ++++++++++++------- .../modules/adgenerationBidAdapter_spec.js | 12 +++--- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/modules/adgenerationBidAdapter.js b/modules/adgenerationBidAdapter.js index 0e4e9ef6805..199dd0955c7 100644 --- a/modules/adgenerationBidAdapter.js +++ b/modules/adgenerationBidAdapter.js @@ -28,7 +28,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const ADGENE_PREBID_VERSION = '1.4.0'; + const ADGENE_PREBID_VERSION = '1.5.0'; let serverRequests = []; for (let i = 0, len = validBidRequests.length; i < len; i++) { const validReq = validBidRequests[i]; @@ -59,7 +59,6 @@ export const spec = { data = tryAppendQueryString(data, 'imark', '1'); } - // TODO: is 'page' the right value here? data = tryAppendQueryString(data, 'tp', bidderRequest.refererInfo.page); if (isIos()) { const hyperId = getHyperId(validReq); @@ -210,35 +209,45 @@ function appendChildToBody(ad, data) { return ad.replace(/<\/\s?body>/, `${data}`); } +/** + * create APVTag + * @return {string} + */ function createAPVTag() { const APVURL = 'https://cdn.apvdr.com/js/VideoAd.min.js'; - let apvScript = document.createElement('script'); - apvScript.type = 'text/javascript'; - apvScript.id = 'apv'; - apvScript.src = APVURL; - return apvScript.outerHTML; + return `` } +/** + * create ADGBrowserMTag + * @return {string} + */ function createADGBrowserMTag() { const ADGBrowserMURL = 'https://i.socdm.com/sdk/js/adg-browser-m.js'; return ``; } +/** + * create APVTag & insertVast + * @param targetId + * @param vastXml + * @return {string} + */ function insertVASTMethodForAPV(targetId, vastXml) { let apvVideoAdParam = { s: targetId }; - let script = document.createElement(`script`); - script.type = 'text/javascript'; - script.innerHTML = `(function(){ new APV.VideoAd(${escapeUnsafeChars(JSON.stringify(apvVideoAdParam))}).load('${vastXml.replace(/\r?\n/g, '')}'); })();`; - return script.outerHTML; + return `` } +/** + * create ADGBrowserMTag & insertVast + * @param vastXml + * @param marginTop + * @return {string} + */ function insertVASTMethodForADGBrowserM(vastXml, marginTop) { - const script = document.createElement(`script`); - script.type = 'text/javascript'; - script.innerHTML = `window.ADGBrowserM.init({vastXml: '${vastXml.replace(/\r?\n/g, '')}', marginTop: '${marginTop}'});`; - return script.outerHTML; + return `` } /** diff --git a/test/spec/modules/adgenerationBidAdapter_spec.js b/test/spec/modules/adgenerationBidAdapter_spec.js index 8583c226e50..cc7cbfcb902 100644 --- a/test/spec/modules/adgenerationBidAdapter_spec.js +++ b/test/spec/modules/adgenerationBidAdapter_spec.js @@ -144,12 +144,12 @@ describe('AdgenerationAdapter', function () { } }; const data = { - banner: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.4.0&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerUSD: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=USD&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.4.0&imark=1&tp=https%3A%2F%2Fexample.com`, - native: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=1x1¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.4.0&tp=https%3A%2F%2Fexample.com`, - bannerWithHyperId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.4.0&imark=1&tp=https%3A%2F%2Fexample.com&hyper_id=novatiqId`, - bannerWithAdgextCriteoId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.4.0&adgext_criteo_id=criteo-id-test-1234567890&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerWithAdgextId5Id: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.4.0&adgext_id5_id=id5-id-test-1234567890&adgext_id5_id_link_type=2&imark=1&tp=https%3A%2F%2Fexample.com`, + banner: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.5.0&imark=1&tp=https%3A%2F%2Fexample.com`, + bannerUSD: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=USD&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.5.0&imark=1&tp=https%3A%2F%2Fexample.com`, + native: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=1x1¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.5.0&tp=https%3A%2F%2Fexample.com`, + bannerWithHyperId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.5.0&imark=1&tp=https%3A%2F%2Fexample.com&hyper_id=novatiqId`, + bannerWithAdgextCriteoId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.5.0&adgext_criteo_id=criteo-id-test-1234567890&imark=1&tp=https%3A%2F%2Fexample.com`, + bannerWithAdgextId5Id: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.5.0&adgext_id5_id=id5-id-test-1234567890&adgext_id5_id_link_type=2&imark=1&tp=https%3A%2F%2Fexample.com`, }; it('sends bid request to ENDPOINT via GET', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; From 9f1dd22293dfad79008f6abd22ff2ba8689e5c34 Mon Sep 17 00:00:00 2001 From: kapil-tuptewar <91458408+kapil-tuptewar@users.noreply.github.com> Date: Mon, 8 May 2023 19:58:31 +0530 Subject: [PATCH 12/13] PubmaticAnalyticsAdapter - Passing GPT slot name for "au" field. (#9912) * Get gptslot and push it in au field * Code review changes * Added comment for better understanding --- modules/pubmaticAnalyticsAdapter.js | 5 +++-- test/spec/modules/pubmaticAnalyticsAdapter_spec.js | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index 9e2a5b1cfeb..71a5bfef25b 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { _each, pick, logWarn, isStr, isArray, logError } from '../src/utils.js'; +import { _each, pick, logWarn, isStr, isArray, logError, getGptSlotInfoForAdUnitCode } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; @@ -353,9 +353,10 @@ function executeBidsLoggerCall(e, highestCpmBids) { outputObj.s = Object.keys(auctionCache.adUnitCodes).reduce(function(slotsArray, adUnitId) { let adUnit = auctionCache.adUnitCodes[adUnitId]; let origAdUnit = getAdUnit(auctionCache.origAdUnits, adUnitId) || {}; + // getGptSlotInfoForAdUnitCode returns gptslot corresponding to adunit provided as input. let slotObject = { 'sn': adUnitId, - 'au': origAdUnit.adUnitId || adUnitId, + 'au': origAdUnit.adUnitId || getGptSlotInfoForAdUnitCode(adUnitId)?.gptSlot || adUnitId, 'mt': getAdUnitAdFormats(origAdUnit), 'sz': getSizesForAdUnit(adUnit, adUnitId), 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestCpmBids.filter(bid => bid.adUnitCode === adUnitId)), diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index bd35297b027..048244ee4fb 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -371,7 +371,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].ps.length).to.equal(1); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); expect(data.s[0].ps[0].bidid).to.equal('2ecff0db240757'); @@ -481,6 +482,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); @@ -553,6 +555,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); expect(data.s[0].ps.length).to.equal(1); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); expect(data.s[0].ps[0].bidid).to.equal('2ecff0db240757'); @@ -1029,6 +1032,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic_alias'); @@ -1147,6 +1151,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('groupm'); From ca2f974ba269ce634554266bb90b91f0c873a889 Mon Sep 17 00:00:00 2001 From: rs-guian <119166974+rs-guian@users.noreply.github.com> Date: Mon, 8 May 2023 17:07:08 +0200 Subject: [PATCH 13/13] Retailspot Bid Adapter : initial release (#9824) * add bidder adapter * fix env url * add unit tests * minor: fix bids access in request * add details in bid response * fix unit tests * fix adId undefined case * reset package lock json * handle vastXml as base64 string --- modules/retailspotBidAdapter .md | 32 ++ modules/retailspotBidAdapter.js | 186 ++++++++ .../spec/modules/retailspotBidAdapter_spec.js | 426 ++++++++++++++++++ 3 files changed, 644 insertions(+) create mode 100644 modules/retailspotBidAdapter .md create mode 100644 modules/retailspotBidAdapter.js create mode 100644 test/spec/modules/retailspotBidAdapter_spec.js diff --git a/modules/retailspotBidAdapter .md b/modules/retailspotBidAdapter .md new file mode 100644 index 00000000000..a9b4cb4bec3 --- /dev/null +++ b/modules/retailspotBidAdapter .md @@ -0,0 +1,32 @@ +# Overview + +Module Name: RetailSpot Bidder Adapter +Module Type: Bidder Adapter +Maintainer: guillaume@retail-spot.io + +# Description + +Module that connects to RetailSpot demand sources. +Banner and Video ad formats are supported. + +# Test Parameters +``` + var adUnits = { + "code": "test-div", + "mediaTypes": { + "banner": { + "sizes": ["300x250"] + }, + "video": { + context: "instream", + playerSize: [[640, 480]] + } + }, + bids: [{ + bidder: "retailspot", + params: { + placement: "test-12345" + } + }] + }; +``` diff --git a/modules/retailspotBidAdapter.js b/modules/retailspotBidAdapter.js new file mode 100644 index 00000000000..616b638e840 --- /dev/null +++ b/modules/retailspotBidAdapter.js @@ -0,0 +1,186 @@ +import {buildUrl, deepAccess, parseSizesInput} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'retailspot'; +const DEFAULT_SUBDOMAIN = 'ssp'; +const PREPROD_SUBDOMAIN = 'ssp-preprod'; +const HOST = 'retail-spot.io'; +const ENDPOINT = '/prebid'; +const DEV_URL = 'http://localhost:8090/prebid'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + aliases: ['rs'], // short code + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + const sizes = getSize(getSizeArray(bid)); + const sizeValid = sizes.width > 0 && sizes.height > 0; + + return deepAccess(bid, 'params.placement') && sizeValid; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {bidRequests} - bidRequests.bids[] is an array of AdUnits and bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (bidRequests, bidderRequest) { + const payload = bidderRequest; + payload.rs_pbjs_version = '$prebid.version$'; + + const data = JSON.stringify(payload); + const options = { + withCredentials: true + }; + + const envParam = bidRequests[0].params.env; + var subDomain = DEFAULT_SUBDOMAIN; + if (envParam === 'preprod') { + subDomain = PREPROD_SUBDOMAIN; + } + + let url = buildUrl({ + protocol: 'https', + host: `${subDomain}.${HOST}`, + pathname: ENDPOINT + }); + + if (envParam === 'dev') { + url = DEV_URL; + } + + return { + method: 'POST', + url, + data, + options + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, request) { + const bidResponses = []; + var bidRequests = {}; + + try { + bidRequests = JSON.parse(request.data).bids; + } catch (err) { + // json error initial request can't be read + } + + // For this adapter, serverResponse is a list + serverResponse.body.forEach(response => { + const bid = createBid(response, bidRequests); + if (bid) { + bidResponses.push(bid); + } + }); + + return bidResponses; + } +} + +function getSizeArray(bid) { + let inputSize = bid.sizes || []; + + if (bid.mediaTypes && bid.mediaTypes.banner) { + inputSize = bid.mediaTypes.banner.sizes || []; + } + + // handle size in bid.params in formats: [w, h] and [[w,h]]. + if (bid.params && Array.isArray(bid.params.size)) { + inputSize = bid.params.size; + if (!Array.isArray(inputSize[0])) { + inputSize = [inputSize] + } + } + + return parseSizesInput(inputSize); +} + +/* Get parsed size from request size */ +function getSize(sizesArray) { + const parsed = {}; + // the main requested size is the first one + const size = sizesArray[0]; + + if (typeof size !== 'string') { + return parsed; + } + + const parsedSize = size.toUpperCase().split('X'); + const width = parseInt(parsedSize[0], 10); + if (width) { + parsed.width = width; + } + + const height = parseInt(parsedSize[1], 10); + if (height) { + parsed.height = height; + } + + return parsed; +} + +/* Create bid from response */ +function createBid(response, bidRequests) { + if (!response || !response.mediaType || + (response.mediaType === 'video' && !response.vastXml) || + (response.mediaType === 'banner' && !response.ad)) { + return; + } + + const request = bidRequests && bidRequests.length && bidRequests.find(itm => response.requestId === itm.bidId); + // In case we don't retreive the size from the adserver, use the given one. + if (request) { + if (!response.width || response.width === '0') { + response.width = request.width; + } + + if (!response.height || response.height === '0') { + response.height = request.height; + } + } + + const bid = { + bidderCode: BIDDER_CODE, + width: response.width, + height: response.height, + requestId: response.requestId, + ttl: response.ttl || 3600, + creativeId: response.creativeId, + cpm: response.cpm, + netRevenue: response.netRevenue, + currency: response.currency, + meta: response.meta || { advertiserDomains: ['retail-spot.io'] }, + mediaType: response.mediaType + }; + + // retreive video response if present + if (response.mediaType === 'video') { + bid.vastXml = window.atob(response.vastXml); + } else { + bid.ad = response.ad; + } + if (response.adId) { + bid.adId = response.adId; + } + if (response.dealId) { + bid.dealId = response.dealId; + } + + return bid; +} + +registerBidder(spec); diff --git a/test/spec/modules/retailspotBidAdapter_spec.js b/test/spec/modules/retailspotBidAdapter_spec.js new file mode 100644 index 00000000000..39cddb323b8 --- /dev/null +++ b/test/spec/modules/retailspotBidAdapter_spec.js @@ -0,0 +1,426 @@ +import { expect } from 'chai'; + +import { spec } from 'modules/retailspotBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('RetailSpot Adapter', function () { + const canonicalUrl = 'https://canonical.url/?t=%26'; + const referrerUrl = 'http://referrer.url/?param=value'; + const pageUrl = 'http://page.url/?param=value'; + const domain = 'domain:123'; + const env = 'preprod'; + const consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true + }, + refererInfo: {location: referrerUrl, canonicalUrl, domain, topmostLocation: 'fakePageURL'}, + ortb2: {site: {page: pageUrl, ref: referrerUrl}} + }; + + const bidRequestWithSinglePlacement = [ + { + 'bidId': 'bid_id_0', + 'bidder': 'retailspot', + 'placementCode': 'adunit/hb-0', + 'params': { + 'placement': 'placement_0' + }, + 'sizes': '300x250', + 'mediaTypes': { + 'banner': { + 'sizes': ['300x250'] + }, + }, + 'transactionId': 'bid_id_0_transaction_id' + } + ]; + + const bidRequestWithMultipleMediatype = [ + { + 'bidId': 'bid_id_0', + 'bidder': 'retailspot', + 'placementCode': 'adunit/hb-0', + 'params': { + 'placement': 'placement_0' + }, + 'sizes': '300x250', + 'mediaTypes': { + 'banner': { + 'sizes': ['640x480'] + }, + 'video': { + 'playerSize': [640, 480], + 'context': 'outstream' + } + }, + 'transactionId': 'bid_id_0_transaction_id' + } + ]; + + const sentBidVideo = [ + { + 'bidId': 'bid_id_0', + 'placement': 'test-1234', + 'video': { + 'playerSize': [640, 480] + } + } + ]; + + const bidRequestWithDevPlacement = [ + { + 'bidId': 'bid_id_0', + 'bidder': 'retailspot', + 'placementCode': 'adunit/hb-0', + 'params': { + 'placement': 'placement_0', + 'env': 'dev' + }, + 'sizes': '300x250', + 'mediaTypes': + { 'banner': + {'sizes': ['300x250'] + } + }, + 'transactionId': 'bid_id_0_transaction_id' + } + ]; + + const bidRequestMultiPlacements = [ + { + 'bidId': 'bid_id_0', + 'bidder': 'retailspot', + 'placementCode': 'adunit/hb-0', + 'params': { + 'placement': 'placement_0' + }, + 'sizes': '300x250', + 'mediaTypes': + { 'banner': + {'sizes': ['300x250'] + } + }, + 'transactionId': 'bid_id_0_transaction_id' + }, + { + 'bidId': 'bid_id_1', + 'bidder': 'retailspot', + 'placementCode': 'adunit/hb-1', + 'params': { + 'placement': 'placement_1' + }, + 'sizes': [[300, 600]], + 'mediaTypes': + { 'banner': + {'sizes': ['300x600'] + } + }, + 'transactionId': 'bid_id_1_transaction_id' + }, + { + 'bidId': 'bid_id_2', + 'bidder': 'retailspot', + 'placementCode': 'adunit/hb-2', + 'params': {}, + 'sizes': '300x400', + 'transactionId': 'bid_id_2_transaction_id' + }, + { + 'bidId': 'bid_id_3', + 'bidder': 'retailspot', + 'placementCode': 'adunit/hb-3', + 'params': { + 'placement': 'placement_3' + }, + 'transactionId': 'bid_id_3_transaction_id' + } + ]; + + const requestDataOnePlacement = [ + { + 'bidId': 'bid_id_0', + 'placement': 'e622af275681965d3095808561a1e510', + 'width': 300, + 'height': 600 + } + ] + + const requestDataMultiPlacement = [ + { + 'bidId': 'bid_id_0', + 'placement': 'e622af275681965d3095808561a1e510', + 'width': 300, + 'height': 600 + }, + { + 'bidId': 'bid_id_1', + 'placement': 'e622af275681965d3095808561a1e510', + 'width': 400, + 'height': 250 + } + ] + + const testMetaObject = { + 'networkId': 123, + 'advertiserId': '3', + 'advertiserName': 'foobar', + 'advertiserDomains': ['foobar.com'], + 'brandId': '345', + 'brandName': 'Foo', + 'primaryCatId': '34', + 'secondaryCatIds': ['IAB-222', 'IAB-333'], + 'mediaType': 'banner' + }; + const admSample = "\u003cscript id=\"ayl-prebid-a11a121205932e75e622af275681965d\"\u003e\n(function(){\n\twindow.isPrebid = true\n\tvar prebidResults = /*PREBID*/{\"OnEvents\": {\"CLICK\": [{\"Kind\": \"PIXEL_URL\",\"Url\": \"https://testPixelCLICK.com/fake\"}],\"IMPRESSION\": [{\"Kind\": \"PIXEL_URL\",\"Url\": \"https://testPixelIMP.com/fake\"}, {\"Kind\": \"JAVASCRIPT_URL\",\"Url\": \"https://testJsIMP.com/fake.js\"}]},\"Disabled\": false,\"Attempt\": \"a11a121205932e75e622af275681965d\",\"ApiPrefix\": \"https://fo-api.omnitagjs.com/fo-api\",\"TrackingPrefix\": \"https://tracking.omnitagjs.com/tracking\",\"DynamicPrefix\": \"https://tag-dyn.omnitagjs.com/fo-dyn\",\"StaticPrefix\": \"https://fo-static.omnitagjs.com/fo-static\",\"BlobPrefix\": \"https://fo-api.omnitagjs.com/fo-api/blobs\",\"SspPrefix\": \"https://fo-ssp.omnitagjs.com/fo-ssp\",\"VisitorPrefix\": \"https://visitor.omnitagjs.com/visitor\",\"Trusted\": true,\"Placement\": \"e622af275681965d3095808561a1e510\",\"PlacementAccess\": \"ALL\",\"Site\": \"6e2df7a92203c3c7a25561ed63f25a27\",\"Lang\": \"EN\",\"SiteLogo\": null,\"HasSponsorImage\": true,\"ResizeIframe\": true,\"IntegrationConfig\": {\"Kind\": \"WIDGET\",\"Widget\": {\"ExtraStyleSheet\": \"\",\"Placeholders\": {\"Body\": {\"Color\": {\"R\": 77,\"G\": 21,\"B\": 82,\"A\": 100},\"BackgroundColor\": {\"R\": 255,\"G\": 255,\"B\": 255,\"A\": 100},\"FontFamily\": \"Lato\",\"Width\": \"100%\",\"Align\": \"\",\"BoxShadow\": true},\"CallToAction\": {\"Color\": {\"R\": 26,\"G\": 157,\"B\": 212,\"A\": 100}},\"Description\": {\"Length\": 130},\"Image\": {\"Width\": 600,\"Height\": 600,\"Lowres\": false,\"Raw\": false},\"Size\": {\"Height\": \"250px\",\"Width\": \"300px\"},\"Title\": {\"Color\": {\"R\": 219,\"G\": 181,\"B\": 255,\"A\": 100}}},\"Selector\": {\"Kind\": \"CSS\",\"Css\": \"#ayl-prebid-a11a121205932e75e622af275681965d\"},\"Insertion\": \"AFTER\",\"ClickFormat\": true,\"Creative20\": true,\"WidgetKind\": \"CREATIVE_TEMPLATE_4\"}},\"Legal\": \"Sponsored\",\"ForcedCampaign\": \"f1c80d4bb5643c222ae8de75e9b2f991\",\"ForcedTrack\": \"\",\"ForcedCreative\": \"\",\"ForcedSource\": \"\",\"DisplayMode\": \"DEFAULT\",\"Campaign\": \"f1c80d4bb5643c222ae8de75e9b2f991\",\"CampaignAccess\": \"ALL\",\"CampaignKind\": \"AD_TRAFFIC\",\"DataSource\": \"LOCAL\",\"DataSourceUrl\": \"\",\"DataSourceOnEventsIsolated\": false,\"DataSourceWithoutCookie\": false,\"Content\": {\"Preview\": {\"Thumbnail\": {\"Image\": {\"Kind\": \"EXTERNAL\",\"Data\": {\"External\": {\"Url\": \"https://tag-dyn.omnitagjs.com/fo-dyn/native/preview/image?key=fd4362d35bb174d6f1c80d4bb5643c22\\u0026kind=INTERNAL\\u0026ztop=0.000000\\u0026zleft=0.000000\\u0026zwidth=0.333333\\u0026zheight=1.000000\\u0026width=[width]\\u0026height=[height]\"}},\"ZoneTop\": 0,\"ZoneLeft\": 0,\"ZoneWidth\": 1,\"ZoneHeight\": 1,\"Smart\": false,\"NoTransform\": false,\"Quality\": \"NORMAL\"}},\"Text\": {\"CALLTOACTION\": \"Click here to learn more\",\"DESCRIPTION\": \"Considérant l'extrémité conjoncturelle, il serait bon d'anticiper toutes les voies de bon sens.\",\"SPONSOR\": \"Tested by\",\"TITLE\": \"Adserver Traffic Redirect Internal\"},\"Sponsor\": {\"Name\": \"QA Team\",\"Logo\": {\"Resource\": {\"Kind\": \"EXTERNAL\",\"Data\": {\"External\": {\"Url\": \"https://fo-static.omnitagjs.com/fo-static/native/images/info-ayl.svg\"}},\"ZoneTop\": 0,\"ZoneLeft\": 0,\"ZoneWidth\": 1,\"ZoneHeight\": 1,\"Smart\": false,\"NoTransform\": false,\"Quality\": \"NORMAL\"}}},\"Credit\": {\"Logo\": {\"Resource\": {\"Kind\": \"EXTERNAL\",\"Data\": {\"External\": {\"Url\": \"https://fo-static.omnitagjs.com/fo-static/native/images/info-ayl.png\"}},\"ZoneTop\": 0,\"ZoneLeft\": 0,\"ZoneWidth\": 1,\"ZoneHeight\": 1,\"Smart\": false,\"NoTransform\": false,\"Quality\": \"NORMAL\"}},\"Url\": \"https://blobs.omnitagjs.com/adchoice/\"}},\"Landing\": {\"Url\": \"https://www.w3.org/People/mimasa/test/xhtml/entities/entities-11.xhtml#lat1\",\"LegacyTracking\": false},\"ViewButtons\": {\"Close\": {\"Skip\": 6000}},\"InternalContentFields\": {\"AnimatedImage\": false}},\"AdDomain\": \"retailspot.com\",\"Opener\": \"REDIRECT\",\"PerformUITriggers\": [\"CLICK\"],\"RedirectionTarget\": \"TAB\"}/*PREBID*/;\n\tvar insertAds = function insertAds() {\insertAds();\n\t}\n})();\n\u003c/script\u003e"; + const responseWithSinglePlacement = [ + { + 'requestId': 'bid_id_0', + 'placement': 'placement_0', + 'ad': admSample, + 'cpm': 0.5, + 'height': 250, + 'width': 300, + 'meta': testMetaObject, + 'mediaType': 'banner' + } + ]; + + const responseWithSingleVideo = [{ + 'requestId': 'bid_id_0', + 'placement': 'placement_0', + 'vastXml': 'PFZBU1Q+RW1wdHkgc2FtcGxlPC92YXN0Pg==', + 'cpm': 0.5, + 'height': 300, + 'width': 530, + 'mediaType': 'video', + 'creativeId': 'testvideo123', + 'netRevenue': true, + 'currency': 'USD', + 'adId': 'fakeAdID', + 'dealId': 'fakeDealId' + }]; + + const videoResult = [{ + bidderCode: 'retailspot', + cpm: 0.5, + creativeId: 'testvideo123', + currency: 'USD', + height: 300, + netRevenue: true, + requestId: 'bid_id_0', + ttl: 3600, + mediaType: 'video', + meta: { + advertiserDomains: ['retail-spot.io'] + }, + vastXml: 'Empty sample', + width: 530, + adId: 'fakeAdID', + dealId: 'fakeDealId' + }]; + + const responseWithMultiplePlacements = [ + { + 'requestId': 'bid_id_0', + 'mediaType': 'banner', + 'placement': 'placement_0', + 'ad': 'placement_0', + 'cpm': 0.5, + 'height': 0, // test with wrong value + 'width': 300, + }, + { + 'requestId': 'bid_id_1', + 'mediaType': 'banner', + 'placement': 'placement_1', + 'ad': 'placement_1', + 'cpm': 0.6, + 'height': 250 + // 'width' test with missing value + } + ]; + const adapter = newBidder(spec); + + const DEV_URL = 'http://localhost:8090/'; + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidId': 'bid_id_1', + 'bidder': 'retailspot', + 'placementCode': 'adunit/hb-1', + 'params': { + 'placement': 'placement_1' + }, + 'sizes': [[300, 600]], + 'transactionId': 'bid_id_1_transaction_id' + }; + + let bidWSize = { + 'bidId': 'bid_id_1', + 'bidder': 'retailspot', + 'placementCode': 'adunit/hb-1', + 'params': { + 'placement': 'placement_1', + 'size': [250, 300], + }, + 'transactionId': 'bid_id_1_transaction_id' + }; + + it('should return true when required params found', function () { + expect(!!spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true when required params found with size in bid params', function () { + expect(!!spec.isBidRequestValid(bidWSize)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.size; + + expect(!!spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placement': 0 + }; + expect(!!spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('should add gdpr/usp consent information to the request', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let uspConsentData = '1YCC'; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true + }, + 'uspConsent': uspConsentData + }; + + bidderRequest.Bids = bidRequestWithSinglePlacement; + + const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdprConsent).to.exist; + expect(payload.gdprConsent.consentString).to.exist.and.to.equal(consentString); + expect(payload.uspConsent).to.exist.and.to.equal(uspConsentData); + }); + + it('sends bid request to endpoint with single placement', function () { + bidderRequest.Bids = bidRequestWithSinglePlacement; + + const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); + const payload = JSON.parse(request.data); + + expect(request.url).to.contain('https://ssp.retail-spot.io/prebid'); + expect(request.method).to.equal('POST'); + + expect(payload).to.deep.equal(bidderRequest); + }); + + it('sends bid request to endpoint with single placement multiple mediatype', function () { + bidderRequest.Bids = bidRequestWithMultipleMediatype; + const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); + const payload = JSON.parse(request.data); + + expect(request.url).to.contain('https://ssp.retail-spot.io/prebid'); + expect(request.method).to.equal('POST'); + + expect(payload).to.deep.equal(bidderRequest); + }); + + it('sends bid request to endpoint with multiple placements', function () { + bidderRequest.Bids = bidRequestMultiPlacements; + const request = spec.buildRequests(bidRequestMultiPlacements, bidderRequest); + const payload = JSON.parse(request.data); + + expect(request.url).to.contain('https://ssp.retail-spot.io/prebid'); + expect(request.method).to.equal('POST'); + + expect(payload).to.deep.equal(bidderRequest); + }); + + it('sends bid request to endpoint setted by parameters', function () { + const request = spec.buildRequests(bidRequestWithDevPlacement, bidderRequest); + expect(request.url).to.contain(DEV_URL); + }); + }); + // + describe('interpretResponse', function () { + let serverResponse; + + beforeEach(function () { + serverResponse = { + body: {} + } + }); + + it('handles nobid responses', function () { + let response = [{ + requestId: '123dfsdf', + placement: '12df1' + }]; + serverResponse.body = response; + let result = spec.interpretResponse(serverResponse, []); + expect(result).deep.equal([]); + }); + + it('receive reponse with single placement', function () { + serverResponse.body = responseWithSinglePlacement; + let result = spec.interpretResponse(serverResponse, {data: '{"bids":' + JSON.stringify(requestDataOnePlacement) + '}'}); + + expect(result.length).to.equal(1); + expect(result[0].cpm).to.equal(0.5); + expect(result[0].ad).to.equal(admSample); + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].meta).to.deep.equal(testMetaObject); + }); + + it('receive reponse with multiple placement', function () { + serverResponse.body = responseWithMultiplePlacements; + let result = spec.interpretResponse(serverResponse, {data: '{"bids":' + JSON.stringify(requestDataMultiPlacement) + '}'}); + + expect(result.length).to.equal(2); + + expect(result[0].cpm).to.equal(0.5); + expect(result[0].ad).to.equal('placement_0'); + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(600); + + expect(result[1].cpm).to.equal(0.6); + expect(result[1].ad).to.equal('placement_1'); + expect(result[1].width).to.equal(400); + expect(result[1].height).to.equal(250); + }); + + it('receive Vast reponse with Video ad', function () { + serverResponse.body = responseWithSingleVideo; + let result = spec.interpretResponse(serverResponse, {data: '{"bids":' + JSON.stringify(sentBidVideo) + '}'}); + + expect(result.length).to.equal(1); + expect(result).to.deep.equal(videoResult); + }); + }); +});