From 0c8a23af94b3d52270aded6fffaf198813b21301 Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Tue, 19 Sep 2017 15:26:25 -0700 Subject: [PATCH 01/54] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 515f726c5b6..c5017e3f149 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "0.29.0", + "version": "0.30.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 336f06f72bd0c26378bf0882fdfc66973fd2dd2a Mon Sep 17 00:00:00 2001 From: Yann Pravo Date: Thu, 21 Sep 2017 23:07:06 +0200 Subject: [PATCH 02/54] Adomik Analytics Adapter (#1536) * add Adomik Analytics Adapter * clean code * refactoring & tests --- modules/adomikAnalyticsAdapter.js | 209 ++++++++++++++++++ .../modules/adomikAnalyticsAdapter_spec.js | 132 +++++++++++ 2 files changed, 341 insertions(+) create mode 100644 modules/adomikAnalyticsAdapter.js create mode 100644 test/spec/modules/adomikAnalyticsAdapter_spec.js diff --git a/modules/adomikAnalyticsAdapter.js b/modules/adomikAnalyticsAdapter.js new file mode 100644 index 00000000000..46e917b8a3a --- /dev/null +++ b/modules/adomikAnalyticsAdapter.js @@ -0,0 +1,209 @@ +import adapter from 'src/AnalyticsAdapter'; +import CONSTANTS from 'src/constants.json'; +import adaptermanager from 'src/adaptermanager'; +// import utils from 'src/utils'; + +// Events used in adomik analytics adapter +const auctionInit = CONSTANTS.EVENTS.AUCTION_INIT; +const auctionEnd = CONSTANTS.EVENTS.AUCTION_END; +const bidRequested = CONSTANTS.EVENTS.BID_REQUESTED; +const bidResponse = CONSTANTS.EVENTS.BID_RESPONSE; +const bidWon = CONSTANTS.EVENTS.BID_WON; +const bidTimeout = CONSTANTS.EVENTS.BID_TIMEOUT; + +let bidwonTimeout = 1000; + +let adomikAdapter = Object.assign(adapter({}), + { + // Track every event needed + track({ eventType, args }) { + switch (eventType) { + case auctionInit: + adomikAdapter.currentContext.id = args.requestId + adomikAdapter.currentContext.timeout = args.timeout + if (args.config.bidwonTimeout !== undefined && typeof args.config.bidwonTimeout === 'number') { + bidwonTimeout = args.config.bidwonTimeout; + } + break; + + case bidTimeout: + adomikAdapter.currentContext.timeouted = true; + break; + + case bidResponse: + adomikAdapter.bucketEvents.push({ + type: 'response', + event: adomikAdapter.buildBidResponse(args) + }); + break; + + case bidWon: + adomikAdapter.bucketEvents.push({ + type: 'winner', + event: { + id: args.adId, + placementCode: args.adUnitCode + } + }); + break; + + case bidRequested: + args.bids.forEach(function(bid) { + adomikAdapter.bucketEvents.push({ + type: 'request', + event: { + bidder: bid.bidder.toUpperCase(), + placementCode: bid.placementCode + } + }); + }); + break; + + case auctionEnd: + setTimeout(() => { + if (adomikAdapter.bucketEvents.length > 0) { + adomikAdapter.sendTypedEvent(); + } + }, bidwonTimeout); + break; + } + } + } +); + +adomikAdapter.sendTypedEvent = function() { + const groupedTypedEvents = adomikAdapter.buildTypedEvents(); + + const bulkEvents = { + uid: adomikAdapter.currentContext.uid, + ahbaid: adomikAdapter.currentContext.id, + timeout: adomikAdapter.currentContext.timeout, + hostname: window.location.hostname, + eventsByPlacementCode: groupedTypedEvents.map(function(typedEventsByType) { + let sizes = []; + const eventKeys = ['request', 'response', 'winner']; + let events = {}; + + eventKeys.forEach((eventKey) => { + events[`${eventKey}s`] = []; + if (typedEventsByType[eventKey] !== undefined) { + typedEventsByType[eventKey].forEach((typedEvent) => { + if (typedEvent.event.size !== undefined) { + const size = adomikAdapter.sizeUtils.handleSize(sizes, typedEvent.event.size); + if (size !== null) { + sizes = [...sizes, size]; + } + } + events[`${eventKey}s`] = [...events[`${eventKey}s`], typedEvent.event]; + }); + } + }); + + return { + placementCode: typedEventsByType.placementCode, + sizes, + events + }; + }) + }; + + // Encode object in base64 + const encodedBuf = window.btoa(JSON.stringify(bulkEvents)); + + // Create final url and split it in 1600 characters max (+endpoint length) + const encodedUri = encodeURIComponent(encodedBuf); + const splittedUrl = encodedUri.match(/.{1,1600}/g); + + splittedUrl.forEach((split, i) => { + const partUrl = `${split}&id=${adomikAdapter.currentContext.id}&part=${i}&on=${splittedUrl.length - 1}`; + const img = new Image(1, 1); + img.src = 'http://' + adomikAdapter.currentContext.url + '/?q=' + partUrl; + }) +}; + +adomikAdapter.buildBidResponse = function (bid) { + return { + bidder: bid.bidderCode.toUpperCase(), + placementCode: bid.adUnitCode, + id: bid.adId, + status: (bid.statusMessage === 'Bid available') ? 'VALID' : 'EMPTY_OR_ERROR', + cpm: parseFloat(bid.cpm), + size: { + width: Number(bid.width), + height: Number(bid.height) + }, + timeToRespond: bid.timeToRespond, + afterTimeout: adomikAdapter.currentContext.timeouted + }; +} + +adomikAdapter.sizeUtils = { + sizeAlreadyExists: (sizes, typedEventSize) => { + return sizes.find((size) => size.height === typedEventSize.height && size.width === typedEventSize.width); + }, + formatSize: (typedEventSize) => { + return { + width: Number(typedEventSize.width), + height: Number(typedEventSize.height) + }; + }, + handleSize: (sizes, typedEventSize) => { + let formattedSize = null; + if (adomikAdapter.sizeUtils.sizeAlreadyExists(sizes, typedEventSize) === undefined) { + formattedSize = adomikAdapter.sizeUtils.formatSize(typedEventSize); + } + return formattedSize; + } +}; + +adomikAdapter.buildTypedEvents = function () { + const groupedTypedEvents = []; + adomikAdapter.bucketEvents.forEach(function(typedEvent, i) { + const [placementCode, type] = [typedEvent.event.placementCode, typedEvent.type]; + let existTypedEvent = groupedTypedEvents.findIndex((groupedTypedEvent) => groupedTypedEvent.placementCode === placementCode); + + if (existTypedEvent === -1) { + groupedTypedEvents.push({ + placementCode: placementCode, + [type]: [typedEvent] + }); + existTypedEvent = groupedTypedEvents.length - 1; + } + + if (groupedTypedEvents[existTypedEvent][type]) { + groupedTypedEvents[existTypedEvent][type] = [...groupedTypedEvents[existTypedEvent][type], typedEvent]; + } else { + groupedTypedEvents[existTypedEvent][type] = [typedEvent]; + } + }); + + return groupedTypedEvents; +} + +// Initialize adomik object +adomikAdapter.currentContext = {}; +adomikAdapter.bucketEvents = []; + +adomikAdapter.adapterEnableAnalytics = adomikAdapter.enableAnalytics; + +adomikAdapter.enableAnalytics = function (config) { + const initOptions = config.options; + if (initOptions) { + adomikAdapter.currentContext = { + uid: initOptions.id, + url: initOptions.url, + debug: initOptions.debug, + id: '', + timeouted: false, + timeout: 0, + } + adomikAdapter.adapterEnableAnalytics(config); + } +}; + +adaptermanager.registerAnalyticsAdapter({ + adapter: adomikAdapter, + code: 'adomik' +}); + +export default adomikAdapter; diff --git a/test/spec/modules/adomikAnalyticsAdapter_spec.js b/test/spec/modules/adomikAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..40d2328122e --- /dev/null +++ b/test/spec/modules/adomikAnalyticsAdapter_spec.js @@ -0,0 +1,132 @@ +import adomikAnalytics from 'modules/adomikAnalyticsAdapter'; +import {expect} from 'chai'; +let events = require('src/events'); +let adaptermanager = require('src/adaptermanager'); +let constants = require('src/constants.json'); + +describe('Adomik Prebid Analytic', function () { + describe('enableAnalytics', function () { + beforeEach(() => { + sinon.spy(adomikAnalytics, 'track'); + sinon.spy(adomikAnalytics, 'sendTypedEvent'); + }); + + afterEach(() => { + adomikAnalytics.track.restore(); + adomikAnalytics.sendTypedEvent.restore(); + }); + + it('should catch all events', function () { + adaptermanager.registerAnalyticsAdapter({ + code: 'adomik', + adapter: adomikAnalytics + }); + + const initOptions = { + id: '123456', + url: 'testurl', + }; + + const bid = { + bidderCode: 'adomik_test_bid', + width: 10, + height: 10, + statusMessage: 'Bid available', + adId: '1234', + requestId: '', + responseTimestamp: 1496410856397, + requestTimestamp: 1496410856295, + cpm: 0.1, + bidder: 'biddertest', + adUnitCode: '0000', + timeToRespond: 100, + placementCode: 'placementtest' + } + + // Step 1: Initialize adapter + adaptermanager.enableAnalytics({ + provider: 'adomik', + options: initOptions + }); + expect(adomikAnalytics.currentContext).to.deep.equal({ + uid: '123456', + url: 'testurl', + debug: undefined, + id: '', + timeouted: false, + timeout: 0, + }); + + // Step 1: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, requestId: 'test-test-test', timeout: 3000}); + + expect(adomikAnalytics.currentContext).to.deep.equal({ + uid: '123456', + url: 'testurl', + debug: undefined, + id: 'test-test-test', + timeouted: false, + timeout: 3000, + }); + + // Step 2: Send bid requested event + events.emit(constants.EVENTS.BID_REQUESTED, { bids: [bid] }); + + expect(adomikAnalytics.bucketEvents.length).to.equal(1); + expect(adomikAnalytics.bucketEvents[0]).to.deep.equal({ + type: 'request', + event: { + bidder: 'BIDDERTEST', + placementCode: 'placementtest', + } + }); + + // Step 3: Send bid response event + events.emit(constants.EVENTS.BID_RESPONSE, bid); + + expect(adomikAnalytics.bucketEvents.length).to.equal(2); + expect(adomikAnalytics.bucketEvents[1]).to.deep.equal({ + type: 'response', + event: { + bidder: 'ADOMIK_TEST_BID', + placementCode: '0000', + id: '1234', + status: 'VALID', + cpm: 0.1, + size: { + width: 10, + height: 10 + }, + timeToRespond: 100, + afterTimeout: false, + } + }); + + // Step 4: Send bid won event + events.emit(constants.EVENTS.BID_WON, bid); + + expect(adomikAnalytics.bucketEvents.length).to.equal(3); + expect(adomikAnalytics.bucketEvents[2]).to.deep.equal({ + type: 'winner', + event: { + id: '1234', + placementCode: '0000', + } + }); + + // Step 5: Send bid timeout event + events.emit(constants.EVENTS.BID_TIMEOUT, {}); + + expect(adomikAnalytics.currentContext.timeouted).to.equal(true); + + // Step 6: Send auction end event + events.emit(constants.EVENTS.AUCTION_END, {}); + + setTimeout(function() { + sinon.assert.callCount(adomikAnalytics.sendTypedEvent, 1); + }, 3000) + + sinon.assert.callCount(adomikAnalytics.track, 6); + }); + }); +}); From 74e5f9fd1ad330f4a19a859a593682cedfb331cb Mon Sep 17 00:00:00 2001 From: Rich Loveland Date: Thu, 21 Sep 2017 22:34:32 -0400 Subject: [PATCH 03/54] Improve JSDoc for `setConfig` (#1579) Specifically, brings it to ~parity with the existing API docs on Prebid.org. --- src/prebid.js | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/prebid.js b/src/prebid.js index 14f88021ca9..6dd1688b568 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -758,8 +758,50 @@ $$PREBID_GLOBAL$$.setS2SConfig = function(options) { $$PREBID_GLOBAL$$.getConfig = config.getConfig; /** - * Set Prebid config options - * @param {Object} options + * Set Prebid config options. + * (Added in version 0.27.0). + * + * `setConfig` is designed to allow for advanced configuration while + * reducing the surface area of the public API. For more information + * about the move to `setConfig` (and the resulting deprecations of + * some other public methods), see [the Prebid 1.0 public API + * proposal](https://gist.github.com/mkendall07/51ee5f6b9f2df01a89162cf6de7fe5b6). + * + * #### Troubleshooting your configuration + * + * If you call `pbjs.setConfig` without an object, e.g., + * + * `pbjs.setConfig('debug', 'true'))` + * + * then Prebid.js will print an error to the console that says: + * + * ``` + * ERROR: setConfig options must be an object + * ``` + * + * If you don't see that message, you can assume the config object is valid. + * + * @param {Object} options Global Prebid configuration object. Must be JSON - no JavaScript functions are allowed. + * @param {string} options.bidderSequence The order in which bidders are called. Example: `pbjs.setConfig({ bidderSequence: "fixed" })`. Allowed values: `"fixed"` (order defined in `adUnit.bids` array on page), `"random"`. + * @param {boolean} options.debug Turn debug logging on/off. Example: `pbjs.setConfig({ debug: true })`. + * @param {string} options.priceGranularity The bid price granularity to use. Example: `pbjs.setConfig({ priceGranularity: "medium" })`. Allowed values: `"low"` ($0.50), `"medium"` ($0.10), `"high"` ($0.01), `"auto"` (sliding scale), `"dense"` (like `"auto"`, with smaller increments at lower CPMs), or a custom price bucket object, e.g., `{ "buckets" : [{"min" : 0,"max" : 20,"increment" : 0.1,"cap" : true}]}`. + * @param {boolean} options.enableSendAllBids Turn "send all bids" mode on/off. Example: `pbjs.setConfig({ enableSendAllBids: true })`. + * @param {number} options.bidderTimeout Set a global bidder timeout, in milliseconds. Example: `pbjs.setConfig({ bidderTimeout: 3000 })`. Note that it's still possible for a bid to get into the auction that responds after this timeout. This is due to how [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) works in JS: it queues the callback in the event loop in an approximate location that should execute after this time but it is not guaranteed. For more information about the asynchronous event loop and `setTimeout`, see [How JavaScript Timers Work](https://johnresig.com/blog/how-javascript-timers-work/). + * @param {string} options.publisherDomain The publisher's domain where Prebid is running, for cross-domain iFrame communication. Example: `pbjs.setConfig({ publisherDomain: "https://www.theverge.com" })`. + * @param {number} options.cookieSyncDelay A delay (in milliseconds) for requesting cookie sync to stay out of the critical path of page load. Example: `pbjs.setConfig({ cookieSyncDelay: 100 })`. + * @param {Object} options.s2sConfig The configuration object for [server-to-server header bidding](http://prebid.org/dev-docs/get-started-with-prebid-server.html). Example: + * ``` + * pbjs.setConfig({ + * s2sConfig: { + * accountId: '1', + * enabled: true, + * bidders: ['appnexus', 'pubmatic'], + * timeout: 1000, + * adapter: 'prebidServer', + * endpoint: 'https://prebid.adnxs.com/pbs/v1/auction' + * } + * }) + * ``` */ $$PREBID_GLOBAL$$.setConfig = config.setConfig; From 63a846946a5ec265813f4b5c299fa770ee44db55 Mon Sep 17 00:00:00 2001 From: Matt Kendall Date: Fri, 22 Sep 2017 16:51:30 -0400 Subject: [PATCH 04/54] Update ISSUE_TEMPLATE.md (#1613) Make the test page requirement more explicit. --- .github/ISSUE_TEMPLATE.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 665395940ce..3621d9bb00e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -6,8 +6,12 @@ ## Steps to reproduce + +## Test page + From 434956353fec8c0155e2e5505d34ac81d1a9b050 Mon Sep 17 00:00:00 2001 From: harpere Date: Fri, 22 Sep 2017 18:25:26 -0400 Subject: [PATCH 05/54] Bugfix/user sync setconfig (#1615) * usersync should register configs set with config.setConfig() * do not overwrite user-sync config * fixed lint error --- src/userSync.js | 22 +++++++++++++--------- src/utils.js | 2 +- test/spec/userSync_spec.js | 11 +++++++++++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/userSync.js b/src/userSync.js index eb8b3ef5e64..9433590ade0 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -19,7 +19,11 @@ export function newUserSync(userSyncDependencies) { let numAdapterBids = {}; // Use what is in config by default - const config = userSyncDependencies.config; + let usConfig = userSyncDependencies.config; + // Update if it's (re)set + config.getConfig('userSync', (conf) => { + usConfig = Object.assign(usConfig, conf.userSync); + }); /** * @function getDefaultQueue @@ -40,7 +44,7 @@ export function newUserSync(userSyncDependencies) { * @private */ function fireSyncs() { - if (!config.syncEnabled || !userSyncDependencies.browserSupportsCookies || hasFired) { + if (!usConfig.syncEnabled || !userSyncDependencies.browserSupportsCookies || hasFired) { return; } @@ -63,7 +67,7 @@ export function newUserSync(userSyncDependencies) { * @private */ function fireImagePixels() { - if (!config.pixelEnabled) { + if (!usConfig.pixelEnabled) { return; } // Randomize the order of the pixels before firing @@ -83,7 +87,7 @@ export function newUserSync(userSyncDependencies) { * @private */ function loadIframes() { - if (!config.iframeEnabled) { + if (!usConfig.iframeEnabled) { return; } // Randomize the order of these syncs just like the pixels above @@ -125,18 +129,18 @@ export function newUserSync(userSyncDependencies) { * userSync.registerSync('image', 'rubicon', 'http://example.com/pixel') */ publicApi.registerSync = (type, bidder, url) => { - if (!config.syncEnabled || !utils.isArray(queue[type])) { + if (!usConfig.syncEnabled || !utils.isArray(queue[type])) { return utils.logWarn(`User sync type "{$type}" not supported`); } if (!bidder) { return utils.logWarn(`Bidder is required for registering sync`); } - if (Number(numAdapterBids[bidder]) >= config.syncsPerBidder) { + if (Number(numAdapterBids[bidder]) >= usConfig.syncsPerBidder) { return utils.logWarn(`Number of user syncs exceeded for "{$bidder}"`); } // All bidders are enabled by default. If specified only register for enabled bidders. - let hasEnabledBidders = config.enabledBidders && config.enabledBidders.length; - if (hasEnabledBidders && config.enabledBidders.indexOf(bidder) < 0) { + let hasEnabledBidders = usConfig.enabledBidders && usConfig.enabledBidders.length; + if (hasEnabledBidders && usConfig.enabledBidders.indexOf(bidder) < 0) { return utils.logWarn(`Bidder "${bidder}" not supported`); } queue[type].push([bidder, url]); @@ -162,7 +166,7 @@ export function newUserSync(userSyncDependencies) { * @public */ publicApi.triggerUserSyncs = () => { - if (config.enableOverride) { + if (usConfig.enableOverride) { publicApi.syncUsers(); } }; diff --git a/src/utils.js b/src/utils.js index 15e0ef30b8d..00a06fcb091 100644 --- a/src/utils.js +++ b/src/utils.js @@ -463,7 +463,7 @@ exports.triggerPixel = function (url) { * @param {string} encodeUri boolean if URL should be encoded before inserted. Defaults to true */ exports.insertUserSyncIframe = function(url) { - let iframeHtml = this.createTrackPixelIframeHtml(url, false, 'allow-scripts'); + let iframeHtml = this.createTrackPixelIframeHtml(url, false, 'allow-scripts allow-same-origin'); let div = document.createElement('div'); div.innerHTML = iframeHtml; let iframe = div.firstChild; diff --git a/test/spec/userSync_spec.js b/test/spec/userSync_spec.js index 4eabe254113..fe527ef8574 100644 --- a/test/spec/userSync_spec.js +++ b/test/spec/userSync_spec.js @@ -174,4 +174,15 @@ describe('user sync', () => { expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('http://example.com/'); expect(triggerPixelStub.getCall(1)).to.be.null; }); + + it('should register config set after instantiation', () => { + // start with userSync off + const userSync = newTestUserSync({syncEnabled: false}); + // turn it on with setConfig() + config.setConfig({userSync: {syncEnabled: true}}); + userSync.registerSync('image', 'testBidder', 'http://example.com'); + userSync.syncUsers(); + expect(triggerPixelStub.getCall(0)).to.not.be.null; + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.equal('http://example.com'); + }); }); From fddb66cf446548f15e8755fafd5c9901c3a9f7d5 Mon Sep 17 00:00:00 2001 From: Prebid-Team Date: Tue, 26 Sep 2017 02:13:16 +0530 Subject: [PATCH 06/54] Global object name can be sent with request params. (#1619) * call to custom global function added * space issue resolved --- modules/vertozBidAdapter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/vertozBidAdapter.js b/modules/vertozBidAdapter.js index 3334a5a34c6..b6966dd62d1 100644 --- a/modules/vertozBidAdapter.js +++ b/modules/vertozBidAdapter.js @@ -33,7 +33,8 @@ function VertozAdapter() { _rqsrc: reqSrc, _cb: cb, _slotBidId: slotBidId, - _cpm: cpm + _cpm: cpm, + _cbn: '$$PREBID_GLOBAL$$' }; let queryParamValue = JSON.stringify(vzReq); From 80208ae16f1666fb849ccd91f8d810118d22a153 Mon Sep 17 00:00:00 2001 From: Ryan Chou Date: Wed, 27 Sep 2017 19:49:05 +0800 Subject: [PATCH 07/54] Remove unnecessary parameters (#1618) * Add a new ucfunnel Adapter and test page * Add a new ucfunnel Adapter and test page * 1. Use prebid lib in the repo to keep updated 2. Replace var with let 3. Put JSON.parse(JSON.stringify()) into try catch block * utils.getTopWindowLocation is a function * Change to modules from adapters * Migrate to module design * [Dev Fix] Remove width and height which can be got from ad unit id --- integrationExamples/gpt/pbjs_ucfunnel_gpt.html | 4 +--- modules/ucfunnelBidAdapter.js | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/integrationExamples/gpt/pbjs_ucfunnel_gpt.html b/integrationExamples/gpt/pbjs_ucfunnel_gpt.html index 67881d7168d..cda2af03b18 100644 --- a/integrationExamples/gpt/pbjs_ucfunnel_gpt.html +++ b/integrationExamples/gpt/pbjs_ucfunnel_gpt.html @@ -13,9 +13,7 @@ bids: [{ bidder: 'ucfunnel', params: { - adid: "test-ad-83444226E44368D1E32E49EEBE6D29", //String - required - width: 300, - height: 250 + adid: "test-ad-83444226E44368D1E32E49EEBE6D29" //String - required } } ] diff --git a/modules/ucfunnelBidAdapter.js b/modules/ucfunnelBidAdapter.js index f171604db7c..978c7508002 100644 --- a/modules/ucfunnelBidAdapter.js +++ b/modules/ucfunnelBidAdapter.js @@ -58,8 +58,6 @@ function UcfunnelAdapter() { 'u', page, 'ru', refer, 'adid', bid.params.adid, - 'w', bid.params.width, - 'h', bid.params.height, 'ver', VER ]; From 1d2a7c69e6cf2ca8f62b5b4b30d42ac63a185471 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Wed, 27 Sep 2017 07:18:21 -0600 Subject: [PATCH 08/54] some fixes for intermittent tests failing (#1626) --- .../modules/adomikAnalyticsAdapter_spec.js | 9 +++++-- .../modules/appnexusAnalyticsAdapter_spec.js | 24 ------------------- test/spec/modules/appnexusBidAdapter_spec.js | 21 ++++++++++++---- test/spec/modules/criteoBidAdapter_spec.js | 12 ++++++++++ test/spec/modules/currency_spec.js | 17 ++++++------- .../modules/justpremiumBidAdapter_spec.js | 4 ---- test/spec/modules/komoonaBidAdapter_spec.js | 2 +- test/spec/modules/marsmediaBidAdapter_spec.js | 1 + .../modules/sharethroughBidAdapter_spec.js | 1 - 9 files changed, 46 insertions(+), 45 deletions(-) delete mode 100644 test/spec/modules/appnexusAnalyticsAdapter_spec.js diff --git a/test/spec/modules/adomikAnalyticsAdapter_spec.js b/test/spec/modules/adomikAnalyticsAdapter_spec.js index 40d2328122e..a3e0d214e5a 100644 --- a/test/spec/modules/adomikAnalyticsAdapter_spec.js +++ b/test/spec/modules/adomikAnalyticsAdapter_spec.js @@ -16,7 +16,7 @@ describe('Adomik Prebid Analytic', function () { adomikAnalytics.sendTypedEvent.restore(); }); - it('should catch all events', function () { + it('should catch all events', function (done) { adaptermanager.registerAnalyticsAdapter({ code: 'adomik', adapter: adomikAnalytics @@ -120,11 +120,16 @@ describe('Adomik Prebid Analytic', function () { expect(adomikAnalytics.currentContext.timeouted).to.equal(true); // Step 6: Send auction end event + var clock = sinon.useFakeTimers(); events.emit(constants.EVENTS.AUCTION_END, {}); setTimeout(function() { sinon.assert.callCount(adomikAnalytics.sendTypedEvent, 1); - }, 3000) + done(); + }, 3000); + + clock.tick(5000); + clock.restore(); sinon.assert.callCount(adomikAnalytics.track, 6); }); diff --git a/test/spec/modules/appnexusAnalyticsAdapter_spec.js b/test/spec/modules/appnexusAnalyticsAdapter_spec.js deleted file mode 100644 index 7f68359de07..00000000000 --- a/test/spec/modules/appnexusAnalyticsAdapter_spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import appnexusAnalytics from 'modules/appnexusAnalyticsAdapter'; -import { assert } from 'chai'; -import { getBidRequestedPayload } from 'test/fixtures/fixtures'; - -const spyEnqueue = sinon.spy(appnexusAnalytics, 'enqueue'); -const spyTrack = sinon.spy(appnexusAnalytics, 'track'); - -const bidRequestedPayload = getBidRequestedPayload(); - -// describe(` -// FEATURE: AppNexus Prebid Analytics Adapter (APA) -// STORY: As a publisher I use APA to collect data for auction events\n`, ()=> { -// describe(`SCENARIO: Bids are received from bidder -// GIVEN: A publisher page requests bids -// WHEN: The bidRequested event fires`, () => { -// appnexusAnalytics.enqueue('bidRequested', bidRequestedPayload); -// it(`THEN: APA enqueue is called with event payload -// AND: APA track does not get called`, () => { -// assert.ok(spyEnqueue.calledWith('bidRequested')); -// assert.deepEqual(spyEnqueue.args[0][1], bidRequestedPayload); -// assert.ok(!spyTrack.called); -// }); -// }); -// }); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index e7f5310d90c..96916f3fa35 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -30,12 +30,23 @@ describe('AppNexus Adapter', () => { 'start': 1469479810130 }; - sinon.stub(bidManager, 'addBidResponse'); - const adLoaderStub = sinon.stub(adLoader, 'loadScript'); + let sandbox; + let adLoaderStub; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(bidManager, 'addBidResponse'); + adLoaderStub = sandbox.stub(adLoader, 'loadScript'); + }); + + afterEach(() => { + sandbox.restore(); + }); describe('callBids', () => { - adapter = new Adapter(); - adapter.callBids(REQUEST); - expect(adLoaderStub.getCall(0).args[0]).to.contain('traffic_source_code=source'); + it('should contain traffic_source_code', () => { + adapter = new Adapter(); + adapter.callBids(REQUEST); + expect(adLoaderStub.getCall(0).args[0]).to.contain('traffic_source_code=source'); + }); }); }); diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index c8a49283c7a..68c554c7cb4 100644 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -131,6 +131,10 @@ describe('criteo adapter test', () => { server.respondWith(JSON.stringify(validResponse)); }); + afterEach(() => { + server.restore(); + }); + it('adds bid for valid request', (done) => { stubAddBidResponse = sinon.stub(bidManager, 'addBidResponse', function (adUnitCode, bid) { expect(bid).to.satisfy(bid => { return bid.getStatusCode() == CONSTANTS.STATUS.GOOD }); @@ -185,6 +189,10 @@ describe('criteo adapter test', () => { server.respondWith(JSON.stringify(validNativeResponse)); }); + afterEach(() => { + server.restore(); + }); + it('adds creative to the response of a native valid request', (done) => { stubAddBidResponse = sinon.stub( bidManager, 'addBidResponse', @@ -215,6 +223,10 @@ describe('criteo adapter test', () => { server = sinon.fakeServer.create({ autoRespond: true, respondImmediately: true }); }); + afterEach(() => { + server.restore(); + }); + it('no bid if cdb handler responds with no bid empty string response', (done) => { server.respondWith(''); diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index f11ef18a35a..937e6a084e4 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -15,6 +15,15 @@ var assert = require('chai').assert; var expect = require('chai').expect; describe('currency', function () { + let fakeCurrencyFileServer; + beforeEach(() => { + fakeCurrencyFileServer = sinon.fakeServer.create(); + }); + + afterEach(() => { + fakeCurrencyFileServer.restore(); + }); + describe('setConfig', () => { it('results in currencySupportEnabled = false when currency not configured', () => { setConfig({}); @@ -22,7 +31,6 @@ describe('currency', function () { }); it('results in currencySupportEnabled = true and currencyRates being loaded when configured', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); fakeCurrencyFileServer.respond(); @@ -103,7 +111,6 @@ describe('currency', function () { it('not run until currency rates file is loaded', () => { setConfig({}); - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); var marker = false; @@ -158,7 +165,6 @@ describe('currency', function () { }); it('should result in NO_BID when fromCurrency is not supported in file', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); fakeCurrencyFileServer.respond(); @@ -172,7 +178,6 @@ describe('currency', function () { }); it('should result in NO_BID when adServerCurrency is not supported in file', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'ABC' }); fakeCurrencyFileServer.respond(); @@ -186,7 +191,6 @@ describe('currency', function () { }); it('should return 1 when currency support is enabled and same currency code is requested as is set to adServerCurrency', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); fakeCurrencyFileServer.respond(); @@ -201,7 +205,6 @@ describe('currency', function () { }); it('should return direct conversion rate when fromCurrency is one of the configured bases', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'GBP' }); fakeCurrencyFileServer.respond(); @@ -216,7 +219,6 @@ describe('currency', function () { }); it('should return reciprocal conversion rate when adServerCurrency is one of the configured bases, but fromCurrency is not', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'GBP' }); fakeCurrencyFileServer.respond(); @@ -231,7 +233,6 @@ describe('currency', function () { }); it('should return intermediate conversion rate when neither fromCurrency nor adServerCurrency is one of the configured bases', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'CNY' }); fakeCurrencyFileServer.respond(); diff --git a/test/spec/modules/justpremiumBidAdapter_spec.js b/test/spec/modules/justpremiumBidAdapter_spec.js index d4bb8547eba..8416d662572 100644 --- a/test/spec/modules/justpremiumBidAdapter_spec.js +++ b/test/spec/modules/justpremiumBidAdapter_spec.js @@ -324,8 +324,6 @@ describe('justpremium adapter', () => { expect(bidPlacementCode).to.equal('div-gpt-ad-1471513102552-1'); expect(bidResponse.getStatusCode()).to.equal(PREBID_CONSTANTS.STATUS.NO_BID); expect(bidResponse.bidderCode).to.equal('justpremium'); - - stubAddBidResponse.restore(); }); it('should add bid if tag contains any', () => { @@ -383,8 +381,6 @@ describe('justpremium adapter', () => { expect(bidResponse1.cpm).to.equal(responseData.cpm); expect(bidResponse1.format).to.equal(responseData.format); expect(bidResponse1.ad).to.equal(responseData.ad); - - stubAddBidResponse.restore(); }); }); }); diff --git a/test/spec/modules/komoonaBidAdapter_spec.js b/test/spec/modules/komoonaBidAdapter_spec.js index acb4981c1bd..2657c658ba2 100644 --- a/test/spec/modules/komoonaBidAdapter_spec.js +++ b/test/spec/modules/komoonaBidAdapter_spec.js @@ -99,7 +99,7 @@ describe('komoonaAdapter', () => { }); afterEach(() => { - server.restore() + server.restore(); bidmanager.addBidResponse.restore(); }); diff --git a/test/spec/modules/marsmediaBidAdapter_spec.js b/test/spec/modules/marsmediaBidAdapter_spec.js index 5db56cda976..c9381eb3c5f 100644 --- a/test/spec/modules/marsmediaBidAdapter_spec.js +++ b/test/spec/modules/marsmediaBidAdapter_spec.js @@ -75,6 +75,7 @@ describe('MarsMedia adapter implementation', () => { afterEach(() => { sandbox.restore(); + server.restore(); }); describe('should receive a valid request bid -', () => { diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 5453e594155..94c8f438cc0 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -120,7 +120,6 @@ describe('sharethrough adapter', () => { afterEach(() => { server.restore(); - stubAddBidResponse.reset(); }); it('should add a bid object for each bid', () => { From 16d5dd4bd988c1a9352de3e895d9839d264ec7ed Mon Sep 17 00:00:00 2001 From: lntho Date: Wed, 27 Sep 2017 09:27:43 -0700 Subject: [PATCH 09/54] OpenX adapter: 1) changing custom parameters to be on a per-slot basis (#1596) 2) Add ability to specify custom floors per slot 3) Change BC parameter to be configurable 4) Give ability to turn off building BO pixels given a flag --- modules/openxBidAdapter.js | 51 ++++++++++++-- test/spec/modules/openxBidAdapter_spec.js | 86 ++++++++++++++++++++++- 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 426362e0d9c..1b9766553c2 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -12,6 +12,7 @@ const OpenxAdapter = function OpenxAdapter() { const BIDDER_VERSION = '1.0.1'; let startTime; let timeout = config.getConfig('bidderTimeout'); + let shouldSendBoPixel = true; let pdNode = null; @@ -64,7 +65,9 @@ const OpenxAdapter = function OpenxAdapter() { beaconParams.bp = adUnit.pub_rev; beaconParams.ts = adUnit.ts; addBidResponse(adUnit, bid); - buildBoPixel(adUnit.creative[0], beaconParams); + if (shouldSendBoPixel === true) { + buildBoPixel(adUnit.creative[0], beaconParams); + } } }; @@ -182,6 +185,16 @@ const OpenxAdapter = function OpenxAdapter() { return found; } + function formatCustomParms(customKey, customParams) { + let value = customParams[customKey]; + if (Array.isArray(value)) { + // if value is an array, join them with commas first + value = value.join(','); + } + // return customKey=customValue format, escaping + to . and / to _ + return (customKey + '=' + value).replace('+', '.').replace('/', '_') + } + function buildRequest(bids, params, delDomain) { if (!utils.isArray(bids)) { return; @@ -193,13 +206,35 @@ const OpenxAdapter = function OpenxAdapter() { return utils.parseSizesInput(bid.sizes).join(','); }).join('|'); + let customParamsForAllBids = []; + let hasCustomParam = false; bids.forEach(function (bid) { - for (let customParam in bid.params.customParams) { - if (bid.params.customParams.hasOwnProperty(customParam)) { - params['c.' + customParam] = bid.params.customParams[customParam]; - } + if (bid.params.customParams) { + let customParamsForBid = utils._map(Object.keys(bid.params.customParams), customKey => formatCustomParms(customKey, bid.params.customParams)); + let formattedCustomParams = window.btoa(customParamsForBid.join('&')); + hasCustomParam = true; + customParamsForAllBids.push(formattedCustomParams); + } else { + customParamsForAllBids.push(''); + } + }); + if (hasCustomParam) { + params.tps = customParamsForAllBids.join('%2C'); + } + + let customFloorsForAllBids = []; + let hasCustomFloor = false; + bids.forEach(function (bid) { + if (bid.params.customFloor) { + customFloorsForAllBids.push(bid.params.customFloor * 1000); + hasCustomFloor = true; + } else { + customFloorsForAllBids.push(0); } }); + if (hasCustomFloor) { + params.aumfs = customFloorsForAllBids.join('%2C'); + } try { let queryString = buildQueryStringFromParams(params); @@ -227,11 +262,15 @@ const OpenxAdapter = function OpenxAdapter() { } let delDomain = bids[0].params.delDomain; + let bcOverride = bids[0].params.bc; startTime = new Date(params.start); if (params.timeout) { timeout = params.timeout; } + if (bids[0].params.hasOwnProperty('sendBoPixel') && typeof (bids[0].params.sendBoPixel) === 'boolean') { + shouldSendBoPixel = bids[0].params.sendBoPixel; + } buildRequest(bids, { ju: currentURL, @@ -243,7 +282,7 @@ const OpenxAdapter = function OpenxAdapter() { tws: getViewportDimensions(isIfr), ef: 'bt%2Cdb', be: 1, - bc: `${BIDDER_CONFIG}_${BIDDER_VERSION}`, + bc: bcOverride || `${BIDDER_CONFIG}_${BIDDER_VERSION}`, nocache: new Date().getTime() }, delDomain); diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index 21ceb5a6dec..c08e8c256e6 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -136,12 +136,15 @@ describe('openx adapter tests', function () { }); describe('test openx ad requests', () => { let spyAjax; + let spyBtoa; beforeEach(() => { spyAjax = sinon.spy(ajax, 'ajax'); + spyBtoa = sinon.spy(window, 'btoa'); sinon.stub(document.body, 'appendChild'); }); afterEach(() => { spyAjax.restore(); + spyBtoa.restore(); document.body.appendChild.restore(); }); @@ -179,19 +182,96 @@ describe('openx adapter tests', function () { params: { delDomain: 'testdelDomain', unit: 1234, - customParams: {'test1': 'testval1'} + customParams: {'test1': 'testval1+', 'test2': ['testval2/', 'testval3']} } } ] }; adapter.callBids(params); - sinon.assert.calledOnce(spyAjax); + sinon.assert.calledOnce(spyAjax); + sinon.assert.calledWith(spyBtoa, 'test1=testval1.&test2=testval2_,testval3'); let bidUrl = spyAjax.getCall(0).args[0]; expect(bidUrl).to.include('testdelDomain'); expect(bidUrl).to.include('1234'); expect(bidUrl).to.include('300x250,300x600'); - expect(bidUrl).to.include('c.test1=testval1'); + }); + + it('should send out custom floors on bids that have customFloors specified', () => { + let params = { + bids: [ + { + sizes: [[300, 250], [300, 600]], + params: { + delDomain: 'testdelDomain', + unit: 1234, + customFloor: 1 + } + }, + { + sizes: [[320, 50]], + params: { + delDomain: 'testdelDomain', + unit: 1234 + } + }, + { + sizes: [[728, 90]], + params: { + delDomain: 'testdelDomain', + unit: 1234, + customFloor: 1.5 + } + } + ] + }; + adapter.callBids(params); + + sinon.assert.calledOnce(spyAjax); + let bidUrl = spyAjax.getCall(0).args[0]; + expect(bidUrl).to.include('testdelDomain'); + expect(bidUrl).to.include('1234'); + expect(bidUrl).to.include('300x250,300x600|320x50|728x90'); + expect(bidUrl).to.include('aumfs=1000%2C0%2C1500'); + }); + + it('should change bc param if configureable bc is specified', () => { + let params = { + bids: [ + { + sizes: [[300, 250], [300, 600]], + params: { + delDomain: 'testdelDomain', + unit: 1234, + bc: 'hb_pb_test' + } + }, + { + sizes: [[320, 50]], + params: { + delDomain: 'testdelDomain', + unit: 1234, + bc: 'hb_pb_test' + } + }, + { + sizes: [[728, 90]], + params: { + delDomain: 'testdelDomain', + unit: 1234, + bc: 'hb_pb_test' + } + } + ] + }; + adapter.callBids(params); + + sinon.assert.calledOnce(spyAjax); + let bidUrl = spyAjax.getCall(0).args[0]; + expect(bidUrl).to.include('testdelDomain'); + expect(bidUrl).to.include('1234'); + expect(bidUrl).to.include('300x250,300x600|320x50|728x90'); + expect(bidUrl).to.include('bc=hb_pb_test'); }); }); }); From 6cf71d002735c63744c0266236386b97a2f10f58 Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Wed, 27 Sep 2017 10:05:43 -0700 Subject: [PATCH 10/54] Safely reference native keyword to fix compression (#1621) --- modules/adxcgBidAdapter.js | 10 +++++----- modules/appnexusAstBidAdapter.js | 24 ++++++++++++------------ modules/criteoBidAdapter.js | 4 ++-- modules/gumgumBidAdapter.js | 4 ++-- modules/pulsepointLiteBidAdapter.js | 16 ++++++++-------- modules/unrulyBidAdapter.js | 10 +++++----- src/bidmanager.js | 6 +++--- src/native.js | 28 +++++++++++++++++----------- 8 files changed, 54 insertions(+), 48 deletions(-) diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index ccb0287a866..476cb5989e0 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -88,26 +88,26 @@ function AdxcgAdapter() { let nativeResponse = adxcgBidReponse.nativeResponse; - bid.native = { + bid['native'] = { clickUrl: escape(nativeResponse.link.url), impressionTrackers: nativeResponse.imptrackers }; nativeResponse.assets.forEach(asset => { if (asset.title && asset.title.text) { - bid.native.title = asset.title.text; + bid['native'].title = asset.title.text; } if (asset.img && asset.img.url) { - bid.native.image = asset.img.url; + bid['native'].image = asset.img.url; } if (asset.data && asset.data.label == 'DESC' && asset.data.value) { - bid.native.body = asset.data.value; + bid['native'].body = asset.data.value; } if (asset.data && asset.data.label == 'SPONSORED' && asset.data.value) { - bid.native.sponsoredBy = asset.data.value; + bid['native'].sponsoredBy = asset.data.value; } }); } diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index 326d423d192..50fb328b35d 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -189,17 +189,17 @@ function newBid(serverBid, rtbBid) { bid.adResponse.ad = bid.adResponse.ads[0]; bid.adResponse.ad.video = bid.adResponse.ad.rtb.video; } - } else if (rtbBid.rtb.native) { - const native = rtbBid.rtb.native; - bid.native = { - title: native.title, - body: native.desc, - cta: native.ctatext, - sponsoredBy: native.sponsored, - image: native.main_img && native.main_img.url, - icon: native.icon && native.icon.url, - clickUrl: native.link.url, - impressionTrackers: native.impression_trackers, + } else if (rtbBid.rtb['native']) { + const nativeAd = rtbBid.rtb['native']; + bid['native'] = { + title: nativeAd.title, + body: nativeAd.desc, + cta: nativeAd.ctatext, + sponsoredBy: nativeAd.sponsored, + image: nativeAd.main_img && nativeAd.main_img.url, + icon: nativeAd.icon && nativeAd.icon.url, + clickUrl: nativeAd.link.url, + impressionTrackers: nativeAd.impression_trackers, }; } else { Object.assign(bid, { @@ -286,7 +286,7 @@ function bidToTag(bid) { ); }); - tag.native = {layouts: [nativeRequest]}; + tag['native'] = {layouts: [nativeRequest]}; } } diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index f46c3dc2fd1..7adbe6f6ae6 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -148,13 +148,13 @@ var CriteoAdapter = function CriteoAdapter() { bidObject.cpm = bidResponse.cpm; // in case of native - if (slot.nativeCallback && bidResponse.native) { + if (slot.nativeCallback && bidResponse['native']) { if (typeof slot.nativeCallback !== 'function') { utils.logError('Criteo bid: nativeCallback parameter is not a function'); } else { // store the callbacks in a global object window.criteo_pubtag.native_slots = window.criteo_pubtag.native_slots || {}; - window.criteo_pubtag.native_slots['' + bidObject.adId] = { callback: slot.nativeCallback, nativeResponse: bidResponse.native }; + window.criteo_pubtag.native_slots['' + bidObject.adId] = { callback: slot.nativeCallback, nativeResponse: bidResponse['native'] }; // this code is executed in an iframe, we need to get a reference to the // publishertag in the main window to retrieve native responses and callbacks. diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 17cc04c9752..521dfabfb2d 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -63,7 +63,7 @@ const GumgumAdapter = function GumgumAdapter() { } = bidRequest; const timestamp = _getTimeStamp(); const trackingId = params.inScreen; - const nativeId = params.native; + const nativeId = params['native']; const slotId = params.inSlot; const bid = { tmax: $$PREBID_GLOBAL$$.cbTimeout }; @@ -72,7 +72,7 @@ const GumgumAdapter = function GumgumAdapter() { case !!(params.inImage): bid.pi = 1; break; case !!(params.inScreen): bid.pi = 2; break; case !!(params.inSlot): bid.pi = 3; break; - case !!(params.native): bid.pi = 5; break; + case !!(params['native']): bid.pi = 5; break; default: return utils.logWarn( `[GumGum] No product selected for the placement ${placementCode}` + ', please check your implementation.' diff --git a/modules/pulsepointLiteBidAdapter.js b/modules/pulsepointLiteBidAdapter.js index a9485823efc..c80e45eb2f0 100644 --- a/modules/pulsepointLiteBidAdapter.js +++ b/modules/pulsepointLiteBidAdapter.js @@ -72,7 +72,7 @@ function PulsePointLiteAdapter() { bid.cpm = idToBidMap[id].price; bid.adId = id; if (isNative(idToSlotMap[id])) { - bid.native = nativeResponse(idToSlotMap[id], idToBidMap[id]); + bid['native'] = nativeResponse(idToSlotMap[id], idToBidMap[id]); bid.mediaType = 'native'; } else { bid.ad = idToBidMap[id].adm; @@ -96,7 +96,7 @@ function PulsePointLiteAdapter() { return { id: slot.bidId, banner: banner(slot), - native: native(slot), + 'native': nativeImpression(slot), tagid: slot.params.ct.toString(), }; } @@ -115,7 +115,7 @@ function PulsePointLiteAdapter() { /** * Produces an OpenRTB Native object for the slot given. */ - function native(slot) { + function nativeImpression(slot) { if (slot.nativeParams) { const assets = []; addAsset(assets, titleAsset(assets.length + 1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); @@ -255,18 +255,18 @@ function PulsePointLiteAdapter() { if (slot.nativeParams) { const nativeAd = parse(bid.adm); const keys = {}; - if (nativeAd && nativeAd.native && nativeAd.native.assets) { - nativeAd.native.assets.forEach((asset) => { + if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) { + nativeAd['native'].assets.forEach((asset) => { keys.title = asset.title ? asset.title.text : keys.title; keys.body = asset.data && asset.data.type === 2 ? asset.data.value : keys.body; keys.sponsoredBy = asset.data && asset.data.type === 1 ? asset.data.value : keys.sponsoredBy; keys.image = asset.img && asset.img.type === 3 ? asset.img.url : keys.image; keys.icon = asset.img && asset.img.type === 1 ? asset.img.url : keys.icon; }); - if (nativeAd.native.link) { - keys.clickUrl = encodeURIComponent(nativeAd.native.link.url); + if (nativeAd['native'].link) { + keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url); } - keys.impressionTrackers = nativeAd.native.imptrackers; + keys.impressionTrackers = nativeAd['native'].imptrackers; return keys; } } diff --git a/modules/unrulyBidAdapter.js b/modules/unrulyBidAdapter.js index fd9e94859c9..0f6b6e40901 100644 --- a/modules/unrulyBidAdapter.js +++ b/modules/unrulyBidAdapter.js @@ -8,20 +8,20 @@ import adaptermanager from 'src/adaptermanager' function createRenderHandler({ bidResponseBid, rendererConfig }) { function createApi() { - parent.window.unruly.native.prebid = parent.window.unruly.native.prebid || {} - parent.window.unruly.native.prebid.uq = parent.window.unruly.native.prebid.uq || [] + parent.window.unruly['native'].prebid = parent.window.unruly['native'].prebid || {} + parent.window.unruly['native'].prebid.uq = parent.window.unruly['native'].prebid.uq || [] return { render(bidResponseBid) { - parent.window.unruly.native.prebid.uq.push(['render', bidResponseBid]) + parent.window.unruly['native'].prebid.uq.push(['render', bidResponseBid]) }, onLoaded(bidResponseBid) {} } } parent.window.unruly = parent.window.unruly || {} - parent.window.unruly.native = parent.window.unruly.native || {} - parent.window.unruly.native.siteId = parent.window.unruly.native.siteId || rendererConfig.siteId + parent.window.unruly['native'] = parent.window.unruly['native'] || {} + parent.window.unruly['native'].siteId = parent.window.unruly['native'].siteId || rendererConfig.siteId const api = createApi() return { diff --git a/src/bidmanager.js b/src/bidmanager.js index d561a855000..800a0fe9579 100644 --- a/src/bidmanager.js +++ b/src/bidmanager.js @@ -276,10 +276,10 @@ function getKeyValueTargetingPairs(bidderCode, custBidObj) { } // set native key value targeting - if (custBidObj.native) { - Object.keys(custBidObj.native).forEach(asset => { + if (custBidObj['native']) { + Object.keys(custBidObj['native']).forEach(asset => { const key = NATIVE_KEYS[asset]; - const value = custBidObj.native[asset]; + const value = custBidObj['native'][asset]; if (key) { keyValues[key] = value; } }); } diff --git a/src/native.js b/src/native.js index 8a46fa07dbc..b6edabbde07 100644 --- a/src/native.js +++ b/src/native.js @@ -17,12 +17,12 @@ export const NATIVE_TARGETING_KEYS = Object.keys(NATIVE_KEYS).map( ); const IMAGE = { - image: {required: true}, - title: {required: true}, - sponsoredBy: {required: true}, - clickUrl: {required: true}, - body: {required: false}, - icon: {required: false}, + image: { required: true }, + title: { required: true }, + sponsoredBy: { required: true }, + clickUrl: { required: true }, + body: { required: false }, + icon: { required: false }, }; const SUPPORTED_TYPES = { @@ -70,15 +70,21 @@ export const hasNonNativeBidder = adUnit => */ export function nativeBidIsValid(bid) { const bidRequest = getBidRequest(bid.adId); - if (!bidRequest) { return false; } + if (!bidRequest) { + return false; + } const requestedAssets = bidRequest.nativeParams; - if (!requestedAssets) { return true; } + if (!requestedAssets) { + return true; + } const requiredAssets = Object.keys(requestedAssets).filter( key => requestedAssets[key].required ); - const returnedAssets = Object.keys(bid.native).filter(key => bid.native[key]); + const returnedAssets = Object.keys(bid['native']).filter( + key => bid['native'][key] + ); return requiredAssets.every(asset => returnedAssets.includes(asset)); } @@ -88,8 +94,8 @@ export function nativeBidIsValid(bid) { * impression tracker urls for the given ad object and fires them. */ export function fireNativeImpressions(adObject) { - const impressionTrackers = adObject.native && - adObject.native.impressionTrackers; + const impressionTrackers = + adObject['native'] && adObject['native'].impressionTrackers; (impressionTrackers || []).forEach(tracker => { triggerPixel(tracker); From 68454d2ab4a453815d73086ba818baa02ce2bc95 Mon Sep 17 00:00:00 2001 From: jbAdyoulike Date: Thu, 28 Sep 2017 23:19:57 +0200 Subject: [PATCH 11/54] Add transaction ids to adyoulike adapter (#1611) --- modules/adyoulikeBidAdapter.js | 9 ++++--- test/spec/modules/adyoulikeBidAdapter_spec.js | 27 ++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 0ba497c5eed..5192270a94e 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -8,7 +8,7 @@ import { STATUS } from 'src/constants'; import adaptermanager from 'src/adaptermanager'; var AdyoulikeAdapter = function AdyoulikeAdapter() { - const _VERSION = '0.1'; + const _VERSION = '0.2'; const baseAdapter = new Adapter('adyoulike'); @@ -21,7 +21,7 @@ var AdyoulikeAdapter = function AdyoulikeAdapter() { const placements = validBids.map(bid => bid.params.placement); if (!utils.isEmpty(placements)) { - const body = createBody(placements); + const body = createBody(bidRequests, placements); const endpoint = createEndpoint(); ajax(endpoint, (response) => { @@ -61,10 +61,11 @@ var AdyoulikeAdapter = function AdyoulikeAdapter() { } /* Create request body */ - function createBody(placements) { + function createBody(bidRequests, placements) { const body = { Version: _VERSION, Placements: placements, + TransactionIds: {} }; // performance isn't supported by mobile safari iOS7. window.performance works, but @@ -80,6 +81,8 @@ var AdyoulikeAdapter = function AdyoulikeAdapter() { body.PageRefreshed = false; } + placements.forEach(placement => { body.TransactionIds[placement] = bidRequests[placement].transactionId; }); + return JSON.stringify(body); } diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index 8911762b583..05c112c2a99 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -29,7 +29,8 @@ describe('Adyoulike Adapter', () => { 'placementCode': 'adunit/hb-0', 'params': { 'placement': 'placement_0' - } + }, + 'transactionId': 'bid_id_0_transaction_id' } ], }; @@ -43,7 +44,8 @@ describe('Adyoulike Adapter', () => { 'params': { 'placement': 'placement_0' }, - 'sizes': '300x250' + 'sizes': '300x250', + 'transactionId': 'bid_id_0_transaction_id' } ], }; @@ -57,7 +59,8 @@ describe('Adyoulike Adapter', () => { 'params': { 'placement': 'placement_0' }, - 'sizes': '300x250' + 'sizes': '300x250', + 'transactionId': 'bid_id_0_transaction_id' }, { 'bidId': 'bid_id_1', @@ -66,14 +69,16 @@ describe('Adyoulike Adapter', () => { 'params': { 'placement': 'placement_1' }, - 'sizes': [[300, 600]] + 'sizes': [[300, 600]], + 'transactionId': 'bid_id_1_transaction_id' }, { 'bidId': 'bid_id_2', 'bidder': 'adyoulike', 'placementCode': 'adunit/hb-2', 'params': {}, - 'sizes': '300x400' + 'sizes': '300x400', + 'transactionId': 'bid_id_2_transaction_id' }, { 'bidId': 'bid_id_3', @@ -81,7 +86,8 @@ describe('Adyoulike Adapter', () => { 'placementCode': 'adunit/hb-3', 'params': { 'placement': 'placement_3' - } + }, + 'transactionId': 'bid_id_3_transaction_id' } ], }; @@ -175,9 +181,10 @@ describe('Adyoulike Adapter', () => { expect(requests[0].url).to.contains('CanonicalUrl=' + encodeURIComponent(canonicalUrl)); let body = JSON.parse(requests[0].requestBody); - expect(body.Version).to.equal('0.1'); + expect(body.Version).to.equal('0.2'); expect(body.Placements).deep.equal(['placement_0']); expect(body.PageRefreshed).to.equal(false); + expect(body.TransactionIds).deep.equal({'placement_0': 'bid_id_0_transaction_id'}); }); it('sends bid request to endpoint with single placement without canonical', () => { @@ -190,9 +197,10 @@ describe('Adyoulike Adapter', () => { expect(requests[0].url).to.not.contains('CanonicalUrl=' + encodeURIComponent(canonicalUrl)); let body = JSON.parse(requests[0].requestBody); - expect(body.Version).to.equal('0.1'); + expect(body.Version).to.equal('0.2'); expect(body.Placements).deep.equal(['placement_0']); expect(body.PageRefreshed).to.equal(false); + expect(body.TransactionIds).deep.equal({'placement_0': 'bid_id_0_transaction_id'}); }); it('sends bid request to endpoint with multiple placements', () => { @@ -203,9 +211,10 @@ describe('Adyoulike Adapter', () => { expect(requests[0].url).to.contains('CanonicalUrl=' + encodeURIComponent(canonicalUrl)); let body = JSON.parse(requests[0].requestBody); - expect(body.Version).to.equal('0.1'); + expect(body.Version).to.equal('0.2'); expect(body.Placements).deep.equal(['placement_0', 'placement_1']); expect(body.PageRefreshed).to.equal(false); + expect(body.TransactionIds).deep.equal({'placement_0': 'bid_id_0_transaction_id', 'placement_1': 'bid_id_1_transaction_id'}); }); }); From 6a60f21edbb65fb416b40a315775161c8bd16136 Mon Sep 17 00:00:00 2001 From: Justin Grimes Date: Fri, 29 Sep 2017 10:03:49 -0400 Subject: [PATCH 12/54] Added onefiftytwo alias for serverbid adapter (#1610) --- modules/serverbidBidAdapter.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/serverbidBidAdapter.js b/modules/serverbidBidAdapter.js index 5a6fa385877..f5044fe4ae1 100644 --- a/modules/serverbidBidAdapter.js +++ b/modules/serverbidBidAdapter.js @@ -17,6 +17,10 @@ ServerBidAdapter = function ServerBidAdapter() { 'connectad': { 'BASE_URI': 'https://i.connectad.io/api/v2', 'SMARTSYNC_BASE_URI': 'https://s.zkcdn.net/ss' + }, + 'onefiftytwo': { + 'BASE_URI': 'https://e.serverbid.com/api/v2', + 'SMARTSYNC_BASE_URI': 'https://s.zkcdn.net/ss' } }; @@ -199,5 +203,6 @@ ServerBidAdapter.createNew = function() { adaptermanager.registerBidAdapter(new ServerBidAdapter(), 'serverbid'); adaptermanager.aliasBidAdapter('serverbid', 'connectad'); +adaptermanager.aliasBidAdapter('serverbid', 'onefiftytwo'); module.exports = ServerBidAdapter; From f9fa75972fdc954bcfa76ed285df5210e4051022 Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Fri, 29 Sep 2017 07:04:52 -0700 Subject: [PATCH 13/54] Use mediaTypes on video examples (#1638) --- integrationExamples/gpt/pbjs_video_adUnit.html | 2 +- test/pages/video.html | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/integrationExamples/gpt/pbjs_video_adUnit.html b/integrationExamples/gpt/pbjs_video_adUnit.html index 0895e2a9622..080ca9be142 100644 --- a/integrationExamples/gpt/pbjs_video_adUnit.html +++ b/integrationExamples/gpt/pbjs_video_adUnit.html @@ -36,7 +36,7 @@ var videoAdUnit = { code: 'video1', sizes: [640,480], - mediaType: 'video', + mediaTypes: { video: {} }, bids: [ { bidder: 'appnexusAst', diff --git a/test/pages/video.html b/test/pages/video.html index 755815111d1..8d28650cbfc 100644 --- a/test/pages/video.html +++ b/test/pages/video.html @@ -31,7 +31,9 @@ var videoAdUnit = { code: 'video1', sizes: [640,480], - mediaType: 'video', + mediaTypes: { + video: {context: 'instream'} + }, bids: [ { bidder: 'appnexusAst', From 0038e8e853cd55b561205bc1221177e1c3bbb24d Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Fri, 29 Sep 2017 17:27:30 -0600 Subject: [PATCH 14/54] Fix to add proper --tag when doing gulp build or bundle (#1635) --- gulpfile.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index aceed6046fa..f80cfe91d34 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -88,14 +88,22 @@ function bundle(dev, moduleArr) { var entries = [helpers.getBuiltPrebidCoreFile(dev)].concat(helpers.getBuiltModules(dev, modules)); + var outputFileName = argv.bundleName ? argv.bundleName : 'prebid.js'; + + // change output filename if argument --tag given + if (argv.tag && argv.tag.length) { + outputFileName = outputFileName.replace(/\.js$/, `.${argv.tag}.js`); + } + gutil.log('Concatenating files:\n', entries); gutil.log('Appending ' + prebid.globalVarName + '.processQueue();'); + gutil.log('Generating bundle:', outputFileName); return gulp.src( entries ) .pipe(gulpif(dev, sourcemaps.init({loadMaps: true}))) - .pipe(concat(argv.bundleName ? argv.bundleName : 'prebid.js')) + .pipe(concat(outputFileName)) .pipe(gulpif(!argv.manualEnable, footer('\n<%= global %>.processQueue();', { global: prebid.globalVarName } @@ -142,11 +150,6 @@ gulp.task('devpack', ['clean'], function () { gulp.task('webpack', ['clean'], function () { var cloned = _.cloneDeep(webpackConfig); - // change output filename if argument --tag given - if (argv.tag && argv.tag.length) { - cloned.output.filename = 'prebid.' + argv.tag + '.js'; - } - delete cloned.devtool; var externalModules = helpers.getArgModules(); From b2da81d31eff10e156ea19d0db8eb4a02345502b Mon Sep 17 00:00:00 2001 From: Yann Pravo Date: Tue, 3 Oct 2017 18:43:17 +0200 Subject: [PATCH 15/54] http -> https (#1651) --- modules/adomikAnalyticsAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/adomikAnalyticsAdapter.js b/modules/adomikAnalyticsAdapter.js index 46e917b8a3a..c9bd7990ec8 100644 --- a/modules/adomikAnalyticsAdapter.js +++ b/modules/adomikAnalyticsAdapter.js @@ -117,7 +117,7 @@ adomikAdapter.sendTypedEvent = function() { splittedUrl.forEach((split, i) => { const partUrl = `${split}&id=${adomikAdapter.currentContext.id}&part=${i}&on=${splittedUrl.length - 1}`; const img = new Image(1, 1); - img.src = 'http://' + adomikAdapter.currentContext.url + '/?q=' + partUrl; + img.src = 'https://' + adomikAdapter.currentContext.url + '/?q=' + partUrl; }) }; From 2fd1fd722bb548aa1dbd0d3b11a9a3c4539662bf Mon Sep 17 00:00:00 2001 From: "N. Faure" Date: Tue, 3 Oct 2017 18:59:24 +0200 Subject: [PATCH 16/54] Add publisher sub-id support to the Criteo adapter (#1629) --- modules/criteoBidAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 7adbe6f6ae6..f89aa077349 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -56,8 +56,8 @@ var CriteoAdapter = function CriteoAdapter() { var w = parseInt(sizeString.substring(0, xIndex)); var h = parseInt(sizeString.substring(xIndex + 1, sizeString.length)) return new Criteo.PubTag.DirectBidding.Size(w, h); - } - ) + }), + bid.params.publisherSubId ) ); From e508a649bb615ac952ddccf12c3f77b7117b8910 Mon Sep 17 00:00:00 2001 From: adxcgcom <31470944+adxcgcom@users.noreply.github.com> Date: Tue, 3 Oct 2017 19:04:37 +0200 Subject: [PATCH 17/54] Adxcg analytics adapter (#1599) --- modules/adxcgAnalyticsAdapter.js | 94 +++++++++++++++++++ .../modules/adxcgAnalyticsAdapter_spec.js | 81 ++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 modules/adxcgAnalyticsAdapter.js create mode 100644 test/spec/modules/adxcgAnalyticsAdapter_spec.js diff --git a/modules/adxcgAnalyticsAdapter.js b/modules/adxcgAnalyticsAdapter.js new file mode 100644 index 00000000000..5d2040c8e08 --- /dev/null +++ b/modules/adxcgAnalyticsAdapter.js @@ -0,0 +1,94 @@ +import {ajax} from 'src/ajax'; +import adapter from 'src/AnalyticsAdapter'; +import adaptermanager from 'src/adaptermanager'; +import * as url from 'src/url'; +import * as utils from 'src/utils'; + +const emptyUrl = ''; +const analyticsType = 'endpoint'; +const adxcgAnalyticsVersion = 'v1.04'; + +let initOptions; +let auctionTimestamp; +let events = { + bidRequests: [], + bidResponses: [] +}; + +var adxcgAnalyticsAdapter = Object.assign(adapter( + { + emptyUrl, + analyticsType + }), { + track({eventType, args}) { + if (typeof args !== 'undefined') { + if (eventType === 'bidTimeout') { + events.bidTimeout = args; + } else if (eventType === 'auctionInit') { + events.auctionInit = args; + auctionTimestamp = args.timestamp; + } else if (eventType === 'bidRequested') { + events.bidRequests.push(args); + } else if (eventType === 'bidResponse') { + events.bidResponses.push(mapBidResponse(args)); + } else if (eventType === 'bidWon') { + send({ + bidWon: mapBidResponse(args) + }); + } + } + + if (eventType === 'auctionEnd') { + send(events); + } + } +}); + +function mapBidResponse(bidResponse) { + return { + adUnitCode: bidResponse.adUnitCode, + statusMessage: bidResponse.statusMessage, + bidderCode: bidResponse.bidderCode, + adId: bidResponse.adId, + mediaType: bidResponse.mediaType, + creative_id: bidResponse.creative_id, + width: bidResponse.width, + height: bidResponse.height, + cpm: bidResponse.cpm, + timeToRespond: bidResponse.timeToRespond + }; +} + +function send(data) { + data.initOptions = initOptions; + data.auctionTimestamp = auctionTimestamp; + + let location = utils.getTopWindowLocation(); + let secure = location.protocol == 'https:'; + + let adxcgAnalyticsRequestUrl = url.format({ + protocol: secure ? 'https' : 'http', + hostname: secure ? 'hbarxs.adxcg.net' : 'hbarx.adxcg.net', + pathname: '/pbrx', + search: { + auctionTimestamp: auctionTimestamp, + adxcgAnalyticsVersion: adxcgAnalyticsVersion, + prebidVersion: $$PREBID_GLOBAL$$.version + } + }); + + ajax(adxcgAnalyticsRequestUrl, undefined, JSON.stringify(data), {method: 'POST'}); +} + +adxcgAnalyticsAdapter.originEnableAnalytics = adxcgAnalyticsAdapter.enableAnalytics; +adxcgAnalyticsAdapter.enableAnalytics = function (config) { + initOptions = config.options; + adxcgAnalyticsAdapter.originEnableAnalytics(config); +}; + +adaptermanager.registerAnalyticsAdapter({ + adapter: adxcgAnalyticsAdapter, + code: 'adxcg' +}); + +export default adxcgAnalyticsAdapter; diff --git a/test/spec/modules/adxcgAnalyticsAdapter_spec.js b/test/spec/modules/adxcgAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..790a39789b2 --- /dev/null +++ b/test/spec/modules/adxcgAnalyticsAdapter_spec.js @@ -0,0 +1,81 @@ +import adxcgAnalyticsAdapter from 'modules/adxcgAnalyticsAdapter'; +import { expect } from 'chai'; +let adaptermanager = require('src/adaptermanager'); +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('adxcg analytics adapter', () => { + let xhr; + let requests; + + beforeEach(() => { + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = request => requests.push(request); + }); + + afterEach(() => { + xhr.restore(); + }); + + describe('track', () => { + it('builds and sends auction data', () => { + let auctionTimestamp = 42; + let initOptions = { + publisherId: '42' + }; + let bidRequest = { + requestId: 'requestIdData' + }; + let bidResponse = { + adId: 'adIdData', + ad: 'adContent' + }; + + adaptermanager.registerAnalyticsAdapter({ + code: 'adxcg', + adapter: adxcgAnalyticsAdapter + }); + + adaptermanager.enableAnalytics({ + provider: 'adxcg', + options: initOptions + }); + + events.emit(constants.EVENTS.AUCTION_INIT, { + timestamp: auctionTimestamp + }); + events.emit(constants.EVENTS.BID_REQUESTED, bidRequest); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponse); + events.emit(constants.EVENTS.AUCTION_END, {}); + + expect(requests.length).to.equal(1); + + let auctionEventData = JSON.parse(requests[0].requestBody); + + expect(auctionEventData.bidRequests.length).to.equal(1); + expect(auctionEventData.bidRequests[0]).to.deep.equal(bidRequest); + + expect(auctionEventData.bidResponses.length).to.equal(1); + expect(auctionEventData.bidResponses[0].adId).to.equal(bidResponse.adId); + expect(auctionEventData.bidResponses[0]).to.not.have.property('ad'); + + expect(auctionEventData.initOptions).to.deep.equal(initOptions); + expect(auctionEventData.auctionTimestamp).to.equal(auctionTimestamp); + + events.emit(constants.EVENTS.BID_WON, { + adId: 'adIdData', + ad: 'adContent' + }); + + expect(requests.length).to.equal(2); + + let winEventData = JSON.parse(requests[1].requestBody); + + expect(winEventData.bidWon.adId).to.equal(bidResponse.adId); + expect(winEventData.bidWon).to.not.have.property('ad'); + expect(winEventData.initOptions).to.deep.equal(initOptions); + expect(winEventData.auctionTimestamp).to.equal(auctionTimestamp); + }); + }); +}); From 3ef99d0a2477e209f5d4ef7bee950a2c1479151b Mon Sep 17 00:00:00 2001 From: Connor Doherty Date: Tue, 3 Oct 2017 13:04:59 -0400 Subject: [PATCH 18/54] Add placementId request param to Yieldmo bid adapter (#1632) * Add ym_placement_id request param to yieldmo bid adapter * Add yieldmo placementId param to integration examples --- integrationExamples/gpt/pbjs_example_gpt.html | 4 +++- modules/yieldmoBidAdapter.js | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/integrationExamples/gpt/pbjs_example_gpt.html b/integrationExamples/gpt/pbjs_example_gpt.html index 1a23a235d40..e670c6f5b93 100644 --- a/integrationExamples/gpt/pbjs_example_gpt.html +++ b/integrationExamples/gpt/pbjs_example_gpt.html @@ -340,7 +340,9 @@ }, { bidder: 'yieldmo', - params: {} + params: { + placementId: 'ym_12341234' // Optional Yieldmo Placement ID + } }, { bidder: 'adequant', diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 75077c65fd7..d311bb5722c 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -51,6 +51,10 @@ var YieldmoAdapter = function YieldmoAdapter() { placement.placement_id = bid.placementCode; placement.sizes = bid.sizes; + if (bid.params && bid.params.placementId) { + placement.ym_placement_id = bid.params.placementId; + } + placements.push(placement); } From 6b6a1fbb8354d05fa2fd445278b9241c69040081 Mon Sep 17 00:00:00 2001 From: harpere Date: Tue, 3 Oct 2017 13:08:03 -0400 Subject: [PATCH 19/54] fixed PBS cookie syncs (#1637) * fixed PBS cookie syncs * added comment --- modules/prebidServerBidAdapter.js | 26 ++++++++-- src/userSync.js | 4 +- .../modules/prebidServerBidAdapter_spec.js | 48 +++++++++++++++++-- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index d68406b016c..1c226ccfe2b 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -4,7 +4,6 @@ import bidmanager from 'src/bidmanager'; import * as utils from 'src/utils'; import { ajax } from 'src/ajax'; import { STATUS, S2S } from 'src/constants'; -import { userSync } from 'src/userSync.js'; import { cookieSet } from 'src/cookie.js'; import adaptermanager from 'src/adaptermanager'; import { config } from 'src/config'; @@ -135,6 +134,27 @@ function PrebidServer() { return unit.sizes && unit.sizes.length; } + /** + * Run a cookie sync for the given type, url, and bidder + * + * @param {string} type the type of sync, "image", "redirect", "iframe" + * @param {string} url the url to sync + * @param {string} bidder name of bidder doing sync for + */ + function doBidderSync(type, url, bidder) { + if (!url) { + utils.logError(`No sync url for bidder "${bidder}": ${url}`); + } else if (type === 'image' || type === 'redirect') { + utils.logMessage(`Invoking image pixel user sync for bidder: "${bidder}"`); + utils.triggerPixel(url); + } else if (type == 'iframe') { + utils.logMessage(`Invoking iframe user sync for bidder: "${bidder}"`); + utils.insertUserSyncIframe(url); + } else { + utils.logError(`User sync type "${type}" not supported for bidder: "${bidder}"`); + } + } + /* Notify Prebid of bid responses so bids can get in the auction */ function handleResponse(response, requestedBidders) { let result; @@ -145,7 +165,7 @@ function PrebidServer() { if (result.bidder_status) { result.bidder_status.forEach(bidder => { if (bidder.no_cookie && !_cookiesQueued) { - userSync.registerSync(bidder.usersync.type, bidder.bidder, bidder.usersync.url); + doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder); } }); } @@ -230,7 +250,7 @@ function PrebidServer() { if (response.status === 'ok') { bidderCodes.forEach(code => StorageManager.add(pbjsSyncsKey, code, true)); } - response.bidder_status.forEach(bidder => queueSync({bidder: bidder.bidder, url: bidder.usersync.url, type: bidder.usersync.type})); + response.bidder_status.forEach(bidder => doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder)); } catch (e) { utils.logError(e); } diff --git a/src/userSync.js b/src/userSync.js index 9433590ade0..8fb8c04cd24 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -94,7 +94,7 @@ export function newUserSync(userSyncDependencies) { utils.shuffle(queue.iframe).forEach((sync) => { let [bidderName, iframeUrl] = sync; utils.logMessage(`Invoking iframe user sync for bidder: ${bidderName}`); - // Create image object and add the src url + // Insert iframe into DOM utils.insertUserSyncIframe(iframeUrl); }); } @@ -130,7 +130,7 @@ export function newUserSync(userSyncDependencies) { */ publicApi.registerSync = (type, bidder, url) => { if (!usConfig.syncEnabled || !utils.isArray(queue[type])) { - return utils.logWarn(`User sync type "{$type}" not supported`); + return utils.logWarn(`User sync type "${type}" not supported`); } if (!bidder) { return utils.logWarn(`Bidder is required for registering sync`); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 44850249adc..5ac21578ea7 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -153,6 +153,26 @@ const RESPONSE_NO_PBS_COOKIE = { }] }; +const RESPONSE_NO_PBS_COOKIE_ERROR = { + 'tid': '882fe33e-2981-4257-bd44-bd3b0394545f', + 'status': 'no_cookie', + 'bidder_status': [{ + 'bidder': 'rubicon', + 'no_cookie': true, + 'usersync': { + 'url': 'https://pixel.rubiconproject.com/exchange/sync.php?p=prebid', + 'type': 'jsonp' + } + }, { + 'bidder': 'pubmatic', + 'no_cookie': true, + 'usersync': { + 'url': '', + 'type': 'iframe' + } + }] +}; + describe('S2S Adapter', () => { let adapter; @@ -219,7 +239,9 @@ describe('S2S Adapter', () => { beforeEach(() => { server = sinon.fakeServer.create(); - sinon.stub(userSync, 'registerSync'); + sinon.stub(utils, 'triggerPixel'); + sinon.stub(utils, 'insertUserSyncIframe'); + sinon.stub(utils, 'logError'); sinon.stub(cookie, 'cookieSet'); sinon.stub(bidmanager, 'addBidResponse'); sinon.stub(utils, 'getBidderRequestAllAdUnits').returns({ @@ -238,7 +260,9 @@ describe('S2S Adapter', () => { bidmanager.addBidResponse.restore(); utils.getBidderRequestAllAdUnits.restore(); utils.getBidRequest.restore(); - userSync.registerSync.restore(); + utils.triggerPixel.restore(); + utils.insertUserSyncIframe.restore(); + utils.logError.restore(); cookie.cookieSet.restore(); }); @@ -379,13 +403,29 @@ describe('S2S Adapter', () => { expect(bid_request_passed).to.have.property('adId', '123'); }); - it('queue cookie sync when no_cookie response', () => { + it('does cookie sync when no_cookie response', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); adapter.setConfig(CONFIG); adapter.callBids(REQUEST); server.respond(); - sinon.assert.calledTwice(userSync.registerSync); + + sinon.assert.calledOnce(utils.triggerPixel); + sinon.assert.calledWith(utils.triggerPixel, 'https://pixel.rubiconproject.com/exchange/sync.php?p=prebid'); + sinon.assert.calledOnce(utils.insertUserSyncIframe); + sinon.assert.calledWith(utils.insertUserSyncIframe, '//ads.pubmatic.com/AdServer/js/user_sync.html?predirect=https%3A%2F%2Fprebid.adnxs.com%2Fpbs%2Fv1%2Fsetuid%3Fbidder%3Dpubmatic%26uid%3D'); + }); + + it('logs error when no_cookie response is missing type or url', () => { + server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE_ERROR)); + + adapter.setConfig(CONFIG); + adapter.callBids(REQUEST); + server.respond(); + + sinon.assert.notCalled(utils.triggerPixel); + sinon.assert.notCalled(utils.insertUserSyncIframe); + sinon.assert.calledTwice(utils.logError); }); it('does not call cookieSet cookie sync when no_cookie response && not opted in', () => { From c0a608d8cd449b76812b132be598dea878165c62 Mon Sep 17 00:00:00 2001 From: Oz Weiss Date: Tue, 3 Oct 2017 20:08:35 +0300 Subject: [PATCH 20/54] Add ignore-loader to handle .md files (#1646) --- package.json | 3 ++- webpack.conf.js | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c5017e3f149..80eedcfa1b4 100644 --- a/package.json +++ b/package.json @@ -48,11 +48,11 @@ "gulp-clean": "^0.3.2", "gulp-concat": "^2.6.0", "gulp-connect": "^5.0.0", + "gulp-documentation": "^3.2.1", "gulp-eslint": "^4.0.0", "gulp-footer": "^1.0.5", "gulp-header": "^1.7.1", "gulp-if": "^2.0.2", - "gulp-documentation": "^3.2.1", "gulp-optimize-js": "^1.1.0", "gulp-rename": "^1.2.0", "gulp-replace": "^0.4.0", @@ -60,6 +60,7 @@ "gulp-uglify": "^3.0.0", "gulp-util": "^3.0.0", "gulp-webdriver": "^1.0.1", + "ignore-loader": "^0.1.2", "istanbul": "^0.4.5", "istanbul-instrumenter-loader": "^3.0.0", "json-loader": "^0.5.1", diff --git a/webpack.conf.js b/webpack.conf.js index 59d03790a4b..68db3a389f2 100644 --- a/webpack.conf.js +++ b/webpack.conf.js @@ -51,6 +51,10 @@ module.exports = { test: /\.json$/, loader: 'json-loader' }, + { + test: /\.md$/, + loader: 'ignore-loader' + }, { test: /constants.json$/, include: /(src)/, From 684ddf307508b4436e95e16aaed1807e6aa68ae6 Mon Sep 17 00:00:00 2001 From: Rich Loveland Date: Tue, 3 Oct 2017 14:32:44 -0400 Subject: [PATCH 21/54] Add JSDoc for `pbjs.getAllWinningBids` (#1566) * Add JSDoc for `pbjs.getAllWinningBids` * Add a `Bid` type; update return value This type will be useful in a few other places around the API docs (that work will happen on separate PRs). * s/Bid/AdapterBidResponse/g --- src/prebid.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/prebid.js b/src/prebid.js index 6dd1688b568..4d94fb475ce 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -654,6 +654,45 @@ $$PREBID_GLOBAL$$.enableSendAllBids = function () { config.setConfig({ enableSendAllBids: true }); }; +/** + * The bid response object returned by an external bidder adapter during the auction. + * @typedef {Object} AdapterBidResponse + * @property {string} pbAg Auto granularity price bucket; CPM <= 5 ? increment = 0.05 : CPM > 5 && CPM <= 10 ? increment = 0.10 : CPM > 10 && CPM <= 20 ? increment = 0.50 : CPM > 20 ? priceCap = 20.00. Example: `"0.80"`. + * @property {string} pbCg Custom price bucket. For example setup, see {@link setPriceGranularity}. Example: `"0.84"`. + * @property {string} pbDg Dense granularity price bucket; CPM <= 3 ? increment = 0.01 : CPM > 3 && CPM <= 8 ? increment = 0.05 : CPM > 8 && CPM <= 20 ? increment = 0.50 : CPM > 20? priceCap = 20.00. Example: `"0.84"`. + * @property {string} pbLg Low granularity price bucket; $0.50 increment, capped at $5, floored to two decimal places. Example: `"0.50"`. + * @property {string} pbMg Medium granularity price bucket; $0.10 increment, capped at $20, floored to two decimal places. Example: `"0.80"`. + * @property {string} pbHg High granularity price bucket; $0.01 increment, capped at $20, floored to two decimal places. Example: `"0.84"`. + * + * @property {string} bidder The string name of the bidder. This *may* be the same as the `bidderCode`. For For a list of all bidders and their codes, see [Bidders' Params](http://prebid.org/dev-docs/bidders.html). + * @property {string} bidderCode The unique string that identifies this bidder. For a list of all bidders and their codes, see [Bidders' Params](http://prebid.org/dev-docs/bidders.html). + * + * @property {string} requestId The [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) representing the bid request. + * @property {number} requestTimestamp The time at which the bid request was sent out, expressed in milliseconds. + * @property {number} responseTimestamp The time at which the bid response was received, expressed in milliseconds. + * @property {number} timeToRespond How long it took for the bidder to respond with this bid, expressed in milliseconds. + * + * @property {string} size The size of the ad creative, expressed in `"AxB"` format, where A and B are numbers of pixels. Example: `"320x50"`. + * @property {string} width The width of the ad creative in pixels. Example: `"320"`. + * @property {string} height The height of the ad creative in pixels. Example: `"50"`. + * + * @property {string} ad The actual ad creative content, often HTML with CSS, JavaScript, and/or links to additional content. Example: `"",`. + * @property {number} ad_id The ad ID of the creative, as understood by the bidder's system. Used by the line item's [creative in the ad server](http://prebid.org/adops/send-all-bids-adops.html#step-3-add-a-creative). + * @property {string} adUnitCode The code used to uniquely identify the ad unit on the publisher's page. + * + * @property {string} statusMessage The status of the bid. Allowed values: `"Bid available"` or `"Bid returned empty or error response"`. + * @property {number} cpm The exact bid price from the bidder, expressed to the thousandths place. Example: `"0.849"`. + * + * @property {Object} adserverTargeting An object whose values represent the ad server's targeting on the bid. + * @property {string} adserverTargeting.hb_adid The ad ID of the creative, as understood by the ad server. + * @property {string} adserverTargeting.hb_pb The price paid to show the creative, as logged in the ad server. + * @property {string} adserverTargeting.hb_bidder The winning bidder whose ad creative will be served by the ad server. +*/ + +/** + * Get all of the bids that have won their respective auctions. Useful for [troubleshooting your integration](http://prebid.org/dev-docs/prebid-troubleshooting-guide.html). + * @return {Array} A list of bids that have won their respective auctions. +*/ $$PREBID_GLOBAL$$.getAllWinningBids = function () { return $$PREBID_GLOBAL$$._winningBids; }; From fed1688b0d006ab73455716caf03bf433a438697 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Tue, 3 Oct 2017 12:38:14 -0600 Subject: [PATCH 22/54] rubicon converted to bidderFactory (#1624) * rubicon converted to bidderFactory plus few updates to factory * name change updates to bidderFactory * added transactionId => tid to rubicon request --- modules/rubiconBidAdapter.js | 534 +++++++++----------- src/adapters/bidderFactory.js | 26 +- test/spec/modules/rubiconBidAdapter_spec.js | 184 +++---- 3 files changed, 324 insertions(+), 420 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 598319faca1..526e0e129c2 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -1,12 +1,5 @@ -import Adapter from 'src/adapter'; -import bidfactory from 'src/bidfactory'; -import bidmanager from 'src/bidmanager'; -import adaptermanager from 'src/adaptermanager'; import * as utils from 'src/utils'; -import { ajax } from 'src/ajax'; -import { STATUS } from 'src/constants'; -import { userSync } from 'src/userSync'; -const RUBICON_BIDDER_CODE = 'rubicon'; +import { registerBidder } from 'src/adapters/bidderFactory'; // use deferred function call since version isn't defined yet at this point function getIntegration() { @@ -20,6 +13,7 @@ function isSecure() { // use protocol relative urls for http or https const FASTLANE_ENDPOINT = '//fastlane.rubiconproject.com/a/api/fastlane.json'; const VIDEO_ENDPOINT = '//fastlane-adv.rubiconproject.com/v1/auction/video'; +const SYNC_ENDPOINT = 'https://tap-secure.rubiconproject.com/partner/scripts/rubicon/emily.html?rtb_ext=1'; const TIMEOUT_BUFFER = 500; @@ -72,256 +66,176 @@ var sizeMap = { }; utils._each(sizeMap, (item, key) => sizeMap[item] = key); -function RubiconAdapter() { - var baseAdapter = new Adapter(RUBICON_BIDDER_CODE); - var hasUserSyncFired = false; - - function _callBids(bidderRequest) { - var bids = bidderRequest.bids || []; - - bids.forEach(bid => { - try { - // Video endpoint only accepts POST calls - if (bid.mediaType === 'video') { - ajax( - VIDEO_ENDPOINT, - { - success: bidCallback, - error: bidError - }, - buildVideoRequestPayload(bid, bidderRequest), - { - withCredentials: true - } - ); - } else { - ajax( - buildOptimizedCall(bid), - { - success: bidCallback, - error: bidError - }, - undefined, - { - withCredentials: true - } - ); - } - } catch (err) { - utils.logError('Error sending rubicon request for placement code ' + bid.placementCode, null, err); - addErrorBid(); - } - - function bidCallback(responseText) { - try { - utils.logMessage('XHR callback function called for ad ID: ' + bid.bidId); - handleRpCB(responseText, bid); - } catch (err) { - if (typeof err === 'string') { - utils.logWarn(`${err} when processing rubicon response for placement code ${bid.placementCode}`); - } else { - utils.logError('Error processing rubicon response for placement code ' + bid.placementCode, null, err); - } - addErrorBid(); - } - } - - function bidError(err, xhr) { - utils.logError('Request for rubicon responded with:', xhr.status, err); - addErrorBid(); - } - - function addErrorBid() { - let badBid = bidfactory.createBid(STATUS.NO_BID, bid); - badBid.bidderCode = baseAdapter.getBidderCode(); - bidmanager.addBidResponse(bid.placementCode, badBid); - } - }); - } - - function _getScreenResolution() { - return [window.screen.width, window.screen.height].join('x'); - } - - function _getDigiTrustQueryParams() { - function getDigiTrustId() { - let digiTrustUser = window.DigiTrust && ($$PREBID_GLOBAL$$.getConfig('digiTrustId') || window.DigiTrust.getUser({member: 'T9QSFKPDN9'})); - return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; +export const spec = { + code: 'rubicon', + aliases: ['rubiconLite'], + supportedMediaTypes: ['video'], + /** + * @param {object} bid + * @return boolean + */ + isBidRequestValid: function(bid) { + if (typeof bid.params !== 'object') { + return false; } - let digiTrustId = getDigiTrustId(); - // Verify there is an ID and this user has not opted out - if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) { - return []; - } - return [ - 'dt.id', digiTrustId.id, - 'dt.keyv', digiTrustId.keyv, - 'dt.pref', 0 - ]; - } - - function buildVideoRequestPayload(bid, bidderRequest) { - bid.startTime = new Date().getTime(); - let params = bid.params; - if (!params || typeof params.video !== 'object') { - throw 'Invalid Video Bid'; + if (!/^\d+$/.test(params.accountId)) { + return false; } - let size; - if (params.video.playerWidth && params.video.playerHeight) { - size = [ - params.video.playerWidth, - params.video.playerHeight - ]; - } else if ( - Array.isArray(bid.sizes) && bid.sizes.length > 0 && - Array.isArray(bid.sizes[0]) && bid.sizes[0].length > 1 - ) { - size = bid.sizes[0]; - } else { - throw 'Invalid Video Bid - No size provided'; - } - - let postData = { - page_url: !params.referrer ? utils.getTopWindowUrl() : params.referrer, - resolution: _getScreenResolution(), - account_id: params.accountId, - integration: getIntegration(), - timeout: bidderRequest.timeout - (Date.now() - bidderRequest.auctionStart + TIMEOUT_BUFFER), - stash_creatives: true, - ae_pass_through_parameters: params.video.aeParams, - slots: [] - }; - - // Define the slot object - let slotData = { - site_id: params.siteId, - zone_id: params.zoneId, - position: params.position || 'btf', - floor: parseFloat(params.floor) > 0.01 ? params.floor : 0.01, - element_id: bid.placementCode, - name: bid.placementCode, - language: params.video.language, - width: size[0], - height: size[1] - }; - - // check and add inventory, keywords, visitor and size_id data - if (params.video.size_id) { - slotData.size_id = params.video.size_id; - } else { - throw 'Invalid Video Bid - Invalid Ad Type!'; - } - - if (params.inventory && typeof params.inventory === 'object') { - slotData.inventory = params.inventory; - } - - if (params.keywords && Array.isArray(params.keywords)) { - slotData.keywords = params.keywords; + let parsedSizes = parseSizes(bid); + if (parsedSizes.length < 1) { + return false; } - if (params.visitor && typeof params.visitor === 'object') { - slotData.visitor = params.visitor; + if (bid.mediaType === 'video') { + if (typeof params.video !== 'object' || !params.video.size_id) { + return false; + } } + return true; + }, + /** + * @param {BidRequest[]} bidRequests + * @param bidderRequest + * @return ServerRequest[] + */ + buildRequests: function(bidRequests, bidderRequest) { + return bidRequests.map(bidRequest => { + bidRequest.startTime = new Date().getTime(); - postData.slots.push(slotData); - - return (JSON.stringify(postData)); - } - - function buildOptimizedCall(bid) { - bid.startTime = new Date().getTime(); - - var { - accountId, - siteId, - zoneId, - position, - floor, - keywords, - visitor, - inventory, - userId, - referrer: pageUrl - } = bid.params; - - // defaults - floor = (floor = parseFloat(floor)) > 0.01 ? floor : 0.01; - position = position || 'btf'; - - // use rubicon sizes if provided, otherwise adUnit.sizes - var parsedSizes = RubiconAdapter.masSizeOrdering(Array.isArray(bid.params.sizes) - ? bid.params.sizes.map(size => (sizeMap[size] || '').split('x')) : bid.sizes - ); - - if (parsedSizes.length < 1) { - throw 'no valid sizes'; - } + if (bidRequest.mediaType === 'video') { + let params = bidRequest.params; + let size = parseSizes(bidRequest); + + let data = { + page_url: !params.referrer ? utils.getTopWindowUrl() : params.referrer, + resolution: _getScreenResolution(), + account_id: params.accountId, + integration: getIntegration(), + timeout: bidderRequest.timeout - (Date.now() - bidderRequest.auctionStart + TIMEOUT_BUFFER), + stash_creatives: true, + ae_pass_through_parameters: params.video.aeParams, + slots: [] + }; + + // Define the slot object + let slotData = { + site_id: params.siteId, + zone_id: params.zoneId, + position: params.position || 'btf', + floor: parseFloat(params.floor) > 0.01 ? params.floor : 0.01, + element_id: bidRequest.placementCode, + name: bidRequest.placementCode, + language: params.video.language, + width: size[0], + height: size[1], + size_id: params.video.size_id + }; + + if (params.inventory && typeof params.inventory === 'object') { + slotData.inventory = params.inventory; + } - if (!/^\d+$/.test(accountId)) { - throw 'invalid accountId provided'; - } + if (params.keywords && Array.isArray(params.keywords)) { + slotData.keywords = params.keywords; + } - // using array to honor ordering. if order isn't important (it shouldn't be), an object would probably be preferable - var queryString = [ - 'account_id', accountId, - 'site_id', siteId, - 'zone_id', zoneId, - 'size_id', parsedSizes[0], - 'alt_size_ids', parsedSizes.slice(1).join(',') || undefined, - 'p_pos', position, - 'rp_floor', floor, - 'rp_secure', isSecure() ? '1' : '0', - 'tk_flint', getIntegration(), - 'p_screen_res', _getScreenResolution(), - 'kw', keywords, - 'tk_user_key', userId - ]; - - if (visitor !== null && typeof visitor === 'object') { - utils._each(visitor, (item, key) => queryString.push(`tg_v.${key}`, item)); - } + if (params.visitor && typeof params.visitor === 'object') { + slotData.visitor = params.visitor; + } - if (inventory !== null && typeof inventory === 'object') { - utils._each(inventory, (item, key) => queryString.push(`tg_i.${key}`, item)); - } + data.slots.push(slotData); - queryString.push( - 'rand', Math.random(), - 'rf', !pageUrl ? utils.getTopWindowUrl() : pageUrl - ); + return { + method: 'POST', + url: VIDEO_ENDPOINT, + data, + bidRequest + } + } - queryString = queryString.concat(_getDigiTrustQueryParams()); + // non-video request builder + let { + accountId, + siteId, + zoneId, + position, + floor, + keywords, + visitor, + inventory, + userId, + referrer: pageUrl + } = bidRequest.params; + + // defaults + floor = (floor = parseFloat(floor)) > 0.01 ? floor : 0.01; + position = position || 'btf'; + + // use rubicon sizes if provided, otherwise adUnit.sizes + let parsedSizes = parseSizes(bidRequest); + + // using array to honor ordering. if order isn't important (it shouldn't be), an object would probably be preferable + let data = [ + 'account_id', accountId, + 'site_id', siteId, + 'zone_id', zoneId, + 'size_id', parsedSizes[0], + 'alt_size_ids', parsedSizes.slice(1).join(',') || undefined, + 'p_pos', position, + 'rp_floor', floor, + 'rp_secure', isSecure() ? '1' : '0', + 'tk_flint', getIntegration(), + 'tid', bidRequest.transactionId, + 'p_screen_res', _getScreenResolution(), + 'kw', keywords, + 'tk_user_key', userId + ]; - return queryString.reduce( - (memo, curr, index) => - index % 2 === 0 && queryString[index + 1] !== undefined - ? memo + curr + '=' + encodeURIComponent(queryString[index + 1]) + '&' : memo, - FASTLANE_ENDPOINT + '?' - ).slice(0, -1); // remove trailing & - } + if (visitor !== null && typeof visitor === 'object') { + utils._each(visitor, (item, key) => data.push(`tg_v.${key}`, item)); + } - let _renderCreative = (script, impId) => ` - - - -
- -
- -`; + if (inventory !== null && typeof inventory === 'object') { + utils._each(inventory, (item, key) => data.push(`tg_i.${key}`, item)); + } - function handleRpCB(responseText, bidRequest) { - const responseObj = JSON.parse(responseText); // can throw + data.push( + 'rand', Math.random(), + 'rf', !pageUrl ? utils.getTopWindowUrl() : pageUrl + ); + + data = data.concat(_getDigiTrustQueryParams()); + + data = data.reduce( + (memo, curr, index) => + index % 2 === 0 && data[index + 1] !== undefined + ? memo + curr + '=' + encodeURIComponent(data[index + 1]) + '&' : memo, + '' + ).slice(0, -1); // remove trailing & + + return { + method: 'GET', + url: FASTLANE_ENDPOINT, + data, + bidRequest + }; + }); + }, + /** + * @param {*} responseObj + * @param {bidRequest} bidRequest + * @return {Bid[]} An array of bids which + */ + interpretResponse: function(responseObj, {bidRequest}) { let ads = responseObj.ads; const adResponseKey = bidRequest.placementCode; // check overall response if (typeof responseObj !== 'object' || responseObj.status !== 'ok') { - throw 'bad response'; + return []; } // video ads array is wrapped in an object @@ -331,25 +245,25 @@ function RubiconAdapter() { // check the ad response if (!Array.isArray(ads) || ads.length < 1) { - throw 'invalid ad response'; + return []; } // if there are multiple ads, sort by CPM ads = ads.sort(_adCpmSort); - ads.forEach(ad => { + let bids = ads.reduce((bids, ad) => { if (ad.status !== 'ok') { - throw 'bad ad status'; + return; } - // store bid response - // bid status is good (indicating 1) - var bid = bidfactory.createBid(STATUS.GOOD, bidRequest); - bid.currency = 'USD'; - bid.creative_id = ad.creative_id; - bid.bidderCode = baseAdapter.getBidderCode(); - bid.cpm = ad.cpm || 0; - bid.dealId = ad.deal; + let bid = { + requestId: bidRequest.bidId, + currency: 'USD', + creative_id: ad.creative_id, + bidderCode: spec.code, + cpm: ad.cpm || 0, + dealId: ad.deal + }; if (bidRequest.mediaType === 'video') { bid.width = bidRequest.params.video.playerWidth; bid.height = bidRequest.params.video.playerHeight; @@ -368,26 +282,84 @@ function RubiconAdapter() { return memo; }, {'rpfl_elemid': bidRequest.placementCode}); - try { - bidmanager.addBidResponse(bidRequest.placementCode, bid); - } catch (err) { - utils.logError('Error from addBidResponse', null, err); - } - }); - // Run the Emily user sync - hasUserSyncFired = syncEmily(hasUserSyncFired); + bids.push(bid); + + return bids; + }, []); + + return bids; + }, + getUserSyncs: function() { + if (!hasSynced) { + hasSynced = true; + return { + type: 'iframe', + url: SYNC_ENDPOINT + }; + } } +}; + +function _adCpmSort(adA, adB) { + return (adB.cpm || 0.0) - (adA.cpm || 0.0); +} - function _adCpmSort(adA, adB) { - return (adB.cpm || 0.0) - (adA.cpm || 0.0); +function _getScreenResolution() { + return [window.screen.width, window.screen.height].join('x'); +} + +function _getDigiTrustQueryParams() { + function getDigiTrustId() { + let digiTrustUser = window.DigiTrust && ($$PREBID_GLOBAL$$.getConfig('digiTrustId') || window.DigiTrust.getUser({member: 'T9QSFKPDN9'})); + return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; } + let digiTrustId = getDigiTrustId(); + // Verify there is an ID and this user has not opted out + if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) { + return []; + } + return [ + 'dt.id', digiTrustId.id, + 'dt.keyv', digiTrustId.keyv, + 'dt.pref', 0 + ]; +} - return Object.assign(this, baseAdapter, { - callBids: _callBids - }); +function _renderCreative(script, impId) { + return ` + + + +
+ +
+ +`; } -RubiconAdapter.masSizeOrdering = function(sizes) { +function parseSizes(bid) { + let params = bid.params; + if (bid.mediaType === 'video') { + let size = []; + if (params.video.playerWidth && params.video.playerHeight) { + size = [ + params.video.playerWidth, + params.video.playerHeight + ]; + } else if ( + Array.isArray(bid.sizes) && bid.sizes.length > 0 && + Array.isArray(bid.sizes[0]) && bid.sizes[0].length > 1 + ) { + size = bid.sizes[0]; + } + return size; + } + return masSizeOrdering(Array.isArray(params.sizes) + ? params.sizes.map(size => (sizeMap[size] || '').split('x')) : bid.sizes + ); +} + +export function masSizeOrdering(sizes) { const MAS_SIZE_PRIORITY = [15, 2, 9]; return utils.parseSizesInput(sizes) @@ -417,41 +389,11 @@ RubiconAdapter.masSizeOrdering = function(sizes) { // and finally ascending order return first - second; }); -}; - -/** - * syncEmily - * @summary A user sync dependency for the Rubicon Project adapter - * Registers an Emily iframe user sync to be called/created later by Prebid - * Only registers once except that with each winning creative there will be additional, similar calls to the same service. Must enable iframe syncs which are off by default -@example - * // Config example for iframe user sync - * $$PREBID_GLOBAL$$.setConfig({ userSync: { - * syncEnabled: true, - * pixelEnabled: true, - * syncsPerBidder: 5, - * syncDelay: 3000, - * iframeEnabled: true - * }}); - * @return {boolean} Whether or not Emily synced - */ -function syncEmily(hasSynced) { - // Check that it has not already been triggered - only meant to fire once - if (hasSynced) { - return true; - } - - const iframeUrl = 'https://tap-secure.rubiconproject.com/partner/scripts/rubicon/emily.html?rtb_ext=1'; - - // register the sync with the Prebid (to be called later) - userSync.registerSync('iframe', 'rubicon', iframeUrl); - - return true; } -adaptermanager.registerBidAdapter(new RubiconAdapter(), RUBICON_BIDDER_CODE, { - supportedMediaTypes: ['video'] -}); -adaptermanager.aliasBidAdapter(RUBICON_BIDDER_CODE, 'rubiconLite'); +var hasSynced = false; +export function resetUserSync() { + hasSynced = false; +} -module.exports = RubiconAdapter; +registerBidder(spec); diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 6b5a0090ac3..641af863f15 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -24,7 +24,7 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * aliases: ['alias1', 'alias2'], * supportedMediaTypes: ['video', 'native'], * isBidRequestValid: function(paramsObject) { return true/false }, - * buildRequests: function(bidRequests) { return some ServerRequest(s) }, + * buildRequests: function(bidRequests, bidderRequest) { return some ServerRequest(s) }, * interpretResponse: function(oneServerResponse) { return some Bids, or throw an error. } * }); * @@ -41,7 +41,7 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @property {MediaType[]} [supportedMediaTypes]: A list of Media Types which the adapter supports. * @property {function(object): boolean} isBidRequestValid Determines whether or not the given bid has all the params * needed to make a valid request. - * @property {function(BidRequest[]): ServerRequest|ServerRequest[]} buildRequests Build the request to the Server + * @property {function(BidRequest[], bidderRequest): ServerRequest|ServerRequest[]} buildRequests Build the request to the Server * which requests Bids for the given array of Requests. Each BidRequest in the argument array is guaranteed to have * passed the isBidRequestValid() test. * @property {function(*, BidRequest): Bid[]} interpretResponse Given a successful response from the Server, @@ -79,6 +79,8 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @property {string} ad A URL which can be used to load this ad, if it's chosen by the publisher. * @property {string} currency The currency code for the cpm value * @property {number} cpm The bid price, in US cents per thousand impressions. + * @property {number} ttl Time-to-live - how long (in seconds) Prebid can use this bid. + * @property {boolean} netRevenue Boolean defining whether the bid is Net or Gross. The default is true (Net). * @property {number} height The height of the ad, in pixels. * @property {number} width The width of the ad, in pixels. * @@ -152,18 +154,26 @@ export function newBidder(spec) { const adUnitCodesHandled = {}; function addBidWithCode(adUnitCode, bid) { adUnitCodesHandled[adUnitCode] = true; - bidmanager.addBidResponse(adUnitCode, bid); + addBid(adUnitCode, bid); } function fillNoBids() { bidderRequest.bids .map(bidRequest => bidRequest.placementCode) .forEach(adUnitCode => { if (adUnitCode && !adUnitCodesHandled[adUnitCode]) { - bidmanager.addBidResponse(adUnitCode, newEmptyBid()); + addBid(adUnitCode, newEmptyBid()); } }); } + function addBid(code, bid) { + try { + bidmanager.addBidResponse(code, bid); + } catch (err) { + logError('Error adding bid', code, err); + } + } + // After all the responses have come back, fill up the "no bid" bids and // register any required usersync pixels. const responses = []; @@ -185,17 +195,17 @@ export function newBidder(spec) { } } - const bidRequests = bidderRequest.bids.filter(filterAndWarn); - if (bidRequests.length === 0) { + const validBidRequests = bidderRequest.bids.filter(filterAndWarn); + if (validBidRequests.length === 0) { afterAllResponses(); return; } const bidRequestMap = {}; - bidRequests.forEach(bid => { + validBidRequests.forEach(bid => { bidRequestMap[bid.bidId] = bid; }); - let requests = spec.buildRequests(bidRequests, bidderRequest); + let requests = spec.buildRequests(validBidRequests, bidderRequest); if (!requests || requests.length === 0) { afterAllResponses(); return; diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 183b79f7ec9..5573de34f27 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1,8 +1,9 @@ import { expect } from 'chai'; import adapterManager from 'src/adaptermanager'; import bidManager from 'src/bidmanager'; -import RubiconAdapter from 'modules/rubiconBidAdapter'; +import { spec, masSizeOrdering, resetUserSync } from 'modules/rubiconBidAdapter'; import { parse as parseQuery } from 'querystring'; +import { newBidder } from 'src/adapters/bidderFactory'; import { userSync } from 'src/userSync'; var CONSTANTS = require('src/constants.json'); @@ -55,6 +56,8 @@ describe('the rubicon adapter', () => { beforeEach(() => { sandbox = sinon.sandbox.create(); + sandbox.useFakeServer(); + adUnit = { code: '/19968336/header-bid-tag-0', sizes: [[300, 250], [320, 50]], @@ -111,7 +114,8 @@ describe('the rubicon adapter', () => { sizes: [[300, 250], [320, 50]], bidId: '2ffb201a808da7', bidderRequestId: '178e34bad3658f', - requestId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a' + requestId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' } ], start: 1472239426002, @@ -162,8 +166,6 @@ describe('the rubicon adapter', () => { }); describe('MAS mapping / ordering', () => { - let masSizeOrdering = RubiconAdapter.masSizeOrdering; - it('should not include values without a proper mapping', () => { // two invalid sizes included: [42, 42], [1, 1] let ordering = masSizeOrdering([[320, 50], [42, 42], [300, 250], [640, 480], [1, 1], [336, 280]]); @@ -192,31 +194,29 @@ describe('the rubicon adapter', () => { describe('callBids implementation', () => { let rubiconAdapter; - describe('for requests', () => { - let xhr, - bids; - - beforeEach(() => { - rubiconAdapter = new RubiconAdapter(); - - bids = []; + let bids, + addBidResponseAction; - xhr = sandbox.useFakeXMLHttpRequest(); + beforeEach(() => { + rubiconAdapter = newBidder(spec); - sandbox.stub(bidManager, 'addBidResponse', (elemId, bid) => { - bids.push(bid); - }); - }); + bids = []; - afterEach(() => { - xhr.restore(); + sandbox.stub(bidManager, 'addBidResponse', (elemId, bid) => { + bids.push(bid); + if (typeof addBidResponseAction === 'function') { + addBidResponseAction(); + addBidResponseAction = undefined; + } }); + }); + describe('for requests', () => { describe('to fastlane', () => { it('should make a well-formed request', () => { rubiconAdapter.callBids(bidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let [path, query] = request.url.split('?'); query = parseQuery(query); @@ -235,6 +235,7 @@ describe('the rubicon adapter', () => { 'rp_floor': '0.01', 'rp_secure': /[01]/, 'tk_flint': INTEGRATION, + 'tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', 'p_screen_res': /\d+x\d+/, 'tk_user_key': '12346', 'kw': 'a,b,c', @@ -264,7 +265,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(sizesBidderRequest); - let query = parseQuery(xhr.requests[0].url.split('?')[1]); + let query = parseQuery(sandbox.server.requests[0].url.split('?')[1]); expect(query['size_id']).to.equal('55'); expect(query['alt_size_ids']).to.equal('57,59'); @@ -276,7 +277,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(sizesBidderRequest); - expect(xhr.requests.length).to.equal(0); + expect(sandbox.server.requests.length).to.equal(0); expect(bidManager.addBidResponse.calledOnce).to.equal(true); expect(bids).to.be.lengthOf(1); @@ -289,7 +290,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(noAccountBidderRequest); - expect(xhr.requests.length).to.equal(0); + expect(sandbox.server.requests.length).to.equal(0); expect(bidManager.addBidResponse.calledOnce).to.equal(true); expect(bids).to.be.lengthOf(1); expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); @@ -301,7 +302,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(floorBidderRequest); - let query = parseQuery(xhr.requests[0].url.split('?')[1]); + let query = parseQuery(sandbox.server.requests[0].url.split('?')[1]); expect(query['rp_floor']).to.equal('2'); }); @@ -323,7 +324,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let query = request.url.split('?')[1]; query = parseQuery(query); @@ -346,7 +347,7 @@ describe('the rubicon adapter', () => { it('should not send digitrust params when DigiTrust not loaded', () => { rubiconAdapter.callBids(bidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let query = request.url.split('?')[1]; query = parseQuery(query); @@ -376,7 +377,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let query = request.url.split('?')[1]; query = parseQuery(query); @@ -408,7 +409,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let query = request.url.split('?')[1]; query = parseQuery(query); @@ -427,7 +428,7 @@ describe('the rubicon adapter', () => { var origGetConfig; beforeEach(() => { window.DigiTrust = { - getUser: sinon.spy() + getUser: sandbox.spy() }; origGetConfig = window.$$PREBID_GLOBAL$$.getConfig; }); @@ -438,7 +439,7 @@ describe('the rubicon adapter', () => { }); it('should send digiTrustId config params', () => { - sinon.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { var config = { digiTrustId: { success: true, @@ -454,7 +455,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let query = request.url.split('?')[1]; query = parseQuery(query); @@ -475,7 +476,7 @@ describe('the rubicon adapter', () => { }); it('should not send digiTrustId config params due to optout', () => { - sinon.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { var config = { digiTrustId: { success: true, @@ -491,7 +492,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let query = request.url.split('?')[1]; query = parseQuery(query); @@ -508,7 +509,7 @@ describe('the rubicon adapter', () => { }); it('should not send digiTrustId config params due to failure', () => { - sinon.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { var config = { digiTrustId: { success: false, @@ -524,7 +525,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let query = request.url.split('?')[1]; query = parseQuery(query); @@ -541,14 +542,14 @@ describe('the rubicon adapter', () => { }); it('should not send digiTrustId config params if they do not exist', () => { - sinon.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { var config = {}; return config[key]; }); rubiconAdapter.callBids(bidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let query = request.url.split('?')[1]; query = parseQuery(query); @@ -567,16 +568,6 @@ describe('the rubicon adapter', () => { }); describe('for video requests', () => { - /* - beforeEach(() => { - createVideoBidderRequest(); - - sandbox.stub(Date, 'now', () => - bidderRequest.auctionStart + 100 - ); - }); - */ - it('should make a well-formed video request', () => { createVideoBidderRequest(); @@ -586,7 +577,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let url = request.url; let post = JSON.parse(request.requestBody); @@ -653,7 +644,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(floorBidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let post = JSON.parse(request.requestBody); let floor = post.slots[0].floor; @@ -671,7 +662,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(floorBidderRequest); - expect(xhr.requests.length).to.equal(0); + expect(sandbox.server.requests.length).to.equal(0); }); it('should get size from bid.sizes too', () => { @@ -684,7 +675,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(floorBidderRequest); - let request = xhr.requests[0]; + let request = sandbox.server.requests[0]; let post = JSON.parse(request.requestBody); expect(post.slots[0].width).to.equal(300); @@ -694,31 +685,9 @@ describe('the rubicon adapter', () => { }); describe('response handler', () => { - let bids, - server, - addBidResponseAction; - - beforeEach(() => { - bids = []; - - server = sinon.fakeServer.create(); - - sandbox.stub(bidManager, 'addBidResponse', (elemId, bid) => { - bids.push(bid); - if (addBidResponseAction) { - addBidResponseAction(); - addBidResponseAction = undefined; - } - }); - }); - - afterEach(() => { - server.restore(); - }); - describe('for fastlane', () => { it('should handle a success response and sort by cpm', () => { - server.respondWith(JSON.stringify({ + sandbox.server.respondWith(JSON.stringify({ 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -777,7 +746,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); expect(bidManager.addBidResponse.calledTwice).to.equal(true); @@ -809,7 +778,7 @@ describe('the rubicon adapter', () => { }); it('should be fine with a CPM of 0', () => { - server.respondWith(JSON.stringify({ + sandbox.server.respondWith(JSON.stringify({ 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -829,7 +798,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); expect(bidManager.addBidResponse.calledOnce).to.equal(true); expect(bids).to.be.lengthOf(1); @@ -837,7 +806,7 @@ describe('the rubicon adapter', () => { }); it('should return currency "USD"', () => { - server.respondWith(JSON.stringify({ + sandbox.server.respondWith(JSON.stringify({ 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -857,7 +826,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); expect(bidManager.addBidResponse.calledOnce).to.equal(true); expect(bids).to.be.lengthOf(1); @@ -866,7 +835,7 @@ describe('the rubicon adapter', () => { }); it('should handle an error with no ads returned', () => { - server.respondWith(JSON.stringify({ + sandbox.server.respondWith(JSON.stringify({ 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -882,7 +851,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); expect(bidManager.addBidResponse.calledOnce).to.equal(true); expect(bids).to.be.lengthOf(1); @@ -890,7 +859,7 @@ describe('the rubicon adapter', () => { }); it('should handle an error with bad status', () => { - server.respondWith(JSON.stringify({ + sandbox.server.respondWith(JSON.stringify({ 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -908,7 +877,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); expect(bidManager.addBidResponse.calledOnce).to.equal(true); expect(bids).to.be.lengthOf(1); @@ -916,11 +885,11 @@ describe('the rubicon adapter', () => { }); it('should handle an error because of malformed json response', () => { - server.respondWith('{test{'); + sandbox.server.respondWith('{test{'); rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); expect(bidManager.addBidResponse.calledOnce).to.equal(true); expect(bids).to.be.lengthOf(1); @@ -928,11 +897,11 @@ describe('the rubicon adapter', () => { }); it('should handle error contacting endpoint', () => { - server.respondWith([404, {}, '']); + sandbox.server.respondWith([404, {}, '']); rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); expect(bidManager.addBidResponse.calledOnce).to.equal(true); expect(bids).to.be.lengthOf(1); @@ -940,7 +909,7 @@ describe('the rubicon adapter', () => { }); it('should not register an error bid when a success call to addBidResponse throws an error', () => { - server.respondWith(JSON.stringify({ + sandbox.server.respondWith(JSON.stringify({ 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -964,7 +933,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); // was calling twice for same bid, but should only call once expect(bidManager.addBidResponse.calledOnce).to.equal(true); @@ -978,7 +947,7 @@ describe('the rubicon adapter', () => { }); it('should register a successful bid', () => { - server.respondWith(JSON.stringify({ + sandbox.server.respondWith(JSON.stringify({ 'status': 'ok', 'ads': { '/19968336/header-bid-tag-0': [ @@ -1007,7 +976,7 @@ describe('the rubicon adapter', () => { rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); // was calling twice for same bid, but should only call once expect(bidManager.addBidResponse.calledOnce).to.equal(true); @@ -1029,29 +998,16 @@ describe('the rubicon adapter', () => { }); describe('user sync', () => { - let bids; - let server; - let addBidResponseAction; let rubiconAdapter; let userSyncStub; const emilyUrl = 'https://tap-secure.rubiconproject.com/partner/scripts/rubicon/emily.html?rtb_ext=1'; beforeEach(() => { - bids = []; - - server = sinon.fakeServer.create(); // monitor userSync registrations - userSyncStub = sinon.stub(userSync, 'registerSync'); + userSyncStub = sandbox.stub(userSync, 'registerSync'); + sandbox.stub(bidManager, 'addBidResponse'); - sandbox.stub(bidManager, 'addBidResponse', (elemId, bid) => { - bids.push(bid); - if (addBidResponseAction) { - addBidResponseAction(); - addBidResponseAction = undefined; - } - }); - - server.respondWith(JSON.stringify({ + sandbox.server.respondWith(JSON.stringify({ 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -1093,18 +1049,14 @@ describe('the rubicon adapter', () => { iframes[i].outerHTML = ''; } - rubiconAdapter = new RubiconAdapter(); - }); - - afterEach(() => { - server.restore(); - userSyncStub.restore(); + rubiconAdapter = newBidder(spec); + resetUserSync(); }); it('should register the Emily iframe', () => { expect(userSyncStub.calledOnce).to.be.false; rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); expect(userSyncStub.calledOnce).to.be.true; expect(userSyncStub.getCall(0).args).to.eql(['iframe', 'rubicon', emilyUrl]); }); @@ -1112,11 +1064,11 @@ describe('the rubicon adapter', () => { it('should not register the Emily iframe more than once', () => { expect(userSyncStub.calledOnce).to.be.false; rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); expect(userSyncStub.calledOnce).to.be.true; // run another auction, should still have only been called once rubiconAdapter.callBids(bidderRequest); - server.respond(); + sandbox.server.respond(); expect(userSyncStub.calledOnce).to.be.true; }); }); From d566700089066fce4a2b9400999360b246c0435a Mon Sep 17 00:00:00 2001 From: Matt Kendall Date: Tue, 3 Oct 2017 15:00:49 -0400 Subject: [PATCH 23/54] Revert changes for switch between client side and server side. (#1653) * Revert changes for switch between client side and server side. * remove accidental change * double comma * update sync flag --- modules/prebidServerBidAdapter.js | 8 ++-- src/adaptermanager.js | 4 +- src/storagemanager.js | 48 ------------------- .../modules/prebidServerBidAdapter_spec.js | 32 ------------- test/spec/unit/core/adapterManager_spec.js | 15 ------ test/spec/userSync_spec.js | 1 - 6 files changed, 4 insertions(+), 104 deletions(-) delete mode 100644 src/storagemanager.js diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 1c226ccfe2b..155bdc8f906 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -7,12 +7,12 @@ import { STATUS, S2S } from 'src/constants'; import { cookieSet } from 'src/cookie.js'; import adaptermanager from 'src/adaptermanager'; import { config } from 'src/config'; -import { StorageManager, pbjsSyncsKey } from 'src/storagemanager'; const getConfig = config.getConfig; const TYPE = S2S.SRC; const cookieSetUrl = 'https://acdn.adnxs.com/cookieset/cs.js'; +let _synced = false; /** * Try to convert a value to a type. @@ -234,12 +234,10 @@ function PrebidServer() { * @param {} {bidders} list of bidders to request user syncs for. */ baseAdapter.queueSync = function({bidderCodes}) { - let syncedList = StorageManager.get(pbjsSyncsKey) || []; - // filter synced bidders - https://github.com/prebid/Prebid.js/issues/1582 - syncedList = bidderCodes.filter(bidder => !syncedList.includes(bidder)); - if (syncedList.length === 0) { + if (_synced) { return; } + _synced = true; const payload = JSON.stringify({ uuid: utils.generateUUID(), bidders: bidderCodes diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 38b7073478a..706ead56a5a 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -3,7 +3,6 @@ import { flatten, getBidderCodes, getDefinedParams, shuffle } from './utils'; import { mapSizes } from './sizeMapping'; import { processNativeAdUnitParams, nativeAdapters } from './native'; -import { StorageManager, pbjsSyncsKey } from './storagemanager'; var utils = require('./utils.js'); var CONSTANTS = require('./constants.json'); @@ -88,7 +87,6 @@ exports.callBids = ({adUnits, cbTimeout}) => { events.emit(CONSTANTS.EVENTS.AUCTION_INIT, auctionInit); let bidderCodes = getBidderCodes(adUnits); - const syncedBidders = StorageManager.get(pbjsSyncsKey); if (_bidderSequence === RANDOM) { bidderCodes = shuffle(bidderCodes); } @@ -101,7 +99,7 @@ exports.callBids = ({adUnits, cbTimeout}) => { if (_s2sConfig.enabled) { // these are called on the s2s adapter - let adaptersServerSide = _s2sConfig.bidders.filter(bidder => syncedBidders.includes(bidder)); + let adaptersServerSide = _s2sConfig.bidders; // don't call these client side bidderCodes = bidderCodes.filter((elm) => { diff --git a/src/storagemanager.js b/src/storagemanager.js deleted file mode 100644 index cf6770dc2a7..00000000000 --- a/src/storagemanager.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Storage Manager aims to provide a consistent but concise API to persist data where conditions may require alternatives - * to localStorage (storing as cookie, in indexedDB, etc), or potentially a mechanism for x-domain storage - * - * Only html5 localStorage implemented currently. - * -*/ - -import { logWarn } from './utils'; - -export const pbjsSyncsKey = 'pbjsSyncs'; - -export function newStorageManager() { - function set(key, item) { - try { - localStorage.setItem(key, JSON.stringify(item)); - } catch (e) { - logWarn('could not set storage item: ', e); - } - } - - function get(key) { - try { - const item = JSON.parse(localStorage.getItem(key)); - return item && item.length ? item : []; - } catch (e) { - logWarn('could not get storage item: ', e); - return []; - } - } - - return { - get, - set, - - add(key, element, unique = false) { - set(key, get(key) - .concat([element]) - .filter((value, index, array) => unique ? array.indexOf(value) === index : true)); - }, - - remove(key, element) { - set(key, get(key).filter(value => value !== element)); - } - } -} - -export const StorageManager = newStorageManager(); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 5ac21578ea7..675a5037bc9 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -5,7 +5,6 @@ import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils'; import cookie from 'src/cookie'; import { userSync } from 'src/userSync'; -import { StorageManager } from 'src/storagemanager'; let CONFIG = { accountId: '1', @@ -178,37 +177,6 @@ describe('S2S Adapter', () => { beforeEach(() => adapter = new Adapter()); - describe('queue sync function', () => { - let server; - let storageManagerAddStub; - - beforeEach(() => { - server = sinon.fakeServer.create(); - storageManagerAddStub = sinon.stub(StorageManager, 'add'); - }); - - afterEach(() => { - server.restore(); - storageManagerAddStub.restore(); - localStorage.removeItem('pbjsSyncs'); - }); - - it('exists and is a function', () => { - expect(adapter.queueSync).to.exist.and.to.be.a('function'); - }); - - it('requests only bidders that are not already synced', () => { - server.respondWith(JSON.stringify({status: 'ok', bidderCodes: ['rubicon'] })); - const reqBidderCodes = ['appnexus', 'newBidder']; - const syncedBidders = ['appnexus', 'rubicon']; - localStorage.setItem('pbjsSyncs', JSON.stringify(syncedBidders)); - adapter.setConfig(CONFIG); - adapter.queueSync({bidderCodes: reqBidderCodes}); - server.respond(); - sinon.assert.calledTwice(storageManagerAddStub); - }); - }); - describe('request function', () => { let xhr; let requests; diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 034700d5cbc..b829aa9b5b2 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -3,7 +3,6 @@ import AdapterManager from 'src/adaptermanager'; import { getAdUnits } from 'test/fixtures/fixtures'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils'; -import { StorageManager } from 'src/storagemanager'; const CONFIG = { enabled: true, @@ -29,23 +28,9 @@ describe('adapterManager tests', () => { AdapterManager.setS2SConfig(CONFIG); AdapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; - stubGetStorageItem = sinon.stub(StorageManager, 'get'); - stubSetStorageItem = sinon.stub(StorageManager, 'set'); - stubSetStorageItem = sinon.stub(StorageManager, 'add'); - stubSetStorageItem = sinon.stub(StorageManager, 'remove'); - - stubGetStorageItem.returns(['appnexus']); - prebidServerAdapterMock.callBids.reset(); }); - afterEach(() => { - StorageManager.get.restore(); - StorageManager.set.restore(); - StorageManager.add.restore(); - StorageManager.remove.restore(); - }); - it('invokes callBids on the S2S adapter', () => { AdapterManager.callBids({adUnits: getAdUnits()}); sinon.assert.calledOnce(prebidServerAdapterMock.callBids); diff --git a/test/spec/userSync_spec.js b/test/spec/userSync_spec.js index fe527ef8574..d6ae525f6d7 100644 --- a/test/spec/userSync_spec.js +++ b/test/spec/userSync_spec.js @@ -1,6 +1,5 @@ import { expect } from 'chai'; import { config } from 'src/config'; -import { StorageManager, pbjsSyncsKey } from 'src/storagemanager'; // Use require since we need to be able to write to these vars const utils = require('../../src/utils'); let { newUserSync } = require('../../src/userSync'); From aef474d8722d04f53ea27564cb3d6b3dfc5cc1b1 Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Tue, 3 Oct 2017 12:02:54 -0700 Subject: [PATCH 24/54] Support aspect ratio specification for native images (#1634) --- modules/appnexusAstBidAdapter.js | 69 ++++++++++++------- .../modules/appnexusAstBidAdapter_spec.js | 32 ++++++++- 2 files changed, 75 insertions(+), 26 deletions(-) diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index 50fb328b35d..d956b5f2dfd 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -14,13 +14,15 @@ const NATIVE_MAPPING = { cta: 'ctatext', image: { serverName: 'main_image', - serverParams: { required: true, sizes: [{}] } + requiredParams: { required: true }, + minimumParams: { sizes: [{}] }, }, icon: { serverName: 'icon', - serverParams: { required: true, sizes: [{}] } + requiredParams: { required: true }, + minimumParams: { sizes: [{}] }, }, - sponsoredBy: 'sponsored_by' + sponsoredBy: 'sponsored_by', }; const SOURCE = 'pbjs'; @@ -264,28 +266,7 @@ function bidToTag(bid) { tag.ad_types = ['native']; if (bid.nativeParams) { - const nativeRequest = {}; - - // map standard prebid native asset identifier to /ut parameters - // e.g., tag specifies `body` but /ut only knows `description` - // mapping may be in form {tag: ''} or - // {tag: {serverName: '', serverParams: {...}}} - Object.keys(bid.nativeParams).forEach(key => { - // check if one of the forms is used, otherwise - // a mapping wasn't specified so pass the key straight through - const requestKey = - (NATIVE_MAPPING[key] && NATIVE_MAPPING[key].serverName) || - NATIVE_MAPPING[key] || - key; - - // if the mapping for this identifier specifies required server - // params via the `serverParams` object, merge that in - nativeRequest[requestKey] = Object.assign({}, - NATIVE_MAPPING[key] && NATIVE_MAPPING[key].serverParams, - bid.nativeParams[key] - ); - }); - + const nativeRequest = buildNativeRequest(bid.nativeParams); tag['native'] = {layouts: [nativeRequest]}; } } @@ -343,6 +324,44 @@ function getRtbBid(tag) { return tag && tag.ads && tag.ads.length && tag.ads.find(ad => ad.rtb); } +function buildNativeRequest(params) { + const request = {}; + + // map standard prebid native asset identifier to /ut parameters + // e.g., tag specifies `body` but /ut only knows `description`. + // mapping may be in form {tag: ''} or + // {tag: {serverName: '', requiredParams: {...}}} + Object.keys(params).forEach(key => { + // check if one of the forms is used, otherwise + // a mapping wasn't specified so pass the key straight through + const requestKey = + (NATIVE_MAPPING[key] && NATIVE_MAPPING[key].serverName) || + NATIVE_MAPPING[key] || + key; + + // required params are always passed on request + const requiredParams = NATIVE_MAPPING[key] && NATIVE_MAPPING[key].requiredParams; + request[requestKey] = Object.assign({}, requiredParams, params[key]); + + // minimum params are passed if no non-required params given on adunit + const minimumParams = NATIVE_MAPPING[key] && NATIVE_MAPPING[key].minimumParams; + + if (requiredParams && minimumParams) { + // subtract required keys from adunit keys + const adunitKeys = Object.keys(params[key]); + const requiredKeys = Object.keys(requiredParams); + const remaining = adunitKeys.filter(key => !requiredKeys.includes(key)); + + // if none are left over, the minimum params needs to be sent + if (remaining.length === 0) { + request[requestKey] = Object.assign({}, request[requestKey], minimumParams); + } + } + }); + + return request; +} + function outstreamRender(bid) { // push to render queue because ANOutstreamVideo may not be loaded yet bid.renderer.push(() => { diff --git a/test/spec/modules/appnexusAstBidAdapter_spec.js b/test/spec/modules/appnexusAstBidAdapter_spec.js index eea8c55882b..61745697f3a 100644 --- a/test/spec/modules/appnexusAstBidAdapter_spec.js +++ b/test/spec/modules/appnexusAstBidAdapter_spec.js @@ -164,7 +164,7 @@ describe('AppNexusAdapter', () => { delete REQUEST.bids[0].params.nativeParams; }); - it('sets required native asset params when not provided on adunit', () => { + it('sets minimum native asset params when not provided on adunit', () => { REQUEST.bids[0].mediaType = 'native'; REQUEST.bids[0].nativeParams = { image: {required: true}, @@ -181,6 +181,36 @@ describe('AppNexusAdapter', () => { delete REQUEST.bids[0].params.nativeParams; }); + it('does not overwrite native ad unit params with mimimum params', () => { + REQUEST.bids[0].mediaType = 'native'; + REQUEST.bids[0].nativeParams = { + image: { + aspect_ratios: [{ + min_width: 100, + ratio_width: 2, + ratio_height: 3, + }] + }, + }; + + adapter.callBids(REQUEST); + + const request = JSON.parse(requests[0].requestBody); + expect(request.tags[0].native.layouts[0]).to.deep.equal({ + main_image: { + required: true, + aspect_ratios: [{ + min_width: 100, + ratio_width: 2, + ratio_height: 3, + }] + }, + }); + + delete REQUEST.bids[0].mediaType; + delete REQUEST.bids[0].params.nativeParams; + }); + it('sends bid request to ENDPOINT via POST', () => { adapter.callBids(REQUEST); expect(requests[0].url).to.equal(ENDPOINT); From 219f5f96093870bbc64b9269185e1f8d6c0c84c9 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Tue, 3 Oct 2017 15:27:26 -0400 Subject: [PATCH 25/54] Appnexus ast unittest updates (#1654) * Updated unit test to use bidderFactory * Updated unit test --- .../modules/appnexusAstBidAdapter_spec.js | 490 +++++++++--------- 1 file changed, 246 insertions(+), 244 deletions(-) diff --git a/test/spec/modules/appnexusAstBidAdapter_spec.js b/test/spec/modules/appnexusAstBidAdapter_spec.js index 61745697f3a..f8288819a55 100644 --- a/test/spec/modules/appnexusAstBidAdapter_spec.js +++ b/test/spec/modules/appnexusAstBidAdapter_spec.js @@ -1,202 +1,200 @@ import { expect } from 'chai'; import { spec } from 'modules/appnexusAstBidAdapter'; import { newBidder } from 'src/adapters/bidderFactory'; -import bidmanager from 'src/bidmanager'; const ENDPOINT = '//ib.adnxs.com/ut/v3/prebid'; -const REQUEST = { - 'bidderCode': 'appnexusAst', - 'requestId': 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', - 'bidderRequestId': '7101db09af0db2', - 'bids': [ - { - 'bidder': 'appnexusAst', - 'params': { - 'placementId': '4799418', - }, - 'placementCode': '/19968336/header-bid-tag1', - 'sizes': [ - [728, 90], - [970, 90] - ], - 'bidId': '84ab500420319d', - 'bidderRequestId': '7101db09af0db2', - 'requestId': 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6' - } - ], - 'start': 1469479810130 -}; - -const RESPONSE = { - 'version': '0.0.1', - 'tags': [{ - 'uuid': '84ab500420319d', - 'tag_id': 4799418, - 'auction_id': '2256922143947979797', - 'no_ad_url': 'http://lax1-ib.adnxs.com/no-ad', - 'timeout_ms': 2500, - 'ads': [{ - 'content_source': 'rtb', - 'ad_type': 'banner', - 'buyer_member_id': 958, - 'creative_id': 33989846, - 'media_type_id': 1, - 'media_subtype_id': 1, - 'cpm': 0.500000, - 'cpm_publisher_currency': 0.500000, - 'publisher_currency_code': '$', - 'client_initiated_ad_counting': true, - 'rtb': { - 'banner': { - 'width': 728, - 'height': 90, - 'content': '' - }, - 'trackers': [{ - 'impression_urls': ['http://lax1-ib.adnxs.com/impression'] - }] - } - }] - }] -}; - describe('AppNexusAdapter', () => { const adapter = newBidder(spec); - describe('request function', () => { - let xhr; - let requests; - - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); - }); - - afterEach(() => xhr.restore()); - + describe('inherited functions', () => { it('exists and is a function', () => { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); + }); - it('requires paramters to make request', () => { - adapter.callBids({}); - expect(requests).to.be.empty; + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'appnexusAst', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('requires member && invCode', () => { - let backup = REQUEST.bids[0].params; - REQUEST.bids[0].params = {member: 1234}; - adapter.callBids(REQUEST); - expect(requests).to.be.empty; - REQUEST.bids[0].params = backup; + it('should return true when required params found', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'member': '1234', + 'invCode': 'ABCD' + }; + + expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('attaches valid video params to the tag', () => { - REQUEST.bids[0].params.video = { - id: 123, - minduration: 100, - foobar: 'invalid' + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placementId': 0 }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); - adapter.callBids(REQUEST); + describe('buildRequests', () => { + let bidRequests = [ + { + 'bidder': 'appnexusAst', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; - const request = JSON.parse(requests[0].requestBody).tags[0]; - expect(request.video).to.deep.equal({ - id: 123, - minduration: 100 + it('should add source and verison to the tag', () => { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.sdk).to.exist; + expect(payload.sdk).to.deep.equal({ + source: 'pbjs', + version: '$prebid.version$' }); - - delete REQUEST.bids[0].params.video; }); - it('attaches valid user params to the tag', () => { - REQUEST.bids[0].params.user = { - external_uid: '123', - foobar: 'invalid' - }; + it('sends bid request to ENDPOINT via POST', () => { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); - adapter.callBids(REQUEST); + it('should attach valid video params to the tag', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + video: { + id: 123, + minduration: 100, + foobar: 'invalid' + } + } + } + ); - const request = JSON.parse(requests[0].requestBody); - expect(request.user).to.exist; - expect(request.user).to.deep.equal({ - external_uid: '123', + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + id: 123, + minduration: 100 }); - - delete REQUEST.bids[0].params.user; }); - it('should add source and verison to the tag', () => { - adapter.callBids(REQUEST); + it('should attach valid user params to the tag', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + user: { + external_uid: '123', + foobar: 'invalid' + } + } + } + ); - const request = JSON.parse(requests[0].requestBody); - expect(request.sdk).to.exist; - expect(request.sdk).to.deep.equal({ - source: 'pbjs', - version: '$prebid.version$' + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.user).to.exist; + expect(payload.user).to.deep.equal({ + external_uid: '123', }); }); - it('attaches native params to the request', () => { - REQUEST.bids[0].mediaType = 'native'; - REQUEST.bids[0].nativeParams = { - title: {required: true}, - body: {required: true}, - image: {required: true, sizes: [{ width: 100, height: 100 }] }, - cta: {required: false}, - sponsoredBy: {required: true} - }; + it('should attache native params to the request', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + title: {required: true}, + body: {required: true}, + image: {required: true, sizes: [{ width: 100, height: 100 }] }, + cta: {required: false}, + sponsoredBy: {required: true} + } + } + ); - adapter.callBids(REQUEST); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); - const request = JSON.parse(requests[0].requestBody); - expect(request.tags[0].native.layouts[0]).to.deep.equal({ + expect(payload.tags[0].native.layouts[0]).to.deep.equal({ title: {required: true}, description: {required: true}, main_image: {required: true, sizes: [{ width: 100, height: 100 }] }, ctatext: {required: false}, sponsored_by: {required: true} }); - - delete REQUEST.bids[0].mediaType; - delete REQUEST.bids[0].params.nativeParams; }); it('sets minimum native asset params when not provided on adunit', () => { - REQUEST.bids[0].mediaType = 'native'; - REQUEST.bids[0].nativeParams = { - image: {required: true}, - }; + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + image: {required: true}, + } + } + ); - adapter.callBids(REQUEST); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); - const request = JSON.parse(requests[0].requestBody); - expect(request.tags[0].native.layouts[0]).to.deep.equal({ + expect(payload.tags[0].native.layouts[0]).to.deep.equal({ main_image: {required: true, sizes: [{}] }, }); - - delete REQUEST.bids[0].mediaType; - delete REQUEST.bids[0].params.nativeParams; }); it('does not overwrite native ad unit params with mimimum params', () => { - REQUEST.bids[0].mediaType = 'native'; - REQUEST.bids[0].nativeParams = { - image: { - aspect_ratios: [{ - min_width: 100, - ratio_width: 2, - ratio_height: 3, - }] - }, - }; + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + image: { + aspect_ratios: [{ + min_width: 100, + ratio_width: 2, + ratio_height: 3, + }] + } + } + } + ); - adapter.callBids(REQUEST); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); - const request = JSON.parse(requests[0].requestBody); - expect(request.tags[0].native.layouts[0]).to.deep.equal({ + expect(payload.tags[0].native.layouts[0]).to.deep.equal({ main_image: { required: true, aspect_ratios: [{ @@ -206,31 +204,30 @@ describe('AppNexusAdapter', () => { }] }, }); - - delete REQUEST.bids[0].mediaType; - delete REQUEST.bids[0].params.nativeParams; - }); - - it('sends bid request to ENDPOINT via POST', () => { - adapter.callBids(REQUEST); - expect(requests[0].url).to.equal(ENDPOINT); - expect(requests[0].method).to.equal('POST'); }); - it('converts keyword params to proper form and attaches to request', () => { - REQUEST.bids[0].params.keywords = { - single: 'val', - singleArr: ['val'], - singleArrNum: [5], - multiValMixed: ['value1', 2, 'value3'], - singleValNum: 123, - badValue: {'foo': 'bar'} // should be dropped - }; + it('should convert keyword params to proper form and attaches to request', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + keywords: { + single: 'val', + singleArr: ['val'], + singleArrNum: [5], + multiValMixed: ['value1', 2, 'value3'], + singleValNum: 123, + badValue: {'foo': 'bar'} // should be dropped + } + } + } + ); - adapter.callBids(REQUEST); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); - const request = JSON.parse(requests[0].requestBody).tags[0]; - expect(request.keywords).to.deep.equal([{ + expect(payload.tags[0].keywords).to.deep.equal([{ 'key': 'single', 'value': ['val'] }, { @@ -246,38 +243,74 @@ describe('AppNexusAdapter', () => { 'key': 'singleValNum', 'value': ['123'] }]); - - delete REQUEST.bids[0].params.keywords; }); - }); - - describe('response handler', () => { - let server; - - beforeEach(() => { - server = sinon.fakeServer.create(); - sinon.stub(bidmanager, 'addBidResponse'); - }); - - afterEach(() => { - server.restore() - bidmanager.addBidResponse.restore(); - }); - - it('registers bids', () => { - server.respondWith(JSON.stringify(RESPONSE)); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property('statusMessage', 'Bid available'); - expect(response).to.have.property('cpm', 0.5); + }) + + describe('interpretResponse', () => { + let response = { + 'version': '3.0.0', + 'tags': [ + { + 'uuid': '3db3773286ee59', + 'tag_id': 10433394, + 'auction_id': '4534722592064951574', + 'nobid': false, + 'no_ad_url': 'http://lax1-ib.adnxs.com/no-ad', + 'timeout_ms': 10000, + 'ad_profile_id': 27079, + 'ads': [ + { + 'content_source': 'rtb', + 'ad_type': 'banner', + 'buyer_member_id': 958, + 'creative_id': 29681110, + 'media_type_id': 1, + 'media_subtype_id': 1, + 'cpm': 0.5, + 'cpm_publisher_currency': 0.5, + 'publisher_currency_code': '$', + 'client_initiated_ad_counting': true, + 'rtb': { + 'banner': { + 'content': '', + 'width': 300, + 'height': 250 + }, + 'trackers': [ + { + 'impression_urls': [ + 'http://lax1-ib.adnxs.com/impression' + ], + 'video_events': {} + } + ] + } + } + ] + } + ] + }; + + it('should get correct bid response', () => { + let expectedResponse = [ + { + 'requestId': '3db3773286ee59', + 'cpm': 0.5, + 'creative_id': 29681110, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner' + } + ]; + + let result = spec.interpretResponse(response); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); it('handles nobid responses', () => { - server.respondWith(JSON.stringify({ + let response = { 'version': '0.0.1', 'tags': [{ 'uuid': '84ab500420319d', @@ -285,21 +318,14 @@ describe('AppNexusAdapter', () => { 'auction_id': '297492697822162468', 'nobid': true }] - })); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + }; - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property( - 'statusMessage', - 'Bid returned empty or error response' - ); + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); }); it('handles non-banner media responses', () => { - server.respondWith(JSON.stringify({ + let response = { 'tags': [{ 'uuid': '84ab500420319d', 'ads': [{ @@ -312,19 +338,18 @@ describe('AppNexusAdapter', () => { } }] }] - })); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + }; - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property('statusMessage', 'Bid available'); + let result = spec.interpretResponse(response); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0]).to.have.property('descriptionUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); }); it('handles native responses', () => { - RESPONSE.tags[0].ads[0].ad_type = 'native'; - RESPONSE.tags[0].ads[0].rtb.native = { + let response1 = Object.assign({}, response); + response1.tags[0].ads[0].ad_type = 'native'; + response1.tags[0].ads[0].rtb.native = { 'title': 'Native Creative', 'desc': 'Cool description great stuff', 'ctatext': 'Do it', @@ -347,34 +372,11 @@ describe('AppNexusAdapter', () => { 'impression_trackers': ['http://example.com'], }; - adapter.callBids(REQUEST); - server.respondWith(JSON.stringify(RESPONSE)); - server.respond(); - - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const response = bidmanager.addBidResponse.firstCall.args[1]; - - expect(response.native.title).to.equal('Native Creative'); - expect(response.native.body).to.equal('Cool description great stuff'); - expect(response.native.cta).to.equal('Do it'); - expect(response.native.image).to.equal('http://cdn.adnxs.com/img.png'); - - RESPONSE.tags[0].ads[0].ad_type = 'banner'; - }); - - it('handles JSON.parse errors', () => { - server.respondWith(''); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property( - 'statusMessage', - 'Bid returned empty or error response' - ); + let result = spec.interpretResponse(response1); + expect(result[0].native.title).to.equal('Native Creative'); + expect(result[0].native.body).to.equal('Cool description great stuff'); + expect(result[0].native.cta).to.equal('Do it'); + expect(result[0].native.image).to.equal('http://cdn.adnxs.com/img.png'); }); }); }); From 36f4ba7b7ee8f7340d6d8e91d2ada5e22fc7e56f Mon Sep 17 00:00:00 2001 From: Anand Venkatraman Date: Wed, 4 Oct 2017 01:00:37 +0530 Subject: [PATCH 26/54] PulsePoint Lite adpater changes (#1630) * ET-1691: Pulsepoint Analytics adapter for Prebid. (#1) * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: cleanup * ET-1691: minor * ET-1691: revert package.json change * Adding bidRequest to bidFactory.createBid method as per https://github.com/prebid/Prebid.js/issues/509 * ET-1765: Adding support for additional params in PulsePoint adapter (#2) * ET-1850: Fixing https://github.com/prebid/Prebid.js/issues/866 * Minor fix * Refactoring current functionality to work with bidderFactory (for 1.0 migration) * More tests. * Adding support for "app" requests. * fixing eslint issues * adding adapter documentation * minor doc update * removing usage of reserved keyword 'native' --- modules/pulsepointLiteBidAdapter.js | 505 +++++++++--------- modules/pulsepointLiteBidAdapter.md | 43 ++ .../modules/pulsepointLiteBidAdapter_spec.js | 271 +++++----- 3 files changed, 437 insertions(+), 382 deletions(-) create mode 100644 modules/pulsepointLiteBidAdapter.md diff --git a/modules/pulsepointLiteBidAdapter.js b/modules/pulsepointLiteBidAdapter.js index c80e45eb2f0..84f955317c0 100644 --- a/modules/pulsepointLiteBidAdapter.js +++ b/modules/pulsepointLiteBidAdapter.js @@ -1,9 +1,14 @@ -import {createBid} from 'src/bidfactory'; -import {addBidResponse} from 'src/bidmanager'; +/* eslint dot-notation:0, quote-props:0 */ import {logError, getTopWindowLocation} from 'src/utils'; -import {ajax} from 'src/ajax'; -import {STATUS} from 'src/constants'; -import adaptermanager from 'src/adaptermanager'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const NATIVE_DEFAULTS = { + TITLE_LEN: 100, + DESCR_LEN: 200, + SPONSORED_BY_LEN: 50, + IMG_MIN: 150, + ICON_MIN: 50, +}; /** * PulsePoint "Lite" Adapter. This adapter implementation is lighter than the @@ -11,291 +16,293 @@ import adaptermanager from 'src/adaptermanager'; * dependencies and relies on a single OpenRTB request to the PulsePoint * bidder instead of separate requests per slot. */ -function PulsePointLiteAdapter() { - const bidUrl = window.location.protocol + '//bid.contextweb.com/header/ortb'; - const ajaxOptions = { - method: 'POST', - withCredentials: true, - contentType: 'text/plain' - }; - const NATIVE_DEFAULTS = { - TITLE_LEN: 100, - DESCR_LEN: 200, - SPONSORED_BY_LEN: 50, - IMG_MIN: 150, - ICON_MIN: 50, - }; +export const spec = { - /** - * Makes the call to PulsePoint endpoint and registers bids. - */ - function _callBids(bidRequest) { - try { - // construct the openrtb bid request from slots - const request = { - imp: bidRequest.bids.map(slot => impression(slot)), - site: site(bidRequest), - device: device(), - }; - ajax(bidUrl, (rawResponse) => { - bidResponseAvailable(bidRequest, rawResponse); - }, JSON.stringify(request), ajaxOptions); - } catch (e) { - // register passback on any exceptions while attempting to fetch response. - logError('pulsepoint.requestBid', 'ERROR', e); - bidResponseAvailable(bidRequest); - } - } + code: 'pulseLite', - /** - * Callback for bids, after the call to PulsePoint completes. - */ - function bidResponseAvailable(bidRequest, rawResponse) { - const idToSlotMap = {}; - const idToBidMap = {}; - // extract the request bids and the response bids, keyed by impr-id - bidRequest.bids.forEach((slot) => { - idToSlotMap[slot.bidId] = slot; - }); - const bidResponse = parse(rawResponse); - if (bidResponse) { - bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach((bid) => { - idToBidMap[bid.impid] = bid; - })); - } - // register the responses - Object.keys(idToSlotMap).forEach((id) => { - if (idToBidMap[id]) { - const size = adSize(idToSlotMap[id]); - const bid = createBid(STATUS.GOOD, bidRequest); - bid.bidderCode = bidRequest.bidderCode; - bid.cpm = idToBidMap[id].price; - bid.adId = id; - if (isNative(idToSlotMap[id])) { - bid['native'] = nativeResponse(idToSlotMap[id], idToBidMap[id]); - bid.mediaType = 'native'; - } else { - bid.ad = idToBidMap[id].adm; - bid.width = size[0]; - bid.height = size[1]; - } - addBidResponse(idToSlotMap[id].placementCode, bid); - } else { - const passback = createBid(STATUS.NO_BID, bidRequest); - passback.bidderCode = bidRequest.bidderCode; - passback.adId = id; - addBidResponse(idToSlotMap[id].placementCode, passback); - } - }); - } + aliases: ['pulsepointLite'], - /** - * Produces an OpenRTBImpression from a slot config. - */ - function impression(slot) { + supportedMediaTypes: ['native'], + + isBidRequestValid: bid => ( + !!(bid && bid.params && bid.params.cp && bid.params.ct) + ), + + buildRequests: bidRequests => { + const request = { + id: bidRequests[0].bidderRequestId, + imp: bidRequests.map(slot => impression(slot)), + site: site(bidRequests), + app: app(bidRequests), + device: device(), + }; return { - id: slot.bidId, - banner: banner(slot), - 'native': nativeImpression(slot), - tagid: slot.params.ct.toString(), + method: 'POST', + url: '//bid.contextweb.com/header/ortb', + data: JSON.stringify(request), }; - } + }, - /** - * Produces an OpenRTB Banner object for the slot given. - */ - function banner(slot) { - const size = adSize(slot); - return slot.nativeParams ? null : { - w: size[0], - h: size[1], - }; - } + interpretResponse: (response, request) => ( + bidResponseAvailable(request, response) + ), - /** - * Produces an OpenRTB Native object for the slot given. - */ - function nativeImpression(slot) { - if (slot.nativeParams) { - const assets = []; - addAsset(assets, titleAsset(assets.length + 1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); - addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); - addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); - addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN)); - addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); - return { - request: JSON.stringify({ assets }), - ver: '1.1', - }; + getUserSyncs: syncOptions => { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: '//bh.contextweb.com/visitormatch' + }]; } - return null; } - /** - * Helper method to add an asset to the assets list. - */ - function addAsset(assets, asset) { - if (asset) { - assets.push(asset); - } - } +}; - /** - * Produces a Native Title asset for the configuration given. - */ - function titleAsset(id, params, defaultLen) { - if (params) { - return { - id: id, - required: params.required ? 1 : 0, - title: { - len: params.len || defaultLen, - }, +/** + * Callback for bids, after the call to PulsePoint completes. + */ +function bidResponseAvailable(bidRequest, bidResponse) { + const idToImpMap = {}; + const idToBidMap = {}; + // extract the request bids and the response bids, keyed by impr-id + const ortbRequest = parse(bidRequest.data); + ortbRequest.imp.forEach(imp => { + idToImpMap[imp.id] = imp; + }); + if (bidResponse) { + bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => { + idToBidMap[bid.impid] = bid; + })); + } + const bids = []; + Object.keys(idToImpMap).forEach(id => { + if (idToBidMap[id]) { + const bid = { + requestId: id, + cpm: idToBidMap[id].price, + creative_id: id, + creativeId: id, + adId: id, }; + if (idToImpMap[id]['native']) { + bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); + bid.mediaType = 'native'; + } else { + bid.ad = idToBidMap[id].adm; + bid.width = idToImpMap[id].banner.w; + bid.height = idToImpMap[id].banner.h; + } + bids.push(bid); } - return null; + }); + return bids; +} + +/** + * Produces an OpenRTBImpression from a slot config. + */ +function impression(slot) { + return { + id: slot.bidId, + banner: banner(slot), + 'native': nativeImpression(slot), + tagid: slot.params.ct.toString(), + }; +} + +/** + * Produces an OpenRTB Banner object for the slot given. + */ +function banner(slot) { + const size = adSize(slot); + return slot.nativeParams ? null : { + w: size[0], + h: size[1], + }; +} + +/** + * Produces an OpenRTB Native object for the slot given. + */ +function nativeImpression(slot) { + if (slot.nativeParams) { + const assets = []; + addAsset(assets, titleAsset(assets.length + 1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); + addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); + addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); + addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN)); + addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); + return { + request: JSON.stringify({ assets }), + ver: '1.1', + }; } + return null; +} - /** - * Produces a Native Image asset for the configuration given. - */ - function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) { - return params ? { - id: id, - required: params.required ? 1 : 0, - img: { - type, - wmin: params.wmin || defaultMinWidth, - hmin: params.hmin || defaultMinHeight, - } - } : null; +/** + * Helper method to add an asset to the assets list. + */ +function addAsset(assets, asset) { + if (asset) { + assets.push(asset); } +} - /** - * Produces a Native Data asset for the configuration given. - */ - function dataAsset(id, params, type, defaultLen) { - return params ? { - id: id, +/** + * Produces a Native Title asset for the configuration given. + */ +function titleAsset(id, params, defaultLen) { + if (params) { + return { + id, required: params.required ? 1 : 0, - data: { - type, + title: { len: params.len || defaultLen, - } - } : null; + }, + }; } + return null; +} + +/** + * Produces a Native Image asset for the configuration given. + */ +function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) { + return params ? { + id, + required: params.required ? 1 : 0, + img: { + type, + wmin: params.wmin || defaultMinWidth, + hmin: params.hmin || defaultMinHeight, + } + } : null; +} + +/** + * Produces a Native Data asset for the configuration given. + */ +function dataAsset(id, params, type, defaultLen) { + return params ? { + id, + required: params.required ? 1 : 0, + data: { + type, + len: params.len || defaultLen, + } + } : null; +} - /** - * Produces an OpenRTB site object. - */ - function site(bidderRequest) { - const pubId = bidderRequest.bids.length > 0 ? bidderRequest.bids[0].params.cp : '0'; +/** + * Produces an OpenRTB site object. + */ +function site(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.cp : '0'; + const appParams = bidderRequest[0].params.app; + if (!appParams) { return { publisher: { id: pubId.toString(), }, ref: referrer(), page: getTopWindowLocation().href, - }; - } - - /** - * Attempts to capture the referrer url. - */ - function referrer() { - try { - return window.top.document.referrer; - } catch (e) { - return document.referrer; } } + return null; +} - /** - * Produces an OpenRTB Device object. - */ - function device() { +/** + * Produces an OpenRTB App object. + */ +function app(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.cp : '0'; + const appParams = bidderRequest[0].params.app; + if (appParams) { return { - ua: navigator.userAgent, - language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), - }; - } - - /** - * Safely parses the input given. Returns null on - * parsing failure. - */ - function parse(rawResponse) { - try { - if (rawResponse) { - return JSON.parse(rawResponse); - } - } catch (ex) { - logError('pulsepointLite.safeParse', 'ERROR', ex); + publisher: { + id: pubId.toString(), + }, + bundle: appParams.bundle, + storeurl: appParams.storeUrl, + domain: appParams.domain, } - return null; } + return null; +} - /** - * Determines the AdSize for the slot. - */ - function adSize(slot) { - if (slot.params.cf) { - const size = slot.params.cf.toUpperCase().split('X'); - const width = parseInt(slot.params.cw || size[0], 10); - const height = parseInt(slot.params.ch || size[1], 10); - return [width, height]; - } - return [1, 1]; +/** + * Attempts to capture the referrer url. + */ +function referrer() { + try { + return window.top.document.referrer; + } catch (e) { + return document.referrer; } +} - /** - * Parses the native response from the Bid given. - */ - function nativeResponse(slot, bid) { - if (slot.nativeParams) { - const nativeAd = parse(bid.adm); - const keys = {}; - if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) { - nativeAd['native'].assets.forEach((asset) => { - keys.title = asset.title ? asset.title.text : keys.title; - keys.body = asset.data && asset.data.type === 2 ? asset.data.value : keys.body; - keys.sponsoredBy = asset.data && asset.data.type === 1 ? asset.data.value : keys.sponsoredBy; - keys.image = asset.img && asset.img.type === 3 ? asset.img.url : keys.image; - keys.icon = asset.img && asset.img.type === 1 ? asset.img.url : keys.icon; - }); - if (nativeAd['native'].link) { - keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url); - } - keys.impressionTrackers = nativeAd['native'].imptrackers; - return keys; - } +/** + * Produces an OpenRTB Device object. + */ +function device() { + return { + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + }; +} + +/** + * Safely parses the input given. Returns null on + * parsing failure. + */ +function parse(rawResponse) { + try { + if (rawResponse) { + return JSON.parse(rawResponse); } - return null; + } catch (ex) { + logError('pulsepointLite.safeParse', 'ERROR', ex); } + return null; +} - /** - * Parses the native response from the Bid given. - */ - function isNative(slot) { - return !!slot.nativeParams; +/** + * Determines the AdSize for the slot. + */ +function adSize(slot) { + if (slot.params.cf) { + const size = slot.params.cf.toUpperCase().split('X'); + const width = parseInt(slot.params.cw || size[0], 10); + const height = parseInt(slot.params.ch || size[1], 10); + return [width, height]; } - - return Object.assign(this, { - callBids: _callBids - }); + return [1, 1]; } /** - * "pulseLite" will be the adapter name going forward. "pulsepointLite" to be - * deprecated, but kept here for backwards compatibility. - * Reason is key truncation. When the Publisher opts for sending all bids to DFP, then - * the keys get truncated due to the limit in key-size (20 characters, detailed - * here https://support.google.com/dfp_premium/answer/1628457?hl=en). Here is an - * example, where keys got truncated when using the "pulsepointLite" alias - "hb_adid_pulsepointLi=1300bd87d59c4c2" -*/ -adaptermanager.registerBidAdapter(new PulsePointLiteAdapter(), 'pulseLite', { - supportedMediaTypes: [ 'native' ] -}); -adaptermanager.aliasBidAdapter('pulseLite', 'pulsepointLite'); + * Parses the native response from the Bid given. + */ +function nativeResponse(imp, bid) { + if (imp['native']) { + const nativeAd = parse(bid.adm); + const keys = {}; + if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) { + nativeAd['native'].assets.forEach(asset => { + keys.title = asset.title ? asset.title.text : keys.title; + keys.body = asset.data && asset.data.type === 2 ? asset.data.value : keys.body; + keys.sponsoredBy = asset.data && asset.data.type === 1 ? asset.data.value : keys.sponsoredBy; + keys.image = asset.img && asset.img.type === 3 ? asset.img.url : keys.image; + keys.icon = asset.img && asset.img.type === 1 ? asset.img.url : keys.icon; + }); + if (nativeAd['native'].link) { + keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url); + } + keys.impressionTrackers = nativeAd['native'].imptrackers; + return keys; + } + } + return null; +} -module.exports = PulsePointLiteAdapter; +registerBidder(spec); diff --git a/modules/pulsepointLiteBidAdapter.md b/modules/pulsepointLiteBidAdapter.md new file mode 100644 index 00000000000..23c96758ca0 --- /dev/null +++ b/modules/pulsepointLiteBidAdapter.md @@ -0,0 +1,43 @@ +# Overview + +**Module Name**: PulsePoint Lite Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: ExchangeTeam@pulsepoint.com + +# Description + +Connects to PulsePoint demand source to fetch bids. +Banner, Outstream and Native formats are supported. +Please use ```pulseLite``` as the bidder code. + +# Test Parameters +``` + var adUnits = [{ + code: 'banner-ad-div', + sizes: [[300, 250]], + bids: [{ + bidder: 'pulsepointLite', + params: { + cf: '300X250', + cp: 512379, + ct: 486653 + } + }] + },{ + code: 'native-ad-div', + sizes: [[1, 1]], + nativeParams: { + title: { required: true, len: 75 }, + image: { required: true }, + body: { len: 200 }, + sponsoredBy: { len: 20 } + }, + bids: [{ + bidder: 'pulseLite', + params: { + cp: 512379, + ct: 505642 + } + }] + }]; +``` diff --git a/test/spec/modules/pulsepointLiteBidAdapter_spec.js b/test/spec/modules/pulsepointLiteBidAdapter_spec.js index f7b7a790302..8e1f12dac93 100644 --- a/test/spec/modules/pulsepointLiteBidAdapter_spec.js +++ b/test/spec/modules/pulsepointLiteBidAdapter_spec.js @@ -1,71 +1,60 @@ +/* eslint dot-notation:0, quote-props:0 */ import {expect} from 'chai'; -import PulsePointAdapter from 'modules/pulsepointLiteBidAdapter'; +import {spec} from 'modules/pulsepointLiteBidAdapter'; import bidManager from 'src/bidmanager'; import {getTopWindowLocation} from 'src/utils'; -import * as ajax from 'src/ajax'; +import {newBidder} from 'src/adapters/bidderFactory'; describe('PulsePoint Lite Adapter Tests', () => { - let pulsepointAdapter = new PulsePointAdapter(); - let slotConfigs; - let nativeSlotConfig; - let ajaxStub; - - beforeEach(() => { - sinon.stub(bidManager, 'addBidResponse'); - ajaxStub = sinon.stub(ajax, 'ajax'); - - slotConfigs = { - bidderCode: 'pulseLite', - bids: [ - { - placementCode: '/DfpAccount1/slot1', - bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', - cf: '300x250' - } - }, { - placementCode: '/DfpAccount2/slot2', - bidId: 'bid23456', - params: { - cp: 'p10000', - ct: 't20000', - cf: '728x90' - } - } - ] - }; - nativeSlotConfig = { - bidderCode: 'pulseLite', - bids: [ - { - placementCode: '/DfpAccount1/slot3', - bidId: 'bid12345', - nativeParams: { - title: { required: true, len: 200 }, - image: { wmin: 100 }, - sponsoredBy: { } - }, - params: { - cp: 'p10000', - ct: 't10000' - } - } - ] - }; - }); - - afterEach(() => { - bidManager.addBidResponse.restore(); - ajaxStub.restore(); - }); + const slotConfigs = [{ + placementCode: '/DfpAccount1/slot1', + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '300x250' + } + }, { + placementCode: '/DfpAccount2/slot2', + bidId: 'bid23456', + params: { + cp: 'p10000', + ct: 't20000', + cf: '728x90' + } + }]; + const nativeSlotConfig = [{ + placementCode: '/DfpAccount1/slot3', + bidId: 'bid12345', + nativeParams: { + title: { required: true, len: 200 }, + image: { wmin: 100 }, + sponsoredBy: { } + }, + params: { + cp: 'p10000', + ct: 't10000' + } + }]; + const appSlotConfig = [{ + placementCode: '/DfpAccount1/slot3', + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + app: { + bundle: 'com.pulsepoint.apps', + storeUrl: 'http://pulsepoint.com/apps', + domain: 'pulsepoint.com', + } + } + }]; - it('Verify requests sent to PulsePoint', () => { - pulsepointAdapter.callBids(slotConfigs); - expect(ajaxStub.callCount).to.equal(1); - expect(ajaxStub.firstCall.args[0]).to.equal('http://bid.contextweb.com/header/ortb'); - const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]); + it('Verify build request', () => { + const request = spec.buildRequests(slotConfigs); + expect(request.url).to.equal('//bid.contextweb.com/header/ortb'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); // site object expect(ortbRequest.site).to.not.equal(null); expect(ortbRequest.site.publisher).to.not.equal(null); @@ -88,11 +77,10 @@ describe('PulsePoint Lite Adapter Tests', () => { expect(ortbRequest.imp[1].banner.h).to.equal(90); }); - it('Verify bid', () => { - pulsepointAdapter.callBids(slotConfigs); - // trigger a mock ajax callback with bid. - const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]); - ajaxStub.firstCall.args[1](JSON.stringify({ + it('Verify parse response', () => { + const request = spec.buildRequests(slotConfigs); + const ortbRequest = JSON.parse(request.data); + const ortbResponse = { seatbid: [{ bid: [{ impid: ortbRequest.imp[0].id, @@ -100,65 +88,40 @@ describe('PulsePoint Lite Adapter Tests', () => { adm: 'This is an Ad' }] }] - })); - expect(bidManager.addBidResponse.callCount).to.equal(2); + }; + const bids = spec.interpretResponse(ortbResponse, request); + expect(bids).to.have.lengthOf(1); // verify first bid - let placement = bidManager.addBidResponse.firstCall.args[0]; - let bid = bidManager.addBidResponse.firstCall.args[1]; - expect(placement).to.equal('/DfpAccount1/slot1'); - expect(bid.bidderCode).to.equal('pulseLite'); + const bid = bids[0]; expect(bid.cpm).to.equal(1.25); expect(bid.ad).to.equal('This is an Ad'); expect(bid.width).to.equal(300); expect(bid.height).to.equal(250); expect(bid.adId).to.equal('bid12345'); - // verify passback on 2nd impression. - placement = bidManager.addBidResponse.secondCall.args[0]; - bid = bidManager.addBidResponse.secondCall.args[1]; - expect(placement).to.equal('/DfpAccount2/slot2'); - expect(bid.adId).to.equal('bid23456'); - expect(bid.bidderCode).to.equal('pulseLite'); - expect(bid.cpm).to.be.undefined; + expect(bid.creative_id).to.equal('bid12345'); + expect(bid.creativeId).to.equal('bid12345'); }); it('Verify full passback', () => { - pulsepointAdapter.callBids(slotConfigs); - // trigger a mock ajax callback with no bid. - ajaxStub.firstCall.args[1](null); - let placement = bidManager.addBidResponse.firstCall.args[0]; - let bid = bidManager.addBidResponse.firstCall.args[1]; - expect(placement).to.equal('/DfpAccount1/slot1'); - expect(bid.bidderCode).to.equal('pulseLite'); - expect(bid).to.not.have.property('ad'); - expect(bid).to.not.have.property('cpm'); - expect(bid.adId).to.equal('bid12345'); - }); - - it('Verify passback when ajax call fails', () => { - ajaxStub.throws(); - pulsepointAdapter.callBids(slotConfigs); - let placement = bidManager.addBidResponse.firstCall.args[0]; - let bid = bidManager.addBidResponse.firstCall.args[1]; - expect(placement).to.equal('/DfpAccount1/slot1'); - expect(bid.bidderCode).to.equal('pulseLite'); - expect(bid).to.not.have.property('ad'); - expect(bid).to.not.have.property('cpm'); - expect(bid.adId).to.equal('bid12345'); + const request = spec.buildRequests(slotConfigs); + const bids = spec.interpretResponse(null, request) + expect(bids).to.have.lengthOf(0); }); it('Verify Native request', () => { - pulsepointAdapter.callBids(nativeSlotConfig); - expect(ajaxStub.callCount).to.equal(1); - expect(ajaxStub.firstCall.args[0]).to.equal('http://bid.contextweb.com/header/ortb'); - const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]); + const request = spec.buildRequests(nativeSlotConfig); + expect(request.url).to.equal('//bid.contextweb.com/header/ortb'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); // native impression expect(ortbRequest.imp[0].tagid).to.equal('t10000'); expect(ortbRequest.imp[0].banner).to.equal(null); - expect(ortbRequest.imp[0].native).to.not.equal(null); - expect(ortbRequest.imp[0].native.ver).to.equal('1.1'); - expect(ortbRequest.imp[0].native.request).to.not.equal(null); + const nativePart = ortbRequest.imp[0]['native']; + expect(nativePart).to.not.equal(null); + expect(nativePart.ver).to.equal('1.1'); + expect(nativePart.request).to.not.equal(null); // native request assets - const nativeRequest = JSON.parse(ortbRequest.imp[0].native.request); + const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); expect(nativeRequest).to.not.equal(null); expect(nativeRequest.assets).to.have.lengthOf(3); // title asset @@ -184,22 +147,22 @@ describe('PulsePoint Lite Adapter Tests', () => { }); it('Verify Native response', () => { - pulsepointAdapter.callBids(nativeSlotConfig); - expect(ajaxStub.callCount).to.equal(1); - expect(ajaxStub.firstCall.args[0]).to.equal('http://bid.contextweb.com/header/ortb'); - const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]); + const request = spec.buildRequests(nativeSlotConfig); + expect(request.url).to.equal('//bid.contextweb.com/header/ortb'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); const nativeResponse = { - native: { + 'native': { assets: [ { title: { text: 'Ad Title'} }, { data: { type: 1, value: 'Sponsored By: Brand' }}, { img: { type: 3, url: 'http://images.cdn.brand.com/123' } } ], link: { url: 'http://brand.clickme.com/' }, - imptrackers: [ 'http://imp1.trackme.com/', 'http://imp1.contextweb.com/' ] + imptrackers: ['http://imp1.trackme.com/', 'http://imp1.contextweb.com/'] } }; - ajaxStub.firstCall.args[1](JSON.stringify({ + const ortbResponse = { seatbid: [{ bid: [{ impid: ortbRequest.imp[0].id, @@ -207,28 +170,70 @@ describe('PulsePoint Lite Adapter Tests', () => { adm: JSON.stringify(nativeResponse) }] }] - })); + }; + const bids = spec.interpretResponse(ortbResponse, request); // verify bid - let placement = bidManager.addBidResponse.firstCall.args[0]; - let bid = bidManager.addBidResponse.firstCall.args[1]; - expect(placement).to.equal('/DfpAccount1/slot3'); - expect(bid.bidderCode).to.equal('pulseLite'); + const bid = bids[0]; expect(bid.cpm).to.equal(1.25); expect(bid.adId).to.equal('bid12345'); expect(bid.ad).to.be.undefined; expect(bid.mediaType).to.equal('native'); - expect(bid.native).to.not.equal(null); - expect(bid.native.title).to.equal('Ad Title'); - expect(bid.native.sponsoredBy).to.equal('Sponsored By: Brand'); - expect(bid.native.image).to.equal('http://images.cdn.brand.com/123'); - expect(bid.native.clickUrl).to.equal(encodeURIComponent('http://brand.clickme.com/')); - expect(bid.native.impressionTrackers).to.have.lengthOf(2); - expect(bid.native.impressionTrackers[0]).to.equal('http://imp1.trackme.com/'); - expect(bid.native.impressionTrackers[1]).to.equal('http://imp1.contextweb.com/'); + const nativeBid = bid['native']; + expect(nativeBid).to.not.equal(null); + expect(nativeBid.title).to.equal('Ad Title'); + expect(nativeBid.sponsoredBy).to.equal('Sponsored By: Brand'); + expect(nativeBid.image).to.equal('http://images.cdn.brand.com/123'); + expect(nativeBid.clickUrl).to.equal(encodeURIComponent('http://brand.clickme.com/')); + expect(nativeBid.impressionTrackers).to.have.lengthOf(2); + expect(nativeBid.impressionTrackers[0]).to.equal('http://imp1.trackme.com/'); + expect(nativeBid.impressionTrackers[1]).to.equal('http://imp1.contextweb.com/'); + }); + + it('Verifies bidder code', () => { + expect(spec.code).to.equal('pulseLite'); + }); + + it('Verifies bidder aliases', () => { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('pulsepointLite'); + }); + + it('Verifies supported media types', () => { + expect(spec.supportedMediaTypes).to.have.lengthOf(1); + expect(spec.supportedMediaTypes[0]).to.equal('native'); + }); + + it('Verifies if bid request valid', () => { + expect(spec.isBidRequestValid(slotConfigs[0])).to.equal(true); + expect(spec.isBidRequestValid(slotConfigs[1])).to.equal(true); + expect(spec.isBidRequestValid(nativeSlotConfig[0])).to.equal(true); + expect(spec.isBidRequestValid({})).to.equal(false); + expect(spec.isBidRequestValid({ params: {} })).to.equal(false); + expect(spec.isBidRequestValid({ params: { ct: 123 } })).to.equal(false); + expect(spec.isBidRequestValid({ params: { cp: 123 } })).to.equal(false); + expect(spec.isBidRequestValid({ params: { ct: 123, cp: 234 }})).to.equal(true); }); - it('Verify adapter interface', function () { - const adapter = new PulsePointAdapter(); - expect(adapter).to.have.property('callBids'); + it('Verifies sync options', () => { + expect(spec.getUserSyncs({})).to.be.undefined; + expect(spec.getUserSyncs({ iframeEnabled: false})).to.be.undefined; + const options = spec.getUserSyncs({ iframeEnabled: true}); + expect(options).to.not.be.undefined; + expect(options).to.have.lengthOf(1); + expect(options[0].type).to.equal('iframe'); + expect(options[0].url).to.equal('//bh.contextweb.com/visitormatch'); + }); + + it('Verify app requests', () => { + const request = spec.buildRequests(appSlotConfig); + const ortbRequest = JSON.parse(request.data); + // site object + expect(ortbRequest.site).to.equal(null); + expect(ortbRequest.app).to.not.be.null; + expect(ortbRequest.app.publisher).to.not.equal(null); + expect(ortbRequest.app.publisher.id).to.equal('p10000'); + expect(ortbRequest.app.bundle).to.equal('com.pulsepoint.apps'); + expect(ortbRequest.app.storeurl).to.equal('http://pulsepoint.com/apps'); + expect(ortbRequest.app.domain).to.equal('pulsepoint.com'); }); }); From abb7d45505c1ed243f86ce6693f9df9af6619075 Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Tue, 3 Oct 2017 12:55:21 -0700 Subject: [PATCH 27/54] Add native param support to mediaTypes (#1625) --- modules/appnexusAstBidAdapter.js | 2 +- src/adaptermanager.js | 14 ++++++++------ src/native.js | 8 ++++++-- test/spec/bidmanager_spec.js | 14 ++++++++------ 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index d956b5f2dfd..b0997992a0c 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -262,7 +262,7 @@ function bidToTag(bid) { tag.keywords = getKeywords(bid.params.keywords); } - if (bid.mediaType === 'native') { + if (bid.mediaType === 'native' || utils.deepAccess(bid, 'mediaTypes.native')) { tag.ad_types = ['native']; if (bid.nativeParams) { diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 706ead56a5a..231a8936a69 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -41,12 +41,6 @@ function getBids({bidderCode, requestId, bidderRequestId, adUnits}) { sizes = sizeMapping; } - if (adUnit.nativeParams) { - bid = Object.assign({}, bid, { - nativeParams: processNativeAdUnitParams(adUnit.nativeParams), - }); - } - if (adUnit.mediaTypes) { if (utils.isValidMediaTypes(adUnit.mediaTypes)) { bid = Object.assign({}, bid, { mediaTypes: adUnit.mediaTypes }); @@ -57,6 +51,14 @@ function getBids({bidderCode, requestId, bidderRequestId, adUnits}) { } } + const nativeParams = + adUnit.nativeParams || utils.deepAccess(adUnit, 'mediaTypes.native'); + if (nativeParams) { + bid = Object.assign({}, bid, { + nativeParams: processNativeAdUnitParams(nativeParams), + }); + } + bid = Object.assign({}, bid, getDefinedParams(adUnit, [ 'mediaType', 'renderer' diff --git a/src/native.js b/src/native.js index b6edabbde07..544258818ec 100644 --- a/src/native.js +++ b/src/native.js @@ -1,4 +1,4 @@ -import { getBidRequest, logError, triggerPixel } from './utils'; +import { deepAccess, getBidRequest, logError, triggerPixel } from './utils'; export const nativeAdapters = []; @@ -59,7 +59,11 @@ function typeIsSupported(type) { * TODO: abstract this and the video helper functions into general * adunit validation helper functions */ -export const nativeAdUnit = adUnit => adUnit.mediaType === 'native'; +export const nativeAdUnit = adUnit => { + const mediaType = adUnit.mediaType === 'native'; + const mediaTypes = deepAccess(adUnit, 'mediaTypes.native'); + return mediaType || mediaTypes; +} export const nativeBidder = bid => nativeAdapters.includes(bid.bidder); export const hasNonNativeBidder = adUnit => adUnit.bids.filter(bid => !nativeBidder(bid)).length; diff --git a/test/spec/bidmanager_spec.js b/test/spec/bidmanager_spec.js index fd5f4fa2b31..5263741885a 100644 --- a/test/spec/bidmanager_spec.js +++ b/test/spec/bidmanager_spec.js @@ -549,10 +549,11 @@ describe('bidmanager.js', function () { sinon.stub(utils, 'getBidRequest', () => ({ start: timestamp(), bidder: 'appnexusAst', - nativeParams: { - title: {'required': true}, + mediaTypes: { + native: { + title: {required: true}, + } }, - mediaType: 'native', })); const bid = Object.assign({}, @@ -575,10 +576,11 @@ describe('bidmanager.js', function () { const bidRequest = () => ({ start: timestamp(), bidder: 'appnexusAst', - nativeParams: { - title: {'required': true}, + mediaTypes: { + native: { + title: {required: true}, + } }, - mediaType: 'native', }); sinon.stub(utils, 'getBidRequest', bidRequest); sinon.stub(utils, 'getBidderRequest', bidRequest); From 0ab545e547b58f876d5788996a1357de81b838a4 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Tue, 3 Oct 2017 17:57:39 -0400 Subject: [PATCH 28/54] Prebid 0.30.0 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 80eedcfa1b4..cf8ad23ed0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "0.30.0-pre", + "version": "0.30.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 0bc0b292e27506bdb03e0397ee52a640b34bd75e Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Tue, 3 Oct 2017 18:07:59 -0400 Subject: [PATCH 29/54] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cf8ad23ed0a..c74124bcd5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "0.30.0", + "version": "0.31.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 42282f57f895f8cb3f9b583242b39559e6ae297e Mon Sep 17 00:00:00 2001 From: bretg Date: Fri, 6 Oct 2017 12:33:33 -0400 Subject: [PATCH 30/54] prebidAdapter secure support (#1655) --- modules/prebidServerBidAdapter.js | 1 + test/spec/modules/prebidServerBidAdapter_spec.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 155bdc8f906..d4d594b39b2 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -111,6 +111,7 @@ function PrebidServer() { tid: bidRequest.tid, max_bids: config.maxBids, timeout_millis: config.timeout, + secure: config.secure, url: utils.getTopWindowUrl(), prebid_version: '$prebid.version$', ad_units: bidRequest.ad_units.filter(hasSizes), diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 675a5037bc9..21098a2859f 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -19,8 +19,9 @@ const REQUEST = { 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', 'max_bids': 1, 'timeout_millis': 1000, + 'secure': 0, 'url': '', - 'prebid_version': '0.21.0-pre', + 'prebid_version': '0.30.0-pre', 'ad_units': [ { 'code': 'div-gpt-ad-1460505748561-0', From 6186a2c64a64f34f83b7279d5ca91a6c9a5deb31 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Fri, 6 Oct 2017 12:33:59 -0400 Subject: [PATCH 31/54] Aliasbidder fix (#1652) * aliasBidder did not work for bidderFactory * added function to get adapter spec * Freezing spec and moved unit tests * Updated test case to not import adapter --- src/adaptermanager.js | 26 ++++++++-- src/adapters/bidderFactory.js | 3 ++ test/spec/unit/core/adapterManager_spec.js | 55 ++++++++++++++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 231a8936a69..48c320548a0 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -3,6 +3,7 @@ import { flatten, getBidderCodes, getDefinedParams, shuffle } from './utils'; import { mapSizes } from './sizeMapping'; import { processNativeAdUnitParams, nativeAdapters } from './native'; +import { newBidder } from './adapters/bidderFactory'; var utils = require('./utils.js'); var CONSTANTS = require('./constants.json'); @@ -194,6 +195,13 @@ function transformHeightWidth(adUnit) { return sizesObj; } +function getSupportedMediaTypes(bidderCode) { + let result = []; + if (exports.videoAdapters.includes(bidderCode)) result.push('video'); + if (nativeAdapters.includes(bidderCode)) result.push('native'); + return result; +} + exports.videoAdapters = []; // added by adapterLoader for now exports.registerBidAdapter = function (bidAdaptor, bidderCode, {supportedMediaTypes = []} = {}) { @@ -220,14 +228,24 @@ exports.aliasBidAdapter = function (bidderCode, alias) { if (typeof existingAlias === 'undefined') { var bidAdaptor = _bidderRegistry[bidderCode]; - if (typeof bidAdaptor === 'undefined') { utils.logError('bidderCode "' + bidderCode + '" is not an existing bidder.', 'adaptermanager.aliasBidAdapter'); } else { try { - let newAdapter = new bidAdaptor.constructor(); - newAdapter.setBidderCode(alias); - this.registerBidAdapter(newAdapter, alias); + let newAdapter; + let supportedMediaTypes = getSupportedMediaTypes(bidderCode); + // Have kept old code to support backward compatibilitiy. + // Remove this if loop when all adapters are supporting bidderFactory. i.e When Prebid.js is 1.0 + if (bidAdaptor.constructor.prototype != Object.prototype) { + newAdapter = new bidAdaptor.constructor(); + newAdapter.setBidderCode(alias); + } else { + let spec = bidAdaptor.getSpec(); + newAdapter = newBidder(Object.assign({}, spec, { code: alias })); + } + this.registerBidAdapter(newAdapter, alias, { + supportedMediaTypes + }); } catch (e) { utils.logError(bidderCode + ' bidder does not currently support aliasing.', 'adaptermanager.aliasBidAdapter'); } diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 641af863f15..becc4afc029 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -138,6 +138,9 @@ export function registerBidder(spec) { */ export function newBidder(spec) { return Object.assign(new Adapter(spec.code), { + getSpec: function() { + return Object.freeze(spec); + }, callBids: function(bidderRequest) { if (!Array.isArray(bidderRequest.bids)) { return; diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index b829aa9b5b2..10bd6074021 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -3,6 +3,7 @@ import AdapterManager from 'src/adaptermanager'; import { getAdUnits } from 'test/fixtures/fixtures'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; const CONFIG = { enabled: true, @@ -81,4 +82,58 @@ describe('adapterManager tests', () => { expect(spy.called).to.equal(false); }); }) + + describe('aliasBidderAdaptor', function() { + const CODE = 'sampleBidder'; + + // Note: remove this describe once Prebid is 1.0 + describe('old way', function() { + let originalRegistry; + + function SampleAdapter() { + return Object.assign(this, { + callBids: sinon.stub(), + setBidderCode: sinon.stub() + }); + } + + before(() => { + originalRegistry = AdapterManager.bidderRegistry; + AdapterManager.bidderRegistry[CODE] = new SampleAdapter(); + }); + + after(() => { + AdapterManager.bidderRegistry = originalRegistry; + }); + + it('should add alias to registry', () => { + const alias = 'testalias'; + AdapterManager.aliasBidAdapter(CODE, alias); + expect(AdapterManager.bidderRegistry).to.have.property(alias); + }); + }); + + describe('using bidderFactory', function() { + let spec; + + beforeEach(() => { + spec = { + code: CODE, + isBidRequestValid: () => {}, + buildRequests: () => {}, + interpretResponse: () => {}, + getUserSyncs: () => {} + }; + }); + + it('should add alias to registry when original adapter is using bidderFactory', function() { + let thisSpec = Object.assign(spec, { supportedMediaTypes: ['video'] }); + registerBidder(thisSpec); + const alias = 'aliasBidder'; + AdapterManager.aliasBidAdapter(CODE, alias); + expect(AdapterManager.bidderRegistry).to.have.property(alias); + expect(AdapterManager.videoAdapters).to.include(alias); + }); + }); + }); }); From a9dda355f2d240d1976706ec10e1d01d96abb162 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Fri, 6 Oct 2017 13:10:28 -0400 Subject: [PATCH 32/54] fixes bug for IE when invalid value passed to parse (#1657) --- src/url.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/url.js b/src/url.js index 5bfeb9c3151..502245f3abd 100644 --- a/src/url.js +++ b/src/url.js @@ -38,7 +38,7 @@ export function parse(url, options) { pathname: parsed.pathname.replace(/^(?!\/)/, '/'), search: parseQS(parsed.search || ''), hash: (parsed.hash || '').replace(/^#/, ''), - host: parsed.host + host: parsed.host || window.location.host }; } From a2db7c424e4efd4be6731487ff1e8d4c31d07a7d Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Fri, 6 Oct 2017 10:14:12 -0700 Subject: [PATCH 33/54] Remove undefined variable usage (#1662) --- modules/prebidServerBidAdapter.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index d4d594b39b2..dcebc14dfa8 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -246,9 +246,6 @@ function PrebidServer() { ajax(config.syncEndpoint, (response) => { try { response = JSON.parse(response); - if (response.status === 'ok') { - bidderCodes.forEach(code => StorageManager.add(pbjsSyncsKey, code, true)); - } response.bidder_status.forEach(bidder => doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder)); } catch (e) { utils.logError(e); From 731c7e4a9b887a278e72fbfe84752eb4ae4eddf9 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Fri, 6 Oct 2017 14:53:20 -0400 Subject: [PATCH 34/54] Prebid 0.30.1 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c74124bcd5f..d8294788ee3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "0.31.0-pre", + "version": "0.30.1", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From a907a4a3b864f3049d03f64f84bd9325fad0ea63 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Wed, 11 Oct 2017 06:59:42 -0600 Subject: [PATCH 35/54] remove bidmanager from rubicon tests (#1671) * remove bidmanager from rubicon tests * updated rubicon tests to not use callBids * remove no longer used artifacts from rubiconBidAdapter_spec --- modules/rubiconBidAdapter.js | 13 +- test/spec/modules/rubiconBidAdapter_spec.js | 481 ++++---------------- 2 files changed, 106 insertions(+), 388 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 526e0e129c2..69981ba2b56 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -226,12 +226,11 @@ export const spec = { }, /** * @param {*} responseObj - * @param {bidRequest} bidRequest + * @param {BidRequest} bidRequest * @return {Bid[]} An array of bids which */ interpretResponse: function(responseObj, {bidRequest}) { let ads = responseObj.ads; - const adResponseKey = bidRequest.placementCode; // check overall response if (typeof responseObj !== 'object' || responseObj.status !== 'ok') { @@ -239,8 +238,8 @@ export const spec = { } // video ads array is wrapped in an object - if (bidRequest.mediaType === 'video' && typeof ads === 'object') { - ads = ads[adResponseKey]; + if (typeof bidRequest === 'object' && bidRequest.mediaType === 'video' && typeof ads === 'object') { + ads = ads[bidRequest.placementCode]; } // check the ad response @@ -251,9 +250,9 @@ export const spec = { // if there are multiple ads, sort by CPM ads = ads.sort(_adCpmSort); - let bids = ads.reduce((bids, ad) => { + return ads.reduce((bids, ad) => { if (ad.status !== 'ok') { - return; + return []; } let bid = { @@ -286,8 +285,6 @@ export const spec = { return bids; }, []); - - return bids; }, getUserSyncs: function() { if (!hasSynced) { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 5573de34f27..620fc56e516 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1,6 +1,5 @@ import { expect } from 'chai'; import adapterManager from 'src/adaptermanager'; -import bidManager from 'src/bidmanager'; import { spec, masSizeOrdering, resetUserSync } from 'modules/rubiconBidAdapter'; import { parse as parseQuery } from 'querystring'; import { newBidder } from 'src/adapters/bidderFactory'; @@ -12,7 +11,6 @@ const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be s describe('the rubicon adapter', () => { let sandbox, - adUnit, bidderRequest; function createVideoBidderRequest() { @@ -56,36 +54,6 @@ describe('the rubicon adapter', () => { beforeEach(() => { sandbox = sinon.sandbox.create(); - sandbox.useFakeServer(); - - adUnit = { - code: '/19968336/header-bid-tag-0', - sizes: [[300, 250], [320, 50]], - mediaType: 'video', - bids: [ - { - bidder: 'rubicon', - params: { - accountId: '14062', - siteId: '70608', - zoneId: '335918', - userId: '12346', - keywords: ['a', 'b', 'c'], - inventory: { - rating: '5-star', - prodtype: 'tech' - }, - visitor: { - ucat: 'new', - lastsearch: 'iphone' - }, - position: 'atf', - referrer: 'localhost' - } - } - ] - }; - bidderRequest = { bidderCode: 'rubicon', requestId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', @@ -128,43 +96,6 @@ describe('the rubicon adapter', () => { sandbox.restore(); }); - describe('callBids public interface', () => { - let rubiconAdapter = adapterManager.bidderRegistry['rubicon']; - - it('should receive a well-formed bidRequest from the adaptermanager', () => { - sandbox.stub(rubiconAdapter, 'callBids'); - - adapterManager.callBids({ - adUnits: [clone(adUnit)] - }); - - let bidderRequest = rubiconAdapter.callBids.getCall(0).args[0]; - - expect(bidderRequest).to.have.property('bids') - .that.is.an('array') - .with.lengthOf(1); - - expect(bidderRequest).to.have.deep.property('bids[0]') - .to.have.property('bidder', 'rubicon'); - - expect(bidderRequest).to.have.deep.property('bids[0]') - .to.have.property('mediaType', 'video'); - - expect(bidderRequest).to.have.deep.property('bids[0]') - .to.have.property('placementCode', adUnit.code); - - expect(bidderRequest).to.have.deep.property('bids[0]') - .with.property('sizes') - .that.is.an('array') - .with.lengthOf(2) - .that.deep.equals(adUnit.sizes); - - expect(bidderRequest).to.have.deep.property('bids[0]') - .with.property('params') - .that.deep.equals(adUnit.bids[0].params); - }); - }); - describe('MAS mapping / ordering', () => { it('should not include values without a proper mapping', () => { // two invalid sizes included: [42, 42], [1, 1] @@ -191,39 +122,16 @@ describe('the rubicon adapter', () => { }); }); - describe('callBids implementation', () => { - let rubiconAdapter; - - let bids, - addBidResponseAction; - - beforeEach(() => { - rubiconAdapter = newBidder(spec); - - bids = []; - - sandbox.stub(bidManager, 'addBidResponse', (elemId, bid) => { - bids.push(bid); - if (typeof addBidResponseAction === 'function') { - addBidResponseAction(); - addBidResponseAction = undefined; - } - }); - }); - + describe('buildRequests implementation', () => { describe('for requests', () => { describe('to fastlane', () => { - it('should make a well-formed request', () => { - rubiconAdapter.callBids(bidderRequest); - - let request = sandbox.server.requests[0]; + it('should make a well-formed request objects', () => { + sandbox.stub(Math, 'random', () => 0.1); - let [path, query] = request.url.split('?'); - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); - expect(path).to.equal( - '//fastlane.rubiconproject.com/a/api/fastlane.json' - ); + expect(request.url).to.equal('//fastlane.rubiconproject.com/a/api/fastlane.json'); let expectedQuery = { 'account_id': '14062', @@ -234,6 +142,7 @@ describe('the rubicon adapter', () => { 'p_pos': 'atf', 'rp_floor': '0.01', 'rp_secure': /[01]/, + 'rand': '0.1', 'tk_flint': INTEGRATION, 'tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', 'p_screen_res': /\d+x\d+/, @@ -250,61 +159,50 @@ describe('the rubicon adapter', () => { Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; if (value instanceof RegExp) { - expect(query[key]).to.match(value); + expect(data[key]).to.match(value); } else { - expect(query[key]).to.equal(value); + expect(data[key]).to.equal(value); } }); - - expect(query).to.have.property('rand'); }); it('should use rubicon sizes if present', () => { var sizesBidderRequest = clone(bidderRequest); sizesBidderRequest.bids[0].params.sizes = [55, 57, 59]; - rubiconAdapter.callBids(sizesBidderRequest); - - let query = parseQuery(sandbox.server.requests[0].url.split('?')[1]); + let [request] = spec.buildRequests(sizesBidderRequest.bids, sizesBidderRequest); + let data = parseQuery(request.data); - expect(query['size_id']).to.equal('55'); - expect(query['alt_size_ids']).to.equal('57,59'); + expect(data['size_id']).to.equal('55'); + expect(data['alt_size_ids']).to.equal('57,59'); }); - it('should not send a request and register an error bid if no valid sizes', () => { + it('should not validate bid request if no valid sizes', () => { var sizesBidderRequest = clone(bidderRequest); sizesBidderRequest.bids[0].sizes = [[620, 250], [300, 251]]; - rubiconAdapter.callBids(sizesBidderRequest); + let result = spec.isBidRequestValid(sizesBidderRequest.bids[0]); - expect(sandbox.server.requests.length).to.equal(0); - - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); + expect(result).to.equal(false); }); - it('should not send a request and register an error if no account id is present', () => { + it('should not validate bid request if no account id is present', () => { var noAccountBidderRequest = clone(bidderRequest); delete noAccountBidderRequest.bids[0].params.accountId; - rubiconAdapter.callBids(noAccountBidderRequest); + let result = spec.isBidRequestValid(noAccountBidderRequest.bids[0]); - expect(sandbox.server.requests.length).to.equal(0); - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); + expect(result).to.equal(false); }); it('should allow a floor override', () => { var floorBidderRequest = clone(bidderRequest); floorBidderRequest.bids[0].params.floor = 2; - rubiconAdapter.callBids(floorBidderRequest); + let [request] = spec.buildRequests(floorBidderRequest.bids, floorBidderRequest); + let data = parseQuery(request.data); - let query = parseQuery(sandbox.server.requests[0].url.split('?')[1]); - - expect(query['rp_floor']).to.equal('2'); + expect(data['rp_floor']).to.equal('2'); }); it('should send digitrust params', () => { @@ -322,12 +220,8 @@ describe('the rubicon adapter', () => { }) ); - rubiconAdapter.callBids(bidderRequest); - - let request = sandbox.server.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let expectedQuery = { 'dt.id': 'testId', @@ -338,25 +232,21 @@ describe('the rubicon adapter', () => { // test that all values above are both present and correct Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; - expect(query[key]).to.equal(value); + expect(data[key]).to.equal(value); }); delete window.DigiTrust; }); it('should not send digitrust params when DigiTrust not loaded', () => { - rubiconAdapter.callBids(bidderRequest); - - let request = sandbox.server.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); }); @@ -375,18 +265,14 @@ describe('the rubicon adapter', () => { }) ); - rubiconAdapter.callBids(bidderRequest); - - let request = sandbox.server.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); delete window.DigiTrust; @@ -407,18 +293,14 @@ describe('the rubicon adapter', () => { }) ); - rubiconAdapter.callBids(bidderRequest); - - let request = sandbox.server.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); delete window.DigiTrust; @@ -453,12 +335,8 @@ describe('the rubicon adapter', () => { return config[key]; }); - rubiconAdapter.callBids(bidderRequest); - - let request = sandbox.server.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let expectedQuery = { 'dt.id': 'testId', @@ -468,7 +346,7 @@ describe('the rubicon adapter', () => { // test that all values above are both present and correct Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; - expect(query[key]).to.equal(value); + expect(data[key]).to.equal(value); }); // should not have called DigiTrust.getUser() @@ -490,18 +368,14 @@ describe('the rubicon adapter', () => { return config[key]; }); - rubiconAdapter.callBids(bidderRequest); - - let request = sandbox.server.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); // should not have called DigiTrust.getUser() @@ -523,18 +397,14 @@ describe('the rubicon adapter', () => { return config[key]; }); - rubiconAdapter.callBids(bidderRequest); - - let request = sandbox.server.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); // should not have called DigiTrust.getUser() @@ -547,18 +417,14 @@ describe('the rubicon adapter', () => { return config[key]; }); - rubiconAdapter.callBids(bidderRequest); - - let request = sandbox.server.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); // should have called DigiTrust.getUser() once @@ -575,12 +441,10 @@ describe('the rubicon adapter', () => { bidderRequest.auctionStart + 100 ); - rubiconAdapter.callBids(bidderRequest); - - let request = sandbox.server.requests[0]; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; let url = request.url; - let post = JSON.parse(request.requestBody); expect(url).to.equal('//fastlane-adv.rubiconproject.com/v1/auction/video'); @@ -642,17 +506,15 @@ describe('the rubicon adapter', () => { // enter an explicit floor price // floorBidderRequest.bids[0].params.floor = 3.25; - rubiconAdapter.callBids(floorBidderRequest); - - let request = sandbox.server.requests[0]; - let post = JSON.parse(request.requestBody); + let [request] = spec.buildRequests(floorBidderRequest.bids, floorBidderRequest); + let post = request.data; let floor = post.slots[0].floor; expect(floor).to.equal(3.25); }); - it('should trap when no video object is passed in', () => { + it('should not validate bid request when no video object is passed in', () => { createVideoBidderRequestNoVideo(); sandbox.stub(Date, 'now', () => bidderRequest.auctionStart + 100 @@ -660,9 +522,9 @@ describe('the rubicon adapter', () => { var floorBidderRequest = clone(bidderRequest); - rubiconAdapter.callBids(floorBidderRequest); + let result = spec.isBidRequestValid(floorBidderRequest.bids[0]); - expect(sandbox.server.requests.length).to.equal(0); + expect(result).to.equal(false); }); it('should get size from bid.sizes too', () => { @@ -673,10 +535,8 @@ describe('the rubicon adapter', () => { var floorBidderRequest = clone(bidderRequest); - rubiconAdapter.callBids(floorBidderRequest); - - let request = sandbox.server.requests[0]; - let post = JSON.parse(request.requestBody); + let [request] = spec.buildRequests(floorBidderRequest.bids, floorBidderRequest); + let post = request.data; expect(post.slots[0].width).to.equal(300); expect(post.slots[0].height).to.equal(250); @@ -684,10 +544,10 @@ describe('the rubicon adapter', () => { }); }); - describe('response handler', () => { + describe('interpretResponse', () => { describe('for fastlane', () => { it('should handle a success response and sort by cpm', () => { - sandbox.server.respondWith(JSON.stringify({ + let response = { 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -742,34 +602,32 @@ describe('the rubicon adapter', () => { ] } ] - })); - - rubiconAdapter.callBids(bidderRequest); - - sandbox.server.respond(); + }; - expect(bidManager.addBidResponse.calledTwice).to.equal(true); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); expect(bids).to.be.lengthOf(2); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); expect(bids[0].bidderCode).to.equal('rubicon'); expect(bids[0].width).to.equal(320); expect(bids[0].height).to.equal(50); expect(bids[0].cpm).to.equal(0.911); expect(bids[0].creative_id).to.equal('crid-9'); + expect(bids[0].currency).to.equal('USD'); expect(bids[0].ad).to.contain(`alert('foo')`) .and.to.contain(``) .and.to.contain(`
`); expect(bids[0].rubiconTargeting.rpfl_elemid).to.equal('/19968336/header-bid-tag-0'); expect(bids[0].rubiconTargeting.rpfl_14062).to.equal('43_tier_all_test'); - expect(bids[1].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); expect(bids[1].bidderCode).to.equal('rubicon'); expect(bids[1].width).to.equal(300); expect(bids[1].height).to.equal(250); expect(bids[1].cpm).to.equal(0.811); expect(bids[1].creative_id).to.equal('crid-9'); + expect(bids[1].currency).to.equal('USD'); expect(bids[1].ad).to.contain(`alert('foo')`) .and.to.contain(``) .and.to.contain(`
`); @@ -778,7 +636,7 @@ describe('the rubicon adapter', () => { }); it('should be fine with a CPM of 0', () => { - sandbox.server.respondWith(JSON.stringify({ + let response = { 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -794,48 +652,18 @@ describe('the rubicon adapter', () => { 'cpm': 0, 'size_id': 15 }] - })); - - rubiconAdapter.callBids(bidderRequest); - - sandbox.server.respond(); - - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - }); - - it('should return currency "USD"', () => { - sandbox.server.respondWith(JSON.stringify({ - 'status': 'ok', - 'account_id': 14062, - 'site_id': 70608, - 'zone_id': 530022, - 'size_id': 15, - 'alt_size_ids': [ - 43 - ], - 'tracking': '', - 'inventory': {}, - 'ads': [{ - 'status': 'ok', - 'cpm': 0, - 'size_id': 15 - }] - })); - - rubiconAdapter.callBids(bidderRequest); + }; - sandbox.server.respond(); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); - expect(bidManager.addBidResponse.calledOnce).to.equal(true); expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bids[0].currency).to.equal('USD'); + expect(bids[0].cpm).to.be.equal(0); }); it('should handle an error with no ads returned', () => { - sandbox.server.respondWith(JSON.stringify({ + let response = { 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -847,19 +675,17 @@ describe('the rubicon adapter', () => { 'tracking': '', 'inventory': {}, 'ads': [] - })); - - rubiconAdapter.callBids(bidderRequest); + }; - sandbox.server.respond(); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); + expect(bids).to.be.lengthOf(0); }); - it('should handle an error with bad status', () => { - sandbox.server.respondWith(JSON.stringify({ + it('should handle an error', () => { + let response = { 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -873,71 +699,23 @@ describe('the rubicon adapter', () => { 'ads': [{ 'status': 'not_ok', }] - })); - - rubiconAdapter.callBids(bidderRequest); + }; - sandbox.server.respond(); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); + expect(bids).to.be.lengthOf(0); }); it('should handle an error because of malformed json response', () => { - sandbox.server.respondWith('{test{'); - - rubiconAdapter.callBids(bidderRequest); - - sandbox.server.respond(); - - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); - }); - - it('should handle error contacting endpoint', () => { - sandbox.server.respondWith([404, {}, '']); - - rubiconAdapter.callBids(bidderRequest); + let response = '{test{'; - sandbox.server.respond(); - - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); - }); - - it('should not register an error bid when a success call to addBidResponse throws an error', () => { - sandbox.server.respondWith(JSON.stringify({ - 'status': 'ok', - 'account_id': 14062, - 'site_id': 70608, - 'zone_id': 530022, - 'size_id': 15, - 'alt_size_ids': [ - 43 - ], - 'tracking': '', - 'inventory': {}, - 'ads': [{ - 'status': 'ok', - 'cpm': 0.8, - 'size_id': 15 - }] - })); - - addBidResponseAction = function() { - throw new Error('test error'); - }; - - rubiconAdapter.callBids(bidderRequest); - - sandbox.server.respond(); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); - // was calling twice for same bid, but should only call once - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); + expect(bids).to.be.lengthOf(0); }); }); @@ -947,7 +725,7 @@ describe('the rubicon adapter', () => { }); it('should register a successful bid', () => { - sandbox.server.respondWith(JSON.stringify({ + let response = { 'status': 'ok', 'ads': { '/19968336/header-bid-tag-0': [ @@ -972,18 +750,14 @@ describe('the rubicon adapter', () => { ] }, 'account_id': 7780 - })); - - rubiconAdapter.callBids(bidderRequest); - - sandbox.server.respond(); + }; - // was calling twice for same bid, but should only call once - expect(bidManager.addBidResponse.calledOnce).to.equal(true); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); expect(bids[0].bidderCode).to.equal('rubicon'); expect(bids[0].creative_id).to.equal('crid-999999'); expect(bids[0].cpm).to.equal(1); @@ -998,78 +772,25 @@ describe('the rubicon adapter', () => { }); describe('user sync', () => { - let rubiconAdapter; - let userSyncStub; const emilyUrl = 'https://tap-secure.rubiconproject.com/partner/scripts/rubicon/emily.html?rtb_ext=1'; beforeEach(() => { - // monitor userSync registrations - userSyncStub = sandbox.stub(userSync, 'registerSync'); - sandbox.stub(bidManager, 'addBidResponse'); - - sandbox.server.respondWith(JSON.stringify({ - 'status': 'ok', - 'account_id': 14062, - 'site_id': 70608, - 'zone_id': 530022, - 'size_id': 15, - 'alt_size_ids': [ - 43 - ], - 'tracking': '', - 'inventory': {}, - 'ads': [ - { - 'status': 'ok', - 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', - 'size_id': '15', - 'ad_id': '6', - 'advertiser': 7, - 'network': 8, - 'creative_id': 9, - 'type': 'script', - 'script': 'alert(\'foo\')', - 'campaign_id': 10, - 'cpm': 0.811, - 'targeting': [ - { - 'key': 'rpfl_14062', - 'values': [ - '15_tier_all_test' - ] - } - ] - } - ] - })); - - // Remove all Emily iframes for a fresh start - let iframes = document.querySelectorAll('[src="' + emilyUrl + '"]'); - for (let i = 0; i < iframes.length; i += 1) { - iframes[i].outerHTML = ''; - } - - rubiconAdapter = newBidder(spec); resetUserSync(); }); it('should register the Emily iframe', () => { - expect(userSyncStub.calledOnce).to.be.false; - rubiconAdapter.callBids(bidderRequest); - sandbox.server.respond(); - expect(userSyncStub.calledOnce).to.be.true; - expect(userSyncStub.getCall(0).args).to.eql(['iframe', 'rubicon', emilyUrl]); + let syncs = spec.getUserSyncs(); + + expect(syncs).to.deep.equal({type: 'iframe', url: emilyUrl}); }); it('should not register the Emily iframe more than once', () => { - expect(userSyncStub.calledOnce).to.be.false; - rubiconAdapter.callBids(bidderRequest); - sandbox.server.respond(); - expect(userSyncStub.calledOnce).to.be.true; - // run another auction, should still have only been called once - rubiconAdapter.callBids(bidderRequest); - sandbox.server.respond(); - expect(userSyncStub.calledOnce).to.be.true; + let syncs = spec.getUserSyncs(); + expect(syncs).to.deep.equal({type: 'iframe', url: emilyUrl}); + + // when called again, should still have only been called once + syncs = spec.getUserSyncs(); + expect(syncs).to.equal(undefined); }); }); }); From f40f0e33afa81681003e9f0ba3d41bf517c41b5f Mon Sep 17 00:00:00 2001 From: harpere Date: Thu, 12 Oct 2017 14:44:02 -0400 Subject: [PATCH 36/54] no longer attaching gpt slots to adUnits, which breaks utils.cloneJson(adUnit) (#1676) --- modules/express.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/express.js b/modules/express.js index 5d3a91c6e8e..8a5dc095476 100644 --- a/modules/express.js +++ b/modules/express.js @@ -20,6 +20,8 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { utils.logWarn('no valid adUnits found, not loading ' + MODULE_NAME); } + // store gpt slots in a more performant hash lookup by elementId (adUnit code) + var gptSlotCache = {}; // put adUnits in a more performant hash lookup by code. var adUnitsCache = adUnits.reduce(function (cache, adUnit) { if (adUnit.code && adUnit.bids) { @@ -72,7 +74,7 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { const adUnit = adUnitsCache[elemId]; if (adUnit) { - adUnit._gptSlot = gptSlot; + gptSlotCache[elemId] = gptSlot; // store by elementId adUnit.sizes = adUnit.sizes || mapGptSlotSizes(gptSlot.getSizes()); adUnits.push(adUnit); gptSlots.splice(i, 1); @@ -141,7 +143,7 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { $$PREBID_GLOBAL$$.setTargetingForGPTAsync(); fGptRefresh.apply(pads(), [ adUnits.map(function (adUnit) { - return adUnit._gptSlot; + return gptSlotCache[adUnit.code]; }) ]); } @@ -157,7 +159,7 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { // get already displayed adUnits from aGptSlots if provided, else all defined gptSlots aGptSlots = defaultSlots(aGptSlots); var adUnits = pickAdUnits(/* mutated: */ aGptSlots).filter(function (adUnit) { - return adUnit._gptSlot._displayed; + return gptSlotCache[adUnit.code]._displayed; }); if (aGptSlots.length) { @@ -171,7 +173,7 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { $$PREBID_GLOBAL$$.setTargetingForGPTAsync(); fGptRefresh.apply(pads(), [ adUnits.map(function (adUnit) { - return adUnit._gptSlot + return gptSlotCache[adUnit.code]; }), options ]); From 2988065482e4530d1d82fc0aa4cfd25d07e3b9e1 Mon Sep 17 00:00:00 2001 From: Matt Probert Date: Fri, 13 Oct 2017 15:44:04 +0200 Subject: [PATCH 37/54] Fix adapter tests that hardcoded pbjs. (#1666) Updated adapters to use `$$PREBID_GLOBAL$$` instead. Now if you change `globalVarName` in `package.json` the tests won't fail. --- .../modules/improvedigitalBidAdapter_spec.js | 20 ++++++++--------- test/spec/modules/orbitsoftBidAdapter_spec.js | 22 +++++++++---------- .../modules/sharethroughBidAdapter_spec.js | 4 ++-- test/spec/modules/trustxBidAdapter_spec.js | 6 ++--- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index c781406da6e..5b0a9d37d57 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -387,7 +387,7 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript with correct parameters', () => { sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); }); }); @@ -402,7 +402,7 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript with correct parameters', () => { sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pubid%22%3A1032%2C%22pkey%22%3A%22data_team_test_hb_smoke_test%22%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pubid%22%3A1032%2C%22pkey%22%3A%22data_team_test_hb_smoke_test%22%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); }); }); @@ -417,7 +417,7 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript with correct parameters', () => { sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); }); }); @@ -432,7 +432,7 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript with correct parameters', () => { sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); }); }); @@ -447,8 +447,8 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript twice with correct parameters', () => { sinon.assert.calledTwice(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); }); }); @@ -463,9 +463,9 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript thrice with correct parameters', () => { sinon.assert.calledThrice(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543212%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%227g8h9i%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543212%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%227g8h9i%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); }); }); @@ -493,7 +493,7 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript twice with correct parameters', () => { sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%2C%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%2C%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); }); }); diff --git a/test/spec/modules/orbitsoftBidAdapter_spec.js b/test/spec/modules/orbitsoftBidAdapter_spec.js index dc37be73483..4b24787f56b 100644 --- a/test/spec/modules/orbitsoftBidAdapter_spec.js +++ b/test/spec/modules/orbitsoftBidAdapter_spec.js @@ -14,7 +14,7 @@ describe('Orbitsoft Adapter tests', function () { describe('test orbitsoft callback response', function () { it('should exist and be a function', function () { - expect(pbjs.handleOASCB).to.exist.and.to.be.a('function'); + expect($$PREBID_GLOBAL$$.handleOASCB).to.exist.and.to.be.a('function'); }); it('should add empty bid responses if no bids returned', function () { @@ -43,8 +43,8 @@ describe('Orbitsoft Adapter tests', function () { cpm: 0 }; - pbjs._bidsRequested.push(bidderRequest); - pbjs.handleOASCB(response); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$.handleOASCB(response); let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; @@ -79,8 +79,8 @@ describe('Orbitsoft Adapter tests', function () { cpm: 0 }; - pbjs._bidsRequested.push(bidderRequest); - pbjs.handleOASCB(response); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$.handleOASCB(response); expect(stubAddBidResponse.getCall(0)).to.equal(null); stubAddBidResponse.restore(); @@ -116,8 +116,8 @@ describe('Orbitsoft Adapter tests', function () { height: 250 }; - pbjs._bidsRequested.push(bidderRequest); - pbjs.handleOASCB(response); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$.handleOASCB(response); let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; @@ -277,9 +277,9 @@ describe('Orbitsoft Adapter tests', function () { height: 250 }; - pbjs._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - pbjs.handleOASCB(response); + $$PREBID_GLOBAL$$.handleOASCB(response); let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; let adUrl = bidResponse1.adUrl; @@ -339,8 +339,8 @@ describe('Orbitsoft Adapter tests', function () { height: 250 }; - pbjs._bidsRequested.push(bidderRequest); - pbjs.handleOASCB(response); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$.handleOASCB(response); let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; let adUrl = bidResponse1.adUrl; diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 94c8f438cc0..b92dfe4d493 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -173,7 +173,7 @@ describe('sharethrough adapter', () => { describe('when bidResponse string cannot be JSON parsed', () => { beforeEach(() => { - pbjs._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); adapter.str.placementCodeSet['foo'] = {}; server.respondWith(/aaaa1111/, 'non JSON string'); @@ -199,7 +199,7 @@ describe('sharethrough adapter', () => { describe('when no fill', () => { beforeEach(() => { - pbjs._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); adapter.str.placementCodeSet['foo'] = {}; let bidderResponse1 = { diff --git a/test/spec/modules/trustxBidAdapter_spec.js b/test/spec/modules/trustxBidAdapter_spec.js index e559a16d71a..7208ebef343 100644 --- a/test/spec/modules/trustxBidAdapter_spec.js +++ b/test/spec/modules/trustxBidAdapter_spec.js @@ -8,10 +8,10 @@ describe('trustx adapter tests', function () { var bidmanager = require('src/bidmanager'); var adLoader = require('src/adloader'); var utils = require('src/utils'); - window.pbjs = window.pbjs || {}; + window.$$PREBID_GLOBAL$$ = window.$$PREBID_GLOBAL$$ || {}; if (typeof (pbjs) === 'undefined') { - var pbjs = window.pbjs; + var pbjs = window.$$PREBID_GLOBAL$$; } let stubLoadScript; beforeEach(function () { @@ -73,7 +73,7 @@ describe('trustx adapter tests', function () { sinon.assert.calledWith(stubLoadScript, bidUrl); var parsedBidUrl = urlParse(bidUrl); var parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); - var generatedCallback = 'pbjs.trustx_callback_wrapper_5_6'; + var generatedCallback = '$$PREBID_GLOBAL$$.trustx_callback_wrapper_5_6'; expect(parsedBidUrl.hostname).to.equal('sofia.trustx.org'); expect(parsedBidUrl.pathname).to.equal('/hb'); expect(parsedBidUrlQueryString).to.have.property('auids').and.to.equal('5,6'); From 2f58bb098890b6d66a7b33c33eb6420c0900c64c Mon Sep 17 00:00:00 2001 From: Jeremy Blencowe Date: Fri, 13 Oct 2017 09:53:20 -0700 Subject: [PATCH 38/54] Fix broken AOL mobile endpoint secure bid requests (#1684) * Add bidfloor parameter to AOL adapter * Remove errant comma * Fixed AOL adapter mobile endpoint secure support --- modules/aolBidAdapter.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/aolBidAdapter.js b/modules/aolBidAdapter.js index 4506ac1512e..5adba83e9df 100644 --- a/modules/aolBidAdapter.js +++ b/modules/aolBidAdapter.js @@ -165,12 +165,17 @@ const AolAdapter = function AolAdapter() { function _buildNexageApiUrl(bid) { let {dcn, pos} = bid.params; + let isSecure = (document.location.protocol === 'https:'); let nexageApi = nexageBaseApiTemplate({ - protocol: (document.location.protocol === 'https:') ? 'https' : 'http', + protocol: isSecure ? 'https' : 'http', host: bid.params.host || NEXAGE_SERVER }); if (dcn && pos) { let ext = ''; + if (isSecure) { + bid.params.ext = bid.params.ext || {}; + bid.params.ext.secure = 1; + } utils._each(bid.params.ext, (value, key) => { ext += `&${key}=${encodeURIComponent(value)}`; }); From fc9cbfb5541148657a4f69ef7a841f0eaffccce6 Mon Sep 17 00:00:00 2001 From: npeceniak Date: Fri, 13 Oct 2017 11:06:18 -0600 Subject: [PATCH 39/54] Update spotx video adapter to set the spotx_ad_key used in DFP (#1614) * Update spotx video adapter to set the spotx_ad_key used in DFP targeting to the bid.adId * Changed spotx adapter to set hb_adid through registerDefaultBidderSetting - Instead of setting the adid to the spotx key in the bid response itself we now set a key bid.spotx_ad_key on the bid and then overwrite the hb_adid using this key. --- modules/spotxBidAdapter.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index e8ed973fe5f..2e2831a028a 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -11,6 +11,20 @@ function Spotx() { let bidReq; let KVP_Object; + const _defaultBidderSettings = { + alwaysUseBid: true, + adserverTargeting: [ + { + key: 'hb_adid', + val: function (bidResponse) { + return bidResponse.spotx_ad_key; + } + } + ] + }; + + bidmanager.registerDefaultBidderSetting('spotx', _defaultBidderSettings); + baseAdapter.callBids = function(bidRequest) { if (!bidRequest || !bidRequest.bids || bidRequest.bids.length === 0) { return; @@ -85,7 +99,7 @@ function Spotx() { bid.cpm = KVP_Object.spotx_bid; bid.vastUrl = url; - bid.ad = url; + bid.spotx_ad_key = KVP_Object.spotx_ad_key; var sizes = utils.isArray(bidReq.sizes[0]) ? bidReq.sizes[0] : bidReq.sizes; bid.height = sizes[1]; From 4eff79f2232ab91ab29d3ddad92efc9b692f84f5 Mon Sep 17 00:00:00 2001 From: Anand Venkatraman Date: Mon, 16 Oct 2017 22:58:53 +0530 Subject: [PATCH 40/54] PulsePoint Lite adapter - Enabling Sync pixel (#1686) * ET-1691: Pulsepoint Analytics adapter for Prebid. (#1) * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter * ET-1691: cleanup * ET-1691: minor * ET-1691: revert package.json change * Adding bidRequest to bidFactory.createBid method as per https://github.com/prebid/Prebid.js/issues/509 * ET-1765: Adding support for additional params in PulsePoint adapter (#2) * ET-1850: Fixing https://github.com/prebid/Prebid.js/issues/866 * Minor fix * Enabling User sync pixel * minor fix --- modules/pulsepointLiteBidAdapter.js | 5 +++++ test/spec/modules/pulsepointLiteBidAdapter_spec.js | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/modules/pulsepointLiteBidAdapter.js b/modules/pulsepointLiteBidAdapter.js index 84f955317c0..00b5c014e98 100644 --- a/modules/pulsepointLiteBidAdapter.js +++ b/modules/pulsepointLiteBidAdapter.js @@ -53,6 +53,11 @@ export const spec = { type: 'iframe', url: '//bh.contextweb.com/visitormatch' }]; + } else if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: '//bh.contextweb.com/visitormatch/prebid' + }]; } } diff --git a/test/spec/modules/pulsepointLiteBidAdapter_spec.js b/test/spec/modules/pulsepointLiteBidAdapter_spec.js index 8e1f12dac93..96f5c7a8d1f 100644 --- a/test/spec/modules/pulsepointLiteBidAdapter_spec.js +++ b/test/spec/modules/pulsepointLiteBidAdapter_spec.js @@ -224,6 +224,14 @@ describe('PulsePoint Lite Adapter Tests', () => { expect(options[0].url).to.equal('//bh.contextweb.com/visitormatch'); }); + it('Verifies image pixel sync', () => { + const options = spec.getUserSyncs({ pixelEnabled: true}); + expect(options).to.not.be.undefined; + expect(options).to.have.lengthOf(1); + expect(options[0].type).to.equal('image'); + expect(options[0].url).to.equal('//bh.contextweb.com/visitormatch/prebid'); + }); + it('Verify app requests', () => { const request = spec.buildRequests(appSlotConfig); const ortbRequest = JSON.parse(request.data); From e1f2d08d55375cdcc2b4b8f3d91931730efb2bde Mon Sep 17 00:00:00 2001 From: PWyrembak Date: Mon, 16 Oct 2017 22:10:14 +0300 Subject: [PATCH 41/54] Code improvement for trustx adapter (#1673) * Add trustx adapter and tests for it * update integration example * Update trustx adapter * Post-review fixes of Trustx adapter * Code improvement for trustx adapter: changed default price type from gross to net --- modules/trustxBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/trustxBidAdapter.js b/modules/trustxBidAdapter.js index 44c00c2c0b3..13f893a841d 100644 --- a/modules/trustxBidAdapter.js +++ b/modules/trustxBidAdapter.js @@ -37,7 +37,7 @@ var TrustxAdapter = function TrustxAdapter() { query.push('u=' + encodeURIComponent(location.href)); query.push('auids=' + encodeURIComponent(auids.join(','))); query.push('cb=' + _makeHandler(auids, placementMap)); - query.push('pt=' + (window.globalPrebidTrustxPriceType === 'net' ? 'net' : 'gross')); + query.push('pt=' + (window.globalPrebidTrustxPriceType === 'gross' ? 'gross' : 'net')); adloader.loadScript(reqHost + path + query.join('&')); } From a20c3f863360b05186ebec0767a6cba3c1f5a689 Mon Sep 17 00:00:00 2001 From: Theodore Rand Date: Mon, 16 Oct 2017 14:02:00 -0600 Subject: [PATCH 42/54] Change Default Content-Type for POST Requests to 'application/json' (#1681) * Update Sovrn adapter. Add test coverage. Enable deal IDs. * HS-271: Avoid using private variables such as _bidsRequested and _bidsReceived in Sovrn adapter and Sovrn tests. * lint * Add bidfloor param to test. * changed post content-type in bidder factory to 'application/json', as this is the stated standard for Prebid 1.0.0. * Revert "changed post content-type in bidder factory to 'application/json', as this is the stated standard for Prebid 1.0.0." This reverts commit 0338ce7eba1effb2a09d63fe5760fa9f69567d49. * Changed method for altering contentType so that it is configurable via the ServerRequest object. * Altered PR to conform to change reviews. added unit tests. * Added comment to pass Trion adapter test. * Removed false-y check for request.options. Added request.options config for GET requests. Added test for this second change and removed tests for passing null or undefined member variables into the request.options object. * small optimization to request.options to remove extra object declaration. --- src/adapters/bidderFactory.js | 9 +++-- test/spec/unit/core/bidderFactory_spec.js | 49 +++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index becc4afc029..e490e17c60d 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -66,6 +66,7 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @property {('GET'|'POST')} method The type of request which this is. * @property {string} url The endpoint for the request. For example, "//bids.example.com". * @property {string|object} data Data to be sent in the request. + * @property {object} options Content-Type set in the header of the bid request, overrides default 'text/plain'. * If this is a GET request, they'll become query params. If it's a POST request, they'll be added to the body. * Strings will be added as-is. Objects will be unpacked into query params based on key/value mappings, or * JSON-serialized into the Request body. @@ -233,10 +234,10 @@ export function newBidder(spec) { error: onFailure }, undefined, - { + Object.assign({ method: 'GET', withCredentials: true - } + }, request.options) ); break; case 'POST': @@ -247,11 +248,11 @@ export function newBidder(spec) { error: onFailure }, typeof request.data === 'string' ? request.data : JSON.stringify(request.data), - { + Object.assign({ method: 'POST', contentType: 'text/plain', withCredentials: true - } + }, request.options) ); break; default: diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 04e4c051365..e91ddcf39a4 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -147,6 +147,31 @@ describe('bidders created by newBidder', () => { }); }); + it('should make the appropriate POST request when options are passed', () => { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const options = { contentType: 'application/json'}; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: url, + data: data, + options: options + }); + + bidder.callBids(MOCK_BIDS_REQUEST); + + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(url); + expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + method: 'POST', + contentType: 'application/json', + withCredentials: true + }); + }); + it('should make the appropriate GET request', () => { const bidder = newBidder(spec); const url = 'test.url.com'; @@ -169,6 +194,30 @@ describe('bidders created by newBidder', () => { }); }); + it('should make the appropriate GET request when options are passed', () => { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const opt = { withCredentials: false } + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'GET', + url: url, + data: data, + options: opt + }); + + bidder.callBids(MOCK_BIDS_REQUEST); + + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2&`); + expect(ajaxStub.firstCall.args[2]).to.be.undefined; + expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + method: 'GET', + withCredentials: false + }); + }); + it('should make multiple calls if the spec returns them', () => { const bidder = newBidder(spec); const url = 'test.url.com'; From 3f8021cab97cede3d272be9cf162e95eac69cb32 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Mon, 16 Oct 2017 16:06:19 -0400 Subject: [PATCH 43/54] AppnexusAst bidadapter markdown file (#1696) * added appnexusAst bidadapter markdown file * updated mediatype native config --- modules/appnexusAstBidAdapter.md | 103 +++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 modules/appnexusAstBidAdapter.md diff --git a/modules/appnexusAstBidAdapter.md b/modules/appnexusAstBidAdapter.md new file mode 100644 index 00000000000..2b370e11616 --- /dev/null +++ b/modules/appnexusAstBidAdapter.md @@ -0,0 +1,103 @@ +# Overview + +``` +Module Name: AppnexusAst Bid Adapter +Module Type: Bidder Adapter +Maintainer: info@prebid.org +``` + +# Description + +Connects to Appnexus exchange for bids. + +AppnexusAst bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + sizes: [[300, 250], [300,600]], + bids: [{ + bidder: 'appnexusAst', + params: { + placementId: '10433394' + } + }] + }, + // Native adUnit + { + code: 'native-div', + sizes: [[300, 250], [300,600]], + mediaTypes: { + native: { + title: { + required: true, + len: 80 + }, + body: { + required: true + }, + brand: { + required: true + }, + image: { + required: true + }, + clickUrl: { + required: true + }, + } + }, + bids: [{ + bidder: 'appnexusAst', + params: { + placementId: '9880618' + } + }] + }, + // Video instream adUnit + { + code: 'video-instream', + sizes: [640, 480], + mediaTypes: { + video: { + context: 'instream' + }, + }, + bids: [{ + bidder: 'appnexusAst', + params: { + placementId: '9333431', + video: { + skippable: true, + playback_methods: ['auto_play_sound_off'] + } + } + }] + }, + // Video outstream adUnit + { + code: 'video-outstream', + sizes: [[640, 480]], + mediaTypes: { + video: { + context: 'outstream' + } + }, + bids: [ + { + bidder: 'appnexusAst', + params: { + placementId: '5768085', + video: { + skippable: true, + playback_method: ['auto_play_sound_off'] + } + } + } + ] + } +]; +``` From 209921cc89ff58ed6dc8f48c2dbc3750aaec45ed Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Mon, 16 Oct 2017 17:22:27 -0400 Subject: [PATCH 44/54] AppnexusAst adapter: logging error message from endpoint (#1697) * logging error message received from endpoint * removed alias --- modules/appnexusAstBidAdapter.js | 32 ++++++++++++------- .../modules/appnexusAstBidAdapter_spec.js | 12 ++++--- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index b0997992a0c..78b997b3ef4 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -46,7 +46,7 @@ export const spec = { * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. * @return ServerRequest Info describing the request to the server. */ - buildRequests: function(bidRequests) { + buildRequests: function(bidRequests, bidderRequest) { const tags = bidRequests.map(bidToTag); const userObjBid = bidRequests.find(hasUserInfo); let userObj; @@ -76,6 +76,7 @@ export const spec = { method: 'POST', url: URL, data: payloadString, + bidderRequest }; }, @@ -85,18 +86,27 @@ export const spec = { * @param {*} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: function(serverResponse) { + interpretResponse: function(serverResponse, {bidderRequest}) { const bids = []; - serverResponse.tags.forEach(serverBid => { - const rtbBid = getRtbBid(serverBid); - if (rtbBid) { - if (rtbBid.cpm !== 0 && SUPPORTED_AD_TYPES.includes(rtbBid.ad_type)) { - const bid = newBid(serverBid, rtbBid); - bid.mediaType = parseMediaType(rtbBid); - bids.push(bid); + if (!serverResponse || serverResponse.error) { + let errorMessage = `in response for ${bidderRequest.bidderCode} adapter`; + if (serverResponse && serverResponse.error) { errorMessage += `: ${serverResponse.error}`; } + utils.logError(errorMessage); + return bids; + } + + if (serverResponse.tags) { + serverResponse.tags.forEach(serverBid => { + const rtbBid = getRtbBid(serverBid); + if (rtbBid) { + if (rtbBid.cpm !== 0 && SUPPORTED_AD_TYPES.includes(rtbBid.ad_type)) { + const bid = newBid(serverBid, rtbBid); + bid.mediaType = parseMediaType(rtbBid); + bids.push(bid); + } } - } - }); + }); + } return bids; }, diff --git a/test/spec/modules/appnexusAstBidAdapter_spec.js b/test/spec/modules/appnexusAstBidAdapter_spec.js index f8288819a55..d07ee6df543 100644 --- a/test/spec/modules/appnexusAstBidAdapter_spec.js +++ b/test/spec/modules/appnexusAstBidAdapter_spec.js @@ -304,8 +304,9 @@ describe('AppNexusAdapter', () => { 'mediaType': 'banner' } ]; + let bidderRequest; - let result = spec.interpretResponse(response); + let result = spec.interpretResponse(response, {bidderRequest}); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); @@ -319,8 +320,9 @@ describe('AppNexusAdapter', () => { 'nobid': true }] }; + let bidderRequest; - let result = spec.interpretResponse(response); + let result = spec.interpretResponse(response, {bidderRequest}); expect(result.length).to.equal(0); }); @@ -339,8 +341,9 @@ describe('AppNexusAdapter', () => { }] }] }; + let bidderRequest; - let result = spec.interpretResponse(response); + let result = spec.interpretResponse(response, {bidderRequest}); expect(result[0]).to.have.property('vastUrl'); expect(result[0]).to.have.property('descriptionUrl'); expect(result[0]).to.have.property('mediaType', 'video'); @@ -371,8 +374,9 @@ describe('AppNexusAdapter', () => { }, 'impression_trackers': ['http://example.com'], }; + let bidderRequest; - let result = spec.interpretResponse(response1); + let result = spec.interpretResponse(response1, {bidderRequest}); expect(result[0].native.title).to.equal('Native Creative'); expect(result[0].native.body).to.equal('Cool description great stuff'); expect(result[0].native.cta).to.equal('Do it'); From 5bfcdc462cc261c4dc48af0686a51b6647199d39 Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Tue, 17 Oct 2017 11:28:35 -0400 Subject: [PATCH 45/54] Add ad units event (#1702) * Adding an addAdUnits event * Fixing errors --- src/constants.json | 3 ++- src/prebid.js | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/constants.json b/src/constants.json index a4649e09c31..806b3790c12 100644 --- a/src/constants.json +++ b/src/constants.json @@ -32,7 +32,8 @@ "BID_RESPONSE": "bidResponse", "BID_WON": "bidWon", "SET_TARGETING": "setTargeting", - "REQUEST_BIDS": "requestBids" + "REQUEST_BIDS": "requestBids", + "ADD_AD_UNITS": "addAdUnits" }, "EVENT_ID_PATHS": { "bidWon": "adUnitCode" diff --git a/src/prebid.js b/src/prebid.js index 4d94fb475ce..4aeb5482100 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -28,6 +28,7 @@ const { syncUsers, triggerUserSyncs } = userSync; var BID_WON = CONSTANTS.EVENTS.BID_WON; var SET_TARGETING = CONSTANTS.EVENTS.SET_TARGETING; +var ADD_AD_UNITS = CONSTANTS.EVENTS.ADD_AD_UNITS; var auctionRunning = false; var bidRequestQueue = []; @@ -447,6 +448,8 @@ $$PREBID_GLOBAL$$.addAdUnits = function (adUnitArr) { adUnitArr.transactionId = utils.generateUUID(); $$PREBID_GLOBAL$$.adUnits.push(adUnitArr); } + // emit event + events.emit(ADD_AD_UNITS); }; /** From 1ed7fb55f0353739b46a53d340e7eed49879f54c Mon Sep 17 00:00:00 2001 From: Rich Loveland Date: Tue, 17 Oct 2017 11:31:30 -0400 Subject: [PATCH 46/54] Update JSDoc for `pbjs.enableAnalytics` (#1565) --- src/prebid.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/prebid.js b/src/prebid.js index 4aeb5482100..5e3168a829d 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -611,8 +611,17 @@ $$PREBID_GLOBAL$$.loadScript = function (tagSrc, callback, useCache) { }; /** - * Will enable sending a prebid.js to data provider specified - * @param {Object} config object {provider : 'string', options : {}} + * Enable sending analytics data to the analytics provider of your + * choice. + * + * For usage, see [Integrate with the Prebid Analytics + * API](http://prebid.org/dev-docs/integrate-with-the-prebid-analytics-api.html). + * + * For a list of supported analytics adapters, see [Analytics for + * Prebid](http://prebid.org/overview/analytics.html). + * @param {Object} config + * @param {string} config.provider The name of the provider, e.g., `"ga"` for Google Analytics. + * @param {Object} config.options The options for this particular analytics adapter. This will likely vary between adapters. */ $$PREBID_GLOBAL$$.enableAnalytics = function (config) { if (config && !utils.isEmpty(config)) { From c89e91159c6f6c41a4b43129cd316e180fcc5d50 Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Tue, 17 Oct 2017 10:22:15 -0700 Subject: [PATCH 47/54] Don't set non-object configurations (#1704) --- src/config.js | 13 +++++++------ test/spec/config_spec.js | 7 ++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/config.js b/src/config.js index 704cb630982..41ba9d25301 100644 --- a/src/config.js +++ b/src/config.js @@ -23,12 +23,12 @@ const DEFAULT_USERSYNC = { }; const GRANULARITY_OPTIONS = { - 'LOW': 'low', - 'MEDIUM': 'medium', - 'HIGH': 'high', - 'AUTO': 'auto', - 'DENSE': 'dense', - 'CUSTOM': 'custom' + LOW: 'low', + MEDIUM: 'medium', + HIGH: 'high', + AUTO: 'auto', + DENSE: 'dense', + CUSTOM: 'custom' }; const ALL_TOPICS = '*'; @@ -174,6 +174,7 @@ export function newConfig() { function setConfig(options) { if (typeof options !== 'object') { utils.logError('setConfig options must be an object'); + return; } Object.assign(config, options); diff --git a/test/spec/config_spec.js b/test/spec/config_spec.js index efdfd911728..14452987091 100644 --- a/test/spec/config_spec.js +++ b/test/spec/config_spec.js @@ -1,4 +1,4 @@ -import { excpet } from 'chai'; +import { expect } from 'chai'; import { assert } from 'chai'; import { newConfig } from 'src/config'; @@ -33,6 +33,11 @@ describe('config API', () => { expect(getConfig('baz')).to.equal('qux'); }); + it('only accepts objects', () => { + setConfig('invalid'); + expect(getConfig('0')).to.not.equal('i'); + }); + it('sets multiple config properties', () => { setConfig({ foo: 'bar' }); setConfig({ biz: 'buz' }); From 54edd806fb9ae1e9a9418842b23260ef5939ec7d Mon Sep 17 00:00:00 2001 From: Bill Newman Date: Tue, 17 Oct 2017 20:46:03 +0300 Subject: [PATCH 48/54] Renaming of "huddledmasses" adapter into colossusssp (#1701) --- integrationExamples/gpt/pbjs_example_gpt.html | 4 ++-- ...BidAdapter.js => colossussspBidAdapter.js} | 22 +++++++++---------- ..._spec.js => colossussspBidAdapter_spec.js} | 16 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) rename modules/{huddledmassesBidAdapter.js => colossussspBidAdapter.js} (85%) rename test/spec/modules/{huddledmassesBidAdapter_spec.js => colossussspBidAdapter_spec.js} (88%) diff --git a/integrationExamples/gpt/pbjs_example_gpt.html b/integrationExamples/gpt/pbjs_example_gpt.html index e670c6f5b93..77c875b9787 100644 --- a/integrationExamples/gpt/pbjs_example_gpt.html +++ b/integrationExamples/gpt/pbjs_example_gpt.html @@ -264,7 +264,7 @@ } }, { - bidder: 'huddledmasses', + bidder: 'colossusssp', params: { placement_id: 0 } @@ -384,7 +384,7 @@ } }, { - bidder: 'huddledmasses', + bidder: 'colossusssp', params: { placement_id: 0 } diff --git a/modules/huddledmassesBidAdapter.js b/modules/colossussspBidAdapter.js similarity index 85% rename from modules/huddledmassesBidAdapter.js rename to modules/colossussspBidAdapter.js index 76511ee129d..8cf6239c8e5 100644 --- a/modules/huddledmassesBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -6,7 +6,7 @@ import {ajax} from 'src/ajax'; import {STATUS} from 'src/constants'; import adaptermanager from 'src/adaptermanager'; -var BIDDER_CODE = 'huddledmasses'; +var BIDDER_CODE = 'colossusssp'; var sizeObj = { 1: '468x60', @@ -39,7 +39,7 @@ var sizeObj = { utils._each(sizeObj, (item, key) => sizeObj[item] = key); -function HuddledMassesAdapter() { +function ColossusSspAdapter() { function _callBids(bidderRequest) { var bids = bidderRequest.bids || []; @@ -50,9 +50,9 @@ function HuddledMassesAdapter() { handleRpCB(responseText, bid); } catch (err) { if (typeof err === 'string') { - utils.logWarn(`${err} when processing huddledmasses response for placement code ${bid.placementCode}`); + utils.logWarn(`${err} when processing colossus response for placement code ${bid.placementCode}`); } else { - utils.logError('Error processing huddledmasses response for placement code ' + bid.placementCode, null, err); + utils.logError('Error processing colossus response for placement code ' + bid.placementCode, null, err); } var badBid = bidfactory.createBid(STATUS.NO_BID, bid); badBid.bidderCode = bid.bidder; @@ -64,7 +64,7 @@ function HuddledMassesAdapter() { try { ajax(buildOptimizedCall(bid), bidCallback, undefined, { withCredentials: true }); } catch (err) { - utils.logError('Error sending huddledmasses request for placement code ' + bid.placementCode, null, err); + utils.logError('Error sending colossus request for placement code ' + bid.placementCode, null, err); } }); } @@ -72,7 +72,7 @@ function HuddledMassesAdapter() { function buildOptimizedCall(bid) { bid.startTime = (new Date()).getTime(); - var parsedSizes = HuddledMassesAdapter.masSizeOrdering( + var parsedSizes = ColossusSspAdapter.masSizeOrdering( Array.isArray(bid.params.sizes) ? bid.params.sizes.map(size => (sizeObj[size] || '').split('x')) : bid.sizes ); @@ -117,7 +117,7 @@ function HuddledMassesAdapter() { index % 2 === 0 && queryString[index + 1] !== undefined ? memo + curr + '=' + encodeURIComponent(queryString[index + 1]) + '&' : memo, - '//huddledmassessupply.com/?' + '//colossusssp.com/?' ).slice(0, -1); } @@ -136,12 +136,12 @@ function HuddledMassesAdapter() { bidmanager.addBidResponse(bidRequest.placementCode, bid); } - return Object.assign(new Adapter(BIDDER_CODE), { // BIDDER_CODE huddledmasses + return Object.assign(new Adapter(BIDDER_CODE), { // BIDDER_CODE colossusssp callBids: _callBids }); } -HuddledMassesAdapter.masSizeOrdering = function (sizes) { +ColossusSspAdapter.masSizeOrdering = function (sizes) { var MAS_SIZE_PRIORITY = [15, 2, 9]; return utils.parseSizesInput(sizes) .reduce((result, size) => { @@ -169,6 +169,6 @@ HuddledMassesAdapter.masSizeOrdering = function (sizes) { }); }; -adaptermanager.registerBidAdapter(new HuddledMassesAdapter(), 'huddledmasses'); +adaptermanager.registerBidAdapter(new ColossusSspAdapter(), BIDDER_CODE); -module.exports = HuddledMassesAdapter; +module.exports = ColossusSspAdapter; diff --git a/test/spec/modules/huddledmassesBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js similarity index 88% rename from test/spec/modules/huddledmassesBidAdapter_spec.js rename to test/spec/modules/colossussspBidAdapter_spec.js index f4cc12dde1b..e8435d90679 100644 --- a/test/spec/modules/huddledmassesBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -1,16 +1,16 @@ import { expect } from 'chai'; -import Adapter from '../../../modules/huddledmassesBidAdapter'; +import Adapter from '../../../modules/colossussspBidAdapter'; import adapterManager from 'src/adaptermanager'; import bidManager from 'src/bidmanager'; import CONSTANTS from 'src/constants.json'; -describe('HuddledMasses adapter tests', function () { +describe('ColossusSSP adapter tests', function () { let sandbox; const adUnit = { - code: 'huddledmasses', + code: 'colossusssp', sizes: [[300, 250], [300, 600]], bids: [{ - bidder: 'huddledmasses', + bidder: 'colossusssp', params: { placement_id: 0 } @@ -34,7 +34,7 @@ describe('HuddledMasses adapter tests', function () { sandbox.restore(); }); - describe('HuddledMasses callBids validation', () => { + describe('ColossusSSP callBids validation', () => { let bids, server; @@ -50,7 +50,7 @@ describe('HuddledMasses adapter tests', function () { server.restore(); }); - let adapter = adapterManager.bidderRegistry['huddledmasses']; + let adapter = adapterManager.bidderRegistry['colossusssp']; it('Valid bid-request', () => { sandbox.stub(adapter, 'callBids'); @@ -65,7 +65,7 @@ describe('HuddledMasses adapter tests', function () { .with.lengthOf(1); expect(bidderRequest).to.have.deep.property('bids[0]') - .to.have.property('bidder', 'huddledmasses'); + .to.have.property('bidder', 'colossusssp'); expect(bidderRequest).to.have.deep.property('bids[0]') .with.property('sizes') @@ -88,7 +88,7 @@ describe('HuddledMasses adapter tests', function () { expect(bids).to.be.lengthOf(1); expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bids[0].bidderCode).to.equal('huddledmasses'); + expect(bids[0].bidderCode).to.equal('colossusssp'); expect(bids[0].width).to.equal(300); expect(bids[0].height).to.equal(250); expect(bids[0].cpm).to.equal(0.712); From 9b5b2111abe69b24aa09c9094e15bd35c1f76232 Mon Sep 17 00:00:00 2001 From: harpere Date: Tue, 17 Oct 2017 13:54:13 -0400 Subject: [PATCH 49/54] Rubicon feature/s2s test module (#1678) * added new server-to-server testing module * fixed linting errors * s2sTesting module fixes * s2sTesting module fixes * removed "both" option from s2s ab testing module * removed leftover StorageManager reference * s2sTesting module improvements * s2sTesting module improvement * Revert "Merge branch 'bugfix/pbs-adapter-storagemanager' into rubicon-feature/s2s-test-module" This reverts commit be232c62a5fdc3c99ea49be778f490ad238789b1, reversing changes made to 4a3abd771c1e0b574f4a750525efa23b5c2a32f5. * minor s2sTesting fixes --- modules/s2sTesting.js | 126 ++++++ src/adaptermanager.js | 48 ++- test/spec/modules/s2sTesting_spec.js | 437 +++++++++++++++++++++ test/spec/unit/core/adapterManager_spec.js | 189 ++++++++- 4 files changed, 788 insertions(+), 12 deletions(-) create mode 100644 modules/s2sTesting.js create mode 100644 test/spec/modules/s2sTesting_spec.js diff --git a/modules/s2sTesting.js b/modules/s2sTesting.js new file mode 100644 index 00000000000..a821383dc2d --- /dev/null +++ b/modules/s2sTesting.js @@ -0,0 +1,126 @@ +import { config } from 'src/config'; +import { setS2STestingModule } from 'src/adaptermanager'; + +var CONSTANTS = require('src/constants.json'); +const AST = CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING; +export const SERVER = 'server'; +export const CLIENT = 'client'; + +var testing = false; // whether testing is turned on +var bidSource = {}; // store bidder sources determined from s2sConfing bidderControl + +// load s2sConfig +config.getConfig('s2sConfig', config => { + testing = config.s2sConfig && config.s2sConfig.testing; + addBidderSourceTargeting(config.s2sConfig) + calculateBidSources(config.s2sConfig); +}); + +// function to add hb_source_ adServerTargeting (AST) kvp to bidder settings +function addBidderSourceTargeting(s2sConfig = {}) { + // bail if testing is not turned on + if (!testing) { + return; + } + var bidderSettings = $$PREBID_GLOBAL$$.bidderSettings || {}; + var bidderControl = s2sConfig.bidderControl || {}; + // for each configured bidder + (s2sConfig.bidders || []).forEach((bidder) => { + // remove any existing kvp setting + if (bidderSettings[bidder] && bidderSettings[bidder][AST]) { + bidderSettings[bidder][AST] = bidderSettings[bidder][AST].filter((kvp) => { + return kvp.key !== `hb_source_${bidder}`; + }); + } + // if includeSourceKvp === true add new kvp setting + if (bidderControl[bidder] && bidderControl[bidder].includeSourceKvp) { + bidderSettings[bidder] = bidderSettings[bidder] || {}; + bidderSettings[bidder][AST] = bidderSettings[bidder][AST] || []; + bidderSettings[bidder][AST].push({ + key: `hb_source_${bidder}`, + val: function (bidResponse) { + // default to client (currently only S2S sets this) + return bidResponse.source || CLIENT; + } + }); + // make sure "alwaysUseBid" is true so targeting is set + bidderSettings[bidder].alwaysUseBid = true; + } + }); +} + +export function getSourceBidderMap(adUnits = []) { + var sourceBidders = {[SERVER]: {}, [CLIENT]: {}}; + + // bail if testing is not turned on + if (!testing) { + return {[SERVER]: [], [CLIENT]: []}; + } + + adUnits.forEach((adUnit) => { + // if any adUnit bidders specify a bidSource, include them + (adUnit.bids || []).forEach((bid) => { + // calculate the source once and store on bid object + bid.calcSource = bid.calcSource || getSource(bid.bidSource); + // if no bidSource at bid level, default to bidSource from bidder + bid.finalSource = bid.calcSource || bidSource[bid.bidder] || CLIENT; // default to client + // add bidder to sourceBidders data structure + sourceBidders[bid.finalSource][bid.bidder] = true; + }); + }); + + // make sure all bidders in bidSource are in sourceBidders + Object.keys(bidSource).forEach((bidder) => { + sourceBidders[bidSource[bidder]][bidder] = true; + }); + + // return map of source => array of bidders + return { + [SERVER]: Object.keys(sourceBidders[SERVER]), + [CLIENT]: Object.keys(sourceBidders[CLIENT]) + }; +} + +/** + * @function calculateBidSources determines the source for each s2s bidder based on bidderControl weightings. these can be overridden at the adUnit level + * @param s2sConfig server-to-server configuration + */ +function calculateBidSources(s2sConfig = {}) { + // bail if testing is not turned on + if (!testing) { + return; + } + bidSource = {}; // reset bid sources + // calculate bid source (server/client) for each s2s bidder + var bidderControl = s2sConfig.bidderControl || {}; + (s2sConfig.bidders || []).forEach((bidder) => { + bidSource[bidder] = getSource(bidderControl[bidder] && bidderControl[bidder].bidSource) || SERVER; // default to server + }); +} + +/** + * @function getSource() gets a random source based on the given sourceWeights (export just for testing) + * @param sourceWeights mapping of relative weights of potential sources. for example {server: 1, client: 3} should do a server request 25% of the time and a client request 75% of the time. + * @param bidSources list of possible bid sources: "server", "client". In theory could get the sources from the sourceWeights keys, but this is publisher config defined, so bidSources let's us constrain that. + * @return the chosen source ("server" or "client"), or undefined if none chosen + */ +export function getSource(sourceWeights = {}, bidSources = [SERVER, CLIENT]) { + var srcIncWeight = {}; // store incremental weights of each source + var totWeight = 0; + bidSources.forEach((source) => { + totWeight += (sourceWeights[source] || 0); + srcIncWeight[source] = totWeight; + }); + if (!totWeight) return; // bail if no source weights + // choose a source randomly based on weights + var rndWeight = Math.random() * totWeight; + for (var i = 0; i < bidSources.length; i++) { + let source = bidSources[i]; + // choose the first source with an incremental weight > random weight + if (rndWeight < srcIncWeight[source]) return source; + } +} + +// inject the s2sTesting module into the adaptermanager rather than importing it +// importing it causes the packager to include it even when it's not explicitly included in the build +setS2STestingModule(exports); diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 48c320548a0..2fa3e6de3b9 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -8,6 +8,7 @@ import { newBidder } from './adapters/bidderFactory'; var utils = require('./utils.js'); var CONSTANTS = require('./constants.json'); var events = require('./events'); +let s2sTestingModule; // store s2sTesting module if it's loaded var _bidderRegistry = {}; exports.bidderRegistry = _bidderRegistry; @@ -100,25 +101,34 @@ exports.callBids = ({adUnits, cbTimeout}) => { s2sAdapter.queueSync({bidderCodes}); } + let clientTestAdapters = []; + let s2sTesting = false; if (_s2sConfig.enabled) { + // if s2sConfig.bidderControl testing is turned on + s2sTesting = _s2sConfig.testing && typeof s2sTestingModule !== 'undefined'; + if (s2sTesting) { + // get all adapters doing client testing + clientTestAdapters = s2sTestingModule.getSourceBidderMap(adUnits)[s2sTestingModule.CLIENT]; + } + // these are called on the s2s adapter let adaptersServerSide = _s2sConfig.bidders; - // don't call these client side + // don't call these client side (unless client request is needed for testing) bidderCodes = bidderCodes.filter((elm) => { - return !adaptersServerSide.includes(elm); + return !adaptersServerSide.includes(elm) || clientTestAdapters.includes(elm); }); - let adUnitsCopy = utils.cloneJson(adUnits); + let adUnitsS2SCopy = utils.cloneJson(adUnits); // filter out client side bids - adUnitsCopy.forEach((adUnit) => { + adUnitsS2SCopy.forEach((adUnit) => { if (adUnit.sizeMapping) { adUnit.sizes = mapSizes(adUnit); delete adUnit.sizeMapping; } adUnit.sizes = transformHeightWidth(adUnit); adUnit.bids = adUnit.bids.filter((bid) => { - return adaptersServerSide.includes(bid.bidder); + return adaptersServerSide.includes(bid.bidder) && (!s2sTesting || bid.finalSource !== s2sTestingModule.CLIENT); }).map((bid) => { bid.bid_id = utils.getUniqueIdentifierStr(); return bid; @@ -126,7 +136,7 @@ exports.callBids = ({adUnits, cbTimeout}) => { }); // don't send empty requests - adUnitsCopy = adUnitsCopy.filter(adUnit => { + adUnitsS2SCopy = adUnitsS2SCopy.filter(adUnit => { return adUnit.bids.length !== 0; }); @@ -138,7 +148,7 @@ exports.callBids = ({adUnits, cbTimeout}) => { requestId, bidderRequestId, tid, - bids: getBids({bidderCode, requestId, bidderRequestId, 'adUnits': adUnitsCopy}), + bids: getBids({bidderCode, requestId, bidderRequestId, 'adUnits': adUnitsS2SCopy}), start: new Date().getTime(), auctionStart: auctionStart, timeout: _s2sConfig.timeout, @@ -149,13 +159,27 @@ exports.callBids = ({adUnits, cbTimeout}) => { } }); - let s2sBidRequest = {tid, 'ad_units': adUnitsCopy}; + let s2sBidRequest = {tid, 'ad_units': adUnitsS2SCopy}; utils.logMessage(`CALLING S2S HEADER BIDDERS ==== ${adaptersServerSide.join(',')}`); if (s2sBidRequest.ad_units.length) { s2sAdapter.callBids(s2sBidRequest); } } + // client side adapters + let adUnitsClientCopy = utils.cloneJson(adUnits); + // filter out s2s bids + adUnitsClientCopy.forEach((adUnit) => { + adUnit.bids = adUnit.bids.filter((bid) => { + return !s2sTesting || bid.finalSource !== s2sTestingModule.SERVER; + }) + }); + + // don't send empty requests + adUnitsClientCopy = adUnitsClientCopy.filter(adUnit => { + return adUnit.bids.length !== 0; + }); + bidderCodes.forEach(bidderCode => { const adapter = _bidderRegistry[bidderCode]; if (adapter) { @@ -164,7 +188,7 @@ exports.callBids = ({adUnits, cbTimeout}) => { bidderCode, requestId, bidderRequestId, - bids: getBids({bidderCode, requestId, bidderRequestId, adUnits}), + bids: getBids({bidderCode, requestId, bidderRequestId, 'adUnits': adUnitsClientCopy}), start: new Date().getTime(), auctionStart: auctionStart, timeout: cbTimeout @@ -296,3 +320,9 @@ exports.setBidderSequence = function (order) { exports.setS2SConfig = function (config) { _s2sConfig = config; }; + +// the s2sTesting module is injected when it's loaded rather than being imported +// importing it causes the packager to include it even when it's not explicitly included in the build +exports.setS2STestingModule = function (module) { + s2sTestingModule = module; +}; diff --git a/test/spec/modules/s2sTesting_spec.js b/test/spec/modules/s2sTesting_spec.js new file mode 100644 index 00000000000..f829087a967 --- /dev/null +++ b/test/spec/modules/s2sTesting_spec.js @@ -0,0 +1,437 @@ +import { getSourceBidderMap, calculateBidSources, getSource } from 'modules/s2sTesting'; +import { config } from 'src/config'; + +var events = require('src/events'); +var CONSTANTS = require('src/constants.json'); +const BID_ADJUSTMENT = CONSTANTS.EVENTS.BID_ADJUSTMENT; + +var expect = require('chai').expect; + +describe('s2sTesting', function () { + let mathRandomStub; + let randomNumber = 0; + + beforeEach(() => { + mathRandomStub = sinon.stub(Math, 'random', () => { return randomNumber; }); + }); + + afterEach(() => { + mathRandomStub.restore(); + }); + + describe('getSource', () => { + // helper function to set random number and get the source + function getExpectedSource(randNumber, sourceWeights, sources) { + // set random number for testing + randomNumber = randNumber; + return getSource(sourceWeights, sources); + } + + it('returns undefined if no sources', () => { + expect(getExpectedSource(0, {})).to.be.undefined; + expect(getExpectedSource(0.5, {})).to.be.undefined; + expect(getExpectedSource(0.9999, {})).to.be.undefined; + }); + + it('returns undefined if no weights', () => { + expect(getExpectedSource(0, {server: 0, client: 0})).to.be.undefined; + expect(getExpectedSource(0.5, {client: 0})).to.be.undefined; + }); + + it('gets the expected source from 3 sources', () => { + var sources = ['server', 'client', 'both']; + expect(getExpectedSource(0, {server: 1, client: 1, both: 2}, sources)).to.equal('server'); + expect(getExpectedSource(0.2499999, {server: 1, client: 1, both: 2}, sources)).to.equal('server'); + expect(getExpectedSource(0.25, {server: 1, client: 1, both: 2}, sources)).to.equal('client'); + expect(getExpectedSource(0.49999, {server: 1, client: 1, both: 2}, sources)).to.equal('client'); + expect(getExpectedSource(0.5, {server: 1, client: 1, both: 2}, sources)).to.equal('both'); + expect(getExpectedSource(0.99999, {server: 1, client: 1, both: 2}, sources)).to.equal('both'); + }); + + it('gets the expected source from 2 sources', () => { + expect(getExpectedSource(0, {server: 2, client: 3})).to.equal('server'); + expect(getExpectedSource(0.39999, {server: 2, client: 3})).to.equal('server'); + expect(getExpectedSource(0.4, {server: 2, client: 3})).to.equal('client'); + expect(getExpectedSource(0.9, {server: 2, client: 3})).to.equal('client'); + var sources = ['server', 'client', 'both']; + expect(getExpectedSource(0, {server: 2, client: 3}, sources)).to.equal('server'); + expect(getExpectedSource(0.39999, {server: 2, client: 3}, sources)).to.equal('server'); + expect(getExpectedSource(0.4, {server: 2, client: 3}, sources)).to.equal('client'); + expect(getExpectedSource(0.9, {server: 2, client: 3}, sources)).to.equal('client'); + }); + + it('gets the expected source from 1 source', () => { + expect(getExpectedSource(0, {client: 2})).to.equal('client'); + expect(getExpectedSource(0.5, {client: 2})).to.equal('client'); + expect(getExpectedSource(0.99999, {client: 2})).to.equal('client'); + }); + + it('ignores an invalid source', () => { + expect(getExpectedSource(0, {client: 2, cache: 2})).to.equal('client'); + expect(getExpectedSource(0.3333, {server: 1, cache: 1, client: 2})).to.equal('server'); + expect(getExpectedSource(0.34, {server: 1, cache: 1, client: 2})).to.equal('client'); + }); + + it('ignores order of sources', () => { + var sources = ['server', 'client', 'both']; + expect(getExpectedSource(0, {client: 1, server: 1, both: 2}, sources)).to.equal('server'); + expect(getExpectedSource(0.2499999, {both: 2, client: 1, server: 1}, sources)).to.equal('server'); + expect(getExpectedSource(0.25, {client: 1, both: 2, server: 1}, sources)).to.equal('client'); + expect(getExpectedSource(0.49999, {server: 1, both: 2, client: 1}, sources)).to.equal('client'); + expect(getExpectedSource(0.5, {both: 2, server: 1, client: 1}, sources)).to.equal('both'); + }); + + it('accepts an array of sources', () => { + expect(getExpectedSource(0.3333, {second: 2, first: 1}, ['first', 'second'])).to.equal('first'); + expect(getExpectedSource(0.34, {second: 2, first: 1}, ['first', 'second'])).to.equal('second'); + expect(getExpectedSource(0.9999, {second: 2, first: 1}, ['first', 'second'])).to.equal('second'); + }); + }); + + describe('getSourceBidderMap', () => { + describe('setting source through s2sConfig', () => { + beforeEach(() => { + // set random number for testing + randomNumber = 0.7; + }); + + it('does not work if testing is "false"', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: false, + bidderControl: {rubicon: {bidSource: {server: 1, client: 1}}} + }}); + expect(getSourceBidderMap()).to.eql({ + server: [], + client: [] + }); + }); + + it('sets one client bidder', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: true, + bidderControl: {rubicon: {bidSource: {server: 1, client: 1}}} + }}); + expect(getSourceBidderMap()).to.eql({ + server: [], + client: ['rubicon'] + }); + }); + + it('sets one server bidder', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: true, + bidderControl: {rubicon: {bidSource: {server: 4, client: 1}}} + }}); + expect(getSourceBidderMap()).to.eql({ + server: ['rubicon'], + client: [] + }); + }); + + it('defaults to server', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: true + }}); + expect(getSourceBidderMap()).to.eql({ + server: ['rubicon'], + client: [] + }); + }); + + it('sets two bidders', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {bidSource: {server: 3, client: 1}}, + appnexus: {bidSource: {server: 1, client: 1}} + }}}); + var serverClientBidders = getSourceBidderMap(); + expect(serverClientBidders.server).to.eql(['rubicon']); + expect(serverClientBidders.client).to.have.members(['appnexus']); + }); + }); + + describe('setting source through adUnits', () => { + beforeEach(() => { + // reset s2sconfig bid sources + config.setConfig({s2sConfig: {testing: true}}); + // set random number for testing + randomNumber = 0.7; + }); + + it('sets one bidder source from one adUnit', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 4, client: 1}} + ]} + ]; + expect(getSourceBidderMap(adUnits)).to.eql({ + server: ['rubicon'], + client: [] + }); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('server'); + expect(adUnits[0].bids[0].finalSource).to.equal('server'); + + adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 1, client: 1}} + ]} + ]; + expect(getSourceBidderMap(adUnits)).to.eql({ + server: [], + client: ['rubicon'] + }); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + }); + + it('defaults to client if no bidSource', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {}} + ]} + ]; + expect(getSourceBidderMap(adUnits)).to.eql({ + server: [], + client: ['rubicon'] + }); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.be.undefined; + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + }); + + it('sets multiple bidders sources from one adUnit', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 2, client: 1}}, + {bidder: 'appnexus', bidSource: {server: 3, client: 1}} + ]} + ]; + var serverClientBidders = getSourceBidderMap(adUnits); + expect(serverClientBidders.server).to.eql(['appnexus']); + expect(serverClientBidders.client).to.have.members(['rubicon']); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + expect(adUnits[0].bids[1].calcSource).to.equal('server'); + expect(adUnits[0].bids[1].finalSource).to.equal('server'); + }); + + it('sets multiple bidders sources from multiple adUnits', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 2, client: 1}}, + {bidder: 'appnexus', bidSource: {server: 1, client: 1}} + ]}, + {bids: [ + {bidder: 'rubicon', bidSource: {server: 4, client: 1}}, + {bidder: 'bidder3', bidSource: {client: 1}} + ]} + ]; + var serverClientBidders = getSourceBidderMap(adUnits); + expect(serverClientBidders.server).to.have.members(['rubicon']); + expect(serverClientBidders.server).to.not.have.members(['appnexus', 'bidder3']); + expect(serverClientBidders.client).to.have.members(['rubicon', 'appnexus', 'bidder3']); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + expect(adUnits[0].bids[1].calcSource).to.equal('client'); + expect(adUnits[0].bids[1].finalSource).to.equal('client'); + expect(adUnits[1].bids[0].calcSource).to.equal('server'); + expect(adUnits[1].bids[0].finalSource).to.equal('server'); + expect(adUnits[1].bids[1].calcSource).to.equal('client'); + expect(adUnits[1].bids[1].finalSource).to.equal('client'); + }); + + it('should reuse calculated sources', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', calcSource: 'client', bidSource: {server: 4, client: 1}}, + {bidder: 'appnexus', calcSource: 'server', bidSource: {server: 1, client: 1}}, + {bidder: 'bidder3', calcSource: 'server', bidSource: {client: 1}} + ]} + ]; + var serverClientBidders = getSourceBidderMap(adUnits); + + expect(serverClientBidders.server).to.have.members(['appnexus', 'bidder3']); + expect(serverClientBidders.server).to.not.have.members(['rubicon']); + expect(serverClientBidders.client).to.have.members(['rubicon']); + expect(serverClientBidders.client).to.not.have.members(['appnexus', 'bidder3']); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + expect(adUnits[0].bids[1].calcSource).to.equal('server'); + expect(adUnits[0].bids[1].finalSource).to.equal('server'); + expect(adUnits[0].bids[2].calcSource).to.equal('server'); + expect(adUnits[0].bids[2].finalSource).to.equal('server'); + }); + }); + + describe('setting source through s2sconfig and adUnits', () => { + beforeEach(() => { + // reset s2sconfig bid sources + config.setConfig({s2sConfig: {testing: true}}); + // set random number for testing + randomNumber = 0.7; + }); + + it('should get sources from both', () => { + // set rubicon: server and appnexus: client + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 4, client: 1}}, + {bidder: 'appnexus', bidSource: {client: 1}} + ]} + ]; + + // set rubicon: client and appnexus: server + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {bidSource: {server: 2, client: 1}}, + appnexus: {bidSource: {server: 1}} + } + }}); + + var serverClientBidders = getSourceBidderMap(adUnits); + expect(serverClientBidders.server).to.have.members(['rubicon', 'appnexus']); + expect(serverClientBidders.client).to.have.members(['rubicon', 'appnexus']); + }); + }); + }); + + describe('addBidderSourceTargeting', () => { + const AST = CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING; + + function checkTargeting(bidder) { + var targeting = window.pbjs.bidderSettings[bidder][AST]; + var srcTargeting = targeting[targeting.length - 1]; + expect(srcTargeting.key).to.equal(`hb_source_${bidder}`); + expect(srcTargeting.val).to.be.a('function'); + expect(window.pbjs.bidderSettings[bidder].alwaysUseBid).to.be.true; + } + + function checkNoTargeting(bidder) { + var bs = window.pbjs.bidderSettings; + var targeting = bs[bidder] && bs[bidder][AST]; + if (!targeting) { + expect(targeting).to.be.undefined; + return; + } + expect(targeting.find((kvp) => { + return kvp.key === `hb_source_${bidder}`; + })).to.be.undefined; + } + + function checkTargetingVal(bidResponse, expectedVal) { + var targeting = window.pbjs.bidderSettings[bidResponse.bidderCode][AST]; + var targetingFunc = targeting[targeting.length - 1].val; + expect(targetingFunc(bidResponse)).to.equal(expectedVal); + } + + beforeEach(() => { + // set bidderSettings + window.pbjs.bidderSettings = {}; + }); + + it('should not set hb_source_ unless testing is on and includeSourceKvp is set', () => { + config.setConfig({s2sConfig: {bidders: ['rubicon', 'appnexus']}}); + expect(window.pbjs.bidderSettings).to.eql({}); + + config.setConfig({s2sConfig: {bidders: ['rubicon', 'appnexus'], testing: true}}); + expect(window.pbjs.bidderSettings).to.eql({}); + + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {bidSource: {server: 2, client: 1}}, + appnexus: {bidSource: {server: 1}} + } + }}); + expect(window.pbjs.bidderSettings).to.eql({}); + + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: false, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + expect(window.pbjs.bidderSettings).to.eql({}); + }); + + it('should set hb_source_ if includeSourceKvp is set', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + checkTargeting('rubicon'); + checkTargeting('appnexus'); + checkTargetingVal({bidderCode: 'rubicon', source: 'server'}, 'server'); + checkTargetingVal({bidderCode: 'appnexus', source: 'client'}, 'client'); + + // turn off appnexus + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: false} + } + }}); + checkTargeting('rubicon'); + checkNoTargeting('appnexus'); + checkTargetingVal({bidderCode: 'rubicon', source: 'client'}, 'client'); + + // should default to "client" + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + checkTargeting('rubicon'); + checkTargeting('appnexus'); + checkTargetingVal({bidderCode: 'rubicon'}, 'client'); + checkTargetingVal({bidderCode: 'appnexus'}, 'client'); + }); + + it('should reset adServerTargeting when a new config is set', () => { + // set config with targeting + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + checkTargeting('rubicon'); + checkTargeting('appnexus'); + + // set config without targeting + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true + }}); + checkNoTargeting('rubicon'); + checkNoTargeting('appnexus'); + }); + }); +}); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 10bd6074021..6da22ed8984 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -4,6 +4,7 @@ import { getAdUnits } from 'test/fixtures/fixtures'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils'; import { registerBidder } from 'src/adapters/bidderFactory'; +var s2sTesting = require('../../../../modules/s2sTesting'); const CONFIG = { enabled: true, @@ -19,12 +20,21 @@ var prebidServerAdapterMock = { setConfig: sinon.stub(), queueSync: sinon.stub() }; +var adequantAdapterMock = { + bidder: 'adequant', + callBids: sinon.stub(), + setConfig: sinon.stub(), + queueSync: sinon.stub() +}; +var appnexusAdapterMock = { + bidder: 'appnexus', + callBids: sinon.stub(), + setConfig: sinon.stub(), + queueSync: sinon.stub() +}; describe('adapterManager tests', () => { describe('S2S tests', () => { - var stubGetStorageItem; - var stubSetStorageItem; - beforeEach(() => { AdapterManager.setS2SConfig(CONFIG); AdapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; @@ -83,6 +93,179 @@ describe('adapterManager tests', () => { }); }) + describe('s2sTesting', () => { + function getTestAdUnits() { + // copy adUnits + return JSON.parse(JSON.stringify(getAdUnits())); + } + + function checkServerCalled(numAdUnits, numBids) { + sinon.assert.calledOnce(prebidServerAdapterMock.callBids); + var requestObj = prebidServerAdapterMock.callBids.firstCall.args[0]; + expect(requestObj.ad_units.length).to.equal(numAdUnits); + for (let i = 0; i < numAdUnits; i++) { + expect(requestObj.ad_units[i].bids.filter((bid) => { + return bid.bidder === 'appnexus' || bid.bidder === 'adequant'; + }).length).to.equal(numBids); + } + } + + function checkClientCalled(adapter, numBids) { + sinon.assert.calledOnce(adapter.callBids); + expect(adapter.callBids.firstCall.args[0].bids.length).to.equal(numBids); + } + + var TESTING_CONFIG; + var stubGetSourceBidderMap; + + beforeEach(() => { + TESTING_CONFIG = Object.assign(CONFIG, { + bidders: ['appnexus', 'adequant'], + testing: true + }); + + AdapterManager.setS2SConfig(CONFIG); + AdapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; + AdapterManager.bidderRegistry['adequant'] = adequantAdapterMock; + AdapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; + + stubGetSourceBidderMap = sinon.stub(s2sTesting, 'getSourceBidderMap'); + + prebidServerAdapterMock.callBids.reset(); + adequantAdapterMock.callBids.reset(); + appnexusAdapterMock.callBids.reset(); + }); + + afterEach(() => { + s2sTesting.getSourceBidderMap.restore(); + }); + + it('calls server adapter if no sources defined', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: [], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + AdapterManager.callBids({adUnits: getTestAdUnits()}); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + sinon.assert.notCalled(appnexusAdapterMock.callBids); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client adapter if one client source defined', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + AdapterManager.callBids({adUnits: getTestAdUnits()}); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client adapters if client sources defined', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + AdapterManager.callBids({adUnits: getTestAdUnits()}); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('does not call server adapter for bidders that go to client', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[0].bids[1].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[1].finalSource = s2sTesting.CLIENT; + AdapterManager.callBids({adUnits}); + + // server adapter + sinon.assert.notCalled(prebidServerAdapterMock.callBids); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('does not call client adapters for bidders that go to server', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.SERVER; + adUnits[0].bids[1].finalSource = s2sTesting.SERVER; + adUnits[1].bids[0].finalSource = s2sTesting.SERVER; + adUnits[1].bids[1].finalSource = s2sTesting.SERVER; + AdapterManager.callBids({adUnits}); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + sinon.assert.notCalled(appnexusAdapterMock.callBids); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client and server adapters for bidders that go to both', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.BOTH; + adUnits[0].bids[1].finalSource = s2sTesting.BOTH; + adUnits[1].bids[0].finalSource = s2sTesting.BOTH; + adUnits[1].bids[1].finalSource = s2sTesting.BOTH; + AdapterManager.callBids({adUnits}); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('makes mixed client/server adapter calls for mixed bidder sources', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[0].bids[1].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[0].finalSource = s2sTesting.SERVER; + adUnits[1].bids[1].finalSource = s2sTesting.SERVER; + AdapterManager.callBids({adUnits}); + + // server adapter + checkServerCalled(1, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 1); + + // adequant + checkClientCalled(adequantAdapterMock, 1); + }); + }); + describe('aliasBidderAdaptor', function() { const CODE = 'sampleBidder'; From ac40506edb33f8d1505497fcd8f55d4f7b236619 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Tue, 17 Oct 2017 14:30:36 -0400 Subject: [PATCH 50/54] Fixes: Immediate adapter response may end auction (#1690) --- src/adaptermanager.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 2fa3e6de3b9..2204e997084 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -166,6 +166,7 @@ exports.callBids = ({adUnits, cbTimeout}) => { } } + let _bidderRequests = []; // client side adapters let adUnitsClientCopy = utils.cloneJson(adUnits); // filter out s2s bids @@ -189,20 +190,29 @@ exports.callBids = ({adUnits, cbTimeout}) => { requestId, bidderRequestId, bids: getBids({bidderCode, requestId, bidderRequestId, 'adUnits': adUnitsClientCopy}), - start: new Date().getTime(), auctionStart: auctionStart, timeout: cbTimeout }; if (bidderRequest.bids && bidderRequest.bids.length !== 0) { - utils.logMessage(`CALLING BIDDER ======= ${bidderCode}`); $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidderRequest); - adapter.callBids(bidderRequest); + _bidderRequests.push(bidderRequest); } - } else { - utils.logError(`Adapter trying to be called which does not exist: ${bidderCode} adaptermanager.callBids`); } }); + + _bidderRequests.forEach(bidRequest => { + bidRequest.start = new Date().getTime(); + const adapter = _bidderRegistry[bidRequest.bidderCode]; + if (adapter) { + if (bidRequest.bids && bidRequest.bids.length !== 0) { + utils.logMessage(`CALLING BIDDER ======= ${bidRequest.bidderCode}`); + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + adapter.callBids(bidRequest); + } + } else { + utils.logError(`Adapter trying to be called which does not exist: ${bidRequest.bidderCode} adaptermanager.callBids`); + } + }) }; function transformHeightWidth(adUnit) { From 17115fcbd20ade531cc9c78505739c6b5d75071a Mon Sep 17 00:00:00 2001 From: Matt Kendall Date: Tue, 17 Oct 2017 14:46:33 -0400 Subject: [PATCH 51/54] Initial commit for video support for pbs (#1706) * initial commit for video support for pbs * Don't mutate bidRequest --- modules/prebidServerBidAdapter.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index dcebc14dfa8..0906a1a0b3d 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -105,7 +105,16 @@ function PrebidServer() { /* Prebid executes this function when the page asks to send out bid requests */ baseAdapter.callBids = function(bidRequest) { const isDebug = !!getConfig('debug'); - convertTypes(bidRequest.ad_units); + const adUnits = utils.cloneJson(bidRequest.ad_units); + adUnits.forEach(adUnit => { + let videoMediaType = utils.deepAccess(adUnit, 'mediaTypes.video'); + if (videoMediaType) { + // pbs expects a ad_unit.video attribute if the imp is video + adUnit.video = Object.assign({}, videoMediaType); + delete adUnit.mediaTypes.video; + } + }) + convertTypes(adUnits); let requestJson = { account_id: config.accountId, tid: bidRequest.tid, @@ -114,7 +123,7 @@ function PrebidServer() { secure: config.secure, url: utils.getTopWindowUrl(), prebid_version: '$prebid.version$', - ad_units: bidRequest.ad_units.filter(hasSizes), + ad_units: adUnits.filter(hasSizes), is_debug: isDebug }; From 819f8fcc9f1cbbacf15a0199edc4f162f92f966f Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Tue, 17 Oct 2017 12:02:11 -0700 Subject: [PATCH 52/54] Support native click tracking (#1691) * Implement native click tracking * Fire based on postMessage in adserver creative * Fix tests, add comments * Require landing page urls on native bid responses * Address code review comments --- modules/appnexusAstBidAdapter.js | 1 + src/bidmanager.js | 9 +---- src/native.js | 63 ++++++++++++++++++++++++++++---- src/secureCreatives.js | 4 +- test/spec/bidmanager_spec.js | 5 ++- test/spec/native_spec.js | 46 +++++++++++++++++++++++ 6 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 test/spec/native_spec.js diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index 78b997b3ef4..c4e2686db15 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -211,6 +211,7 @@ function newBid(serverBid, rtbBid) { image: nativeAd.main_img && nativeAd.main_img.url, icon: nativeAd.icon && nativeAd.icon.url, clickUrl: nativeAd.link.url, + clickTrackers: nativeAd.link.click_trackers, impressionTrackers: nativeAd.impression_trackers, }; } else { diff --git a/src/bidmanager.js b/src/bidmanager.js index 800a0fe9579..5d6a336b2a8 100644 --- a/src/bidmanager.js +++ b/src/bidmanager.js @@ -1,6 +1,6 @@ import { uniques, flatten, adUnitsFilter, getBidderRequest } from './utils'; import { getPriceBucketString } from './cpmBucketManager'; -import { NATIVE_KEYS, nativeBidIsValid } from './native'; +import { nativeBidIsValid, getNativeTargeting } from './native'; import { isValidVideoBid } from './video'; import { getCacheUrl, store } from './videoCache'; import { Renderer } from 'src/Renderer'; @@ -275,13 +275,8 @@ function getKeyValueTargetingPairs(bidderCode, custBidObj) { custBidObj.sendStandardTargeting = defaultBidderSettingsMap[bidderCode].sendStandardTargeting; } - // set native key value targeting if (custBidObj['native']) { - Object.keys(custBidObj['native']).forEach(asset => { - const key = NATIVE_KEYS[asset]; - const value = custBidObj['native'][asset]; - if (key) { keyValues[key] = value; } - }); + keyValues = Object.assign({}, keyValues, getNativeTargeting(custBidObj)); } return keyValues; diff --git a/src/native.js b/src/native.js index 544258818ec..c992cf9ad61 100644 --- a/src/native.js +++ b/src/native.js @@ -78,6 +78,11 @@ export function nativeBidIsValid(bid) { return false; } + // all native bid responses must define a landing page url + if (!deepAccess(bid, 'native.clickUrl')) { + return false; + } + const requestedAssets = bidRequest.nativeParams; if (!requestedAssets) { return true; @@ -94,14 +99,58 @@ export function nativeBidIsValid(bid) { } /* - * Native responses may have impression trackers. This retrieves the - * impression tracker urls for the given ad object and fires them. + * Native responses may have associated impression or click trackers. + * This retrieves the appropriate tracker urls for the given ad object and + * fires them. As a native creatives may be in a cross-origin frame, it may be + * necessary to invoke this function via postMessage. secureCreatives is + * configured to fire this function when it receives a `message` of 'Prebid Native' + * and an `adId` with the value of the `bid.adId`. When a message is posted with + * these parameters, impression trackers are fired. To fire click trackers, the + * message should contain an `action` set to 'click'. + * + * // Native creative template example usage + * + * %%PATTERN:hb_native_title%% + * + * + * */ -export function fireNativeImpressions(adObject) { - const impressionTrackers = - adObject['native'] && adObject['native'].impressionTrackers; +export function fireNativeTrackers(message, adObject) { + let trackers; + + if (message.action === 'click') { + trackers = adObject['native'] && adObject['native'].clickTrackers; + } else { + trackers = adObject['native'] && adObject['native'].impressionTrackers; + } - (impressionTrackers || []).forEach(tracker => { - triggerPixel(tracker); + (trackers || []).forEach(triggerPixel); +} + +/** + * Gets native targeting key-value paris + * @param {Object} bid + * @return {Object} targeting + */ +export function getNativeTargeting(bid) { + let keyValues = {}; + + Object.keys(bid['native']).forEach(asset => { + const key = NATIVE_KEYS[asset]; + const value = bid['native'][asset]; + if (key) { + keyValues[key] = value; + } }); + + return keyValues; } diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 2402ba755f4..efc1386fde3 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -4,7 +4,7 @@ */ import events from './events'; -import { fireNativeImpressions } from './native'; +import { fireNativeTrackers } from './native'; import { EVENTS } from './constants'; const BID_WON = EVENTS.BID_WON; @@ -42,7 +42,7 @@ function receiveMessage(ev) { // adId: '%%PATTERN:hb_adid%%' // }), '*'); if (data.message === 'Prebid Native') { - fireNativeImpressions(adObject); + fireNativeTrackers(data, adObject); $$PREBID_GLOBAL$$._winningBids.push(adObject); events.emit(BID_WON, adObject); } diff --git a/test/spec/bidmanager_spec.js b/test/spec/bidmanager_spec.js index 5263741885a..8434206c4bf 100644 --- a/test/spec/bidmanager_spec.js +++ b/test/spec/bidmanager_spec.js @@ -590,7 +590,10 @@ describe('bidmanager.js', function () { { bidderCode: 'appnexusAst', mediaType: 'native', - native: {title: 'foo'} + native: { + title: 'foo', + clickUrl: 'example.link' + } } ); diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js new file mode 100644 index 00000000000..977575a4d19 --- /dev/null +++ b/test/spec/native_spec.js @@ -0,0 +1,46 @@ +import { expect } from 'chai'; +import { fireNativeTrackers, getNativeTargeting } from 'src/native'; +const utils = require('src/utils'); + +const bid = { + native: { + title: 'Native Creative', + body: 'Cool description great stuff', + cta: 'Do it', + sponsoredBy: 'AppNexus', + clickUrl: 'https://www.link.example', + clickTrackers: ['https://tracker.example'], + impressionTrackers: ['https://impression.example'], + } +}; + +describe('native.js', () => { + let triggerPixelStub; + + beforeEach(() => { + triggerPixelStub = sinon.stub(utils, 'triggerPixel'); + }); + + afterEach(() => { + utils.triggerPixel.restore(); + }); + + it('gets native targeting keys', () => { + const targeting = getNativeTargeting(bid); + expect(targeting.hb_native_title).to.equal(bid.native.title); + expect(targeting.hb_native_body).to.equal(bid.native.body); + expect(targeting.hb_native_linkurl).to.equal(bid.native.clickUrl); + }); + + it('fires impression trackers', () => { + fireNativeTrackers({}, bid); + sinon.assert.calledOnce(triggerPixelStub); + sinon.assert.calledWith(triggerPixelStub, bid.native.impressionTrackers[0]); + }); + + it('fires click trackers', () => { + fireNativeTrackers({ action: 'click' }, bid); + sinon.assert.calledOnce(triggerPixelStub); + sinon.assert.calledWith(triggerPixelStub, bid.native.clickTrackers[0]); + }); +}); From d0140ebaf346b60cc2a2dd6957269ce01808ef87 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal Date: Tue, 17 Oct 2017 16:13:07 -0400 Subject: [PATCH 53/54] Prebid 0.31.0 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d8294788ee3..c757a971847 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "0.30.1", + "version": "0.31.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From e74ea2a9f5e8c3f64b1510ccee8a41433da180fe Mon Sep 17 00:00:00 2001 From: Yvan Date: Tue, 17 Oct 2017 22:28:21 +0200 Subject: [PATCH 54/54] Fix for #1628 (allowing standard bidCpmAdjustment) (#1645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added standard bidCpmAdjustment * Fix missing «typeof» --- src/bidmanager.js | 93 +++++++++++++++++++----------------- test/spec/bidmanager_spec.js | 26 +++++++++- 2 files changed, 75 insertions(+), 44 deletions(-) diff --git a/src/bidmanager.js b/src/bidmanager.js index 5d6a336b2a8..c12cc4828e6 100644 --- a/src/bidmanager.js +++ b/src/bidmanager.js @@ -426,10 +426,16 @@ events.on(CONSTANTS.EVENTS.BID_ADJUSTMENT, function (bid) { function adjustBids(bid) { var code = bid.bidderCode; var bidPriceAdjusted = bid.cpm; - if (code && $$PREBID_GLOBAL$$.bidderSettings && $$PREBID_GLOBAL$$.bidderSettings[code]) { - if (typeof $$PREBID_GLOBAL$$.bidderSettings[code].bidCpmAdjustment === 'function') { + let bidCpmAdjustment; + if ($$PREBID_GLOBAL$$.bidderSettings) { + if (code && $$PREBID_GLOBAL$$.bidderSettings[code] && typeof $$PREBID_GLOBAL$$.bidderSettings[code].bidCpmAdjustment === 'function') { + bidCpmAdjustment = $$PREBID_GLOBAL$$.bidderSettings[code].bidCpmAdjustment; + } else if ($$PREBID_GLOBAL$$.bidderSettings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD] && typeof $$PREBID_GLOBAL$$.bidderSettings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD].bidCpmAdjustment === 'function') { + bidCpmAdjustment = $$PREBID_GLOBAL$$.bidderSettings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD].bidCpmAdjustment; + } + if (bidCpmAdjustment) { try { - bidPriceAdjusted = $$PREBID_GLOBAL$$.bidderSettings[code].bidCpmAdjustment.call(null, bid.cpm, Object.assign({}, bid)); + bidPriceAdjusted = bidCpmAdjustment(bid.cpm, Object.assign({}, bid)); } catch (e) { utils.logError('Error during bid adjustment', 'bidmanager.js', e); } @@ -449,48 +455,49 @@ function getStandardBidderSettings() { let granularity = config.getConfig('priceGranularity'); let bidder_settings = $$PREBID_GLOBAL$$.bidderSettings; if (!bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD]) { - bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD] = { - adserverTargeting: [ - { - key: 'hb_bidder', - val: function (bidResponse) { - return bidResponse.bidderCode; - } - }, { - key: 'hb_adid', - val: function (bidResponse) { - return bidResponse.adId; - } - }, { - key: 'hb_pb', - val: function (bidResponse) { - if (granularity === CONSTANTS.GRANULARITY_OPTIONS.AUTO) { - return bidResponse.pbAg; - } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.DENSE) { - return bidResponse.pbDg; - } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.LOW) { - return bidResponse.pbLg; - } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.MEDIUM) { - return bidResponse.pbMg; - } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.HIGH) { - return bidResponse.pbHg; - } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.CUSTOM) { - return bidResponse.pbCg; - } - } - }, { - key: 'hb_size', - val: function (bidResponse) { - return bidResponse.size; - } - }, { - key: 'hb_deal', - val: function (bidResponse) { - return bidResponse.dealId; + bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD] = {}; + } + if (!bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD][CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING]) { + bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD][CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] = [ + { + key: 'hb_bidder', + val: function (bidResponse) { + return bidResponse.bidderCode; + } + }, { + key: 'hb_adid', + val: function (bidResponse) { + return bidResponse.adId; + } + }, { + key: 'hb_pb', + val: function (bidResponse) { + if (granularity === CONSTANTS.GRANULARITY_OPTIONS.AUTO) { + return bidResponse.pbAg; + } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.DENSE) { + return bidResponse.pbDg; + } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.LOW) { + return bidResponse.pbLg; + } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.MEDIUM) { + return bidResponse.pbMg; + } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.HIGH) { + return bidResponse.pbHg; + } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.CUSTOM) { + return bidResponse.pbCg; } } - ] - }; + }, { + key: 'hb_size', + val: function (bidResponse) { + return bidResponse.size; + } + }, { + key: 'hb_deal', + val: function (bidResponse) { + return bidResponse.dealId; + } + } + ]; } return bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD]; } diff --git a/test/spec/bidmanager_spec.js b/test/spec/bidmanager_spec.js index 8434206c4bf..967258a7fbc 100644 --- a/test/spec/bidmanager_spec.js +++ b/test/spec/bidmanager_spec.js @@ -198,7 +198,7 @@ describe('bidmanager.js', function () { assert.deepEqual(response, expected); }); - it('Custom bidCpmAdjustment for one bidder and inherit standard', function () { + it('Custom bidCpmAdjustment for one bidder and inherit standard but doesn\'t use standard bidCpmAdjustment', function () { $$PREBID_GLOBAL$$.bidderSettings = { appnexus: { @@ -207,6 +207,9 @@ describe('bidmanager.js', function () { }, }, standard: { + bidCpmAdjustment: function (bidCpm) { + return 200; + }, adserverTargeting: [ { key: 'hb_bidder', @@ -235,6 +238,27 @@ describe('bidmanager.js', function () { assert.deepEqual(response, expected); }); + it('Standard bidCpmAdjustment changes the bid of any bidder', function () { + const bid = Object.assign({}, + bidfactory.createBid(2), + fixtures.getBidResponses()[5] + ); + + assert.equal(bid.cpm, 0.5); + + $$PREBID_GLOBAL$$.bidderSettings = + { + standard: { + bidCpmAdjustment: function (bidCpm) { + return bidCpm * 0.5; + } + } + }; + + bidmanager.adjustBids(bid) + assert.equal(bid.cpm, 0.25); + }); + it('Custom bidCpmAdjustment AND custom configuration for one bidder and inherit standard settings', function () { $$PREBID_GLOBAL$$.bidderSettings = {