From b60a7529f8e91f5d74b053a812e2b3dc45cfc34a Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Wed, 20 Jun 2018 13:21:19 -0400 Subject: [PATCH 01/33] update some unit tests to clean-up consentManagement hooks (#2711) * update some unit tests to clean-up gdpr module hooks * add cleanup for pubCommonId requestBids hook --- .../modules/prebidServerBidAdapter_spec.js | 138 +++++++++--------- test/spec/modules/pubCommonId_spec.js | 3 + .../modules/smartadserverBidAdapter_spec.js | 86 ++++++----- 3 files changed, 121 insertions(+), 106 deletions(-) diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index b155d61d8e5..8bc3d577ede 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -402,97 +402,101 @@ describe('S2S Adapter', () => { expect(requestBid.ad_units[0].bids[0].params.member).to.exist.and.to.be.a('string'); }); - it('adds gdpr consent information to ortb2 request depending on presence of module', () => { - let ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + describe('gdpr tests', () => { + afterEach(() => { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); + }); - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: ortb2Config }; - config.setConfig(consentConfig); + it('adds gdpr consent information to ortb2 request depending on presence of module', () => { + let ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = { - consentString: 'abc123', - gdprApplies: true - }; + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: ortb2Config }; + config.setConfig(consentConfig); - adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(requests[0].requestBody); + let gdprBidRequest = utils.deepClone(BID_REQUESTS); + gdprBidRequest[0].gdprConsent = { + consentString: 'abc123', + gdprApplies: true + }; - expect(requestBid.regs.ext.gdpr).is.equal(1); - expect(requestBid.user.ext.consent).is.equal('abc123'); + adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); - config.resetConfig(); - config.setConfig({s2sConfig: CONFIG}); + expect(requestBid.regs.ext.gdpr).is.equal(1); + expect(requestBid.user.ext.consent).is.equal('abc123'); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); - requestBid = JSON.parse(requests[1].requestBody); + config.resetConfig(); + config.setConfig({s2sConfig: CONFIG}); - expect(requestBid.regs).to.not.exist; - expect(requestBid.user).to.not.exist; + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + requestBid = JSON.parse(requests[1].requestBody); - config.resetConfig(); - $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); - }); + expect(requestBid.regs).to.not.exist; + expect(requestBid.user).to.not.exist; + }); - it('check gdpr info gets added into cookie_sync request: have consent data', () => { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + it('check gdpr info gets added into cookie_sync request: have consent data', () => { + let cookieSyncConfig = utils.deepClone(CONFIG); + cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; + config.setConfig(consentConfig); - let gdprBidRequest = utils.deepClone(BID_REQUESTS); + let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = { - consentString: 'abc123def', - gdprApplies: true - }; + gdprBidRequest[0].gdprConsent = { + consentString: 'abc123def', + gdprApplies: true + }; - adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(requests[0].requestBody); + adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); - expect(requestBid.gdpr).is.equal(1); - expect(requestBid.gdpr_consent).is.equal('abc123def'); - }); + expect(requestBid.gdpr).is.equal(1); + expect(requestBid.gdpr_consent).is.equal('abc123def'); + }); - it('check gdpr info gets added into cookie_sync request: have consent data but gdprApplies is false', () => { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + it('check gdpr info gets added into cookie_sync request: have consent data but gdprApplies is false', () => { + let cookieSyncConfig = utils.deepClone(CONFIG); + cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; + config.setConfig(consentConfig); - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = { - consentString: 'xyz789abcc', - gdprApplies: false - }; + let gdprBidRequest = utils.deepClone(BID_REQUESTS); + gdprBidRequest[0].gdprConsent = { + consentString: 'xyz789abcc', + gdprApplies: false + }; - adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(requests[0].requestBody); + adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); - expect(requestBid.gdpr).is.equal(0); - expect(requestBid.gdpr_consent).is.undefined; - }); + expect(requestBid.gdpr).is.equal(0); + expect(requestBid.gdpr_consent).is.undefined; + }); - it('checks gdpr info gets added to cookie_sync request: consent data unknown', () => { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + it('checks gdpr info gets added to cookie_sync request: consent data unknown', () => { + let cookieSyncConfig = utils.deepClone(CONFIG); + cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; + config.setConfig(consentConfig); - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = { - consentString: undefined, - gdprApplies: undefined - }; + let gdprBidRequest = utils.deepClone(BID_REQUESTS); + gdprBidRequest[0].gdprConsent = { + consentString: undefined, + gdprApplies: undefined + }; - adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(requests[0].requestBody); + adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(requests[0].requestBody); - expect(requestBid.gdpr).is.undefined; - expect(requestBid.gdpr_consent).is.undefined; + expect(requestBid.gdpr).is.undefined; + expect(requestBid.gdpr_consent).is.undefined; + }); }); it('sets invalid cacheMarkup value to 0', () => { diff --git a/test/spec/modules/pubCommonId_spec.js b/test/spec/modules/pubCommonId_spec.js index 50ca4616a4b..bdb9d4f0545 100644 --- a/test/spec/modules/pubCommonId_spec.js +++ b/test/spec/modules/pubCommonId_spec.js @@ -18,6 +18,9 @@ const COOKIE_NAME = '_pubcid'; const TIMEOUT = 2000; describe('Publisher Common ID', function () { + afterEach(() => { + $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidHook); + }); describe('Decorate adUnits', function () { before(function() { window.document.cookie = COOKIE_NAME + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'; diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index 57c070e9748..e6364de94a9 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -11,6 +11,7 @@ import { config } from 'src/config'; import * as utils from 'src/utils'; +import { requestBidsHook } from 'modules/consentManagement'; // Default params with optional ones describe('Smart bid adapter tests', () => { @@ -99,49 +100,56 @@ describe('Smart bid adapter tests', () => { expect(requestContent).to.have.property('ckid').and.to.equal(42); }); - it('Verify build request with GDPR', () => { - config.setConfig({ - 'currency': { - 'adServerCurrency': 'EUR' - }, - consentManagement: { - cmp: 'iab', - consentRequired: true, - timeout: 1000, - allowAuctionWithoutConsent: true - } + describe('gdpr tests', () => { + afterEach(() => { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); }); - const request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, { - gdprConsent: { - consentString: 'BOKAVy4OKAVy4ABAB8AAAAAZ+A==', - gdprApplies: true - } - }); - const requestContent = JSON.parse(request[0].data); - expect(requestContent).to.have.property('gdpr').and.to.equal(true); - expect(requestContent).to.have.property('gdpr_consent').and.to.equal('BOKAVy4OKAVy4ABAB8AAAAAZ+A=='); - }); - it('Verify build request with GDPR without gdprApplies', () => { - config.setConfig({ - 'currency': { - 'adServerCurrency': 'EUR' - }, - consentManagement: { - cmp: 'iab', - consentRequired: true, - timeout: 1000, - allowAuctionWithoutConsent: true - } + it('Verify build request with GDPR', () => { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + }, + consentManagement: { + cmp: 'iab', + consentRequired: true, + timeout: 1000, + allowAuctionWithoutConsent: true + } + }); + const request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, { + gdprConsent: { + consentString: 'BOKAVy4OKAVy4ABAB8AAAAAZ+A==', + gdprApplies: true + } + }); + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('gdpr').and.to.equal(true); + expect(requestContent).to.have.property('gdpr_consent').and.to.equal('BOKAVy4OKAVy4ABAB8AAAAAZ+A=='); }); - const request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, { - gdprConsent: { - consentString: 'BOKAVy4OKAVy4ABAB8AAAAAZ+A==' - } + + it('Verify build request with GDPR without gdprApplies', () => { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + }, + consentManagement: { + cmp: 'iab', + consentRequired: true, + timeout: 1000, + allowAuctionWithoutConsent: true + } + }); + const request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, { + gdprConsent: { + consentString: 'BOKAVy4OKAVy4ABAB8AAAAAZ+A==' + } + }); + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.not.have.property('gdpr'); + expect(requestContent).to.have.property('gdpr_consent').and.to.equal('BOKAVy4OKAVy4ABAB8AAAAAZ+A=='); }); - const requestContent = JSON.parse(request[0].data); - expect(requestContent).to.not.have.property('gdpr'); - expect(requestContent).to.have.property('gdpr_consent').and.to.equal('BOKAVy4OKAVy4ABAB8AAAAAZ+A=='); }); it('Verify parse response', () => { From b90f12d79948c1720d0444c93e1c1828eb18ef80 Mon Sep 17 00:00:00 2001 From: Jeremy Hernandez Date: Wed, 20 Jun 2018 21:36:45 +0200 Subject: [PATCH 02/33] fix(AdyoulikeAdapter): set withCredentials option to true (#2661) --- modules/adyoulikeBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index a61719fe495..b9f57115e21 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -55,7 +55,7 @@ export const spec = { const data = JSON.stringify(payload); const options = { - withCredentials: false + withCredentials: true }; return { From 44fb86607209b6ba21ad96f4aa249893f1830aca Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Wed, 20 Jun 2018 16:17:32 -0400 Subject: [PATCH 03/33] update consentManagement error logic/handling (#2723) --- modules/consentManagement.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index af668523fa4..fcaeab81544 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -219,11 +219,17 @@ export function requestBidsHook(reqBidsConfigObj, fn) { * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) */ function processCmpData(consentObject, hookConfig) { + let gdprApplies = consentObject && consentObject.getConsentData && consentObject.getConsentData.gdprApplies; if ( - !utils.isPlainObject(consentObject) || - (!utils.isPlainObject(consentObject.getVendorConsents) || Object.keys(consentObject.getVendorConsents).length === 0) || - (!utils.isPlainObject(consentObject.getConsentData) || Object.keys(consentObject.getConsentData).length === 0)) { - cmpFailed(`CMP returned unexpected value during lookup process; returned value was (${consentObject}).`, hookConfig); + (typeof gdprApplies !== 'boolean') || + (gdprApplies === true && + !(utils.isStr(consentObject.getConsentData.consentData) && + utils.isPlainObject(consentObject.getVendorConsents) && + Object.keys(consentObject.getVendorConsents).length > 1 + ) + ) + ) { + cmpFailed(`CMP returned unexpected value during lookup process.`, hookConfig, consentObject); } else { clearTimeout(hookConfig.timer); storeConsentData(consentObject); @@ -243,15 +249,16 @@ function cmpTimedOut(hookConfig) { * This function contains the controlled steps to perform when there's a problem with CMP. * @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened. * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging */ -function cmpFailed(errMsg, hookConfig) { +function cmpFailed(errMsg, hookConfig, extraArgs) { clearTimeout(hookConfig.timer); // still set the consentData to undefined when there is a problem as per config options if (allowAuction) { storeConsentData(undefined); } - exitModule(errMsg, hookConfig); + exitModule(errMsg, hookConfig, extraArgs); } /** @@ -282,8 +289,9 @@ function storeConsentData(cmpConsentObject) { * 3. bad exit with auction canceled (error message is logged). * @param {string} errMsg optional; only to be used when there was a 'bad' exit. String is a descriptive message for the failure/issue encountered. * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging */ -function exitModule(errMsg, hookConfig) { +function exitModule(errMsg, hookConfig, extraArgs) { if (hookConfig.haveExited === false) { hookConfig.haveExited = true; @@ -293,10 +301,10 @@ function exitModule(errMsg, hookConfig) { if (errMsg) { if (allowAuction) { - utils.logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.'); + utils.logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.', extraArgs); nextFn.apply(context, args); } else { - utils.logError(errMsg + ' Canceling auction as per consentManagement config.'); + utils.logError(errMsg + ' Canceling auction as per consentManagement config.', extraArgs); if (typeof hookConfig.bidsBackHandler === 'function') { hookConfig.bidsBackHandler(); } else { From f60e239366c629039c0340b7e723f4fa2fde6402 Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Wed, 20 Jun 2018 16:20:37 -0400 Subject: [PATCH 04/33] enhance logWarn message (#2724) --- src/utils.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/utils.js b/src/utils.js index 5135a1df21d..09a52660796 100644 --- a/src/utils.js +++ b/src/utils.js @@ -14,8 +14,10 @@ var t_Numb = 'Number'; var t_Object = 'Object'; var toString = Object.prototype.toString; let infoLogger = null; +let warnLogger = null; try { infoLogger = console.info.bind(window.console); + warnLogger = console.warn.bind(window.console); } catch (e) { } @@ -257,9 +259,15 @@ exports.getTopWindowReferrer = function() { } }; -exports.logWarn = function (msg) { +exports.logWarn = function (msg, args) { if (debugTurnedOn() && console.warn) { - console.warn('WARNING: ' + msg); + if (warnLogger) { + if (!args || args.length === 0) { + args = ''; + } + + warnLogger('WARNING: ' + msg + ((args === '') ? '' : ' : params : '), args); + } } }; From 960ffddf6df7056035bc7ba77447b6448bbae20c Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Wed, 20 Jun 2018 14:29:49 -0600 Subject: [PATCH 05/33] Debugging (#2687) * new debugging functionality with bid overrides * update name from bidderOverrides to debugging * change sessionStorage to window.sessionStorage * solve sinon stubbing sessionStorage issue with dependency injection --- src/debugging.js | 89 ++++++++++++++++++++++ src/hook.js | 3 + src/prebid.js | 4 + test/spec/debugging_spec.js | 142 ++++++++++++++++++++++++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 src/debugging.js create mode 100644 test/spec/debugging_spec.js diff --git a/src/debugging.js b/src/debugging.js new file mode 100644 index 00000000000..fe5c269a18d --- /dev/null +++ b/src/debugging.js @@ -0,0 +1,89 @@ + +import { config } from 'src/config'; +import { logMessage as utilsLogMessage, logWarn as utilsLogWarn } from 'src/utils'; +import { addBidResponse } from 'src/auction'; + +const OVERRIDE_KEY = '$$PREBID_GLOBAL$$:debugging'; + +export let boundHook; + +function logMessage(msg) { + utilsLogMessage('DEBUG: ' + msg); +} + +function logWarn(msg) { + utilsLogWarn('DEBUG: ' + msg); +} + +function enableOverrides(overrides, fromSession = false) { + config.setConfig({'debug': true}); + logMessage(`bidder overrides enabled${fromSession ? ' from session' : ''}`); + + if (boundHook) { + addBidResponse.removeHook(boundHook); + } + + boundHook = addBidResponseHook.bind(null, overrides); + addBidResponse.addHook(boundHook, 5); +} + +export function disableOverrides() { + if (boundHook) { + addBidResponse.removeHook(boundHook); + logMessage('bidder overrides disabled'); + } +} + +export function addBidResponseHook(overrides, adUnitCode, bid, next) { + if (Array.isArray(overrides.bidders) && overrides.bidders.indexOf(bid.bidderCode) === -1) { + logWarn(`bidder '${bid.bidderCode}' excluded from auction by bidder overrides`); + return; + } + + if (Array.isArray(overrides.bids)) { + overrides.bids.forEach(overrideBid => { + if (overrideBid.bidder && overrideBid.bidder !== bid.bidderCode) { + return; + } + if (overrideBid.adUnitCode && overrideBid.adUnitCode !== adUnitCode) { + return; + } + + bid = Object.assign({}, bid); + + Object.keys(overrideBid).filter(key => ['bidder', 'adUnitCode'].indexOf(key) === -1).forEach((key) => { + let value = overrideBid[key]; + logMessage(`bidder overrides changed '${adUnitCode}/${bid.bidderCode}' bid.${key} from '${bid[key]}' to '${value}'`); + bid[key] = value; + }); + }); + } + + next(adUnitCode, bid); +} + +export function getConfig(debugging) { + if (!debugging.enabled) { + disableOverrides(); + try { + window.sessionStorage.removeItem(OVERRIDE_KEY); + } catch (e) {} + } else { + try { + window.sessionStorage.setItem(OVERRIDE_KEY, JSON.stringify(debugging)); + } catch (e) {} + enableOverrides(debugging); + } +} +config.getConfig('debugging', ({debugging}) => getConfig(debugging)); + +export function sessionLoader(storage = window.sessionStorage) { + let overrides; + try { + overrides = JSON.parse(storage.getItem(OVERRIDE_KEY)); + } catch (e) { + } + if (overrides) { + enableOverrides(overrides, true); + } +} diff --git a/src/hook.js b/src/hook.js index 6c6cefdc56c..fef62a37c3d 100644 --- a/src/hook.js +++ b/src/hook.js @@ -60,6 +60,9 @@ export function createHook(type, fn, hookName) { }, removeHook: function(removeFn) { _hooks = _hooks.filter(hook => hook.fn === fn || hook.fn !== removeFn); + }, + hasHook: function(fn) { + return _hooks.some(hook => hook.fn === fn); } }; diff --git a/src/prebid.js b/src/prebid.js index e1691c20f79..56c9d1d0c7c 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -9,6 +9,7 @@ import { config } from './config'; import { auctionManager } from './auctionManager'; import { targeting, getHighestCpmBidsFromBidPool, RENDERED, BID_TARGETING_SET } from './targeting'; import { createHook } from 'src/hook'; +import { sessionLoader } from 'src/debugging'; import includes from 'core-js/library/fn/array/includes'; const $$PREBID_GLOBAL$$ = getGlobal(); @@ -27,6 +28,9 @@ const eventValidators = { bidWon: checkDefinedPlacement }; +// initialize existing debugging sessions if present +sessionLoader(); + /* Public vars */ $$PREBID_GLOBAL$$.bidderSettings = $$PREBID_GLOBAL$$.bidderSettings || {}; diff --git a/test/spec/debugging_spec.js b/test/spec/debugging_spec.js new file mode 100644 index 00000000000..286df26f7ba --- /dev/null +++ b/test/spec/debugging_spec.js @@ -0,0 +1,142 @@ + +import { expect } from 'chai'; +import { sessionLoader, addBidResponseHook, getConfig, disableOverrides, boundHook } from 'src/debugging'; +import { addBidResponse } from 'src/auction'; +import { config } from 'src/config'; + +describe('bid overrides', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + window.sessionStorage.clear(); + sandbox.restore(); + }); + + describe('initialization', () => { + beforeEach(() => { + sandbox.stub(config, 'setConfig'); + }); + + afterEach(() => { + disableOverrides(); + }); + + it('should happen when enabled with setConfig', () => { + getConfig({ + enabled: true + }); + + expect(addBidResponse.hasHook(boundHook)).to.equal(true); + }); + + it('should happen when configuration found in sessionStorage', () => { + sessionLoader({ + getItem: () => ('{"enabled": true}') + }); + expect(addBidResponse.hasHook(boundHook)).to.equal(true); + }); + + it('should not throw if sessionStorage is inaccessible', () => { + expect(() => { + sessionLoader({ + getItem() { + throw new Error('test'); + } + }); + }).not.to.throw(); + }); + }); + + describe('hook', () => { + let mockBids; + let bids; + + beforeEach(() => { + let baseBid = { + 'bidderCode': 'rubicon', + 'width': 970, + 'height': 250, + 'statusMessage': 'Bid available', + 'mediaType': 'banner', + 'source': 'client', + 'currency': 'USD', + 'cpm': 0.5, + 'ttl': 300, + 'netRevenue': false, + 'adUnitCode': '/19968336/header-bid-tag-0' + }; + mockBids = []; + mockBids.push(baseBid); + mockBids.push(Object.assign({}, baseBid, { + bidderCode: 'appnexus' + })); + + bids = []; + }); + + function run(overrides) { + mockBids.forEach(bid => { + addBidResponseHook(overrides, bid.adUnitCode, bid, (adUnitCode, bid) => { + bids.push(bid); + }) + }); + } + + it('should allow us to exclude bidders', () => { + run({ + enabled: true, + bidders: ['appnexus'] + }); + + expect(bids.length).to.equal(1); + expect(bids[0].bidderCode).to.equal('appnexus'); + }); + + it('should allow us to override all bids', () => { + run({ + enabled: true, + bids: [{ + cpm: 2 + }] + }); + + expect(bids.length).to.equal(2); + expect(bids[0].cpm).to.equal(2); + expect(bids[1].cpm).to.equal(2); + }); + + it('should allow us to override bids by bidder', () => { + run({ + enabled: true, + bids: [{ + bidder: 'rubicon', + cpm: 2 + }] + }); + + expect(bids.length).to.equal(2); + expect(bids[0].cpm).to.equal(2); + expect(bids[1].cpm).to.equal(0.5); + }); + + it('should allow us to override bids by adUnitCode', () => { + mockBids[1].adUnitCode = 'test'; + + run({ + enabled: true, + bids: [{ + adUnitCode: 'test', + cpm: 2 + }] + }); + + expect(bids.length).to.equal(2); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[1].cpm).to.equal(2); + }); + }); +}); From 8873b21569ea562177f4ba214908f2c423e5ac28 Mon Sep 17 00:00:00 2001 From: AdmixerTech <35560933+AdmixerTech@users.noreply.github.com> Date: Wed, 20 Jun 2018 23:58:39 +0300 Subject: [PATCH 06/33] add encodeURIComponent (#2660) --- modules/admixerBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index 6851a7d3bd5..679e11270ab 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -25,7 +25,7 @@ export const spec = { buildRequests: function (bidderRequest) { const payload = { imps: [], - referrer: utils.getTopWindowUrl(), + referrer: encodeURIComponent(utils.getTopWindowUrl()), }; bidderRequest.forEach((bid) => { if (bid.bidder === BIDDER_CODE) { From 4daab3d229e4268a062f4e3919d5f639642c1933 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Wed, 20 Jun 2018 17:06:29 -0400 Subject: [PATCH 07/33] Function name was not logical (#2751) --- src/targeting.js | 4 ++-- test/spec/unit/core/targeting_spec.js | 10 +++++----- test/spec/unit/pbjs_api_spec.js | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/targeting.js b/src/targeting.js index e33f1a88f10..d645a8ed20d 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -16,7 +16,7 @@ const MAX_DFP_KEYLENGTH = 20; const TTL_BUFFER = 1000; // return unexpired bids -export const isBidExpired = (bid) => (bid.responseTimestamp + bid.ttl * 1000 + TTL_BUFFER) > timestamp(); +export const isBidNotExpired = (bid) => (bid.responseTimestamp + bid.ttl * 1000 + TTL_BUFFER) > timestamp(); // return bids whose status is not set. Winning bid can have status `targetingSet` or `rendered`. const isUnusedBid = (bid) => bid && ((bid.status && !includes([BID_TARGETING_SET, RENDERED], bid.status)) || !bid.status); @@ -195,7 +195,7 @@ export function newTargeting(auctionManager) { function getBidsReceived() { const bidsReceived = auctionManager.getBidsReceived() .filter(isUnusedBid) - .filter(exports.isBidExpired) + .filter(exports.isBidNotExpired) ; return getHighestCpmBidsFromBidPool(bidsReceived, getOldestHighestCpmBid); diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index cf08c65d1fb..0ba5e23159f 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -111,13 +111,13 @@ describe('targeting tests', () => { amGetAdUnitsStub = sinon.stub(auctionManager, 'getAdUnitCodes').callsFake(function() { return ['/123456/header-bid-tag-0']; }); - bidExpiryStub = sinon.stub(targetingModule, 'isBidExpired').returns(true); + bidExpiryStub = sinon.stub(targetingModule, 'isBidNotExpired').returns(true); }); afterEach(() => { auctionManager.getBidsReceived.restore(); auctionManager.getAdUnitCodes.restore(); - targetingModule.isBidExpired.restore(); + targetingModule.isBidNotExpired.restore(); }); it('selects the top bid when _sendAllBids true', () => { @@ -149,13 +149,13 @@ describe('targeting tests', () => { amGetAdUnitsStub = sinon.stub(auctionManager, 'getAdUnitCodes').callsFake(function() { return ['/123456/header-bid-tag-0']; }); - bidExpiryStub = sinon.stub(targetingModule, 'isBidExpired').returns(true); + bidExpiryStub = sinon.stub(targetingModule, 'isBidNotExpired').returns(true); }); afterEach(() => { auctionManager.getBidsReceived.restore(); auctionManager.getAdUnitCodes.restore(); - targetingModule.isBidExpired.restore(); + targetingModule.isBidNotExpired.restore(); }); it('returns targetingSet correctly', () => { @@ -171,7 +171,7 @@ describe('targeting tests', () => { let bidExpiryStub; let auctionManagerStub; beforeEach(() => { - bidExpiryStub = sinon.stub(targetingModule, 'isBidExpired').returns(true); + bidExpiryStub = sinon.stub(targetingModule, 'isBidNotExpired').returns(true); auctionManagerStub = sinon.stub(auctionManager, 'getBidsReceived'); }); diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 93904dcfaf8..ab5da55f420 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -150,12 +150,12 @@ window.apntag = { describe('Unit: Prebid Module', function () { let bidExpiryStub; before(() => { - bidExpiryStub = sinon.stub(targetingModule, 'isBidExpired').callsFake(() => true); + bidExpiryStub = sinon.stub(targetingModule, 'isBidNotExpired').callsFake(() => true); }); after(function() { $$PREBID_GLOBAL$$.adUnits = []; - targetingModule.isBidExpired.restore(); + targetingModule.isBidNotExpired.restore(); }); describe('getAdserverTargetingForAdUnitCodeStr', function () { From 050494e0cb3a7310c690651e2348e9f747fdcbbf Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Wed, 20 Jun 2018 15:06:41 -0600 Subject: [PATCH 08/33] Max origin concurrent auctions (#2743) * initial attempt at limiting concurrenet auctions by origin * fix queueing of auctions for max origin * don't decrement on timeout as it is already called by onreadystatechange * move auction timer so it doesn't start until queued auction starts * set default max concurrent origin requests to 4 and make configurable * fix tests to not queue for auction.callBids * change MAX_REQUEST_PER_ORIGIN to local var --- src/adaptermanager.js | 12 +++- src/ajax.js | 84 ++++++++++---------------- src/auction.js | 104 ++++++++++++++++++++++++++++---- test/spec/unit/pbjs_api_spec.js | 1 + 4 files changed, 136 insertions(+), 65 deletions(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index f6c0d5c421e..3d5d56d6ee2 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -273,7 +273,7 @@ exports.checkBidRequestSizes = (adUnits) => { return adUnits; } -exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { +exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb, requestCallbacks) => { if (!bidRequests.length) { utils.logWarn('callBids executed with no bidRequests. Were they filtered by labels or sizing?'); return; @@ -285,7 +285,10 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { }, [[], []]); if (serverBidRequests.length) { - const s2sAjax = ajaxBuilder(serverBidRequests[0].timeout); + const s2sAjax = ajaxBuilder(serverBidRequests[0].timeout, requestCallbacks ? { + request: requestCallbacks.request.bind(null, 's2s'), + done: requestCallbacks.done + } : undefined); let adaptersServerSide = _s2sConfig.bidders; const s2sAdapter = _bidderRegistry[_s2sConfig.adapter]; let tid = serverBidRequests[0].tid; @@ -336,7 +339,6 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { } } - const ajax = (clientBidRequests.length) ? ajaxBuilder(clientBidRequests[0].timeout) : null; // handle client adapter requests clientBidRequests.forEach(bidRequest => { bidRequest.start = timestamp(); @@ -347,6 +349,10 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); bidRequest.doneCbCallCount = 0; let done = doneCb(bidRequest.bidderRequestId); + let ajax = ajaxBuilder(clientBidRequests[0].timeout, requestCallbacks ? { + request: requestCallbacks.request.bind(null, bidRequest.bidderCode), + done: requestCallbacks.done + } : undefined); adapter.callBids(bidRequest, addBidResponse, done, ajax); } else { utils.logError(`Adapter trying to be called which does not exist: ${bidRequest.bidderCode} adaptermanager.callBids`); diff --git a/src/ajax.js b/src/ajax.js index ded2f95f8a5..e17f782ac30 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -15,12 +15,13 @@ const XHR_DONE = 4; */ export const ajax = ajaxBuilder(); -export function ajaxBuilder(timeout = 3000) { +export function ajaxBuilder(timeout = 3000, {request, done} = {}) { return function(url, callback, data, options = {}) { try { let x; - let useXDomainRequest = false; let method = options.method || (data ? 'POST' : 'GET'); + let parser = document.createElement('a'); + parser.href = url; let callbacks = typeof callback === 'object' && callback !== null ? callback : { success: function() { @@ -35,46 +36,24 @@ export function ajaxBuilder(timeout = 3000) { callbacks.success = callback; } - if (!window.XMLHttpRequest) { - useXDomainRequest = true; - } else { - x = new window.XMLHttpRequest(); - if (x.responseType === undefined) { - useXDomainRequest = true; - } - } - - if (useXDomainRequest) { - x = new window.XDomainRequest(); - x.onload = function () { - callbacks.success(x.responseText, x); - }; + x = new window.XMLHttpRequest(); - // http://stackoverflow.com/questions/15786966/xdomainrequest-aborts-post-on-ie-9 - x.onerror = function () { - callbacks.error('error', x); - }; - x.ontimeout = function () { - callbacks.error('timeout', x); - }; - x.onprogress = function() { - utils.logMessage('xhr onprogress'); - }; - } else { - x.onreadystatechange = function () { - if (x.readyState === XHR_DONE) { - let status = x.status; - if ((status >= 200 && status < 300) || status === 304) { - callbacks.success(x.responseText, x); - } else { - callbacks.error(x.statusText, x); - } + x.onreadystatechange = function () { + if (x.readyState === XHR_DONE) { + if (typeof done === 'function') { + done(parser.origin); } - }; - x.ontimeout = function () { - utils.logError(' xhr timeout after ', x.timeout, 'ms'); - }; - } + let status = x.status; + if ((status >= 200 && status < 300) || status === 304) { + callbacks.success(x.responseText, x); + } else { + callbacks.error(x.statusText, x); + } + } + }; + x.ontimeout = function () { + utils.logError(' xhr timeout after ', x.timeout, 'ms'); + }; if (method === 'GET' && data) { let urlInfo = parseURL(url, options); @@ -86,18 +65,21 @@ export function ajaxBuilder(timeout = 3000) { // IE needs timoeut to be set after open - see #1410 x.timeout = timeout; - if (!useXDomainRequest) { - if (options.withCredentials) { - x.withCredentials = true; - } - utils._each(options.customHeaders, (value, header) => { - x.setRequestHeader(header, value); - }); - if (options.preflight) { - x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - } - x.setRequestHeader('Content-Type', options.contentType || 'text/plain'); + if (options.withCredentials) { + x.withCredentials = true; } + utils._each(options.customHeaders, (value, header) => { + x.setRequestHeader(header, value); + }); + if (options.preflight) { + x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + } + x.setRequestHeader('Content-Type', options.contentType || 'text/plain'); + + if (typeof request === 'function') { + request(parser.origin); + } + if (method === 'POST' && data) { x.send(data); } else { diff --git a/src/auction.js b/src/auction.js index 8992f16218e..5722bcc6990 100644 --- a/src/auction.js +++ b/src/auction.js @@ -74,6 +74,11 @@ events.on(CONSTANTS.EVENTS.BID_ADJUSTMENT, function (bid) { adjustBids(bid); }); +const MAX_REQUESTS_PER_ORIGIN = 4; +const outstandingRequests = {}; +const sourceInfo = {}; +const queuedCalls = []; + /** * Creates new auction instance * @@ -176,26 +181,103 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels}) } function callBids() { - startAuctionTimer(); _auctionStatus = AUCTION_STARTED; _auctionStart = Date.now(); - const auctionInit = { - timestamp: _auctionStart, - auctionId: _auctionId, - timeout: _timeout - }; - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, auctionInit); - let bidRequests = adaptermanager.makeBidRequests(_adUnits, _auctionStart, _auctionId, _timeout, _labels); utils.logInfo(`Bids Requested for Auction with id: ${_auctionId}`, bidRequests); bidRequests.forEach(bidRequest => { addBidRequests(bidRequest); }); - _auctionStatus = AUCTION_IN_PROGRESS; - adaptermanager.callBids(_adUnits, bidRequests, addBidResponse.bind(this), done.bind(this)); - }; + let requests = {}; + + let call = { + bidRequests, + run: () => { + startAuctionTimer(); + + _auctionStatus = AUCTION_IN_PROGRESS; + + const auctionInit = { + timestamp: _auctionStart, + auctionId: _auctionId, + timeout: _timeout + }; + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, auctionInit); + + adaptermanager.callBids(_adUnits, bidRequests, addBidResponse.bind(this), done.bind(this), { + request(source, origin) { + increment(outstandingRequests, origin); + increment(requests, source); + + if (!sourceInfo[source]) { + sourceInfo[source] = { + SRA: true, + origin + }; + } + if (requests[source] > 1) { + sourceInfo[source].SRA = false; + } + }, + done(origin) { + outstandingRequests[origin]--; + if (queuedCalls[0]) { + if (runIfOriginHasCapacity(queuedCalls[0])) { + queuedCalls.shift(); + } + } + } + }); + } + }; + + if (!runIfOriginHasCapacity(call)) { + utils.logWarn('queueing auction due to limited endpoint capacity'); + queuedCalls.push(call); + } + + function runIfOriginHasCapacity(call) { + let hasCapacity = true; + + let maxRequests = config.getConfig('maxRequestsPerOrigin') || MAX_REQUESTS_PER_ORIGIN; + + call.bidRequests.some(bidRequest => { + let requests = 1; + let source = (typeof bidRequest.src !== 'undefined' && bidRequest.src === CONSTANTS.S2S.SRC) ? 's2s' + : bidRequest.bidderCode; + // if we have no previous info on this source just let them through + if (sourceInfo[source]) { + if (sourceInfo[source].SRA === false) { + // some bidders might use more than the MAX_REQUESTS_PER_ORIGIN in a single auction. In those cases + // set their request count to MAX_REQUESTS_PER_ORIGIN so the auction isn't permanently queued waiting + // for capacity for that bidder + requests = Math.min(bidRequest.bids.length, maxRequests); + } + if (outstandingRequests[sourceInfo[source].origin] + requests > maxRequests) { + hasCapacity = false; + } + } + // return only used for terminating this .some() iteration early if it is determined we don't have capacity + return !hasCapacity; + }); + + if (hasCapacity) { + call.run(); + } + + return hasCapacity; + } + + function increment(obj, prop) { + if (typeof obj[prop] === 'undefined') { + obj[prop] = 1 + } else { + obj[prop]++; + } + } + } return { addBidReceived, diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index ab5da55f420..3156ea671f7 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1305,6 +1305,7 @@ describe('Unit: Prebid Module', function () { ] }]; adUnitCodes = ['adUnit-code']; + configObj.setConfig({maxRequestsPerOrigin: Number.MAX_SAFE_INTEGER || 99999999}); let auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: timeout}); spyCallBids = sinon.spy(adaptermanager, 'callBids'); createAuctionStub = sinon.stub(auctionModule, 'newAuction'); From 80368385479e15a38d11df01741a54ee036a83eb Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Wed, 20 Jun 2018 17:07:00 -0400 Subject: [PATCH 09/33] allow s2s bidders call pbs without need of a client adapter file (#2704) * allow s2s adapters call pbs without client adapter file * add support in aliasBidAdapter for s2s-only bidders --- modules/prebidServerBidAdapter.js | 13 ++++++-- src/adaptermanager.js | 14 ++++++-- src/constants.json | 1 + src/prebid.js | 8 ++++- .../modules/prebidServerBidAdapter_spec.js | 4 +-- test/spec/unit/core/adapterManager_spec.js | 33 ++++++++++++++++++- 6 files changed, 63 insertions(+), 10 deletions(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 6533d9d00ee..c217b2934ed 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -589,7 +589,7 @@ const OPEN_RTB_PROTOCOL = { response.seatbid.forEach(seatbid => { (seatbid.bid || []).forEach(bid => { const bidRequest = utils.getBidRequest( - this.bidMap[`${bid.impid}${seatbid.seat}`], + this.bidMap[`${bid.impid}${seatbid.seat}`].bid_id, bidderRequests ); @@ -704,11 +704,12 @@ export function PrebidServer() { /* Notify Prebid of bid responses so bids can get in the auction */ function handleResponse(response, requestedBidders, bidderRequests, addBidResponse, done) { let result; + let bids = []; try { result = JSON.parse(response); - const bids = protocolAdapter().interpretResponse( + bids = protocolAdapter().interpretResponse( result, bidderRequests, requestedBidders @@ -734,7 +735,13 @@ export function PrebidServer() { utils.logError('error parsing response: ', result.status); } - done(); + const videoBid = bids.some(bidResponse => bidResponse.bid.mediaType === 'video'); + const cacheEnabled = config.getConfig('cache.url'); + + // video bids with cache enabled need to be cached first before they are considered done + if (!(videoBid && cacheEnabled)) { + done(); + } doClientSideSyncs(requestedBidders); } diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 3d5d56d6ee2..4c60b01d8fa 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -393,12 +393,20 @@ exports.registerBidAdapter = function (bidAdaptor, bidderCode, {supportedMediaTy }; exports.aliasBidAdapter = function (bidderCode, alias) { - var existingAlias = _bidderRegistry[alias]; + let existingAlias = _bidderRegistry[alias]; if (typeof existingAlias === 'undefined') { - var bidAdaptor = _bidderRegistry[bidderCode]; + let bidAdaptor = _bidderRegistry[bidderCode]; if (typeof bidAdaptor === 'undefined') { - utils.logError('bidderCode "' + bidderCode + '" is not an existing bidder.', 'adaptermanager.aliasBidAdapter'); + // check if alias is part of s2sConfig and allow them to register if so (as base bidder may be s2s-only) + const s2sConfig = config.getConfig('s2sConfig'); + const s2sBidders = s2sConfig && s2sConfig.bidders; + + if (!(s2sBidders && includes(s2sBidders, alias))) { + utils.logError('bidderCode "' + bidderCode + '" is not an existing bidder.', 'adaptermanager.aliasBidAdapter'); + } else { + exports.aliasRegistry[alias] = bidderCode; + } } else { try { let newAdapter; diff --git a/src/constants.json b/src/constants.json index c8a7c3ebefc..3bbad70585a 100644 --- a/src/constants.json +++ b/src/constants.json @@ -66,6 +66,7 @@ ], "S2S" : { "SRC" : "s2s", + "DEFAULT_ENDPOINT" : "https://prebid.adnxs.com/pbs/v1/openrtb2/auction", "SYNCED_BIDDERS_KEY": "pbjsSyncs" } } diff --git a/src/prebid.js b/src/prebid.js index 56c9d1d0c7c..767180f3286 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -340,9 +340,15 @@ $$PREBID_GLOBAL$$.requestBids = createHook('asyncSeries', function ({ bidsBackHa const adUnitMediaTypes = Object.keys(adUnit.mediaTypes || {'banner': 'banner'}); // get the bidder's mediaTypes - const bidders = adUnit.bids.map(bid => bid.bidder); + const allBidders = adUnit.bids.map(bid => bid.bidder); const bidderRegistry = adaptermanager.bidderRegistry; + const s2sConfig = config.getConfig('s2sConfig'); + const s2sBidders = s2sConfig && s2sConfig.bidders; + const bidders = (s2sBidders) ? allBidders.filter(bidder => { + return !includes(s2sBidders, bidder); + }) : allBidders; + if (!adUnit.transactionId) { adUnit.transactionId = utils.generateUUID(); } diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 8bc3d577ede..cd2022bcab7 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -246,7 +246,7 @@ const RESPONSE_OPENRTB = { 'bid': [ { 'id': '8750901685062148', - 'impid': '123', + 'impid': 'div-gpt-ad-1460505748561-0', 'price': 0.5, 'adm': '', 'adid': '29681110', @@ -285,7 +285,7 @@ const RESPONSE_OPENRTB_VIDEO = { bid: [ { id: '1987250005171537465', - impid: '/19968336/header-bid-tag-0', + impid: 'div-gpt-ad-1460505748561-0', price: 10, adm: 'adnxs', adid: '81877115', diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index df03af5f354..0e33fc005d5 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -19,7 +19,8 @@ const CONFIG = { timeout: 1000, maxBids: 1, adapter: 'prebidServer', - bidders: ['appnexus'] + bidders: ['appnexus'], + accountId: 'abc' }; var prebidServerAdapterMock = { bidder: 'prebidServer', @@ -719,6 +720,36 @@ describe('adapterManager tests', () => { expect(AdapterManager.videoAdapters).to.include(alias); }); }); + + describe('special case for s2s-only bidders', () => { + beforeEach(() => { + sinon.stub(utils, 'logError'); + }); + + afterEach(() => { + config.resetConfig(); + utils.logError.restore(); + }); + + it('should allow an alias if alias is part of s2sConfig.bidders', () => { + let testS2sConfig = utils.deepClone(CONFIG); + testS2sConfig.bidders = ['s2sAlias']; + config.setConfig({s2sConfig: testS2sConfig}); + + AdapterManager.aliasBidAdapter('s2sBidder', 's2sAlias'); + expect(AdapterManager.aliasRegistry).to.have.property('s2sAlias'); + }); + + it('should throw an error if alias + bidder are unknown and not part of s2sConfig.bidders', () => { + let testS2sConfig = utils.deepClone(CONFIG); + testS2sConfig.bidders = ['s2sAlias']; + config.setConfig({s2sConfig: testS2sConfig}); + + AdapterManager.aliasBidAdapter('s2sBidder1', 's2sAlias1'); + sinon.assert.calledOnce(utils.logError); + expect(AdapterManager.aliasRegistry).to.not.have.property('s2sAlias1'); + }); + }); }); describe('makeBidRequests', () => { From 64f8e01e88358810bafe55b70a04fa6de3fa5372 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Wed, 20 Jun 2018 17:20:51 -0400 Subject: [PATCH 10/33] Prebid 1.15.0 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2628096af7..d107cdd66a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "1.15.0-pre", + "version": "1.15.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 0dda32f4fa633109c1fb75e62bf9c240d5a71930 Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Thu, 21 Jun 2018 00:54:19 +0100 Subject: [PATCH 11/33] Add GDPR support for Quantcast adapter (#2733) * Add GDPR support for Quantcast adapter * Fix lint error --- modules/quantcastBidAdapter.js | 9 +++++++-- modules/quantcastBidAdapter.md | 4 ++-- test/spec/modules/quantcastBidAdapter_spec.js | 11 ++++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index f10fd48502f..3639a5a6bb7 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -41,9 +41,10 @@ export const spec = { * `BidRequests`. * * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be send to Quantcast server + * @param bidderRequest * @return ServerRequest information describing the request to the server. */ - buildRequests(bidRequests) { + buildRequests(bidRequests, bidderRequest) { const bids = bidRequests || []; const referrer = utils.getTopWindowUrl(); @@ -75,6 +76,8 @@ export const spec = { }); }); + const gdprConsent = bidderRequest ? bidderRequest.gdprConsent : {}; + // Request Data Format can be found at https://wiki.corp.qc/display/adinf/QCX const requestData = { publisherId: bid.params.publisherId, @@ -94,7 +97,9 @@ export const spec = { referrer, domain }, - bidId: bid.bidId + bidId: bid.bidId, + gdprSignal: gdprConsent.gdprApplies ? 1 : 0, + gdprConsent: gdprConsent.consentString }; const data = JSON.stringify(requestData); diff --git a/modules/quantcastBidAdapter.md b/modules/quantcastBidAdapter.md index 20cf25bffbf..efc21466c75 100644 --- a/modules/quantcastBidAdapter.md +++ b/modules/quantcastBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Quantcast Bidder Adapter Module Type: Bidder Adapter -Maintainer: xli@quantcast.com +Maintainer: igor.soarez@quantcast.com ``` # Description @@ -28,4 +28,4 @@ const adUnits = [{ } ] }]; -``` \ No newline at end of file +``` diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index 6fd5c3abdf9..f6e2924515c 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -129,13 +129,22 @@ describe('Quantcast adapter', () => { referrer, domain }, - bidId: '2f7b179d443f14' + bidId: '2f7b179d443f14', + gdprSignal: 0 }; expect(requests[0].data).to.equal(JSON.stringify(expectedBidRequest)); }); }); + it('propagates GDPR consent string and signal', () => { + const gdprConsent = { gdprApplies: true, consentString: 'consentString' } + const requests = qcSpec.buildRequests([bidRequest], { gdprConsent }); + const parsed = JSON.parse(requests[0].data) + expect(parsed.gdprSignal).to.equal(1); + expect(parsed.gdprConsent).to.equal(gdprConsent.consentString); + }); + describe('`interpretResponse`', () => { // The sample response is from https://wiki.corp.qc/display/adinf/QCX const body = { From ae287c38373dbb7ff0d52aadc02bf08a1d1446cd Mon Sep 17 00:00:00 2001 From: Jesse Date: Wed, 20 Jun 2018 16:56:12 -0700 Subject: [PATCH 12/33] ixBidAdapter.js: allow siteId param to be number (#2729) * ixBidAdapter.js: allow siteId param to be number In v0.x, the siteID param would be string or number. Somehow, this was restricted to just a string in v1.x. * ixBidAdapter.js logical error (not enough coffee) * ixBidAdapter_spec.js: allow number for backwards compat --- modules/ixBidAdapter.js | 2 +- test/spec/modules/ixBidAdapter_spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index f3ac8185f17..89506a5659b 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -153,7 +153,7 @@ export const spec = { return false; } - if (typeof bid.params.siteId !== 'string') { + if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { return false; } diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 92a7190bcb6..36b2b0e9629 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -77,10 +77,10 @@ describe('IndexexchangeAdapter', () => { expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should return false when siteID is number', () => { + it('should return true when siteID is number', () => { const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); bid.params.siteId = 123; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(bid)).to.equal(true); }); it('should return false when siteID is missing', () => { From 341866517ed874978e1553cc94c08607377049a5 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Thu, 21 Jun 2018 00:57:08 +0100 Subject: [PATCH 13/33] Audience Network: add debug params to bid requests (#2657) Remove deprecated pbv param --- modules/audienceNetworkBidAdapter.js | 8 ++++++-- test/spec/modules/audienceNetworkBidAdapter_spec.js | 10 +++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/audienceNetworkBidAdapter.js b/modules/audienceNetworkBidAdapter.js index 612357e0e4a..7256ce99e73 100644 --- a/modules/audienceNetworkBidAdapter.js +++ b/modules/audienceNetworkBidAdapter.js @@ -15,7 +15,9 @@ const url = 'https://an.facebook.com/v2/placementbid.json'; const supportedMediaTypes = ['banner', 'video']; const netRevenue = true; const hb_bidder = 'fan'; -const pbv = '$prebid.version$'; +const platver = '$prebid.version$'; +const platform = '241394079772386'; +const adapterver = '1.0.0'; /** * Does this bid request contain valid parameters? @@ -166,7 +168,9 @@ const buildRequests = bids => { testmode, pageurl, sdk, - pbv + adapterver, + platform, + platver }; const video = findIndex(adformats, isVideo); if (video !== -1) { diff --git a/test/spec/modules/audienceNetworkBidAdapter_spec.js b/test/spec/modules/audienceNetworkBidAdapter_spec.js index 2b4f87450e8..c61cd04c422 100644 --- a/test/spec/modules/audienceNetworkBidAdapter_spec.js +++ b/test/spec/modules/audienceNetworkBidAdapter_spec.js @@ -19,7 +19,7 @@ const placementId = 'test-placement-id'; const playerwidth = 320; const playerheight = 180; const requestId = 'test-request-id'; -const pbv = '$prebid.version$'; +const debug = 'adapterver=1.0.0&platform=241394079772386&platver=$prebid.version$'; const pageUrl = encodeURIComponent(utils.getTopWindowUrl()); describe('AudienceNetwork adapter', () => { @@ -140,7 +140,7 @@ describe('AudienceNetwork adapter', () => { requestIds: [requestId], sizes: ['300x250'], url: 'https://an.facebook.com/v2/placementbid.json', - data: `placementids[]=test-placement-id&adformats[]=300x250&testmode=false&pageurl=${pageUrl}&sdk[]=5.5.web&pbv=${pbv}` + data: `placementids[]=test-placement-id&adformats[]=300x250&testmode=false&pageurl=${pageUrl}&sdk[]=5.5.web&${debug}` }]); }); @@ -159,7 +159,7 @@ describe('AudienceNetwork adapter', () => { requestIds: [requestId], sizes: ['640x480'], url: 'https://an.facebook.com/v2/placementbid.json', - data: `placementids[]=test-placement-id&adformats[]=video&testmode=false&pageurl=${pageUrl}&sdk[]=&pbv=${pbv}&playerwidth=640&playerheight=480` + data: `placementids[]=test-placement-id&adformats[]=video&testmode=false&pageurl=${pageUrl}&sdk[]=&${debug}&playerwidth=640&playerheight=480` }]); }); @@ -178,7 +178,7 @@ describe('AudienceNetwork adapter', () => { requestIds: [requestId], sizes: ['728x90'], url: 'https://an.facebook.com/v2/placementbid.json', - data: `placementids[]=test-placement-id&adformats[]=native&testmode=false&pageurl=${pageUrl}&sdk[]=5.5.web&pbv=${pbv}` + data: `placementids[]=test-placement-id&adformats[]=native&testmode=false&pageurl=${pageUrl}&sdk[]=5.5.web&${debug}` }]); }); @@ -197,7 +197,7 @@ describe('AudienceNetwork adapter', () => { requestIds: [requestId], sizes: ['300x250'], url: 'https://an.facebook.com/v2/placementbid.json', - data: `placementids[]=test-placement-id&adformats[]=fullwidth&testmode=false&pageurl=${pageUrl}&sdk[]=5.5.web&pbv=${pbv}` + data: `placementids[]=test-placement-id&adformats[]=fullwidth&testmode=false&pageurl=${pageUrl}&sdk[]=5.5.web&${debug}` }]); }); From 9fc70451401f87c9951adfb1c5bed8b78298d3cb Mon Sep 17 00:00:00 2001 From: Pascal S Date: Thu, 21 Jun 2018 15:11:15 +0200 Subject: [PATCH 14/33] Update CONTRIBUTING.md (#2757) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc8d80ec384..7f4127cf3ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ When you are adding code to Prebid.js, or modifying code that isn't covered by a Prebid.js already has many tests. Read them to see how Prebid.js is tested, and for inspiration: - Look in `test/spec` and its subdirectories -- Tests for bidder adaptors are located in `test/spec/adapters` +- Tests for bidder adaptors are located in `test/spec/modules` A test module might have the following general structure: From 2507f4bceb548473959c0883307758b039a37352 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Thu, 21 Jun 2018 11:06:19 -0400 Subject: [PATCH 15/33] Increment Pre Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d107cdd66a3..1223fc5f6ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "1.15.0", + "version": "1.16.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 3868077184afa49100924646fee8d535e648ebdd Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Thu, 21 Jun 2018 11:10:46 -0400 Subject: [PATCH 16/33] Temporarily remove ios browsers from browserstack testing (#2759) * update ios browsers * removing ios browsers --- browsers.json | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/browsers.json b/browsers.json index cb523addc7e..703bf44d41d 100644 --- a/browsers.json +++ b/browsers.json @@ -62,21 +62,5 @@ "browser_version": "8.0", "device": null, "os": "OS X" - }, - "bs_ios_9": { - "base": "BrowserStack", - "os": "ios", - "os_version": "9.1", - "browser": "iphone", - "device": "iPhone 6S", - "browser_version": null - }, - "bs_ios_8": { - "base": "BrowserStack", - "os": "ios", - "os_version": "8.3", - "browser": "iphone", - "device": "iPhone 6", - "browser_version": null } } \ No newline at end of file From d30de3275b657dc1cfd5a30dcc55100c520a9546 Mon Sep 17 00:00:00 2001 From: bretg Date: Thu, 21 Jun 2018 11:57:27 -0400 Subject: [PATCH 17/33] Rubicon adapter: add support for new size (#2760) --- modules/rubiconBidAdapter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 5f4250352a5..e75c8df083a 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -71,7 +71,8 @@ var sizeMap = { 199: '640x200', 213: '1030x590', 214: '980x360', - 232: '580x400' + 232: '580x400', + 257: '400x600' }; utils._each(sizeMap, (item, key) => sizeMap[item] = key); From 0c25ef60b665a2667736d79cb8d7081affbacd96 Mon Sep 17 00:00:00 2001 From: bretg Date: Thu, 21 Jun 2018 15:31:39 -0400 Subject: [PATCH 18/33] Update RELEASE_SCHEDULE.md (#2749) --- RELEASE_SCHEDULE.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/RELEASE_SCHEDULE.md b/RELEASE_SCHEDULE.md index efdc45f7f9f..4f5c7fc4e9a 100644 --- a/RELEASE_SCHEDULE.md +++ b/RELEASE_SCHEDULE.md @@ -91,6 +91,21 @@ Announcements regarding releases will be made to the #headerbidding-dev channel git push ``` +## Beta Releases + +Prebid.js features may be released as Beta or as Generally Available (GA). + +Characteristics of a `Beta` release: +- May be a partial implementation (e.g. more work needed to flesh out the feature) +- May not be fully tested with other features +- Limited documentation, focused on technical aspects +- Few users + +Characteristics of a `GA` release: +- Complete set of functionality +- Significant user base with no major issues for at least a month +- Decent documentation that includes business need, use cases, and examples + ## FAQs From 7e53e83af643cb06d1dac6d2c5eb08cf322eea69 Mon Sep 17 00:00:00 2001 From: devweborama <39480777+devweborama@users.noreply.github.com> Date: Thu, 21 Jun 2018 23:51:12 +0300 Subject: [PATCH 19/33] Added Weborama bid adapter (#2710) --- integrationExamples/gpt/pbjs_example_gpt.html | 7 ++ modules/weboramaBidAdapter.js | 117 +++++++++++++++++ modules/weboramaBidAdapter.md | 27 ++++ test/spec/modules/weboramaBidAdapter_spec.js | 118 ++++++++++++++++++ 4 files changed, 269 insertions(+) create mode 100644 modules/weboramaBidAdapter.js create mode 100644 modules/weboramaBidAdapter.md create mode 100644 test/spec/modules/weboramaBidAdapter_spec.js diff --git a/integrationExamples/gpt/pbjs_example_gpt.html b/integrationExamples/gpt/pbjs_example_gpt.html index f1ec912fd26..e54a604e281 100644 --- a/integrationExamples/gpt/pbjs_example_gpt.html +++ b/integrationExamples/gpt/pbjs_example_gpt.html @@ -269,6 +269,13 @@ placement_id: 0 } }, + { + bidder: 'weborama', + params: { + placementId: 0, + traffic: 'banner' + } + }, { bidder: 'pollux', params: { diff --git a/modules/weboramaBidAdapter.js b/modules/weboramaBidAdapter.js new file mode 100644 index 00000000000..2fe6f30b361 --- /dev/null +++ b/modules/weboramaBidAdapter.js @@ -0,0 +1,117 @@ +import { registerBidder } from 'src/adapters/bidderFactory'; +import { BANNER, NATIVE, VIDEO } from 'src/mediaTypes'; +import * as utils from 'src/utils'; + +const BIDDER_CODE = 'weborama'; +const URL = '//supply.nl.weborama.fr/?c=o&m=multi'; +const URL_SYNC = '//supply.nl.weborama.fr/?c=o&m=cookie'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl); + case NATIVE: + return Boolean(bid.native); + default: + return false; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && + bid.params && + !isNaN(bid.params.placementId) && + spec.supportedMediaTypes.indexOf(bid.params.traffic) !== -1 + ); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: (validBidRequests) => { + let winTop; + try { + winTop = utils.getWindowTop(); + winTop.location.toString(); + } catch (e) { + utils.logMessage(e); + winTop = window; + }; + + const location = utils.getTopWindowLocation(); + const placements = []; + const request = { + 'secure': (location.protocol === 'https:') ? 1 : 0, + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + + for (let i = 0; i < validBidRequests.length; i++) { + const bid = validBidRequests[i]; + const params = bid.params; + placements.push({ + placementId: params.placementId, + bidId: bid.bidId, + sizes: bid.sizes, + traffic: params.traffic + }); + } + return { + method: 'POST', + url: URL, + data: request + }; + }, + + /** + * 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: (serverResponse) => { + let response = []; + try { + serverResponse = serverResponse.body; + for (let i = 0; i < serverResponse.length; i++) { + let resItem = serverResponse[i]; + if (isBidResponseValid(resItem)) { + response.push(resItem); + } + } + } catch (e) { + utils.logMessage(e); + }; + return response; + }, + + getUserSyncs: () => { + return [{ + type: 'image', + url: URL_SYNC + }]; + } +}; + +registerBidder(spec); diff --git a/modules/weboramaBidAdapter.md b/modules/weboramaBidAdapter.md new file mode 100644 index 00000000000..5bdca0bfcd1 --- /dev/null +++ b/modules/weboramaBidAdapter.md @@ -0,0 +1,27 @@ +# Overview + +``` +Module Name: Weborama SSP Bidder Adapter +Module Type: Bidder Adapter +Maintainer: devweborama@gmail.com +``` + +# Description + +Module that connects to Weborama SSP demand sources + +# Test Parameters +``` + var adUnits = [{ + code: 'placementCode', + sizes: [[300, 250]], + bids: [{ + bidder: 'weborama', + params: { + placementId: 0, + traffic: 'banner' + } + }] + } + ]; +``` diff --git a/test/spec/modules/weboramaBidAdapter_spec.js b/test/spec/modules/weboramaBidAdapter_spec.js new file mode 100644 index 00000000000..ef8414eb487 --- /dev/null +++ b/test/spec/modules/weboramaBidAdapter_spec.js @@ -0,0 +1,118 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/weboramaBidAdapter'; + +describe('WeboramaAdapter', () => { + let bid = { + bidId: '2dd581a2b6281d', + bidder: 'weborama', + bidderRequestId: '145e1d6a7837c9', + params: { + placementId: 123, + traffic: 'banner' + }, + placementCode: 'placement_0', + auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', + sizes: [[300, 250]], + transactionId: '3bb2f6da-87a6-4029-aeb0-bfe951372e62' + }; + + describe('isBidRequestValid', () => { + it('Should return true when placementId can be cast to a number', () => { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false when placementId is not a number', () => { + bid.params.placementId = 'aaa'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', () => { + let serverRequest = spec.buildRequests([bid]); + it('Creates a ServerRequest object with method, URL and data', () => { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', () => { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', () => { + expect(serverRequest.url).to.equal('//supply.nl.weborama.fr/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', () => { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + let placements = data['placements']; + for (let i = 0; i < placements.length; i++) { + let placement = placements[i]; + expect(placement).to.have.all.keys('placementId', 'bidId', 'traffic', 'sizes'); + expect(placement.placementId).to.be.a('number'); + expect(placement.bidId).to.be.a('string'); + expect(placement.traffic).to.be.a('string'); + expect(placement.sizes).to.be.an('array'); + } + }); + it('Returns empty data if no valid requests are passed', () => { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', () => { + let resObject = { + body: [ { + requestId: '123', + mediaType: 'banner', + cpm: 0.3, + width: 320, + height: 50, + ad: '

Hello ad

', + ttl: 1000, + creativeId: '123asd', + netRevenue: true, + currency: 'USD' + } ] + }; + let serverResponses = spec.interpretResponse(resObject); + it('Returns an array of valid server responses if response object is valid', () => { + expect(serverResponses).to.be.an('array').that.is.not.empty; + for (let i = 0; i < serverResponses.length; i++) { + let dataItem = serverResponses[i]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'mediaType'); + expect(dataItem.requestId).to.be.a('string'); + expect(dataItem.cpm).to.be.a('number'); + expect(dataItem.width).to.be.a('number'); + expect(dataItem.height).to.be.a('number'); + expect(dataItem.ad).to.be.a('string'); + expect(dataItem.ttl).to.be.a('number'); + expect(dataItem.creativeId).to.be.a('string'); + expect(dataItem.netRevenue).to.be.a('boolean'); + expect(dataItem.currency).to.be.a('string'); + expect(dataItem.mediaType).to.be.a('string'); + } + it('Returns an empty array if invalid response is passed', () => { + serverResponses = spec.interpretResponse('invalid_response'); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + }); + + describe('getUserSyncs', () => { + let userSync = spec.getUserSyncs(); + it('Returns valid URL and `', () => { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync[0].type).to.be.equal('image'); + expect(userSync[0].url).to.be.equal('//supply.nl.weborama.fr/?c=o&m=cookie'); + }); + }); +}); From 4723710b6103622b3b31514e15b085a5c8390824 Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Thu, 21 Jun 2018 16:53:48 -0400 Subject: [PATCH 20/33] move logic to check if CMP frame is not found (#2715) --- modules/consentManagement.js | 10 ++-- test/spec/modules/consentManagement_spec.js | 60 ++++++++++++--------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index fcaeab81544..5f040c63051 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -90,6 +90,10 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { f = f.parent; } + if (!cmpFrame) { + return cmpError('CMP not found.', hookConfig); + } + callCmpWhileInIframe('getConsentData', cmpFrame, callbackHandler.consentDataCallback); callCmpWhileInIframe('getVendorConsents', cmpFrame, callbackHandler.vendorConsentsCallback); } @@ -124,12 +128,6 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { /* Setup up a __cmp function to do the postMessage and stash the callback. This function behaves (from the caller's perspective identicially to the in-frame __cmp call */ window.__cmp = function(cmd, arg, callback) { - if (!cmpFrame) { - removePostMessageListener(); - - let errmsg = 'CMP not found'; - return cmpError(errmsg, hookConfig); - } let callId = Math.random() + ''; let msg = {__cmpCall: { command: cmd, diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index e825de2a184..46ab6c46777 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -68,35 +68,47 @@ describe('consentManagement', function () { }); describe('error checks:', () => { - describe('unknown CMP framework ID:', () => { - beforeEach(() => { - sinon.stub(utils, 'logWarn'); - }); + beforeEach(() => { + didHookReturn = false; + sinon.stub(utils, 'logWarn'); + sinon.stub(utils, 'logError'); + }); - afterEach(() => { - utils.logWarn.restore(); - config.resetConfig(); - $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); - resetConsentData(); - }); + afterEach(() => { + utils.logWarn.restore(); + utils.logError.restore(); + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); + resetConsentData(); + }); + + it('should throw a warning and return to hooked function when an unknown CMP framework ID is used', () => { + let badCMPConfig = { + cmpApi: 'bad' + }; + setConfig(badCMPConfig); + expect(userCMP).to.be.equal(badCMPConfig.cmpApi); - it('should return Warning message and return to hooked function', () => { - let badCMPConfig = { - cmpApi: 'bad' - }; - setConfig(badCMPConfig); - expect(userCMP).to.be.equal(badCMPConfig.cmpApi); + requestBidsHook({}, () => { + didHookReturn = true; + }); + let consent = gdprDataHandler.getConsentData(); + sinon.assert.calledOnce(utils.logWarn); + expect(didHookReturn).to.be.true; + expect(consent).to.be.null; + }); - didHookReturn = false; + it('should throw proper errors when CMP is not found', () => { + setConfig(goodConfigWithCancelAuction); - requestBidsHook({}, () => { - didHookReturn = true; - }); - let consent = gdprDataHandler.getConsentData(); - sinon.assert.calledOnce(utils.logWarn); - expect(didHookReturn).to.be.true; - expect(consent).to.be.null; + requestBidsHook({}, () => { + didHookReturn = true; }); + let consent = gdprDataHandler.getConsentData(); + // throw 2 errors; one for no bidsBackHandler and for CMP not being found (this is an error due to gdpr config) + sinon.assert.calledTwice(utils.logError); + expect(didHookReturn).to.be.false; + expect(consent).to.be.null; }); }); From af9b294b39d452c24cd4d4c1f3932e01851f1b57 Mon Sep 17 00:00:00 2001 From: matimar <36712443+matimar@users.noreply.github.com> Date: Thu, 21 Jun 2018 18:05:24 -0300 Subject: [PATCH 21/33] Add crs parameter to eplanning adapter (#2682) --- modules/eplanningBidAdapter.js | 16 ++++++++++++++++ test/spec/modules/eplanningBidAdapter_spec.js | 13 +++++++++++++ 2 files changed, 29 insertions(+) diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index dfc5f514cf3..6ead42d4b2d 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -17,6 +17,7 @@ export const spec = { isBidRequestValid: function(bid) { return Boolean(bid.params.ci) || Boolean(bid.params.t); }, + buildRequests: function(bidRequests) { const method = 'GET'; const dfpClientId = '1'; @@ -24,6 +25,7 @@ export const spec = { let url; let params; const urlConfig = getUrlConfig(bidRequests); + const pcrs = getCharset(); if (urlConfig.t) { url = urlConfig.isv + '/layers/t_pbjs_2.json'; @@ -40,6 +42,11 @@ export const spec = { pbv: '$prebid.version$', ncb: '1' }; + + if (pcrs) { + params.crs = pcrs; + } + if (referrerUrl) { params.fr = referrerUrl; } @@ -147,6 +154,15 @@ function getSpacesString(bids) { return spacesString; } + +function getCharset() { + try { + return window.top.document.charset || window.top.document.characterSet; + } catch (e) { + return document.charset || document.characterSet; + } +} + function getBidIdMap(bidRequests) { let map = {}; bidRequests.forEach(bid => map[cleanName(bid.adUnitCode)] = bid.bidId); diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index 68b9e1b263f..a56bff42285 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -259,6 +259,19 @@ describe('E-Planning Adapter', () => { stubGetReferrer.restore() }); + it('should return crs parameter with document charset', () => { + let expected; + try { + expected = window.top.document.characterSet; + } catch (e) { + expected = document.characterSet; + } + + const chset = spec.buildRequests(bidRequests).data.crs; + + expect(chset).to.equal(expected); + }); + it('should return the testing url when the request has the t parameter', () => { const url = spec.buildRequests([testBid]).url; const expectedUrl = '//' + TEST_ISV + '/layers/t_pbjs_2.json'; From ead7aa93a1ce9978e63ad0140ca468dccb6c2261 Mon Sep 17 00:00:00 2001 From: tegner Date: Thu, 21 Jun 2018 23:20:22 +0200 Subject: [PATCH 22/33] extracted bidder from recieved object in timeout event (#2741) --- modules/googleAnalyticsAdapter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/googleAnalyticsAdapter.js b/modules/googleAnalyticsAdapter.js index 2993697c09d..901159b14a8 100644 --- a/modules/googleAnalyticsAdapter.js +++ b/modules/googleAnalyticsAdapter.js @@ -235,7 +235,8 @@ function sendBidTimeouts(timedOutBidders) { _analyticsQueue.push(function () { utils._each(timedOutBidders, function (bidderCode) { _eventCount++; - window[_gaGlobal](_trackerSend, 'event', _category, 'Timeouts', bidderCode, _disableInteraction); + var bidderName = bidderCode.bidder; + window[_gaGlobal](_trackerSend, 'event', _category, 'Timeouts', bidderName, _disableInteraction); }); }); From 0c578b0eed2b8ac9eb8270397bc1abe0883d6a66 Mon Sep 17 00:00:00 2001 From: bretg Date: Thu, 21 Jun 2018 22:03:54 -0400 Subject: [PATCH 23/33] adding beta-releases to TOC (#2763) --- RELEASE_SCHEDULE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE_SCHEDULE.md b/RELEASE_SCHEDULE.md index 4f5c7fc4e9a..0c424e76ed4 100644 --- a/RELEASE_SCHEDULE.md +++ b/RELEASE_SCHEDULE.md @@ -1,6 +1,7 @@ **Table of Contents** - [Release Schedule](#release-schedule) - [Release Process](#release-process) +- [Beta Releases](#beta-releases) - [FAQs](#faqs) ## Release Schedule From cdbd9cfb6dc5efac1cac6bf4d4624aa29695bc46 Mon Sep 17 00:00:00 2001 From: Matt Kendall <1870166+mkendall07@users.noreply.github.com> Date: Fri, 22 Jun 2018 10:53:15 -0400 Subject: [PATCH 24/33] Feature/normalize size (#2738) * Fix for incorrectly uppercased keys * normalized the banner sizes param to always be [[h,w]] --- src/adaptermanager.js | 5 ++++- test/spec/unit/core/adapterManager_spec.js | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 4c60b01d8fa..1c8a2ff4a3e 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -230,7 +230,10 @@ exports.checkBidRequestSizes = (adUnits) => { if (mediaTypes && mediaTypes.banner) { const banner = mediaTypes.banner; if (banner.sizes) { - adUnit.sizes = banner.sizes; + // make sure we always send [[h,w]] format + const normalizedSize = utils.getAdUnitSizes(adUnit); + banner.sizes = normalizedSize; + adUnit.sizes = normalizedSize; } else { utils.logError('Detected a mediaTypes.banner object did not include sizes. This is a required field for the mediaTypes.banner object. Removing invalid mediaTypes.banner object from request.'); delete adUnit.mediaTypes.banner; diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 0e33fc005d5..c4dcc87198b 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -1013,6 +1013,20 @@ describe('adapterManager tests', () => { expect(result[0].mediaTypes.video).to.exist; sinon.assert.calledOnce(utils.logInfo); }); + + it('should normalize adUnit.sizes and adUnit.mediaTypes.banner.sizes', () => { + let fullAdUnit = [{ + sizes: [300, 250], + mediaTypes: { + banner: { + sizes: [300, 250] + } + } + }]; + let result = checkBidRequestSizes(fullAdUnit); + expect(result[0].sizes).to.deep.equal([[300, 250]]); + expect(result[0].mediaTypes.banner.sizes).to.deep.equal([[300, 250]]); + }); }); describe('negative tests for validating bid requests', () => { From 33a502bdf1420157898ab594ddbeab42b3d35199 Mon Sep 17 00:00:00 2001 From: Johnny Chau Date: Fri, 22 Jun 2018 08:29:16 -0700 Subject: [PATCH 25/33] Sharethrough - handle iframe bid param, safeframe support (#2762) - if true, Sharethrough ad markup will not break out of iframe - this also adds safeframe support --- modules/sharethroughBidAdapter.js | 95 ++++++++++++++----- modules/sharethroughBidAdapter.md | 5 +- .../modules/sharethroughBidAdapter_spec.js | 87 +++++++++++++---- 3 files changed, 145 insertions(+), 42 deletions(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index bb7f778089a..9aabca9518a 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,12 +1,14 @@ import { registerBidder } from 'src/adapters/bidderFactory'; +const VERSION = '3.0.0'; const BIDDER_CODE = 'sharethrough'; -const VERSION = '2.0.0'; const STR_ENDPOINT = document.location.protocol + '//btlr.sharethrough.com/header-bid/v1'; export const sharethroughAdapterSpec = { code: BIDDER_CODE, + isBidRequestValid: bid => !!bid.params.pkey && bid.bidder === BIDDER_CODE, + buildRequests: (bidRequests, bidderRequest) => { return bidRequests.map(bid => { let query = { @@ -26,67 +28,112 @@ export const sharethroughAdapterSpec = { query.consent_required = !!bidderRequest.gdprConsent.gdprApplies; } + // Data that does not need to go to the server, + // but we need as part of interpretResponse() + const strData = { + stayInIframe: bid.params.iframe, + sizes: bid.sizes + } + return { method: 'GET', url: STR_ENDPOINT, - data: query + data: query, + strData: strData }; }) }, + interpretResponse: ({ body }, req) => { - if (!body || !Object.keys(body).length || !body.creatives.length) { + if (!body || !body.creatives || !body.creatives.length) { return []; } const creative = body.creatives[0]; + let size = [0, 0]; + if (req.strData.stayInIframe) { + size = getLargestSize(req.strData.sizes); + } return [{ requestId: req.data.bidId, - width: 0, - height: 0, + width: size[0], + height: size[1], cpm: creative.cpm, creativeId: creative.creative.creative_key, - deal_id: creative.creative.deal_id, + dealId: creative.creative.deal_id, currency: 'USD', netRevenue: true, ttl: 360, ad: generateAd(body, req) }]; }, + getUserSyncs: (syncOptions, serverResponses) => { const syncs = []; - if (syncOptions.pixelEnabled && serverResponses.length > 0 && serverResponses[0].body) { + const shouldCookieSync = syncOptions.pixelEnabled && + serverResponses.length > 0 && + serverResponses[0].body && + serverResponses[0].body.cookieSyncUrls; + + if (shouldCookieSync) { serverResponses[0].body.cookieSyncUrls.forEach(url => { syncs.push({ type: 'image', url: url }); }); } + return syncs; } } +function getLargestSize(sizes) { + function area(size) { + return size[0] * size[1]; + } + + return sizes.reduce((prev, current) => { + if (area(current) > area(prev)) { + return current + } else { + return prev + } + }, [0, 0]); +} + function generateAd(body, req) { const strRespId = `str_response_${req.data.bidId}`; - return ` + let adMarkup = `
- - `; + ` + + if (req.strData.stayInIframe) { + // Don't break out of iframe + adMarkup = adMarkup + `` + } else { + // Break out of iframe + adMarkup = adMarkup + ` + + ` + } + + return adMarkup; } // See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem diff --git a/modules/sharethroughBidAdapter.md b/modules/sharethroughBidAdapter.md index 8ab44f2a0f2..d2d8030c5f7 100644 --- a/modules/sharethroughBidAdapter.md +++ b/modules/sharethroughBidAdapter.md @@ -26,12 +26,13 @@ Module that connects to Sharethrough's demand sources ] },{ code: 'test-div', - sizes: [[1, 1]], // a mobile size + sizes: [[300,250], [1, 1]], // a mobile size bids: [ { bidder: "sharethrough", params: { - pkey: 'LuB3vxGGFrBZJa6tifXW4xgK' + pkey: 'LuB3vxGGFrBZJa6tifXW4xgK', + iframe: true } } ] diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 2aef88fe7eb..a599ce6cdc8 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -19,17 +19,38 @@ const bidderRequest = [ sizes: [[700, 400]], placementCode: 'bar', params: { - pkey: 'bbbb2222' + pkey: 'bbbb2222', + iframe: true } }]; -const prebidRequest = [{ - method: 'GET', - url: document.location.protocol + '//btlr.sharethrough.com' + '/header-bid/v1', - data: { - bidId: 'bidId', - placement_key: 'pKey' - } -}]; + +const prebidRequests = [ + { + method: 'GET', + url: document.location.protocol + '//btlr.sharethrough.com' + '/header-bid/v1', + data: { + bidId: 'bidId', + placement_key: 'pKey' + }, + strData: { + stayInIframe: false, + sizes: [] + } + }, + { + method: 'GET', + url: document.location.protocol + '//btlr.sharethrough.com' + '/header-bid/v1', + data: { + bidId: 'bidId', + placement_key: 'pKey' + }, + strData: { + stayInIframe: true, + sizes: [[300, 250], [300, 300], [250, 250], [600, 50]] + } + }, +]; + const bidderResponse = { body: { 'adserverRequestId': '40b6afd5-6134-4fbb-850a-bb8972a46994', @@ -48,6 +69,7 @@ const bidderResponse = { }, header: { get: (header) => header } }; + // Mirrors the one in modules/sharethroughBidAdapter.js as the function is unexported const b64EncodeUnicode = (str) => { return btoa( @@ -56,6 +78,7 @@ const b64EncodeUnicode = (str) => { return String.fromCharCode('0x' + p1); })); } + describe('sharethrough adapter spec', () => { describe('.code', () => { it('should return a bidder code of sharethrough', () => { @@ -119,13 +142,27 @@ describe('sharethrough adapter spec', () => { describe('.interpretResponse', () => { it('returns a correctly parsed out response', () => { - expect(spec.interpretResponse(bidderResponse, prebidRequest[0])[0]).to.include( + expect(spec.interpretResponse(bidderResponse, prebidRequests[0])[0]).to.include( { width: 0, height: 0, cpm: 12.34, creativeId: 'aCreativeId', - deal_id: 'aDealId', + dealId: 'aDealId', + currency: 'USD', + netRevenue: true, + ttl: 360, + }); + }); + + it('returns a correctly parsed out response with largest size when strData.stayInIframe is true', () => { + expect(spec.interpretResponse(bidderResponse, prebidRequests[1])[0]).to.include( + { + width: 300, + height: 300, + cpm: 12.34, + creativeId: 'aCreativeId', + dealId: 'aDealId', currency: 'USD', netRevenue: true, ttl: 360, @@ -134,21 +171,21 @@ describe('sharethrough adapter spec', () => { it('returns a blank array if there are no creatives', () => { const bidResponse = { body: { creatives: [] } }; - expect(spec.interpretResponse(bidResponse, prebidRequest[0])).to.be.an('array').that.is.empty; + expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); it('returns a blank array if body object is empty', () => { const bidResponse = { body: {} }; - expect(spec.interpretResponse(bidResponse, prebidRequest[0])).to.be.an('array').that.is.empty; + expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); it('returns a blank array if body is null', () => { const bidResponse = { body: null }; - expect(spec.interpretResponse(bidResponse, prebidRequest[0])).to.be.an('array').that.is.empty; + expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); - it('correctly sends back a sfp script tag', () => { - const adMarkup = spec.interpretResponse(bidderResponse, prebidRequest[0])[0].ad; + it('correctly generates ad markup', () => { + const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[0])[0].ad; let resp = null; expect(() => btoa(JSON.stringify(bidderResponse))).to.throw(); @@ -163,6 +200,19 @@ describe('sharethrough adapter spec', () => { expect(adMarkup).to.match( /window.top.document.getElementsByTagName\('body'\)\[0\].appendChild\(sfp_js\);/) }); + + it('correctly generates ad markup for staying in iframe', () => { + const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[1])[0].ad; + let resp = null; + + expect(() => btoa(JSON.stringify(bidderResponse))).to.throw(); + expect(() => resp = b64EncodeUnicode(JSON.stringify(bidderResponse))).not.to.throw(); + expect(adMarkup).to.match( + /data-str-native-key="pKey" data-stx-response-name=\"str_response_bidId\"/); + expect(!!adMarkup.indexOf(resp)).to.eql(true); + expect(adMarkup).to.match( + /