From 5f407913247f15ac2a7b668d0205ce4da0cc9c08 Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Tue, 31 Oct 2017 14:52:04 -0700 Subject: [PATCH 01/32] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b6f05afd29d..723c763ab15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "0.32.0", + "version": "0.33.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 4519cb0ca162ae75439182678e0ce646eb4cc468 Mon Sep 17 00:00:00 2001 From: Tzafrir Ben Ami Date: Wed, 1 Nov 2017 16:11:33 +0200 Subject: [PATCH 02/32] Prebid 1.0 compliant Komoona bidder adapter (#1743) * Prebid 1.0 compliant bidder adapter * PlacementId and hbId support display test banner * Remove aliases * remove check for aliases, breaks build * Add bid response test with mandatory params * change #1742 (https://github.com/prebid/Prebid.js/issues/1742): rather than interpretResponse(body), the bidderFactory is calling your adapter with interpretResponse({ body: body, ... }) * replace describe with it --- modules/komoonaBidAdapter.js | 204 +++++++------- modules/komoonaBidAdapter.md | 29 ++ test/spec/modules/komoonaBidAdapter_spec.js | 284 ++++++++++---------- 3 files changed, 281 insertions(+), 236 deletions(-) create mode 100644 modules/komoonaBidAdapter.md diff --git a/modules/komoonaBidAdapter.js b/modules/komoonaBidAdapter.js index 7cd3218d927..2a8c8753098 100644 --- a/modules/komoonaBidAdapter.js +++ b/modules/komoonaBidAdapter.js @@ -1,117 +1,121 @@ -import Adapter from 'src/adapter'; -import bidfactory from 'src/bidfactory'; -import bidmanager from 'src/bidmanager'; import * as utils 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 BIDDER_CODE = 'komoona'; const ENDPOINT = '//bidder.komoona.com/v1/GetSBids'; +const USYNCURL = '//s.komoona.com/sync/usync.html'; + +export const spec = { + code: BIDDER_CODE, + + /** + * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: bid => { + return !!(bid && bid.params && bid.params.placementId && bid.params.hbid); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: validBidRequests => { + const tags = validBidRequests.map(bid => { + // map each bid id to bid object to retrieve adUnit code in callback + let tag = { + uuid: bid.bidId, + sizes: bid.sizes, + trid: bid.transactionId, + hbid: bid.params.hbid, + placementid: bid.params.placementId + }; + + // add floor price if specified (not mandatory) + if (bid.params.floorPrice) { + tag.floorprice = bid.params.floorPrice; + } -function KomoonaAdapter() { - let baseAdapter = new Adapter('komoona'); - let bidRequests = {}; - - /* Prebid executes this function when the page asks to send out bid requests */ - baseAdapter.callBids = function(bidRequest) { - const bids = bidRequest.bids || []; - const tags = bids - .filter(bid => valid(bid)) - .map(bid => { - // map request id to bid object to retrieve adUnit code in callback - bidRequests[bid.bidId] = bid; - - let tag = {}; - tag.sizes = bid.sizes; - tag.uuid = bid.bidId; - tag.placementid = bid.params.placementId; - tag.hbid = bid.params.hbid; + return tag; + }); - return tag; - }); + // Komoona server config + const time = new Date().getTime(); + const kbConf = { + ts_as: time, + hb_placements: [], + hb_placement_bidids: {}, + hb_floors: {}, + cb: _generateCb(time), + tz: new Date().getTimezoneOffset(), + }; + + validBidRequests.forEach(bid => { + kbConf.hdbdid = kbConf.hdbdid || bid.params.hbid; + kbConf.encode_bid = kbConf.encode_bid || bid.params.encode_bid; + kbConf.hb_placement_bidids[bid.params.placementId] = bid.bidId; + if (bid.params.floorPrice) { + kbConf.hb_floors[bid.params.placementId] = bid.params.floorPrice; + } + kbConf.hb_placements.push(bid.params.placementId); + }); + let payload = {}; if (!utils.isEmpty(tags)) { - const payload = JSON.stringify({bids: [...tags]}); - - ajax(ENDPOINT, handleResponse, payload, { - contentType: 'text/plain', - withCredentials: true - }); + payload = { bids: [...tags], kbConf: kbConf }; } - }; - - /* Notify Prebid of bid responses so bids can get in the auction */ - function handleResponse(response) { - let parsed; + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(payload) + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} response A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: (response, request) => { + const bidResponses = []; try { - parsed = JSON.parse(response); - } catch (error) { - utils.logError(error); - } - - if (!parsed || parsed.error) { - let errorMessage = `in response for ${baseAdapter.getBidderCode()} adapter`; - if (parsed && parsed.error) { errorMessage += `: ${parsed.error}`; } - utils.logError(errorMessage); - - // signal this response is complete - Object.keys(bidRequests) - .map(bidId => bidRequests[bidId].placementCode) - .forEach(placementCode => { - bidmanager.addBidResponse(placementCode, createBid(STATUS.NO_BID)); + if (response.body && response.body.bids) { + response.body.bids.forEach(bid => { + // The bid ID. Used to tie this bid back to the request. + bid.requestId = bid.uuid; + // The creative payload of the returned bid. + bid.ad = bid.creative; + bidResponses.push(bid); }); - - return; - } - - parsed.bids.forEach(tag => { - let status; - if (tag.cpm > 0 && tag.creative) { - status = STATUS.GOOD; - } else { - status = STATUS.NO_BID; } - - tag.bidId = tag.uuid; // bidfactory looks for bidId on requested bid - const bid = createBid(status, tag); - const placement = bidRequests[bid.adId].placementCode; - - bidmanager.addBidResponse(placement, bid); - }); - } - - /* Check that a bid has required paramters */ - function valid(bid) { - if (bid.params.placementId && bid.params.hbid) { - return bid; - } else { - utils.logError('bid requires placementId and hbid params'); + } catch (error) { + utils.logError(error); } - } - - /* Create and return a bid object based on status and tag */ - function createBid(status, tag) { - let bid = bidfactory.createBid(status, tag); - bid.code = baseAdapter.getBidderCode(); - bid.bidderCode = baseAdapter.getBidderCode(); - - if (status === STATUS.GOOD) { - bid.cpm = tag.cpm; - bid.width = tag.width; - bid.height = tag.height; - bid.ad = tag.creative; + return bidResponses; + }, + /** + * Register User Sync. + */ + getUserSyncs: syncOptions => { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: USYNCURL + }]; } - - return bid; } - - return Object.assign(this, { - callBids: baseAdapter.callBids, - setBidderCode: baseAdapter.setBidderCode, - }); +}; + +/** +* Generated cache baster value to be sent to bid server +* @param {*} time current time to use for creating cb. +*/ +function _generateCb(time) { + return Math.floor((time % 65536) + (Math.floor(Math.random() * 65536) * 65536)); } -adaptermanager.registerBidAdapter(new KomoonaAdapter(), 'komoona'); - -module.exports = KomoonaAdapter; +registerBidder(spec); diff --git a/modules/komoonaBidAdapter.md b/modules/komoonaBidAdapter.md new file mode 100644 index 00000000000..6f88c19dfa6 --- /dev/null +++ b/modules/komoonaBidAdapter.md @@ -0,0 +1,29 @@ +# Overview + +**Module Name**: Komoona Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: support@komoona.com + +# Description + +Connects to Komoona demand source to fetch bids. + +# Test Parameters +``` + var adUnits = [{ + code: 'banner-ad-div', + sizes: [[300, 250]], + + // Replace this object to test a new Adapter! + bids: [{ + bidder: 'komoona', + params: { + placementId: 'e69148e0ba6c4c07977dc2daae5e1577', + hbid: '1f5b2c10e66e419580bd943b9af692ab', + floorPrice: 0.5 + } + }] + }]; +``` + + diff --git a/test/spec/modules/komoonaBidAdapter_spec.js b/test/spec/modules/komoonaBidAdapter_spec.js index 2657c658ba2..82edba28d03 100644 --- a/test/spec/modules/komoonaBidAdapter_spec.js +++ b/test/spec/modules/komoonaBidAdapter_spec.js @@ -1,152 +1,164 @@ import { expect } from 'chai'; -import Adapter from 'modules/komoonaBidAdapter'; -import bidmanager from 'src/bidmanager'; +import { spec } from 'modules/komoonaBidAdapter'; -const ENDPOINT = '//bidder.komoona.com/v1/GetSBids'; - -const REQUEST = { - 'bidderCode': 'komoona', - 'requestId': '1f43cc36a6a7e', - 'bidderRequestId': '25392d757fad47', - 'bids': [ +describe('Komoona.com Adapter Tests', () => { + const bidsRequest = [ { - 'bidder': 'komoona', - 'params': { - 'hbid': 'abcd666dcba', - 'placementId': 'abcd123123dcba' + bidder: 'komoona', + params: { + placementId: '170577', + hbid: 'abc12345678', }, - 'placementCode': 'div-gpt-ad-1438287399331-0', - 'sizes': [ + placementCode: 'div-gpt-ad-1460505748561-0', + transactionId: '9f801c02-bbe8-4683-8ed4-bc816ea186bb', + sizes: [ [300, 250] ], - 'bidId': '30e5e911c00703', - 'bidderRequestId': '25392d757fad47', - 'requestId': '1f43cc36a6a7e' - } - ], - 'start': 1466493146527 -}; - -const RESPONSE = { - 'bids': [ + bidId: '2faedf1095f815', + bidderRequestId: '18065867f8ae39', + requestId: '529e1518-b872-45cf-807c-2d41dfa5bcd3' + }, { - 'placementid': 'abcd123123dcba', - 'uuid': '30e5e911c00703', - 'width': 728, - 'height': 90, - 'cpm': 0.5, - 'creative': '' + bidder: 'komoona', + params: { + placementId: '281277', + hbid: 'abc12345678', + floorPrice: 0.5 + }, + placementCode: 'div-gpt-ad-1460505748561-0', + transactionId: '9f801c02-bbe8-4683-8ed4-bc816ea186bb', + sizes: [ + [728, 90] + ], + bidId: '3c34e2367a3f59', + bidderRequestId: '18065867f8ae39', + requestId: '529e1518-b872-45cf-807c-2d41dfa5bcd3' + }]; + + const bidsResponse = { + body: { + bids: [ + { + placementid: '170577', + uuid: '2faedf1095f815', + width: 300, + height: 250, + cpm: 0.51, + creative: '', + ttl: 360, + currency: 'USD', + netRevenue: true, + creativeId: 'd30b58c2ba' + } + ] } - ] -}; - -describe('komoonaAdapter', () => { - let adapter; - - beforeEach(() => adapter = new Adapter()); - - describe('request function', () => { - let xhr; - let requests; - let pbConfig; - - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); - pbConfig = REQUEST; - // just a single slot - pbConfig.bids = [pbConfig.bids[0]]; - }); - - afterEach(() => xhr.restore()); - - 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; - }); - - it('requires placementid and hbid', () => { - let backup = pbConfig.bids[0].params; - pbConfig.bids[0].params = {placementid: 1234}; // no hbid - adapter.callBids(pbConfig); - expect(requests).to.be.empty; + }; - pbConfig.bids[0].params = {hbid: 1234}; // no placementid - adapter.callBids(pbConfig); - expect(requests).to.be.empty; - - pbConfig.bids[0].params = backup; - }); - - it('sends bid request to ENDPOINT via POST', () => { - adapter.callBids(pbConfig); - expect(requests[0].url).to.equal(ENDPOINT); - expect(requests[0].method).to.equal('POST'); - }); + it('Verifies komoonaAdapter bidder code', () => { + expect(spec.code).to.equal('komoona'); }); - 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); - }); - - it('handles nobid responses', () => { - server.respondWith(JSON.stringify({ - 'bids': [{ - 'cpm': 0, - 'creative': '', - 'uuid': '30e5e911c00703' - }] - })); - - 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' - ); - }); + it('Verifies komoonaAdapter bid request validation', () => { + expect(spec.isBidRequestValid(bidsRequest[0])).to.equal(true); + expect(spec.isBidRequestValid(bidsRequest[1])).to.equal(true); + expect(spec.isBidRequestValid({})).to.equal(false); + expect(spec.isBidRequestValid({ params: {} })).to.equal(false); + expect(spec.isBidRequestValid({ params: { hbid: 12345 } })).to.equal(false); + expect(spec.isBidRequestValid({ params: { placementid: 12345 } })).to.equal(false); + expect(spec.isBidRequestValid({ params: { hbid: 12345, placementId: 67890 } })).to.equal(true); + expect(spec.isBidRequestValid({ params: { hbid: 12345, placementId: 67890, floorPrice: 0.8 } })).to.equal(true); + }); - it('handles JSON.parse errors', () => { - server.respondWith(''); + it('Verify komoonaAdapter build request', () => { + var startTime = new Date().getTime(); + + const request = spec.buildRequests(bidsRequest); + expect(request.url).to.equal('//bidder.komoona.com/v1/GetSBids'); + expect(request.method).to.equal('POST'); + const requestData = JSON.parse(request.data); + + // bids object + let bids = requestData.bids; + expect(bids).to.have.lengthOf(2); + + // first bid request: no floor price + expect(bids[0].uuid).to.equal('2faedf1095f815'); + expect(bids[0].floorprice).to.be.undefined; + expect(bids[0].placementid).to.equal('170577'); + expect(bids[0].hbid).to.equal('abc12345678'); + expect(bids[0].trid).to.equal('9f801c02-bbe8-4683-8ed4-bc816ea186bb'); + expect(bids[0].sizes).to.have.lengthOf(1); + expect(bids[0].sizes[0][0]).to.equal(300); + expect(bids[0].sizes[0][1]).to.equal(250); + + // second bid request: with floor price + expect(bids[1].uuid).to.equal('3c34e2367a3f59'); + expect(bids[1].floorprice).to.equal(0.5); + expect(bids[1].placementid).to.equal('281277'); + expect(bids[1].hbid).to.equal('abc12345678'); + expect(bids[1].trid).to.equal('9f801c02-bbe8-4683-8ed4-bc816ea186bb'); + expect(bids[1]).to.have.property('sizes') + .that.is.an('array') + .of.length(1) + .that.deep.equals([[728, 90]]); + + // kbConf object + let kbConf = requestData.kbConf; + expect(kbConf.hdbdid).to.equal(bids[0].hbid); + expect(kbConf.hdbdid).to.equal(bids[1].hbid); + expect(kbConf.encode_bid).to.be.undefined; + // kbConf timezone and cb + expect(kbConf.cb).not.to.be.undefined; + expect(kbConf.ts_as).to.be.above(startTime - 1); + expect(kbConf.tz).to.equal(new Date().getTimezoneOffset()); + // kbConf bid ids + expect(kbConf.hb_placement_bidids) + .to.have.property(bids[0].placementid) + .that.equal(bids[0].uuid); + expect(kbConf.hb_placement_bidids) + .to.have.property(bids[1].placementid) + .that.equal(bids[1].uuid); + // kbConf floor price + expect(kbConf.hb_floors).not.to.have.property(bids[0].placementid) + expect(kbConf.hb_floors).to.have.property(bids[1].placementid).that.equal(bids[1].floorprice); + // kbConf placement ids + expect(kbConf.hb_placements).to.have.lengthOf(2); + expect(kbConf.hb_placements[0]).to.equal(bids[0].placementid); + expect(kbConf.hb_placements[1]).to.equal(bids[1].placementid); + }); - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + it('Verify komoonaAdapter build response', () => { + const request = spec.buildRequests(bidsRequest); + const bids = spec.interpretResponse(bidsResponse, request); + + // 'server' return single bid + expect(bids).to.have.lengthOf(1); + + // verify bid object + const bid = bids[0]; + const responseBids = bidsResponse.body.bids; + + expect(bid.cpm).to.equal(responseBids[0].cpm); + expect(bid.ad).to.equal(responseBids[0].creative); + expect(bid.requestId).equal(responseBids[0].uuid); + expect(bid.uuid).equal(responseBids[0].uuid); + expect(bid.width).to.equal(responseBids[0].width); + expect(bid.height).to.equal(responseBids[0].height); + expect(bid.ttl).to.equal(responseBids[0].ttl); + expect(bid.currency).to.equal('USD'); + expect(bid.netRevenue).to.equal(true); + expect(bid.creativeId).to.equal(responseBids[0].creativeId); + }); - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property( - 'statusMessage', - 'Bid returned empty or error response' - ); - }); + it('Verifies komoonaAdapter sync options', () => { + // user sync disabled + expect(spec.getUserSyncs({})).to.be.undefined; + expect(spec.getUserSyncs({ iframeEnabled: false })).to.be.undefined; + // user sync enabled + 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('//s.komoona.com/sync/usync.html'); }); }); From d818de488b3955bacb1462f532e511dcee02d40e Mon Sep 17 00:00:00 2001 From: lntho Date: Wed, 1 Nov 2017 15:36:54 -0700 Subject: [PATCH 03/32] OpenX Adapter update to Prebid v1.0 (#1714) * OpenX Adapter update to Prebid v1.0 * Updated the ad unit in the OpenX md file * Updated the test ad unit again to something that should work --- modules/openxBidAdapter.js | 472 ++++++++++------------ modules/openxBidAdapter.md | 30 ++ test/spec/modules/openxBidAdapter_spec.js | 431 +++++++++----------- 3 files changed, 438 insertions(+), 495 deletions(-) create mode 100644 modules/openxBidAdapter.md diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 1b9766553c2..a0bcd2a945f 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -1,298 +1,268 @@ import { config } from 'src/config'; -const bidfactory = require('src/bidfactory.js'); -const bidmanager = require('src/bidmanager.js'); -const ajax = require('src/ajax'); -const CONSTANTS = require('src/constants.json'); -const utils = require('src/utils.js'); -const adaptermanager = require('src/adaptermanager'); - -const OpenxAdapter = function OpenxAdapter() { - const BIDDER_CODE = 'openx'; - const BIDDER_CONFIG = 'hb_pb'; - const BIDDER_VERSION = '1.0.1'; - let startTime; - let timeout = config.getConfig('bidderTimeout'); - let shouldSendBoPixel = true; +import {registerBidder} from 'src/adapters/bidderFactory'; +import * as utils from 'src/utils'; +import {userSync} from 'src/userSync'; +import { BANNER } from 'src/mediaTypes'; + +const SUPPORTED_AD_TYPES = [BANNER]; +const BIDDER_CODE = 'openx'; +const BIDDER_CONFIG = 'hb_pb'; +const BIDDER_VERSION = '2.0.0'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: function(bid) { + return !!(bid.params.unit && bid.params.delDomain); + }, + buildRequests: function(bids) { + let isIfr = utils.inIframe(); + let currentURL = (window.parent !== window) ? document.referrer : window.location.href; + if (bids.length === 0) { + return; + } - let pdNode = null; + let delDomain = bids[0].params.delDomain; + let configuredBc = bids[0].params.bc; + let bc = configuredBc || `${BIDDER_CONFIG}_${BIDDER_VERSION}`; - function oxARJResponse (oxResponseObj) { - try { - oxResponseObj = JSON.parse(oxResponseObj); - } catch (_) { - // Could not parse response, changing to an empty response instead - oxResponseObj = { - ads: {} - }; - } + return buildOXRequest(bids, { + ju: currentURL, + jr: currentURL, + ch: document.charSet || document.characterSet, + res: `${screen.width}x${screen.height}x${screen.colorDepth}`, + ifr: isIfr, + tz: new Date().getTimezoneOffset(), + tws: getViewportDimensions(isIfr), + ef: 'bt%2Cdb', + be: 1, + bc: bc, + nocache: new Date().getTime() + }, + delDomain); + }, + interpretResponse: function({body: oxResponseObj}, bidRequest) { + let bidResponses = []; let adUnits = oxResponseObj.ads.ad; if (oxResponseObj.ads && oxResponseObj.ads.pixels) { - makePDCall(oxResponseObj.ads.pixels); + userSync.registerSync('iframe', BIDDER_CODE, oxResponseObj.ads.pixels); } - if (!adUnits) { adUnits = []; } + bidResponses = createBidResponses(adUnits, bidRequest.payload); + return bidResponses; + } +}; - let bids = $$PREBID_GLOBAL$$._bidsRequested.find(bidSet => bidSet.bidderCode === 'openx').bids; - for (let i = 0; i < bids.length; i++) { - let bid = bids[i]; - let auid = null; - let adUnit = null; - // find the adunit in the response - for (let j = 0; j < adUnits.length; j++) { - adUnit = adUnits[j]; - if (String(bid.params.unit) === String(adUnit.adunitid) && adUnitHasValidSizeFromBid(adUnit, bid) && !adUnit.used) { - auid = adUnit.adunitid; +function createBidResponses(adUnits, {bids, startTime}) { + let bidResponses = []; + let shouldSendBoPixel = bids[0].params.sendBoPixel; + if (shouldSendBoPixel === undefined) { + // Not specified, default to turned on + shouldSendBoPixel = true; + } + for (let i = 0; i < adUnits.length; i++) { + let adUnit = adUnits[i]; + let bidResponse = {}; + if (adUnits.length == bids.length) { + // request and response length match, directly assign the request id based on positioning + bidResponse.requestId = bids[i].bidId; + } else { + for (let j = i; j < bids.length; j++) { + let bid = bids[j]; + if (String(bid.params.unit) === String(adUnit.adunitid) && adUnitHasValidSizeFromBid(adUnit, bid) && !bid.matched) { + // ad unit and size match, this is the correct bid response to bid + bidResponse.requestId = bid.bidId; + bid.matched = true; break; } } - - let beaconParams = { - bd: +(new Date()) - startTime, - br: '0', // may be 0, t, or p - bt: Math.min(timeout, window.PREBID_TIMEOUT || config.getConfig('bidderTimeout')), - bs: window.location.hostname - }; - // no fill :( - if (!auid || !adUnit.pub_rev) { - addBidResponse(null, bid); - continue; - } - adUnit.used = true; - - beaconParams.br = beaconParams.bt < beaconParams.bd ? 't' : 'p'; - beaconParams.bp = adUnit.pub_rev; - beaconParams.ts = adUnit.ts; - addBidResponse(adUnit, bid); - if (shouldSendBoPixel === true) { - buildBoPixel(adUnit.creative[0], beaconParams); - } } - }; - - function getViewportDimensions(isIfr) { - let width; - let height; - let tWin = window; - let tDoc = document; - let docEl = tDoc.documentElement; - let body; - - if (isIfr) { - try { - tWin = window.top; - tDoc = window.top.document; - } catch (e) { - return; - } - docEl = tDoc.documentElement; - body = tDoc.body; - width = tWin.innerWidth || docEl.clientWidth || body.clientWidth; - height = tWin.innerHeight || docEl.clientHeight || body.clientHeight; + if (adUnit.pub_rev) { + bidResponse.cpm = Number(adUnit.pub_rev) / 1000; } else { - docEl = tDoc.documentElement; - width = tWin.innerWidth || docEl.clientWidth; - height = tWin.innerHeight || docEl.clientHeight; + // No fill, do not add the bidresponse + continue; } - - return `${width}x${height}`; - } - - function makePDCall(pixelsUrl) { - let pdFrame = utils.createInvisibleIframe(); - let name = 'openx-pd'; - pdFrame.setAttribute('id', name); - pdFrame.setAttribute('name', name); - let rootNode = document.body; - - if (!rootNode) { - return; + let creative = adUnit.creative[0]; + if (creative) { + bidResponse.width = creative.width; + bidResponse.height = creative.height; } - - pdFrame.src = pixelsUrl; - - if (pdNode) { - pdNode.parentNode.replaceChild(pdFrame, pdNode); - pdNode = pdFrame; - } else { - pdNode = rootNode.appendChild(pdFrame); + bidResponse.creativeId = creative.id; + bidResponse.ad = adUnit.html; + if (adUnit.deal_id) { + bidResponse.dealId = adUnit.deal_id; } - } - - function addBidResponse(adUnit, bid) { - let bidResponse = bidfactory.createBid(adUnit ? CONSTANTS.STATUS.GOOD : CONSTANTS.STATUS.NO_BID, bid); - bidResponse.bidderCode = BIDDER_CODE; - - if (adUnit) { - let creative = adUnit.creative[0]; - bidResponse.ad = adUnit.html; - bidResponse.cpm = Number(adUnit.pub_rev) / 1000; - bidResponse.ad_id = adUnit.adid; - if (adUnit.deal_id) { - bidResponse.dealId = adUnit.deal_id; - } - if (creative) { - bidResponse.width = creative.width; - bidResponse.height = creative.height; - } - if (adUnit.tbd) { - bidResponse.tbd = adUnit.tbd; - } + // default 5 mins + bidResponse.ttl = 300; + // true is net, false is gross + bidResponse.netRevenue = true; + bidResponse.currency = adUnit.currency; + + // additional fields to add + if (adUnit.tbd) { + bidResponse.tbd = adUnit.tbd; } - bidmanager.addBidResponse(bid.placementCode, bidResponse); - } + bidResponse.ts = adUnit.ts; - function buildQueryStringFromParams(params) { - for (let key in params) { - if (params.hasOwnProperty(key)) { - if (!params[key]) { - delete params[key]; - } - } + let bt = config.getConfig('bidderTimeout'); + if (window.PREBID_TIMEOUT) { + bt = Math.min(window.PREBID_TIMEOUT, bt); + } + let beaconParams = { + bd: +(new Date()) - startTime, + br: '0', // may be 0, t, or p + bt: bt, + bs: window.location.hostname + }; + + beaconParams.br = beaconParams.bt < beaconParams.bd ? 't' : 'p'; + beaconParams.bp = adUnit.pub_rev; + beaconParams.ts = adUnit.ts; + let boUrl; + if (shouldSendBoPixel) { + boUrl = getBoUrl(adUnit.creative[0], beaconParams); + } + if (boUrl) { + userSync.registerSync('image', BIDDER_CODE, boUrl); } - return utils._map(Object.keys(params), key => `${key}=${params[key]}`) - .join('&'); + bidResponses.push(bidResponse); } + return bidResponses; +} - function buildBoPixel(creative, params) { - let img = new Image(); - let recordPixel = creative.tracking.impression; - let boBase = recordPixel.match(/([^?]+\/)ri\?/); +function getBoUrl(creative, params) { + let recordPixel = creative.tracking.impression; + let boBase = recordPixel.match(/([^?]+\/)ri\?/); - if (boBase) { - img.src = `${boBase[1]}bo?${buildQueryStringFromParams(params)}`; - } + if (boBase) { + return `${boBase[1]}bo?${buildQueryStringFromParams(params)}`; } +} - function adUnitHasValidSizeFromBid(adUnit, bid) { - let sizes = utils.parseSizesInput(bid.sizes); - let sizeLength = (sizes && sizes.length) || 0; - let found = false; - let creative = adUnit.creative && adUnit.creative[0]; - let creative_size = String(creative.width) + 'x' + String(creative.height); - - if (utils.isArray(sizes)) { - for (let i = 0; i < sizeLength; i++) { - let size = sizes[i]; - if (String(size) === String(creative_size)) { - found = true; - break; - } +function buildQueryStringFromParams(params) { + for (let key in params) { + if (params.hasOwnProperty(key)) { + if (!params[key]) { + delete params[key]; } } - 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('/', '_') + return utils._map(Object.keys(params), key => `${key}=${params[key]}`) + .join('&'); +} + +function adUnitHasValidSizeFromBid(adUnit, bid) { + let sizes = utils.parseSizesInput(bid.sizes); + if (!sizes) { + return false; } - - function buildRequest(bids, params, delDomain) { - if (!utils.isArray(bids)) { - return; - } - - params.auid = utils._map(bids, bid => bid.params.unit).join('%2C'); - params.dddid = utils._map(bids, bid => bid.transactionId).join('%2C'); - params.aus = utils._map(bids, bid => { - return utils.parseSizesInput(bid.sizes).join(','); - }).join('|'); - - let customParamsForAllBids = []; - let hasCustomParam = false; - bids.forEach(function (bid) { - 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); + let found = false; + let creative = adUnit.creative && adUnit.creative[0]; + let creative_size = String(creative.width) + 'x' + String(creative.height); + + if (utils.isArray(sizes)) { + for (let i = 0; i < sizes.length; i++) { + let size = sizes[i]; + if (String(size) === String(creative_size)) { + found = true; + break; } - }); - if (hasCustomFloor) { - params.aumfs = customFloorsForAllBids.join('%2C'); - } - - try { - let queryString = buildQueryStringFromParams(params); - let url = `//${delDomain}/w/1.0/arj?${queryString}`; - ajax.ajax(url, oxARJResponse, void (0), { - withCredentials: true - }); - } catch (err) { - utils.logMessage(`Ajax call failed due to ${err}`); } } - - function callBids(params) { - let isIfr; - const bids = params.bids || []; - let currentURL = (window.parent !== window) ? document.referrer : window.location.href; - currentURL = currentURL && encodeURIComponent(currentURL); + return found; +} + +function getViewportDimensions(isIfr) { + let width; + let height; + let tWin = window; + let tDoc = document; + let docEl = tDoc.documentElement; + let body; + + if (isIfr) { try { - isIfr = window.self !== window.top; + tWin = window.top; + tDoc = window.top.document; } catch (e) { - isIfr = false; - } - if (bids.length === 0) { return; } + docEl = tDoc.documentElement; + body = tDoc.body; + + width = tWin.innerWidth || docEl.clientWidth || body.clientWidth; + height = tWin.innerHeight || docEl.clientHeight || body.clientHeight; + } else { + docEl = tDoc.documentElement; + width = tWin.innerWidth || docEl.clientWidth; + height = tWin.innerHeight || docEl.clientHeight; + } - let delDomain = bids[0].params.delDomain; - let bcOverride = bids[0].params.bc; + return `${width}x${height}`; +} - 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; +function formatCustomParms(customKey, customParams) { + let value = customParams[customKey]; + if (utils.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.toLowerCase() + '=' + value.toLowerCase()).replace('+', '.').replace('/', '_') +} + +function buildOXRequest(bids, oxParams, delDomain) { + if (!utils.isArray(bids)) { + return; + } + + oxParams.auid = utils._map(bids, bid => bid.params.unit).join(','); + oxParams.dddid = utils._map(bids, bid => bid.transactionId).join(','); + oxParams.aus = utils._map(bids, bid => { + return utils.parseSizesInput(bid.sizes).join(','); + }).join('|'); + + let customParamsForAllBids = []; + let hasCustomParam = false; + bids.forEach(function (bid) { + 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) { + oxParams.tps = customParamsForAllBids.join(','); + } - buildRequest(bids, { - ju: currentURL, - jr: currentURL, - ch: document.charSet || document.characterSet, - res: `${screen.width}x${screen.height}x${screen.colorDepth}`, - ifr: isIfr, - tz: startTime.getTimezoneOffset(), - tws: getViewportDimensions(isIfr), - ef: 'bt%2Cdb', - be: 1, - bc: bcOverride || `${BIDDER_CONFIG}_${BIDDER_VERSION}`, - nocache: new Date().getTime() - }, - delDomain); + 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) { + oxParams.aumfs = customFloorsForAllBids.join(','); } + let url = `//${delDomain}/w/1.0/arj`; return { - callBids: callBids + method: 'GET', + url: url, + data: oxParams, + payload: {'bids': bids, 'startTime': new Date()} }; -}; - -adaptermanager.registerBidAdapter(new OpenxAdapter(), 'openx'); +} -module.exports = OpenxAdapter; +registerBidder(spec); diff --git a/modules/openxBidAdapter.md b/modules/openxBidAdapter.md new file mode 100644 index 00000000000..5b3ad77ce6d --- /dev/null +++ b/modules/openxBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +``` +Module Name: OpenX Bidder Adapter +Module Type: Bidder Adapter +Maintainer: team-openx@openx.com +``` + +# Description + +Module that connects to OpenX's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[728, 90]], // a display size + bids: [ + { + bidder: "openx", + params: { + unit: "539439964", + delDomain: "se-demo-d.openx.net" + } + } + ] + }, + ]; +``` diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index c08e8c256e6..cee521b2921 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -1,277 +1,220 @@ -const expect = require('chai').expect; -const assert = require('chai').assert; -const adapter = require('modules/openxBidAdapter')(); -const bidmanager = require('src/bidmanager'); -const adloader = require('src/adloader'); -const CONSTANTS = require('src/constants.json'); -const ajax = require('src/ajax'); +import { expect } from 'chai'; +import { spec } from 'modules/openxBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; -describe('openx adapter tests', function () { - describe('test openx callback response', function () { - let stubAjax; - let stubAddBidResponse; - this.response = null; - let responseHandlerCallback = (_url, callback, _data, _params) => { - return callback(this.response); +const URLBASE = '/w/1.0/arj'; + +describe('OpenxAdapter', () => { + const adapter = newBidder(spec); + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'openx', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', }; - beforeEach(() => { - stubAjax = sinon.stub(ajax, 'ajax', responseHandlerCallback); - stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - sinon.stub(document.body, 'appendChild'); + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); }); - afterEach(() => { - stubAjax.restore(); - stubAddBidResponse.restore(); - this.response = null; - document.body.appendChild.restore(); + + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = {'unit': '12345678'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('should add empty bid responses if no bids returned', () => { - // empty ads in bidresponse - this.response = JSON.stringify({ - 'ads': - { - 'version': 1, - 'count': 1, - 'pixels': 'http://testpixels.net', - 'ad': [] - } - }); + }); - let bidderRequest = { - bidderCode: 'openx', - bids: [ - { - bidId: 'bidId1', - bidder: 'openx', - params: { - delDomain: 'delDomain1', - unit: '1234' - }, - sizes: [[300, 250]], - placementCode: 'test-gpt-div-1234' - } - ] - }; + describe('buildRequests', () => { + let bidRequests = [{ + 'bidder': 'openx', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }]; + + it('should send bid request to openx url via GET', () => { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.equal('//' + bidRequests[0].params.delDomain + URLBASE); + expect(request.method).to.equal('GET'); + }); - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - adapter.callBids(bidderRequest); + it('should have the correct parameters', () => { + const request = spec.buildRequests(bidRequests); + const dataParams = request.data; - let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; - expect(bidPlacementCode1).to.equal('test-gpt-div-1234'); - expect(bidResponse1.getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); - expect(bidResponse1.bidderCode).to.equal('openx'); + expect(dataParams.auid).to.exist; + expect(dataParams.auid).to.equal('12345678'); + expect(dataParams.aus).to.exist; + expect(dataParams.aus).to.equal('300x250,300x600'); }); - it('should add bid responses if bids are returned', () => { - let bidderRequest = { - bidderCode: 'openx', - bids: [ - { - bidId: 'bidId1', - bidder: 'openx', - params: { - delDomain: 'delDomain1', - unit: '1234' - }, - sizes: [[300, 250]], - placementCode: 'test-gpt-div-1234' - } - ] - }; - - this.response = JSON.stringify({ - 'ads': + it('should send out custom params on bids that have customParams specified', () => { + let bidRequest = Object.assign({}, + bidRequests[0], { - 'version': 1, - 'count': 1, - 'pixels': 'http://testpixels.net', - 'ad': [ - { - 'adunitid': 1234, - 'adid': 5678, - 'type': 'html', - 'html': 'test_html', - 'framed': 1, - 'is_fallback': 0, - 'ts': 'ts', - 'cpipc': 1000, - 'pub_rev': '1000', - 'adv_id': 'adv_id', - 'brand_id': '', - 'creative': [ - { - 'width': '300', - 'height': '250', - 'target': '_blank', - 'mime': 'text/html', - 'media': 'test_media', - 'tracking': { - 'impression': 'test_impression', - 'inview': 'test_inview', - 'click': 'test_click' - } - } - ] - }] + params: { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'customParams': {'Test1': 'testval1+', 'test2': ['testval2/', 'testval3']} + } } - }); - - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - adapter.callBids(bidderRequest); + ); - let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; - let bid1width = '300'; - let bid1height = '250'; - let cpm = 1; - expect(bidPlacementCode1).to.equal('test-gpt-div-1234'); - expect(bidResponse1.getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bidResponse1.bidderCode).to.equal('openx'); - expect(bidResponse1.width).to.equal(bid1width); - expect(bidResponse1.height).to.equal(bid1height); - expect(bidResponse1.cpm).to.equal(cpm); - }); - }); - 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(); - }); + const request = spec.buildRequests([bidRequest]); + const dataParams = request.data; - it('should not call ajax when inputting with empty params', () => { - adapter.callBids({}); - assert(!spyAjax.called); + expect(dataParams.tps).to.exist; + expect(dataParams.tps).to.equal(btoa('test1=testval1.&test2=testval2_,testval3')); }); - it('should call ajax with the correct bid url', () => { - let params = { - bids: [ - { - sizes: [[300, 250], [300, 600]], - params: { - delDomain: 'testdelDomain', - unit: 1234 - } + it('should send out custom floors on bids that have customFloors specified', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + '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'); + const request = spec.buildRequests([bidRequest]); + const dataParams = request.data; + + expect(dataParams.aumfs).to.exist; + expect(dataParams.aumfs).to.equal('1500'); }); - it('should send out custom params on bids that have customParams specified', () => { - let params = { - bids: [ - { - sizes: [[300, 250], [300, 600]], - params: { - delDomain: 'testdelDomain', - unit: 1234, - customParams: {'test1': 'testval1+', 'test2': ['testval2/', 'testval3']} - } + it('should send out custom bc parameter, if override is present', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'bc': 'hb_override' } - ] - }; - adapter.callBids(params); + } + ); - 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'); + const request = spec.buildRequests([bidRequest]); + const dataParams = request.data; + + expect(dataParams.bc).to.exist; + expect(dataParams.bc).to.equal('hb_override'); }); + }); - 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 - } - }, + describe('interpretResponse', () => { + let bids = [{ + 'bidder': 'openx', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }]; + let bidRequest = { + method: 'GET', + url: 'url', + data: {}, + payload: {'bids': bids, 'startTime': new Date()} + }; + let bidResponse = { + 'ads': + { + 'version': 1, + 'count': 1, + 'pixels': 'http://testpixels.net', + 'ad': [ { - sizes: [[728, 90]], - params: { - delDomain: 'testdelDomain', - unit: 1234, - customFloor: 1.5 - } - } - ] - }; - adapter.callBids(params); + 'adunitid': 12345678, + 'adid': 5678, + 'type': 'html', + 'html': 'test_html', + 'framed': 1, + 'is_fallback': 0, + 'ts': 'ts', + 'cpipc': 1000, + 'pub_rev': '1000', + 'adv_id': 'adv_id', + 'brand_id': '', + 'creative': [ + { + 'width': '300', + 'height': '250', + 'target': '_blank', + 'mime': 'text/html', + 'media': 'test_media', + 'tracking': { + 'impression': 'test_impression', + 'inview': 'test_inview', + 'click': 'test_click' + } + } + ] + }] + } + }; + it('should return correct bid response', () => { + let expectedResponse = [ + { + 'requestId': '30b31c1838de1e', + 'cpm': 1, + 'width': '300', + 'height': '250', + 'creativeId': 5678, + 'ad': 'test_html', + 'ttl': 300, + 'netRevenue': true, + 'currency': 'USD', + 'ts': 'ts' + } + ]; - 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'); + let result = spec.interpretResponse({body: bidResponse}, bidRequest); + expect(Object.keys(result[0])).to.eql(Object.keys(expectedResponse[0])); }); - 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' - } - } - ] + it('handles nobid responses', () => { + bidResponse = { + 'ads': + { + 'version': 1, + 'count': 1, + 'pixels': 'http://testpixels.net', + 'ad': [] + } }; - 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'); + let result = spec.interpretResponse({body: bidResponse}, bidRequest); + expect(result.length).to.equal(0); }); }); }); From bca5b16e6cc125654280a15f8611209319e7d168 Mon Sep 17 00:00:00 2001 From: adxcgcom <31470944+adxcgcom@users.noreply.github.com> Date: Fri, 3 Nov 2017 16:19:53 +0100 Subject: [PATCH 04/32] updated adxcg adapter for prebid 1.0 (#1798) --- modules/adxcgBidAdapter.js | 6 +++--- test/spec/modules/adxcgBidAdapter_spec.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index 9073a17bda3..0a722000b55 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -57,7 +57,7 @@ export const spec = { pathname: '/get/adi', search: { renderformat: 'javascript', - ver: 'r20171019PB10', + ver: 'r20171102PB10', adzoneid: adZoneIds.join(','), format: sizes.join(','), prebidBidIds: prebidBidIds.join(','), @@ -90,9 +90,9 @@ export const spec = { bid.requestId = serverResponseOneItem.bidId; bid.cpm = serverResponseOneItem.cpm; bid.creativeId = parseInt(serverResponseOneItem.creativeId); - bid.currency = 'USD'; + bid.currency = serverResponseOneItem.currency ? serverResponseOneItem.currency : 'USD'; bid.netRevenue = serverResponseOneItem.netRevenue ? serverResponseOneItem.netRevenue : true; - bid.ttl = 300; + bid.ttl = serverResponseOneItem.ttl ? serverResponseOneItem.ttl : 300; if (serverResponseOneItem.deal_id != null && serverResponseOneItem.deal_id.trim().length > 0) { bid.dealId = serverResponseOneItem.deal_id; diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index dbf7359e98d..afb58361ba1 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -54,7 +54,7 @@ describe('AdxcgAdapter', () => { let query = parsedRequestUrl.search; expect(query.renderformat).to.equal('javascript'); - expect(query.ver).to.equal('r20171019PB10'); + expect(query.ver).to.equal('r20171102PB10'); expect(query.source).to.equal('pbjs10'); expect(query.pbjs).to.equal('$prebid.version$'); expect(query.adzoneid).to.equal('1'); From a1678d3f17aa0a67f5b35727c58b25013e384a0c Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 3 Nov 2017 15:30:12 -0700 Subject: [PATCH 05/32] Update sharethrough bid adapter (#1740) * Update sharethrough bid adapter * add md for sharethroughBidAdpapter * remove bidderCode and parse response * remove linting errors --- modules/sharethroughBidAdapter.js | 190 ++++------ modules/sharethroughBidAdapter.md | 40 +++ .../modules/sharethroughBidAdapter_spec.js | 328 ++++++------------ 3 files changed, 214 insertions(+), 344 deletions(-) create mode 100644 modules/sharethroughBidAdapter.md diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index d53fb0d92db..ef0cf9619c1 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,128 +1,70 @@ -var utils = require('src/utils.js'); -var bidmanager = require('src/bidmanager.js'); -var bidfactory = require('src/bidfactory.js'); -var ajax = require('src/ajax.js').ajax; -var adaptermanager = require('src/adaptermanager'); - -const STR_BIDDER_CODE = 'sharethrough'; -const STR_VERSION = '1.2.0'; - -var SharethroughAdapter = function SharethroughAdapter() { - const str = {}; - str.STR_BTLR_HOST = document.location.protocol + '//btlr.sharethrough.com'; - str.STR_BEACON_HOST = document.location.protocol + '//b.sharethrough.com/butler?'; - str.placementCodeSet = {}; - str.ajax = ajax; - - function _callBids(params) { - const bids = params.bids; - - // cycle through bids - for (let i = 0; i < bids.length; i += 1) { - const bidRequest = bids[i]; - str.placementCodeSet[bidRequest.placementCode] = bidRequest; - const scriptUrl = _buildSharethroughCall(bidRequest); - str.ajax(scriptUrl, _createCallback(bidRequest), undefined, {withCredentials: true}); - } +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'sharethrough'; +const VERSION = '2.0.0'; +const STR_ENDPOINT = document.location.protocol + '//btlr.sharethrough.com/header-bid/v1'; + +export const sharethroughAdapterSpec = { + code: BIDDER_CODE, + isBidRequestValid: bid => !!bid.params.pkey && bid.bidder === BIDDER_CODE, + buildRequests: (bidRequests) => { + return bidRequests.map(bid => { + return { + method: 'GET', + url: STR_ENDPOINT, + data: { + bidId: bid.bidId, + placement_key: bid.params.pkey, + hbVersion: '$prebid.version$', + strVersion: VERSION, + hbSource: 'prebid' + } + }; + }) + }, + interpretResponse: ({ body }, req) => { + if (!Object.keys(body).length) return []; + + const creative = body.creatives[0]; + + return [{ + requestId: req.data.bidId, + width: 0, + height: 0, + cpm: creative.cpm, + creativeId: creative.creative.creative_key, + deal_id: creative.creative.deal_id, + currency: 'USD', + netRevenue: true, + ttl: 360, + ad: generateAd(body, req) + }]; } - - function _createCallback(bidRequest) { - return (bidResponse) => { - _strcallback(bidRequest, bidResponse); - }; - } - - function _buildSharethroughCall(bid) { - const pkey = utils.getBidIdParameter('pkey', bid.params); - - let host = str.STR_BTLR_HOST; - - let url = host + '/header-bid/v1?'; - url = utils.tryAppendQueryString(url, 'bidId', bid.bidId); - url = utils.tryAppendQueryString(url, 'placement_key', pkey); - url = appendEnvFields(url); - - return url; - } - - function _strcallback(bidObj, bidResponse) { - try { - bidResponse = JSON.parse(bidResponse); - } catch (e) { - _handleInvalidBid(bidObj); - return; - } - - if (bidResponse.creatives && bidResponse.creatives.length > 0) { - _handleBid(bidObj, bidResponse); - } else { - _handleInvalidBid(bidObj); - } - } - - function _handleBid(bidObj, bidResponse) { - try { - const bidId = bidResponse.bidId; - const bid = bidfactory.createBid(1, bidObj); - bid.bidderCode = STR_BIDDER_CODE; - bid.cpm = bidResponse.creatives[0].cpm; - const size = bidObj.sizes[0]; - bid.width = size[0]; - bid.height = size[1]; - bid.adserverRequestId = bidResponse.adserverRequestId; - str.placementCodeSet[bidObj.placementCode].adserverRequestId = bidResponse.adserverRequestId; - - bid.pkey = utils.getBidIdParameter('pkey', bidObj.params); - - const windowLocation = `str_response_${bidId}`; - const bidJsonString = JSON.stringify(bidResponse); - bid.ad = `
-
- - ` +} + +function generateAd(body, req) { + const strRespId = `str_response_${req.data.bidId}`; + + return ` +
+
+ + + ` - bid.ad += sfpScriptTag; + const sfp_js = document.createElement('script'); + sfp_js.src = "//native.sharethrough.com/assets/sfp.js"; + sfp_js.type = 'text/javascript'; + sfp_js.charset = 'utf-8'; + try { + window.top.document.getElementsByTagName('body')[0].appendChild(sfp_js); + } catch (e) { + console.log(e); + } } - bidmanager.addBidResponse(bidObj.placementCode, bid); - } catch (e) { - _handleInvalidBid(bidObj); - } - } - - function _handleInvalidBid(bidObj) { - const bid = bidfactory.createBid(2, bidObj); - bid.bidderCode = STR_BIDDER_CODE; - bidmanager.addBidResponse(bidObj.placementCode, bid); - } - - function appendEnvFields(url) { - url = utils.tryAppendQueryString(url, 'hbVersion', '$prebid.version$'); - url = utils.tryAppendQueryString(url, 'strVersion', STR_VERSION); - url = utils.tryAppendQueryString(url, 'hbSource', 'prebid'); - - return url; - } - - return { - callBids: _callBids, - str: str, - }; -}; - -adaptermanager.registerBidAdapter(new SharethroughAdapter(), 'sharethrough'); + })() + `; +} -module.exports = SharethroughAdapter; +registerBidder(sharethroughAdapterSpec); diff --git a/modules/sharethroughBidAdapter.md b/modules/sharethroughBidAdapter.md new file mode 100644 index 00000000000..8ab44f2a0f2 --- /dev/null +++ b/modules/sharethroughBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +``` +Module Name: Sharethrough Bidder Adapter +Module Type: Bidder Adapter +Maintainer: jchau@sharethrough.com && cpan@sharethrough.com +``` + +# Description + +Module that connects to Sharethrough's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[1, 1]], // a display size + bids: [ + { + bidder: "sharethrough", + params: { + pkey: 'LuB3vxGGFrBZJa6tifXW4xgK' + } + } + ] + },{ + code: 'test-div', + sizes: [[1, 1]], // a mobile size + bids: [ + { + bidder: "sharethrough", + params: { + pkey: 'LuB3vxGGFrBZJa6tifXW4xgK' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index b92dfe4d493..fdac8a84d8d 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -1,239 +1,127 @@ import { expect } from 'chai'; -import Adapter from '../../../modules/sharethroughBidAdapter'; -import bidManager from '../../../src/bidmanager'; -import bidfactory from '../../../src/bidfactory'; - -describe('sharethrough adapter', () => { - let adapter; - let sandbox; - let bidsRequestedOriginal; +import { sharethroughAdapterSpec } from 'modules/sharethroughBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; + +const spec = newBidder(sharethroughAdapterSpec).getSpec(); +const bidderRequest = [ + { + bidder: 'sharethrough', + bidId: 'bidId1', + sizes: [[600, 300]], + placementCode: 'foo', + params: { + pkey: 'aaaa1111' + } + }, + { + bidder: 'sharethrough', + bidId: 'bidId2', + sizes: [[700, 400]], + placementCode: 'bar', + params: { + pkey: 'bbbb2222' + } + }]; +const prebidRequest = [{ + method: 'GET', + url: document.location.protocol + '//btlr.sharethrough.com' + '/header-bid/v1', + data: { + bidId: 'bidId', + placement_key: 'pKey' + } +}]; +const bidderResponse = { + body: { + 'adserverRequestId': '40b6afd5-6134-4fbb-850a-bb8972a46994', + 'bidId': 'bidId1', + 'version': 1, + 'creatives': [{ + 'auctionWinId': 'b2882d5e-bf8b-44da-a91c-0c11287b8051', + 'cpm': 12.34, + 'creative': { + 'deal_id': 'aDealId', + 'creative_key': 'aCreativeId' + } + }], + 'stxUserId': '' + }, + header: { get: (header) => header } +}; + +describe('sharethrough adapter spec', () => { + describe('.code', () => { + it('should return a bidder code of sharethrough', () => { + expect(spec.code).to.eql('sharethrough'); + }); + }) - const bidderRequest = { - bidderCode: 'sharethrough', - bids: [ - { - bidder: 'sharethrough', - bidId: 'bidId1', - sizes: [[600, 300]], - placementCode: 'foo', - params: { - pkey: 'aaaa1111' - } - }, - { + describe('.isBidRequestValid', () => { + it('should return false if req has no pkey', () => { + const invalidBidRequest = { bidder: 'sharethrough', - bidId: 'bidId2', - sizes: [[700, 400]], - placementCode: 'bar', params: { - pkey: 'bbbb2222' + notPKey: 'abc123' } - } - ] - }; - - beforeEach(() => { - adapter = new Adapter(); - sandbox = sinon.sandbox.create(); - bidsRequestedOriginal = $$PREBID_GLOBAL$$._bidsRequested; - $$PREBID_GLOBAL$$._bidsRequested = []; - }); - - afterEach(() => { - sandbox.restore(); - - $$PREBID_GLOBAL$$._bidsRequested = bidsRequestedOriginal; - }); - - describe('callBids', () => { - let firstBidUrl; - let secondBidUrl; - - beforeEach(() => { - sandbox.spy(adapter.str, 'ajax'); - }); - - it('should call ajax to make a request for each bid', () => { - adapter.callBids(bidderRequest); - - firstBidUrl = adapter.str.ajax.firstCall.args[0]; - secondBidUrl = adapter.str.ajax.secondCall.args[0]; - - sinon.assert.calledTwice(adapter.str.ajax); - - expect(firstBidUrl).to.contain(adapter.str.STR_BTLR_HOST + '/header-bid/v1?bidId=bidId1&placement_key=aaaa1111&hbVersion=%24prebid.version%24&strVersion=1.2.0&hbSource=prebid&'); - expect(secondBidUrl).to.contain(adapter.str.STR_BTLR_HOST + '/header-bid/v1?bidId=bidId2&placement_key=bbbb2222&hbVersion=%24prebid.version%24&strVersion=1.2.0&hbSource=prebid&'); - }); - }); - - describe('bid requests', () => { - let firstBid; - let secondBid; - let server; - let stubAddBidResponse; - let stubCreateBid; - - beforeEach(() => { - stubAddBidResponse = sandbox.stub(bidManager, 'addBidResponse'); - server = sinon.fakeServer.create(); - - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - adapter.str.placementCodeSet['foo'] = {}; - adapter.str.placementCodeSet['bar'] = {}; - // respond - - let bidderResponse1 = { - 'adserverRequestId': '40b6afd5-6134-4fbb-850a-bb8972a46994', - 'bidId': 'bidId1', - 'creatives': [ - { - 'cpm': 12.34, - 'auctionWinId': 'b2882d5e-bf8b-44da-a91c-0c11287b8051', - 'version': 1 - } - ], - 'stxUserId': '' - }; - - let bidderResponse2 = { - 'adserverRequestId': '40b6afd5-6134-4fbb-850a-bb8972a46994', - 'bidId': 'bidId2', - 'creatives': [ - { - 'cpm': 12.35, - 'auctionWinId': 'b2882d5e-bf8b-44da-a91c-0c11287b8051', - 'version': 1 - } - ], - 'stxUserId': '' }; - - server.respondWith(/aaaa1111/, JSON.stringify(bidderResponse1)); - server.respondWith(/bbbb2222/, JSON.stringify(bidderResponse2)); - adapter.callBids(bidderRequest); - - server.respond(); - - firstBid = bidManager.addBidResponse.firstCall.args[1]; - secondBid = bidManager.addBidResponse.secondCall.args[1]; + expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); - afterEach(() => { - server.restore(); - }); - - it('should add a bid object for each bid', () => { - sinon.assert.calledTwice(bidManager.addBidResponse); - }); - - it('should pass the correct placement code as first param', () => { - let firstPlacementCode = bidManager.addBidResponse.firstCall.args[0]; - let secondPlacementCode = bidManager.addBidResponse.secondCall.args[0]; - - expect(firstPlacementCode).to.eql('foo'); - expect(secondPlacementCode).to.eql('bar'); - }); - - it('should include the bid request bidId as the adId', () => { - expect(firstBid).to.have.property('adId', 'bidId1'); - expect(secondBid).to.have.property('adId', 'bidId2'); - }); - - it('should have a good statusCode', () => { - expect(firstBid.getStatusCode()).to.eql(1); - expect(secondBid.getStatusCode()).to.eql(1); - }); - - it('should add the CPM to the bid object', () => { - expect(firstBid).to.have.property('cpm', 12.34); - expect(secondBid).to.have.property('cpm', 12.35); - }); - - it('should add the bidder code to the bid object', () => { - expect(firstBid).to.have.property('bidderCode', 'sharethrough'); - expect(secondBid).to.have.property('bidderCode', 'sharethrough'); + it('should return false if req has wrong bidder code', () => { + const invalidBidRequest = { + bidder: 'notSharethrough', + params: { + notPKey: 'abc123' + } + }; + expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); - it('should include the ad on the bid object', () => { - expect(firstBid).to.have.property('ad'); - expect(secondBid).to.have.property('ad'); - }); + it('should return true if req is correct', () => { + expect(spec.isBidRequestValid(bidderRequest[0])).to.eq(true); + expect(spec.isBidRequestValid(bidderRequest[1])).to.eq(true); + }) + }); - it('should include the size on the bid object', () => { - expect(firstBid).to.have.property('width', 600); - expect(firstBid).to.have.property('height', 300); - expect(secondBid).to.have.property('width', 700); - expect(secondBid).to.have.property('height', 400); - }); + describe('.buildRequests', () => { + it('should return an array of requests', () => { + const bidRequests = spec.buildRequests(bidderRequest); - it('should include the pkey', () => { - expect(firstBid).to.have.property('pkey', 'aaaa1111'); - expect(secondBid).to.have.property('pkey', 'bbbb2222'); + expect(bidRequests[0].url).to.eq( + 'http://btlr.sharethrough.com/header-bid/v1'); + expect(bidRequests[1].url).to.eq( + 'http://btlr.sharethrough.com/header-bid/v1') + expect(bidRequests[0].method).to.eq('GET'); }); + }); - describe('when bidResponse string cannot be JSON parsed', () => { - beforeEach(() => { - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - adapter.str.placementCodeSet['foo'] = {}; - - server.respondWith(/aaaa1111/, 'non JSON string'); - adapter.callBids(bidderRequest); - - server.respond(); - }); - - afterEach(() => { - server.restore(); - stubAddBidResponse.reset(); - }); - - it('should add a bid response', () => { - sinon.assert.called(bidManager.addBidResponse); - }); - - it('should set bidder code on invalid bid response', () => { - let bidResponse = bidManager.addBidResponse.firstCall.args[1] - expect(bidResponse).to.have.property('bidderCode', 'sharethrough') - }); + describe('.interpretResponse', () => { + it('returns a correctly parsed out response', () => { + expect(spec.interpretResponse(bidderResponse, prebidRequest[0])[0]).to.include( + { + width: 0, + height: 0, + cpm: 12.34, + creativeId: 'aCreativeId', + deal_id: 'aDealId', + currency: 'USD', + netRevenue: true, + ttl: 360, + }); }); - describe('when no fill', () => { - beforeEach(() => { - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - adapter.str.placementCodeSet['foo'] = {}; - - let bidderResponse1 = { - 'adserverRequestId': '40b6afd5-6134-4fbb-850a-bb8972a46994', - 'bidId': 'bidId1', - 'creatives': [ - { - 'cpm': 12.34, - 'auctionWinId': 'b2882d5e-bf8b-44da-a91c-0c11287b8051', - 'version': 1 - } - ], - 'stxUserId': '' - }; - - server.respondWith(/aaaa1111/, JSON.stringify(bidderResponse1)); - adapter.callBids(bidderRequest); - - server.respond(); - }); - - afterEach(() => { - server.restore(); - stubAddBidResponse.reset(); - }); - - it('should add a bid response', () => { - sinon.assert.called(bidManager.addBidResponse); - }); - - it('should set bidder code on invalid bid response', () => { - let bidResponse = bidManager.addBidResponse.firstCall.args[1] - expect(bidResponse).to.have.property('bidderCode', 'sharethrough') - }); + it('correctly sends back a sfp script tag', () => { + const adMarkup = spec.interpretResponse(bidderResponse, prebidRequest[0])[0].ad; + const resp = btoa(JSON.stringify(bidderResponse)); + + expect(adMarkup).to.match( + /data-str-native-key="pKey" data-stx-response-name=\"str_response_bidId\"/); + expect(!!adMarkup.indexOf(resp)).to.eql(true); + expect(adMarkup).to.match( + / `; + bid.mediaType = BANNER; + } else if (tag.vast_url) { + bid.vastUrl = tag.vast_url; + bid.mediaType = VIDEO; + } + return bid; +} + +export const spec = { + + code: 'adkernelAdn', + + supportedMediaTypes: [VIDEO], + + isBidRequestValid: function(bidRequest) { + return 'params' in bidRequest && (typeof bidRequest.params.host === 'undefined' || typeof bidRequest.params.host === 'string') && + typeof bidRequest.params.pubId === 'number'; + }, + + buildRequests: function(bidRequests) { + let transactionId; + let auctionId; + let dispatch = bidRequests.map(buildImp) + .reduce((acc, curr, index) => { + let bidRequest = bidRequests[index]; + let pubId = bidRequest.params.pubId; + let host = bidRequest.params.host || DEFAULT_ADKERNEL_DSP_DOMAIN; + acc[host] = acc[host] || {}; + acc[host][pubId] = acc[host][pubId] || []; + acc[host][pubId].push(curr); + transactionId = bidRequest.transactionId; + auctionId = bidRequest.bidderRequestId; + return acc; + }, {}); + let requests = []; + Object.keys(dispatch).forEach(host => { + Object.keys(dispatch[host]).forEach(pubId => { + let request = buildRequestParams(auctionId, transactionId, dispatch[host][pubId]); + requests.push({ + method: 'POST', + url: `//${host}/tag?account=${pubId}&pb=1${isRtbDebugEnabled() ? '&debug=1' : ''}`, + data: JSON.stringify(request) + }) + }); + }); + return requests; + }, + + interpretResponse: function(serverResponse) { + let response = serverResponse.body; + if (!response.tags) { + return []; + } + if (response.debug) { + utils.logInfo(`ADKERNEL DEBUG:\n${response.debug}`); + } + return response.tags.map(buildBid); + }, + + getUserSyncs: function(syncOptions, serverResponses) { + if (!syncOptions.iframeEnabled || !serverResponses || serverResponses.length === 0) { + return []; + } + return serverResponses.filter(rps => 'syncpages' in rps.body) + .map(rsp => rsp.body.syncpages) + .reduce((a, b) => a.concat(b), []) + .map(sync_url => { + return { + type: 'iframe', + url: sync_url + } + }); + } +}; + +registerBidder(spec); diff --git a/modules/adkernelAdnBidAdapter.md b/modules/adkernelAdnBidAdapter.md new file mode 100644 index 00000000000..d69bf3b8998 --- /dev/null +++ b/modules/adkernelAdnBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: AdKernel ADN Bidder Adapter +Module Type: Bidder Adapter +Maintainer: denis@adkernel.com +``` + +# Description + +Connects to AdKernel Ad Delivery Network +Banner and video formats are supported. + + +# Test Parameters +``` + var adUnits = [ + { + code: 'banner-ad-div', + sizes: [[300, 250], [300, 200]], + bids: [ + { + bidder: 'adkernelAdn', + params: { + pubId: 50357, + host: 'dsp-staging.adkernel.com' + } + } + ] + }, { + code: 'video-ad-player', + sizes: [640, 480], + bids: [ + { + bidder: 'adkernelAdn', + mediaType : 'video', + params: { + pubId: 50357, + host: 'dsp-staging.adkernel.com' + } + } + ] + } + ]; +``` diff --git a/src/utils.js b/src/utils.js index 9efa4f53c57..f14a9bbd46c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -178,6 +178,14 @@ exports.getTopWindowUrl = function () { return href; }; +exports.getTopWindowReferrer = function() { + try { + return window.top.document.referrer; + } catch (e) { + return document.referrer; + } +}; + exports.logWarn = function (msg) { if (debugTurnedOn() && console.warn) { console.warn('WARNING: ' + msg); diff --git a/test/spec/modules/adkernelAdnBidAdapter_spec.js b/test/spec/modules/adkernelAdnBidAdapter_spec.js new file mode 100644 index 00000000000..3ba4012bd0b --- /dev/null +++ b/test/spec/modules/adkernelAdnBidAdapter_spec.js @@ -0,0 +1,254 @@ +import {expect} from 'chai'; +import {spec} from 'modules/adkernelAdnBidAdapter'; +import * as utils from 'src/utils'; + +describe('AdkernelAdn adapter', () => { + const bid1_pub1 = { + bidder: 'adkernelAdn', + transactionId: 'transact0', + bidderRequestId: 'req0', + bidId: 'bidid_1', + params: { + pubId: 1 + }, + placementCode: 'ad-unit-1', + sizes: [[300, 250], [300, 200]] + }, + bid2_pub1 = { + bidder: 'adkernelAdn', + transactionId: 'transact1', + bidderRequestId: 'req1', + bidId: 'bidid_2', + params: { + pubId: 1 + }, + placementCode: 'ad-unit-2', + sizes: [[300, 250]] + }, + bid1_pub2 = { + bidder: 'adkernelAdn', + transactionId: 'transact2', + bidderRequestId: 'req1', + bidId: 'bidid_3', + params: { + pubId: 7, + host: 'dps-test.com' + }, + placementCode: 'ad-unit-2', + sizes: [[728, 90]] + }, bid_video1 = { + bidder: 'adkernelAdn', + transactionId: 'transact3', + bidderRequestId: 'req1', + bidId: 'bidid_4', + mediaType: 'video', + sizes: [640, 300], + placementCode: 'video_wrapper', + params: { + pubId: 7, + video: { + mimes: ['video/mp4', 'video/webm', 'video/x-flv'], + api: [1, 2, 3, 4], + protocols: [1, 2, 3, 4, 5, 6] + } + } + }, bid_video2 = { + bidder: 'adkernelAdn', + transactionId: 'transact3', + bidderRequestId: 'req1', + bidId: 'bidid_5', + mediaTypes: {video: {context: 'instream'}}, + sizes: [640, 300], + placementCode: 'video_wrapper2', + params: { + pubId: 7, + video: { + mimes: ['video/mp4', 'video/webm', 'video/x-flv'], + api: [1, 2, 3, 4], + protocols: [1, 2, 3, 4, 5, 6] + } + } + }; + + const response = { + tags: [{ + id: 'ad-unit-1', + impid: '2c5e951baeeadd', + crid: '108_159810', + bid: 5.0, + tag: '', + w: 300, + h: 250 + }, { + id: 'ad-unit-2', + impid: '31d798477126c4', + crid: '108_21226', + bid: 2.5, + tag: '', + w: 300, + h: 250 + }, { + id: 'video_wrapper', + impid: '57d602ad1c9545', + crid: '108_158802', + bid: 10.0, + vast_url: 'http://vast.com/vast.xml' + }], + syncpages: ['https://dsp.adkernel.com/sync'] + }, usersyncOnlyResponse = { + syncpages: ['https://dsp.adkernel.com/sync'] + }; + + describe('input parameters validation', () => { + it('empty request shouldn\'t generate exception', () => { + expect(spec.isBidRequestValid({ + bidderCode: 'adkernelAdn' + })).to.be.equal(false); + }); + it('request without pubid should be ignored', () => { + expect(spec.isBidRequestValid({ + bidder: 'adkernelAdn', + params: {}, + placementCode: 'ad-unit-0', + sizes: [[300, 250]] + })).to.be.equal(false); + }); + it('request with invalid pubid should be ignored', () => { + expect(spec.isBidRequestValid({ + bidder: 'adkernelAdn', + params: { + pubId: 'invalid id' + }, + placementCode: 'ad-unit-0', + sizes: [[300, 250]] + })).to.be.equal(false); + }); + }); + + describe('banner request building', () => { + let pbRequest; + let tagRequest; + + before(() => { + let mock = sinon.stub(utils, 'getTopWindowLocation', () => { + return { + protocol: 'https:', + hostname: 'example.com', + host: 'example.com', + pathname: '/index.html', + href: 'https://example.com/index.html' + }; + }); + pbRequest = spec.buildRequests([bid1_pub1])[0]; + tagRequest = JSON.parse(pbRequest.data); + mock.restore(); + }); + + it('should have request id', () => { + expect(tagRequest).to.have.property('id'); + }); + it('should have transaction id', () => { + expect(tagRequest).to.have.property('tid'); + }); + it('should have sizes', () => { + expect(tagRequest.imp[0].banner).to.have.property('format'); + expect(tagRequest.imp[0].banner.format).to.be.eql(['300x250', '300x200']); + }); + it('should have impression id', () => { + expect(tagRequest.imp[0]).to.have.property('id', 'bidid_1'); + }); + it('should have tagid', () => { + expect(tagRequest.imp[0]).to.have.property('tagid', 'ad-unit-1'); + }); + it('should create proper site block', () => { + expect(tagRequest.site).to.have.property('page', 'https://example.com/index.html'); + expect(tagRequest.site).to.have.property('secure', 1); + }); + }); + + describe('video request building', () => { + let pbRequest = spec.buildRequests([bid_video1, bid_video2])[0]; + let tagRequest = JSON.parse(pbRequest.data); + + it('should have video object', () => { + expect(tagRequest.imp[0]).to.have.property('video'); + expect(tagRequest.imp[1]).to.have.property('video'); + }); + it('should have tagid', () => { + expect(tagRequest.imp[0]).to.have.property('tagid', 'video_wrapper'); + expect(tagRequest.imp[1]).to.have.property('tagid', 'video_wrapper2'); + }); + }); + + describe('requests routing', () => { + it('should issue a request for each publisher', () => { + let pbRequests = spec.buildRequests([bid1_pub1, bid_video1]); + expect(pbRequests).to.have.length(2); + expect(pbRequests[0].url).to.have.string(`account=${bid1_pub1.params.pubId}`); + expect(pbRequests[1].url).to.have.string(`account=${bid1_pub2.params.pubId}`); + let tagRequest1 = JSON.parse(pbRequests[0].data); + let tagRequest2 = JSON.parse(pbRequests[1].data); + expect(tagRequest1.imp).to.have.length(1); + expect(tagRequest2.imp).to.have.length(1); + }); + it('should issue a request for each host', () => { + let pbRequests = spec.buildRequests([bid1_pub1, bid1_pub2]); + expect(pbRequests).to.have.length(2); + expect(pbRequests[0].url).to.have.string('//tag.adkernel.com/tag'); + expect(pbRequests[1].url).to.have.string(`//${bid1_pub2.params.host}/tag`); + let tagRequest1 = JSON.parse(pbRequests[0].data); + let tagRequest2 = JSON.parse(pbRequests[1].data); + expect(tagRequest1.imp).to.have.length(1); + expect(tagRequest2.imp).to.have.length(1); + }); + }); + + describe('responses processing', () => { + let responses; + before(() => { + responses = spec.interpretResponse({body: response}); + }); + it('should parse all responses', () => { + expect(responses).to.have.length(3); + }); + it('should return fully-initialized bid-response', () => { + let resp = responses[0]; + expect(resp).to.have.property('bidderCode', 'adkernelAdn'); + expect(resp).to.have.property('requestId', '2c5e951baeeadd'); + expect(resp).to.have.property('cpm', 5.0); + expect(resp).to.have.property('width', 300); + expect(resp).to.have.property('height', 250); + expect(resp).to.have.property('creativeId', '108_159810'); + expect(resp).to.have.property('currency'); + expect(resp).to.have.property('ttl'); + expect(resp).to.have.property('mediaType', 'banner'); + expect(resp).to.have.property('ad'); + expect(resp.ad).to.have.string(''); + }); + it('should return fully-initialized video bid-response', () => { + let resp = responses[2]; + expect(resp).to.have.property('bidderCode', 'adkernelAdn'); + expect(resp).to.have.property('requestId', '57d602ad1c9545'); + expect(resp).to.have.property('cpm', 10.0); + expect(resp).to.have.property('creativeId', '108_158802'); + expect(resp).to.have.property('currency'); + expect(resp).to.have.property('ttl'); + expect(resp).to.have.property('mediaType', 'video'); + expect(resp).to.have.property('vastUrl', 'http://vast.com/vast.xml'); + expect(resp).to.not.have.property('ad'); + }); + it('should perform usersync', () => { + let syncs = spec.getUserSyncs({iframeEnabled: false}, [{body: response}]); + expect(syncs).to.have.length(0); + syncs = spec.getUserSyncs({iframeEnabled: true}, [{body: response}]); + expect(syncs).to.have.length(1); + expect(syncs[0]).to.have.property('type', 'iframe'); + expect(syncs[0]).to.have.property('url', 'https://dsp.adkernel.com/sync'); + }); + it('should handle user-sync only response', () => { + let request = spec.buildRequests([bid1_pub1])[0]; + let resp = spec.interpretResponse({body: usersyncOnlyResponse}, request); + expect(resp).to.have.length(0); + }); + }); +}); From c0a1f457faa018a0f9d10803b4df1e5dd24497b2 Mon Sep 17 00:00:00 2001 From: Paul Yang Date: Wed, 8 Nov 2017 10:29:59 -0800 Subject: [PATCH 09/32] Update conversant adapter to include ttl and netRevenue (#1791) --- modules/conversantBidAdapter.js | 18 ++++++++++-------- test/spec/modules/conversantBidAdapter_spec.js | 10 ++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index 7e71d3be8aa..71554c758f9 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -5,7 +5,7 @@ import { VIDEO } from 'src/mediaTypes'; const BIDDER_CODE = 'conversant'; const URL = '//media.msg.dotomi.com/s2s/header/24'; const SYNC_URL = '//media.msg.dotomi.com/w/user.sync'; -const VERSION = '2.2.0'; +const VERSION = '2.2.1'; export const spec = { code: BIDDER_CODE, @@ -75,7 +75,10 @@ export const spec = { copyOptProperty(bid.params, 'tag_id', imp, 'tagid'); if (isVideoRequest(bid)) { - const video = {format: format}; + const video = { + w: format[0].w, + h: format[0].h + }; copyOptProperty(bid.params, 'position', video, 'pos'); copyOptProperty(bid.params, 'mimes', video); @@ -141,17 +144,16 @@ export const spec = { requestId: conversantBid.impid, currency: serverResponse.cur || 'USD', cpm: responseCPM, - creativeId: conversantBid.crid || '' + creativeId: conversantBid.crid || '', + ttl: 300, + netRevenue: true }; if (request.video) { bid.vastUrl = responseAd; bid.mediaType = 'video'; - - if (request.video.format.length >= 1) { - bid.width = request.video.format[0].w; - bid.height = request.video.format[0].h; - } + bid.width = request.video.w; + bid.height = request.video.h; } else { bid.ad = responseAd + ''; bid.width = conversantBid.w; diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index 81da6867132..331eea873dc 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -205,8 +205,8 @@ describe('Conversant adapter tests', function() { expect(payload.imp[3]).to.not.have.property('tagid'); expect(payload.imp[3]).to.have.property('video'); expect(payload.imp[3].video).to.not.have.property('pos'); - expect(payload.imp[3].video).to.have.property('format'); - expect(payload.imp[3].video.format).to.deep.equal([{w: 640, h: 480}]); + expect(payload.imp[3].video).to.have.property('w', 640); + expect(payload.imp[3].video).to.have.property('h', 480); expect(payload.imp[3].video).to.have.property('mimes'); expect(payload.imp[3].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); expect(payload.imp[3].video).to.have.property('protocols'); @@ -243,6 +243,8 @@ describe('Conversant adapter tests', function() { expect(bid).to.have.property('width', 300); expect(bid).to.have.property('height', 250); expect(bid).to.have.property('ad', 'markup000'); + expect(bid).to.have.property('ttl', 300); + expect(bid).to.have.property('netRevenue', true); // There is no bid001 because cpm is $0 @@ -254,6 +256,8 @@ describe('Conversant adapter tests', function() { expect(bid).to.have.property('width', 300); expect(bid).to.have.property('height', 600); expect(bid).to.have.property('ad', 'markup002'); + expect(bid).to.have.property('ttl', 300); + expect(bid).to.have.property('netRevenue', true); bid = response[2]; expect(bid).to.have.property('requestId', 'bid003'); @@ -264,6 +268,8 @@ describe('Conversant adapter tests', function() { expect(bid).to.have.property('height', 480); expect(bid).to.have.property('vastUrl', 'markup003'); expect(bid).to.have.property('mediaType', 'video'); + expect(bid).to.have.property('ttl', 300); + expect(bid).to.have.property('netRevenue', true); }); it('Verify handling of bad responses', function() { From 93768856d615af494c82914d3d719e9345f13c18 Mon Sep 17 00:00:00 2001 From: hadarpeer <32834110+hadarpeer@users.noreply.github.com> Date: Wed, 8 Nov 2017 21:23:07 +0200 Subject: [PATCH 10/32] MobfoxBidAdapter compliant to prebid1.0 (#1757) * Upgrade MobfoxAdapter to be compliant with Prebid 1.0 - missing cpm * MfBidAdapter changes for preBid1.0 * Upgrade MobfoxAdapter to be compliant with Prebid 1.0 - final version * removed accidentally added files * Update MobfoxBidAdapter after review * Update MobfoxBidAdapter after review - Final * Add markdown file containing key information about Mobfox adapter * Fix error field - under request in serverResponse --- modules/mobfoxBidAdapter.js | 183 +++++--------- modules/mobfoxBidAdapter.md | 29 +++ test/spec/modules/mobfoxBidAdapter_spec.js | 272 ++++++++++----------- 3 files changed, 222 insertions(+), 262 deletions(-) create mode 100644 modules/mobfoxBidAdapter.md diff --git a/modules/mobfoxBidAdapter.js b/modules/mobfoxBidAdapter.js index 39d2fe40f1d..ff55d330112 100644 --- a/modules/mobfoxBidAdapter.js +++ b/modules/mobfoxBidAdapter.js @@ -1,34 +1,25 @@ -const bidfactory = require('src/bidfactory.js'); -const bidmanager = require('src/bidmanager.js'); -const ajax = require('src/ajax.js'); -const CONSTANTS = require('src/constants.json'); +import {registerBidder} from 'src/adapters/bidderFactory'; + const utils = require('src/utils.js'); -const adaptermanager = require('src/adaptermanager'); - -function MobfoxAdapter() { - const BIDDER_CODE = 'mobfox'; - const BID_REQUEST_BASE_URL = 'https://my.mobfox.com/request.php'; - - // request - function buildQueryStringFromParams(params) { - for (let key in params) { - if (params.hasOwnProperty(key)) { - if (params[key] === undefined) { - delete params[key]; - } else { - params[key] = encodeURIComponent(params[key]); - } - } +const BIDDER_CODE = 'mobfox'; +const BID_REQUEST_BASE_URL = 'https://my.mobfox.com/request.php'; +const CPM_HEADER = 'X-Pricing-CPM'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['mf'], // short code + isBidRequestValid: function (bid) { + return bid.params.s !== null && bid.params.s !== undefined && bid.requestId !== null && bid.requestId !== undefined; + }, + buildRequests: function (validBidRequests) { + if (validBidRequests.length > 1) { + throw ('invalid number of valid bid requests, expected 1 element') } - return utils._map(Object.keys(params), key => `${key}=${params[key]}`) - .join('&'); - } - - function buildBidRequest(bid) { - let bidParams = bid.params; + let bidParams = validBidRequests[0].params; + let bid = validBidRequests[0]; - let requestParams = { + let params = { // -------------------- Mandatory Parameters ------------------ rt: bidParams.rt || 'api-fetchip', r_type: bidParams.r_type || 'banner', @@ -80,103 +71,63 @@ function MobfoxAdapter() { n_rating_req: bidParams.n_rating_req || undefined }; - return requestParams; - } + let payloadString = buildPayloadString(params); - function sendBidRequest(bid) { - let requestParams = buildBidRequest(bid); - let queryString = buildQueryStringFromParams(requestParams); - - ajax.ajax(`${BID_REQUEST_BASE_URL}?${queryString}`, { - success(resp, xhr) { - if (xhr.getResponseHeader('Content-Type') == 'application/json') { - try { - resp = JSON.parse(resp) - } catch (e) { - resp = {error: resp} - } - } - onBidResponse({ - data: resp, - xhr: xhr - }, bid); - }, - error(err) { - if (xhr.getResponseHeader('Content-Type') == 'application/json') { - try { - err = JSON.parse(err); - } catch (e) { - } - ; - } - onBidResponseError(bid, [err]); + return { + method: 'GET', + url: BID_REQUEST_BASE_URL, + data: payloadString, + requestId: bid.bidId + }; + }, + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + let serverResponseBody = serverResponse.body; + + if (!serverResponseBody || serverResponseBody.error) { + let errorMessage = `in response for ${BIDDER_CODE} adapter`; + if (serverResponseBody && serverResponseBody.error) { + errorMessage += `: ${serverResponseBody.error}`; } - }); - } - - // response - function onBidResponseError(bid, err) { - utils.logError('Bid Response Error', bid, ...err); - let bidResponse = bidfactory.createBid(CONSTANTS.STATUS.NO_BID, bid); - bidResponse.bidderCode = BIDDER_CODE; - bidmanager.addBidResponse(bid.placementCode, bidResponse); - } - - function onBidResponse(bidderResponse, bid) { - // transform the response to a valid prebid response - try { - let bidResponse = transformResponse(bidderResponse, bid); - bidmanager.addBidResponse(bid.placementCode, bidResponse); - } catch (e) { - onBidResponseError(bid, [e]); - } - } - - function transformResponse(bidderResponse, bid) { - let responseBody = bidderResponse.data; - - // Validate Request - let err = responseBody.error; - if (err) { - throw err; + utils.logError(errorMessage); + return bidResponses; } - - let htmlString = responseBody.request && responseBody.request.htmlString; - if (!htmlString) { - throw [`htmlString is missing`, responseBody]; - } - - let cpm; - const cpmHeader = bidderResponse.xhr.getResponseHeader('X-Pricing-CPM'); try { - cpm = Number(cpmHeader); + let serverResponseHeaders = serverResponse.headers; + let bidRequestData = bidRequest.data.split('&'); + const bidResponse = { + requestId: bidRequest.requestId, + cpm: serverResponseHeaders.get(CPM_HEADER), + width: bidRequestData[5].split('=')[1], + height: bidRequestData[6].split('=')[1], + creativeId: bidRequestData[3].split('=')[1], + currency: 'USD', + netRevenue: true, + ttl: 360, + referrer: serverResponseBody.request.clickurl, + ad: serverResponseBody.request.htmlString + }; + bidResponses.push(bidResponse); } catch (e) { - throw ['Invalid CPM value:', cpmHeader]; + throw 'could not build bid response: ' + e; } - - // Validations passed - Got bid - let bidResponse = bidfactory.createBid(CONSTANTS.STATUS.GOOD, bid); - bidResponse.bidderCode = BIDDER_CODE; - - bidResponse.ad = htmlString; - bidResponse.cpm = cpm; - - bidResponse.width = bid.sizes[0][0]; - bidResponse.height = bid.sizes[0][1]; - - return bidResponse; + return bidResponses; } - - // prebid api - function callBids(params) { - let bids = params.bids || []; - bids.forEach(sendBidRequest); +}; + +function buildPayloadString(params) { + for (let key in params) { + if (params.hasOwnProperty(key)) { + if (params[key] === undefined) { + delete params[key]; + } else { + params[key] = encodeURIComponent(params[key]); + } + } } - return { - callBids: callBids - }; + return utils._map(Object.keys(params), key => `${key}=${params[key]}`) + .join('&') } -adaptermanager.registerBidAdapter(new MobfoxAdapter(), 'mobfox'); -module.exports = MobfoxAdapter; +registerBidder(spec); diff --git a/modules/mobfoxBidAdapter.md b/modules/mobfoxBidAdapter.md new file mode 100644 index 00000000000..31b60606d2f --- /dev/null +++ b/modules/mobfoxBidAdapter.md @@ -0,0 +1,29 @@ +# Overview + +``` +Module Name: Mobfox Bidder Adapter +Module Type: Bidder Adapter +Maintainer: solutions-team@matomy.com +``` + +# Description + +Module that connects to Mobfox's demand sources + +# Test Parameters +``` + var adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + sizes: [[320, 480], [300, 250], [300,600]], + + // Replace this object to test a new Adapter! + bids: [{ + bidder: 'mobfox', + params: { + s: "267d72ac3f77a3f447b32cf7ebf20673", // required - The hash of your inventory to identify which app is making the request, + imp_instl: 1 // optional - set to 1 if using interstitial otherwise delete or set to 0 + } + }] + + }]; +``` diff --git a/test/spec/modules/mobfoxBidAdapter_spec.js b/test/spec/modules/mobfoxBidAdapter_spec.js index f2819009908..76bb95430fe 100644 --- a/test/spec/modules/mobfoxBidAdapter_spec.js +++ b/test/spec/modules/mobfoxBidAdapter_spec.js @@ -1,162 +1,142 @@ -describe('mobfox adapter tests', function () { +describe('mobfox adapter tests', () => { const expect = require('chai').expect; const utils = require('src/utils'); const adapter = require('modules/mobfoxBidAdapter'); - const bidmanager = require('src/bidmanager'); - const adloader = require('src/adloader'); - const CONSTANTS = require('src/constants.json'); - const ajax = require('src/ajax.js'); - let mockResponses = { - banner: { - 'request': { - 'type': 'textAd', - 'htmlString': '<\/title><style>body{margin:0;padding:0}#mobfoxCover{background:0 0;margin:0;padding:0;border:none;position:absolute;left:0;top:0;z-index:100}<\/style><\/head><body><div id="mobfoxCover"><\/div><script type="text\/javascript">function checkRedirect(e){return function(){if(state===REDIRECT){state=REDUNDANT;var t=window.document.querySelector("iframe").contentDocument.querySelector("html").innerHTML.toLowerCase();if(!(t.indexOf("<script")<0&&t.indexOf("<iframe")<0)){var o=new XMLHttpRequest,d={creativeId:creativeId,advertiserId:advertiserId,hParam:hParam,dspId:dspId,networkId:networkId,autoPilotInventoryConfId:autoPilotInventoryConfId,stackItemId:stackItemId,adSpaceId:adSpaceId,cId:cId,adomain:adomain,geo:geo,event:e,ua:window.navigator.userAgent,adId:adId,site:window.location.href,md5Hash:md5Hash,snapshot:btoa(unescape(encodeURIComponent(t)))};o.open("POST","http:\/\/my.mobfox.com\/fraud-integration",!1),o.setRequestHeader("Content-type","application\/json"),o.send(JSON.stringify(d))}}}}function init(){window.onbeforeunload=checkRedirect("onbeforeunload"),window.addEventListener("beforeunload",checkRedirect("beforeunload")),window.addEventListener("unload",checkRedirect("unload")),document.addEventListener("visibilitychange",function(){"hidden"===document.visibilityState&&checkRedirect("visibilityState")});var e=document.createElement("iframe");document.body.appendChild(e),e.width="320",e.height="50";var t=document.querySelector("#mobfoxCover");t.style.width=e.width+"px",t.style.height=e.height+"px",e.style.margin="0px",e.style.padding="0px",e.style.border="none",e.scrolling="no",e.style.overflow="hidden",e.sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-top-navigation allow-same-origin";var o=atob(markupB64);setTimeout(function(){state=NORMAL},200),setTimeout(function(){var e=document.querySelector("#mobfoxCover");document.body.removeChild(e)},200);var d="srcdoc"in e,n=o;o.indexOf("<body>")<0&&(n="<html><body style="margin:0">"+o+"<\/body><\/html>"),d?e.srcdoc=n:(e.contentWindow.document.open(),e.contentWindow.document.write(n),e.contentWindow.document.close())}var markupB64="PGEgaHJlZj0iaHR0cDovL3Rva3lvLW15Lm1vYmZveC5jb20vZXhjaGFuZ2UuY2xpY2sucGhwP2g9ZGI1ZjZkOTJiMDk1OGI0ZDFlNjU4ZjZlNWRkNWY0MmUiIHRhcmdldD0iX2JsYW5rIj48aW1nIHNyYz0iaHR0cHM6Ly9jcmVhdGl2ZWNkbi5tb2Jmb3guY29tL2U4ZTkxNWYzMmJhOTVkM2JmMzY4YTM5N2EyMzQ4NzVmLmdpZiIgYm9yZGVyPSIwIi8+PC9hPjxicj48aW1nIHN0eWxlPSJwb3NpdGlvbjphYnNvbHV0ZTsgbGVmdDogLTEwMDAwcHg7IiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBzcmM9Imh0dHA6Ly90b2t5by1teS5tb2Jmb3guY29tL2V4Y2hhbmdlLnBpeGVsLnBocD9oPWRiNWY2ZDkyYjA5NThiNGQxZTY1OGY2ZTVkZDVmNDJlIi8+PHNjcmlwdCB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiPmRvY3VtZW50LndyaXRlKCc8aW1nIHN0eWxlPSJwb3NpdGlvbjphYnNvbHV0ZTsgbGVmdDogLTEwMDAwcHg7IiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBzcmM9Imh0dHA6Ly90b2t5by1teS5tb2Jmb3guY29tL2V4Y2hhbmdlLnBpeGVsLnBocD9oPWRiNWY2ZDkyYjA5NThiNGQxZTY1OGY2ZTVkZDVmNDJlJnRlc3Q9MSIvPicpOzwvc2NyaXB0Pg==",INITIAL=0,REDIRECT=1,REDUNDANT=2,NORMAL=3,state=INITIAL,creativeId="",advertiserId="",hParam="db5f6d92b0958b4d1e658f6e5dd5f42e",dspId="",networkId="",autoPilotInventoryConfId="",stackItemId="392746",serverHost="184.172.209.50",adSpaceId="",adId="",cId="",adomain="",geo="US",md5Hash="f3bd183c0b19faf12c76e75461cb8cac";document.addEventListener("DOMContentLoaded",function(e){state=REDIRECT}),setTimeout(init,1)<\/script><\/body><\/html>', - 'clicktype': 'safari', - 'clickurl': 'http://tokyo-my.mobfox.com/exchange.click.php?h=db5f6d92b0958b4d1e658f6e5dd5f42e', - 'urltype': 'link', - 'refresh': '30', - 'scale': 'no', - 'skippreflight': 'yes' - } - } - }; - - let mockRequestsParams = { - banner: { - rt: 'api', - r_type: 'banner', - i: '69.197.148.18', - s: 'fe96717d9875b9da4339ea5367eff1ec', - u: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4', - adspace_strict: 0, - - // o_iosadvid: '1976F519-26D0-4428-9891-3133253A453F', - // r_floor: '0.2', - // longitude: '12.12', - // latitude: '280.12', - // demo_gender: 'male', - // demo_age: '1982', - // demo_keywords: 'sports', - // adspace_width: 320, - // adspace_height: 50 - } - }; - before(() => sinon.stub(document.body, 'appendChild')); - after(() => document.body.appendChild.restore()); - - let xhrMock = { - getResponseHeader: getResponseHeaderMock - }; - function getResponseHeaderMock(header) { - switch (header) { - case 'Content-Type': - return 'application/json'; - case 'X-Pricing-CPM': - return '1'; - } - } - function createMobfoxErrorStub() { - return sinon.stub(ajax, 'ajax', (url, callbacks) => { - callbacks.success( - JSON.stringify({error: 'No Ad Available'}), - xhrMock - ); + const bidRequest = [{ + code: 'div-gpt-ad-1460505748561-0', + sizes: [[320, 480], [300, 250], [300, 600]], + // Replace this object to test a new Adapter! + bidder: 'mobfox', + bidId: '5t5t5t5', + params: { + s: '267d72ac3f77a3f447b32cf7ebf20673', // required - The hash of your inventory to identify which app is making the request, + imp_instl: 1 // optional - set to 1 if using interstitial otherwise delete or set to 0 + }, + placementCode: 'div-gpt-ad-1460505748561-0', + requestId: 'c241c810-18d9-4aa4-a62f-8c1980d8d36b', + transactionId: '31f42cba-5920-4e47-adad-69c79d0d4fb4' + }]; + + describe('validRequests', () => { + let bidRequestInvalid1 = [{ + code: 'div-gpt-ad-1460505748561-0', + sizes: [[320, 480], [300, 250], [300, 600]], + // Replace this object to test a new Adapter! + bidder: 'mobfox', + bidId: '5t5t5t5', + params: { + imp_instl: 1 // optional - set to 1 if using interstitial otherwise delete or set to 0 + }, + placementCode: 'div-gpt-ad-1460505748561-0', + requestId: 'c241c810-18d9-4aa4-a62f-8c1980d8d36b', + transactionId: '31f42cba-5920-4e47-adad-69c79d0d4fb4' + }]; + + let bidRequestInvalid2 = [{ + code: 'div-gpt-ad-1460505748561-0', + sizes: [[320, 480], [300, 250], [300, 600]], + // Replace this object to test a new Adapter! + bidder: 'mobfox', + bidId: '5t5t5t5', + params: { + s: '267d72ac3f77a3f447b32cf7ebf20673', // required - The hash of your inventory to identify which app is making the request, + imp_instl: 1 // optional - set to 1 if using interstitial otherwise delete or set to 0 + }, + placementCode: 'div-gpt-ad-1460505748561-0', + transactionId: '31f42cba-5920-4e47-adad-69c79d0d4fb4' + }]; + + it('test valid MF request success', () => { + let isValid = adapter.spec.isBidRequestValid(bidRequest[0]); + expect(isValid).to.equal(true); }); - } - function createMobfoxSuccessStub() { - return sinon.stub(ajax, 'ajax', (url, callbacks) => { - callbacks.success( - JSON.stringify(mockResponses.banner) - , xhrMock - ); + it('test valid MF request failed1', () => { + let isValid = adapter.spec.isBidRequestValid(bidRequestInvalid1[0]); + expect(isValid).to.equal(false); }); - } - describe('test mobfox error response', function () { - let stubAddBidResponse, stubAjax; - before(function () { - stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - stubAjax = createMobfoxErrorStub() + it('test valid MF request failed2', () => { + let isValid = adapter.spec.isBidRequestValid(bidRequestInvalid2[0]); + expect(isValid).to.equal(false); }); - - after(function () { - stubAddBidResponse.restore(); - stubAjax.restore(); + }) + + describe('buildRequests', () => { + it('test build MF request', () => { + let request = adapter.spec.buildRequests(bidRequest); + let payload = request.data.split('&'); + expect(payload[0]).to.equal('rt=api-fetchip'); + expect(payload[1]).to.equal('r_type=banner'); + expect(payload[2]).to.equal('r_resp=json'); + expect(payload[3]).to.equal('s=267d72ac3f77a3f447b32cf7ebf20673'); + expect(payload[5]).to.equal('adspace_width=320'); + expect(payload[6]).to.equal('adspace_height=480'); + expect(payload[7]).to.equal('imp_instl=1'); }); - it('should add empty bid responses if no bids returned', function () { - let bidderRequest = { - bidderCode: 'mobfox', - bids: [ - { - bidId: 'bidId1', - bidder: 'mobfox', - params: {}, - sizes: [[300, 250]], - placementCode: 'test-gpt-div-1234' - } - ] - }; - - // empty ads in bidresponse - let requestParams = utils.cloneJson(mockRequestsParams.banner); - requestParams.adspace_width = 1231564; // should return an error - bidderRequest.bids[0].params = requestParams; - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - // adapter needs to be called, in order for the stub to register. - adapter().callBids(bidderRequest); - - let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; - expect(bidPlacementCode1).to.equal('test-gpt-div-1234'); - expect(bidResponse1.getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); - expect(bidResponse1.bidderCode).to.equal('mobfox'); + it('test build MF request', () => { + let request = adapter.spec.buildRequests(bidRequest); + let payload = request.data.split('&'); + expect(payload[0]).to.equal('rt=api-fetchip'); + expect(payload[1]).to.equal('r_type=banner'); + expect(payload[2]).to.equal('r_resp=json'); + expect(payload[3]).to.equal('s=267d72ac3f77a3f447b32cf7ebf20673'); + expect(payload[5]).to.equal('adspace_width=320'); + expect(payload[6]).to.equal('adspace_height=480'); + expect(payload[7]).to.equal('imp_instl=1'); }); - }); - - describe('test mobfox success response', function () { - let stubAddBidResponse, stubAjax; - before(function () { - stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - stubAjax = createMobfoxSuccessStub() - }); - - after(function () { - stubAddBidResponse.restore(); - stubAjax.restore(); + }) + + describe('interceptResponse', () => { + let mockServerResponse = { + body: { + request: { + clicktype: 'safari', + clickurl: 'http://tokyo-my.mobfox.com/exchange.click.php?h=494ef76d5b0287a8b5ac8724855cb5e0', + cpmPrice: 50, + htmlString: 'test', + refresh: '30', + scale: 'no', + skippreflight: 'yes', + type: 'textAd', + urltype: 'link' + } + }, + headers: { + get: function (header) { + if (header === 'X-Pricing-CPM') { + return 50; + } + } + } + }; + it('test intercept response', () => { + let request = adapter.spec.buildRequests(bidRequest); + let bidResponses = adapter.spec.interpretResponse(mockServerResponse, request); + expect(bidResponses.length).to.equal(1); + expect(bidResponses[0].ad).to.equal('test'); + expect(bidResponses[0].cpm).to.equal(50); + expect(bidResponses[0].creativeId).to.equal('267d72ac3f77a3f447b32cf7ebf20673'); + expect(bidResponses[0].requestId).to.equal('5t5t5t5'); + expect(bidResponses[0].currency).to.equal('USD'); + expect(bidResponses[0].height).to.equal('480'); + expect(bidResponses[0].netRevenue).to.equal(true); + expect(bidResponses[0].referrer).to.equal('http://tokyo-my.mobfox.com/exchange.click.php?h=494ef76d5b0287a8b5ac8724855cb5e0'); + expect(bidResponses[0].ttl).to.equal(360); + expect(bidResponses[0].width).to.equal('320'); }); - it('should add a bid response', function () { - let bidderRequest = { - bidderCode: 'mobfox', - bids: [ - { - bidId: 'bidId1', - bidder: 'mobfox', - params: {}, - sizes: [[300, 250]], - placementCode: 'test-gpt-div-1234' - } - ] + it('test intercept response with empty server response', () => { + let request = adapter.spec.buildRequests(bidRequest); + let serverResponse = { + request: { + error: 'cannot get response' + } }; - - let requestParams = utils.cloneJson(mockRequestsParams.banner); - bidderRequest.bids[0].params = requestParams; - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - // adapter needs to be called, in order for the stub to register. - adapter().callBids(bidderRequest); - - let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; - expect(bidPlacementCode1).to.equal('test-gpt-div-1234'); - expect(bidResponse1.getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bidResponse1.bidderCode).to.equal('mobfox'); - - expect(bidResponse1.cpm).to.equal(1); - expect(bidResponse1.width).to.equal(bidderRequest.bids[0].sizes[0][0]); - expect(bidResponse1.height).to.equal(bidderRequest.bids[0].sizes[0][1]); - }); - }); + let bidResponses = adapter.spec.interpretResponse(serverResponse, request); + expect(bidResponses.length).to.equal(0); + }) + }) }); From aed6729e044f7095f3fa3196957f70618c4fe814 Mon Sep 17 00:00:00 2001 From: Miller <arhengel@gmail.com> Date: Wed, 8 Nov 2017 21:26:47 +0200 Subject: [PATCH 11/32] Vertamedia Adapter for 1.0 (#1789) * Vertamedia prebid.js adapter initial * sync * remove https * add http * remove let * fix typo * fix spaces * add descriptionUrl; remove log * fix getSize * add usege parseSizesInput * Add support for vastTag * Add moulty bid request; set vastUrl for bider; * update for vertamedia adapter to Prebid 1.0 * Add vertamedia md file * Add required fields * Remove defaults; fix md file; --- modules/vertamediaBidAdapter.js | 216 +++++++++--------- modules/vertamediaBidAdapter.md | 27 +++ .../spec/modules/vertamediaBidAdapter_spec.js | 191 +++++++--------- 3 files changed, 217 insertions(+), 217 deletions(-) create mode 100644 modules/vertamediaBidAdapter.md diff --git a/modules/vertamediaBidAdapter.js b/modules/vertamediaBidAdapter.js index d87ae56def1..81e2872d371 100644 --- a/modules/vertamediaBidAdapter.js +++ b/modules/vertamediaBidAdapter.js @@ -1,119 +1,121 @@ -import Adapter from 'src/adapter'; -import bidfactory from 'src/bidfactory'; -import bidmanager from 'src/bidmanager'; import * as utils from 'src/utils'; -import { ajax } from 'src/ajax'; -import { STATUS } from 'src/constants'; -import adaptermanager from 'src/adaptermanager'; - -const ENDPOINT = '//rtb.vertamedia.com/hb/'; - -function VertamediaAdapter() { - const baseAdapter = new Adapter('vertamedia'); - let bidRequest; - - baseAdapter.callBids = function (bidRequests) { - if (!bidRequests || !bidRequests.bids || bidRequests.bids.length === 0) { - return; - } - - var RTBDataParams = prepareAndSaveRTBRequestParams(bidRequests.bids[0]); - - if (!RTBDataParams) { - return; - } - - ajax(ENDPOINT, handleResponse, RTBDataParams, { - contentType: 'text/plain', - withCredentials: true, - method: 'GET' +import {registerBidder} from 'src/adapters/bidderFactory'; +import {VIDEO} from 'src/mediaTypes'; + +const URL = '//rtb.vertamedia.com/hb/'; +const BIDDER_CODE = 'vertamedia'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO], + isBidRequestValid: function (bid) { + return Boolean(bid && bid.params && bid.params.aid); + }, + + /** + * Make a server request from the list of BidRequests + * @param bidRequests + * @param bidderRequest + */ + buildRequests: function (bidRequests, bidderRequest) { + return bidRequests.map((bid) => { + return { + data: prepareRTBRequestParams(bid), + bidderRequest, + method: 'GET', + url: URL + } }); - }; - - function prepareAndSaveRTBRequestParams(bid) { - if (!bid || !bid.params || !bid.params.aid || !bid.placementCode) { - return; + }, + + /** + * Unpack the response from the server into a list of bids + * @param serverResponse + * @param bidderRequest + * @return {Bid[]} An array of bids which were nested inside the server + */ + interpretResponse: function (serverResponse, {bidderRequest}) { + serverResponse = serverResponse.body; + const isInvalidValidResp = !serverResponse || !serverResponse.bids || !serverResponse.bids.length; + let bids = []; + + if (isInvalidValidResp) { + let extMessage = serverResponse && serverResponse.ext && serverResponse.ext.message ? `: ${serverResponse.ext.message}` : ''; + let errorMessage = `in response for ${bidderRequest.bidderCode} adapter ${extMessage}`; + + utils.logError(errorMessage); + + return bids; } - bidRequest = bid; - - let size = getSize(bid.sizes); - - bidRequest.width = size.width; - bidRequest.height = size.height; - - return { - aid: bid.params.aid, - w: size.width, - h: size.height, - domain: document.location.hostname - }; - } - - function getSize(requestSizes) { - const parsed = {}; - const size = utils.parseSizesInput(requestSizes)[0]; - - if (typeof size !== 'string') { - return parsed; - } - - let parsedSize = size.toUpperCase().split('X'); - - return { - width: parseInt(parsedSize[0], 10) || undefined, - height: parseInt(parsedSize[1], 10) || undefined - }; - } - - /* Notify Prebid of bid responses so bids can get in the auction */ - function handleResponse(response) { - var parsed; - - try { - parsed = JSON.parse(response); - } catch (error) { - utils.logError(error); - } - - if (!parsed || parsed.error || !parsed.bids || !parsed.bids.length) { - bidmanager.addBidResponse(bidRequest.placementCode, createBid(STATUS.NO_BID)); + serverResponse.bids.forEach(serverBid => { + if (serverBid.cpm !== 0) { + const bid = createBid(serverBid); + bids.push(bid); + } + }); - return; - } + return bids; + }, +}; + +/** + * Prepare all parameters for request + * @param bid {object} + * @returns {object} + */ +function prepareRTBRequestParams(bid) { + let size = getSize(bid.sizes); + + return { + domain: utils.getTopWindowLocation().hostname, + callbackId: bid.bidId, + aid: bid.params.aid, + h: size.height, + w: size.width + }; +} - bidmanager.addBidResponse(bidRequest.placementCode, createBid(STATUS.GOOD, parsed.bids[0])); +/** + * Prepare size for request + * @param requestSizes {array} + * @returns {object} bid The bid to validate + */ +function getSize(requestSizes) { + const size = utils.parseSizesInput(requestSizes)[0]; + const parsed = {}; + + if (typeof size !== 'string') { + return parsed; } - function createBid(status, tag) { - var bid = bidfactory.createBid(status, tag); + let parsedSize = size.toUpperCase().split('X'); - bid.code = baseAdapter.getBidderCode(); - bid.bidderCode = bidRequest.bidder; - - if (!tag || status !== STATUS.GOOD) { - return bid; - } - - bid.mediaType = 'video'; - bid.cpm = tag.cpm; - bid.creative_id = tag.cmpId; - bid.width = bidRequest.width; - bid.height = bidRequest.height; - bid.descriptionUrl = tag.url; - bid.vastUrl = tag.url; - - return bid; - } - - return Object.assign(this, { - callBids: baseAdapter.callBids, - setBidderCode: baseAdapter.setBidderCode - }); + return { + height: parseInt(parsedSize[1], 10) || undefined, + width: parseInt(parsedSize[0], 10) || undefined + }; } -adaptermanager.registerBidAdapter(new VertamediaAdapter(), 'vertamedia', { - supportedMediaTypes: ['video'] -}); +/** + * Configure new bid by response + * @param bidResponse {object} + * @returns {object} + */ +function createBid(bidResponse) { + return { + requestId: bidResponse.requestId, + descriptionUrl: bidResponse.url, + creativeId: bidResponse.cmpId, + vastUrl: bidResponse.vastUrl, + height: bidResponse.height, + currency: bidResponse.cur, + width: bidResponse.width, + cpm: bidResponse.cpm, + mediaType: 'video', + netRevenue: true, + ttl: 3600 + }; +} -module.exports = VertamediaAdapter; +registerBidder(spec); diff --git a/modules/vertamediaBidAdapter.md b/modules/vertamediaBidAdapter.md new file mode 100644 index 00000000000..b64e6b24214 --- /dev/null +++ b/modules/vertamediaBidAdapter.md @@ -0,0 +1,27 @@ +# Overview + +**Module Name**: VertaMedia Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: support@verta.media + +# Description + +Get access to multiple demand partners across VertaMedia AdExchange and maximize your yield with VertaMedia header bidding adapter. + +VertaMedia header bidding adapter connects with VertaMedia demand sources in order to fetch bids. +This adapter provides a solution for accessing Video demand + + +# Test Parameters +``` + var adUnits = [{ + code: 'div-test-div', + sizes: [[640, 480]], // ad size + bids: [{ + bidder: 'vertamedia', // adapter name + params: { + aid: 332842 + } + }] + }]; +``` diff --git a/test/spec/modules/vertamediaBidAdapter_spec.js b/test/spec/modules/vertamediaBidAdapter_spec.js index 11c29dafad0..4f04ea8a615 100644 --- a/test/spec/modules/vertamediaBidAdapter_spec.js +++ b/test/spec/modules/vertamediaBidAdapter_spec.js @@ -1,141 +1,112 @@ -import { expect } from 'chai'; -import Adapter from 'modules/vertamediaBidAdapter'; -import bidmanager from 'src/bidmanager'; - -const ENDPOINT = 'http://rtb.vertamedia.com/hb/?aid=22489&w=640&h=480&domain=localhost'; +import {expect} from 'chai'; +import {spec} from 'modules/vertamediaBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; +const ENDPOINT = '//rtb.vertamedia.com/hb/'; const REQUEST = { - 'bidderCode': 'vertamedia', - 'requestId': 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', + 'bidder': 'vertamedia', + 'params': { + 'aid': 12345 + }, 'bidderRequestId': '7101db09af0db2', - 'bids': [ - { - 'bidder': 'vertamedia', - 'params': { - aid: 22489, - placementId: '123456' - }, - 'placementCode': '/19968336/header-bid-tag1', - 'sizes': [640, 480], - 'bidId': '84ab500420319d', - 'bidderRequestId': '7101db09af0db2', - 'requestId': 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6' - } - ], - 'start': 1469479810130 + 'auctionId': '2e41f65424c87c', + 'adUnitCode': 'adunit-code', + 'bidId': '84ab500420319d', + 'sizes': [640, 480] }; -var RESPONSE = { - 'source': { - 'aid': 22489, - 'pubId': 18016, - 'sid': '0' - }, - 'bids': [ - { - 'cmpId': 9541, - 'cpm': 4.5, - 'url': 'http://rtb.vertamedia.com/vast?adid=BFDB9CC0038AD918', - 'cur': 'USD' - } + +const serverResponse = { + 'source': {'aid': 12345, 'pubId': 54321}, + 'bids': [{ + 'vastUrl': 'http://rtb.vertamedia.com/vast/?adid=44F2AEB9BFC881B3', + 'descriptionUrl': '44F2AEB9BFC881B3', + 'requestId': '2e41f65424c87c', + 'url': '44F2AEB9BFC881B3', + 'creative_id': 342516, + 'cmpId': 342516, + 'height': 480, + 'cur': 'USD', + 'width': 640, + 'cpm': 0.9 + } ] }; -describe('VertamediaAdater', () => { - let adapter; - - beforeEach(() => adapter = new Adapter()); - - describe('request function', () => { - let xhr; - let requests; - - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); - }); - - afterEach(() => xhr.restore()); +describe('vertamediaBidAdapter', () => { + const adapter = newBidder(spec); + 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; - }); - - 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; + describe('isBidRequestValid', () => { + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(REQUEST)).to.equal(true); }); - it('sends bid request to ENDPOINT via POST', () => { - adapter.callBids(REQUEST); - expect(requests[0].url).to.equal(ENDPOINT); - expect(requests[0].method).to.equal('GET'); + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, REQUEST); + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.equal(false); }); }); - describe('response handler', () => { - let server; + describe('buildRequests', () => { + let bidRequests = [REQUEST]; - beforeEach(() => { - server = sinon.fakeServer.create(); - sinon.stub(bidmanager, 'addBidResponse'); - }); + const request = spec.buildRequests(bidRequests, {}); - afterEach(() => { - server.restore(); - bidmanager.addBidResponse.restore(); + it('sends bid request to ENDPOINT via GET', () => { + expect(request[0].method).to.equal('GET'); + }); + it('sends bid request to correct ENDPOINT', () => { + expect(request[0].url).to.equal(ENDPOINT); }); - it('registers bids', () => { - server.respondWith(JSON.stringify(RESPONSE)); + it('sends correct bid parameters', () => { + const bid = Object.assign({}, request[0].data); + delete bid.domain; - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + const eq = { + callbackId: '84ab500420319d', + aid: 12345, + w: 640, + h: 480 + }; - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property('statusMessage', 'Bid available'); - expect(response).to.have.property('cpm', 4.5); + expect(bid).to.deep.equal(eq); }); + }); - it('handles nobid responses', () => { - server.respondWith(JSON.stringify({ - aid: 356465468, - w: 640, - h: 480, - domain: 'localhost' - })); - - 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' - ); + describe('interpretResponse', () => { + let bidderRequest = {bidderCode: 'bidderCode'}; + it('should get correct bid response', () => { + const result = spec.interpretResponse({body: serverResponse}, {bidderRequest}); + const eq = [{ + vastUrl: 'http://rtb.vertamedia.com/vast/?adid=44F2AEB9BFC881B3', + descriptionUrl: '44F2AEB9BFC881B3', + requestId: '2e41f65424c87c', + bidderCode: 'bidderCode', + creativeId: 342516, + mediaType: 'video', + netRevenue: true, + currency: 'USD', + height: 480, + width: 640, + ttl: 3600, + cpm: 0.9 + }]; + + expect(result).to.deep.equal(eq); }); - it('handles JSON.parse errors', () => { - server.respondWith(''); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + it('handles nobid responses', () => { + const nobidServerResponse = {bids: []}; + const nobidResult = spec.interpretResponse({body: nobidServerResponse}, {bidderRequest}); - expect(bidmanager.addBidResponse.firstCall.args[1]).to.have.property( - 'statusMessage', - 'Bid returned empty or error response' - ); + expect(nobidResult.length).to.equal(0); }); }); }); From cf896506d71ab50b4d34e32f81a10f4b8caa26ac Mon Sep 17 00:00:00 2001 From: Jake Miller <jacobkmiller@icloud.com> Date: Wed, 8 Nov 2017 11:27:45 -0800 Subject: [PATCH 12/32] Underdog Media 1.0 Compliance (#1801) * updated underdogmedia adapter for prebid 1.0 * updated with recommended changes --- modules/underdogmediaBidAdapter.js | 145 ++++---- modules/underdogmediaBidAdapter.md | 27 ++ .../modules/underdogmediaBidAdapter_spec.js | 329 ++++++++++++------ 3 files changed, 326 insertions(+), 175 deletions(-) create mode 100644 modules/underdogmediaBidAdapter.md diff --git a/modules/underdogmediaBidAdapter.js b/modules/underdogmediaBidAdapter.js index 3c9e28e9658..0b2009d8133 100644 --- a/modules/underdogmediaBidAdapter.js +++ b/modules/underdogmediaBidAdapter.js @@ -1,112 +1,117 @@ +import * as utils from 'src/utils'; import { config } from 'src/config'; -var bidfactory = require('src/bidfactory.js'); -var bidmanager = require('src/bidmanager.js'); -var adloader = require('src/adloader.js'); -var utils = require('src/utils.js'); -var adaptermanager = require('src/adaptermanager'); - -function UnderdogMediaAdapter() { - const UDM_ADAPTER_VERSION = '1.0.0'; - var getJsStaticUrl = window.location.protocol + '//udmserve.net/udm/img.fetch?tid=1;dt=9;callback=$$PREBID_GLOBAL$$.handleUnderdogMediaCB;'; - var bidParams = {}; - - function _callBids(params) { - bidParams = params; +import { registerBidder } from 'src/adapters/bidderFactory'; +const BIDDER_CODE = 'underdogmedia'; +const UDM_ADAPTER_VERSION = '1.0'; + +export const spec = { + code: BIDDER_CODE, + bidParams: [], + + isBidRequestValid: function (bid) { + return !!((bid.params && bid.params.siteId) && (bid.sizes && bid.sizes.length > 0)); + }, + + buildRequests: function (validBidRequests) { var sizes = []; var siteId = 0; - bidParams.bids.forEach(bidParam => { + validBidRequests.forEach(bidParam => { sizes = utils.flatten(sizes, utils.parseSizesInput(bidParam.sizes)); siteId = bidParam.params.siteId; }); - adloader.loadScript(getJsStaticUrl + 'sid=' + siteId + ';sizes=' + sizes.join(','), null, false); - } - function _callback(response) { - var mids = response.mids; - bidParams.bids.forEach(bidParam => { - var filled = false; - mids.forEach(mid => { + return { + method: 'GET', + url: `${window.location.protocol}//udmserve.net/udm/img.fetch`, + data: `tid=1;dt=10;sid=${siteId};sizes=${sizes.join(',')}`, + bidParams: validBidRequests + }; + }, + + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + bidRequest.bidParams.forEach(bidParam => { + serverResponse.body.mids.forEach(mid => { if (mid.useCount > 0) { return; } + if (!mid.useCount) { mid.useCount = 0; } + var size_not_found = true; utils.parseSizesInput(bidParam.sizes).forEach(size => { if (size === mid.width + 'x' + mid.height) { size_not_found = false; } }); + if (size_not_found) { return; } - var bid = bidfactory.createBid(1, bidParam); - bid.bidderCode = bidParam.bidder; - bid.width = mid.width; - bid.height = mid.height; + const bidResponse = { + requestId: bidParam.bidId, + bidderCode: spec.code, + cpm: parseFloat(mid.cpm), + width: mid.width, + height: mid.height, + ad: mid.ad_code_html, + creativeId: mid.mid, + currency: 'USD', + netRevenue: false, + ttl: config.getConfig('_bidderTimeout'), + }; - bid.cpm = parseFloat(mid.cpm); - if (bid.cpm <= 0) { + if (bidResponse.cpm <= 0) { return; } - mid.useCount++; - bid.ad = mid.ad_code_html; - bid.ad = _makeNotification(bid, mid, bidParam) + bid.ad; - if (!(bid.ad || bid.adUrl)) { + if (bidResponse.ad.length <= 0) { return; } - bidmanager.addBidResponse(bidParam.placementCode, bid); - filled = true; + + mid.useCount++; + + bidResponse.ad += makeNotification(bidResponse, mid, bidParam); + + bidResponses.push(bidResponse); }); - if (!filled) { - var nobid = bidfactory.createBid(2, bidParam); - nobid.bidderCode = bidParam.bidder; - bidmanager.addBidResponse(bidParam.placementCode, nobid); - } }); - } - $$PREBID_GLOBAL$$.handleUnderdogMediaCB = _callback; + return bidResponses; + }, +}; - function _makeNotification(bid, mid, bidParam) { - var url = mid.notification_url; +function makeNotification (bid, mid, bidParam) { + var url = mid.notification_url; - url += UDM_ADAPTER_VERSION; - url += ';cb=' + Math.random(); - url += ';qqq=' + (1 / bid.cpm); - url += ';hbt=' + config.getConfig('bidderTimeout'); - url += ';style=adapter'; - url += ';vis=' + encodeURIComponent(document.visibilityState); + url += UDM_ADAPTER_VERSION; + url += ';cb=' + Math.random(); + url += ';qqq=' + (1 / bid.cpm); + url += ';hbt=' + config.getConfig('_bidderTimeout'); + url += ';style=adapter'; + url += ';vis=' + encodeURIComponent(document.visibilityState); - url += ';traffic_info=' + encodeURIComponent(JSON.stringify(_getUrlVars())); - if (bidParam.params.subId) { - url += ';subid=' + encodeURIComponent(bidParam.params.subId); - } - return '<script async src="' + url + '"></script>'; + url += ';traffic_info=' + encodeURIComponent(JSON.stringify(getUrlVars())); + if (bidParam.params.subId) { + url += ';subid=' + encodeURIComponent(bidParam.params.subId); } + return '<script async src="' + url + '"></script>'; +} - function _getUrlVars() { - var vars = {}; - var hash; - var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); - for (var i = 0; i < hashes.length; i++) { - hash = hashes[i].split('='); - if (!hash[0].match(/^utm/)) { - continue; - } +function getUrlVars() { + var vars = {}; + var hash; + var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); + for (var i = 0; i < hashes.length; i++) { + hash = hashes[i].split('='); + if (hash[0].match(/^utm_/)) { vars[hash[0]] = hash[1].substr(0, 150); } - return vars; } - - return { - callBids: _callBids - }; + return vars; } -adaptermanager.registerBidAdapter(new UnderdogMediaAdapter(), 'underdogmedia'); - -module.exports = UnderdogMediaAdapter; +registerBidder(spec); diff --git a/modules/underdogmediaBidAdapter.md b/modules/underdogmediaBidAdapter.md new file mode 100644 index 00000000000..f652e2fcbbf --- /dev/null +++ b/modules/underdogmediaBidAdapter.md @@ -0,0 +1,27 @@ +# Overview + +**Module Name**: Underdog Media Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: jake@underdogmedia.com + +# Description + +Module that connects to Underdog Media's servers to fetch bids. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], // a display size + bids: [ + { + bidder: "underdogmedia", + params: { + siteId: '12143' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/test/spec/modules/underdogmediaBidAdapter_spec.js b/test/spec/modules/underdogmediaBidAdapter_spec.js index 249111be6ea..1e7a80aaff8 100644 --- a/test/spec/modules/underdogmediaBidAdapter_spec.js +++ b/test/spec/modules/underdogmediaBidAdapter_spec.js @@ -1,122 +1,241 @@ -import Adapter from '../../../modules/underdogmediaBidAdapter'; -import bidManager from '../../../src/bidmanager'; -import adloader from '../../../src/adloader'; - -import { - expect -} from 'chai'; - -describe('underdogmedia adapter test', () => { - let adapter; - let server; - - // The third bid here is an invalid site id and should return a 'no-bid'. - - var bidderRequest = { - bidderCode: 'underdogmedia', - bids: [{ - bidder: 'underdogmedia', - adUnitCode: 'foo', - sizes: [ - [728, 90] - ], - params: { - siteId: '10272' - } - }, - { - bidder: 'underdogmedia', - adUnitCode: 'bar', - sizes: [ - [300, 250] - ], - params: { - siteId: '10272', - subId: 'TEST_SUBID' - } - }, - { - bidder: 'underdogmedia', - adUnitCode: 'nothing', - sizes: [160, 600], - params: { - siteId: '31337' - } - } - ] - }; - var response = { - 'mids': [{ - 'width': 728, - 'notification_url': '//udmserve.net/notification_url', - 'height': 90, - 'cpm': 2.5, - 'ad_code_html': 'Ad HTML for site ID 10272 size 728x90' - }, - { - 'width': 300, - 'notification_url': '//udmserve.net/notification_url', - 'height': 250, - 'cpm': 2.0, - 'ad_code_html': 'Ad HTML for site ID 10272 size 300x250' - } - ] - }; +import { expect } from 'chai'; +import { spec } from 'modules/underdogmediaBidAdapter'; + +describe('UnderdogMedia adapter', () => { + let bidRequests; beforeEach(() => { - adapter = new Adapter(); + bidRequests = [ + { + bidder: 'underdogmedia', + params: { + siteId: 12143 + }, + adUnitCode: '/19968336/header-bid-tag-1', + sizes: [[300, 250], [300, 600], [728, 90], [160, 600], [320, 50]], + bidId: '23acc48ad47af5', + requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' + } + ]; }); - afterEach(() => {}); + describe('implementation', () => { + describe('for requests', () => { + it('should accept valid bid', () => { + let validBid = { + bidder: 'underdogmedia', + params: { + siteId: '12143' + }, + sizes: [[300, 250], [300, 600]] + }; + const isValid = spec.isBidRequestValid(validBid); - describe('adding bids to the manager', () => { - let firstBid; - let secondBid; - let thirdBid; + expect(isValid).to.equal(true); + }); - beforeEach(() => { - sinon.stub(bidManager, 'addBidResponse'); - sinon.stub(adloader, 'loadScript'); + it('should reject invalid bid missing sizes', () => { + let invalidBid = { + bidder: 'underdogmedia', + params: { + siteId: '12143', + } + }; + const isValid = spec.isBidRequestValid(invalidBid); - adapter.callBids(bidderRequest); - $$PREBID_GLOBAL$$.handleUnderdogMediaCB(JSON.parse(JSON.stringify(response))); - firstBid = bidManager.addBidResponse.firstCall.args[1]; - secondBid = bidManager.addBidResponse.secondCall.args[1]; - thirdBid = bidManager.addBidResponse.thirdCall.args[1]; - }); + expect(isValid).to.equal(false); + }); - afterEach(() => { - bidManager.addBidResponse.restore(); - adloader.loadScript.restore(); - }); + it('should reject invalid bid missing siteId', () => { + let invalidBid = { + bidder: 'underdogmedia', + params: {}, + sizes: [[300, 250], [300, 600]] + }; + const isValid = spec.isBidRequestValid(invalidBid); - it('will add a bid object for each bid', () => { - sinon.assert.calledThrice(bidManager.addBidResponse); - }); + expect(isValid).to.equal(false); + }); - it('will add the ad html to the bid object', () => { - expect(firstBid).to.have.property('ad').includes('Ad HTML for site ID 10272 size 728x90'); - expect(secondBid).to.have.property('ad').includes('Ad HTML for site ID 10272 size 300x250').and.includes('TEST_SUBID'); - expect(thirdBid).to.not.have.property('ad'); - }); + it('request data should contain sid', () => { + let bidRequests = [ + { + bidId: '3c9408cdbf2f68', + sizes: [[300, 250]], + bidder: 'underdogmedia', + params: { + siteId: '12143' + }, + requestId: '10b327aa396609', + adUnitCode: '/123456/header-bid-tag-1' + } + ]; + const request = spec.buildRequests(bidRequests); - it('will have the right size attached', () => { - expect(firstBid).to.have.property('width', 728); - expect(firstBid).to.have.property('height', 90); - expect(secondBid).to.have.property('width', 300); - expect(secondBid).to.have.property('height', 250); - }); + expect(request.data).to.have.string('sid=12143'); + }); + + it('request data should contain sizes', () => { + let bidRequests = [ + { + bidId: '3c9408cdbf2f68', + sizes: [[300, 250], [728, 90]], + bidder: 'underdogmedia', + params: { + siteId: '12143' + }, + requestId: '10b327aa396609', + adUnitCode: '/123456/header-bid-tag-1' + } + ]; + const request = spec.buildRequests(bidRequests); - it('will add the CPM to the bid object', () => { - expect(firstBid).to.have.property('cpm', 2.5); - expect(secondBid).to.have.property('cpm', 2.0); - expect(thirdBid).to.not.have.property('cpm'); + expect(request.data).to.have.string('sizes=300x250,728x90'); + }); }); - it('will add the bidder code to the bid object', () => { - expect(firstBid).to.have.property('bidderCode', 'underdogmedia'); - expect(secondBid).to.have.property('bidderCode', 'underdogmedia'); - expect(thirdBid).to.have.property('bidderCode', 'underdogmedia'); + describe('bid responses', () => { + it('should return complete bid response', () => { + let serverResponse = { + body: { + mids: [ + { + ad_code_html: 'ad_code_html', + cpm: 2.5, + height: '600', + mid: '32634', + notification_url: 'notification_url', + tid: '4', + width: '160' + }, + { + ad_code_html: 'ad_code_html', + cpm: 2.5, + height: '250', + mid: '32633', + notification_url: 'notification_url', + tid: '2', + width: '300' + }, + ] + } + }; + const request = spec.buildRequests(bidRequests); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(2); + + expect(bids[0].bidderCode).to.equal('underdogmedia'); + expect(bids[0].cpm).to.equal(2.5); + expect(bids[0].width).to.equal('160'); + expect(bids[0].height).to.equal('600'); + expect(bids[0].ad).to.have.length.above(1); + expect(bids[0].creativeId).to.equal('32634'); + expect(bids[0].currency).to.equal('USD'); + }); + + it('should return empty bid response if mids empty', () => { + let serverResponse = { + body: { + mids: [] + } + }; + const request = spec.buildRequests(bidRequests); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response on incorrect size', () => { + let serverResponse = { + body: { + mids: [ + { + ad_code_html: 'ad_code_html', + cpm: 2.5, + height: '123', + mid: '32634', + notification_url: 'notification_url', + tid: '4', + width: '160' + } + ] + } + }; + const request = spec.buildRequests(bidRequests); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response on 0 cpm', () => { + let serverResponse = { + body: { + mids: [ + { + ad_code_html: 'ad_code_html', + cpm: 0, + height: '600', + mid: '32634', + notification_url: 'notification_url', + tid: '4', + width: '160' + } + ] + } + }; + const request = spec.buildRequests(bidRequests); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response if no ad in response', () => { + let serverResponse = { + body: { + mids: [ + { + ad_code_html: '', + cpm: 2.5, + height: '600', + mid: '32634', + notification_url: 'notification_url', + tid: '4', + width: '160' + } + ] + } + }; + const request = spec.buildRequests(bidRequests); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(0); + }); + + it('ad html string should contain the notification urls', () => { + let serverResponse = { + body: { + mids: [ + { + ad_code_html: 'ad_cod_html', + cpm: 2.5, + height: '600', + mid: '32634', + notification_url: 'notification_url', + tid: '4', + width: '160' + } + ] + } + }; + const request = spec.buildRequests(bidRequests); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids[0].ad).to.have.string('notification_url'); + expect(bids[0].ad).to.have.string(';style=adapter'); + }); }); }); }); From 4e2c2a97c079b888f4968a1dddf4036e5ed90554 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal <jaiminpanchal27@gmail.com> Date: Wed, 8 Nov 2017 15:30:33 -0500 Subject: [PATCH 13/32] Unit test fix (#1812) --- test/spec/modules/vertamediaBidAdapter_spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/spec/modules/vertamediaBidAdapter_spec.js b/test/spec/modules/vertamediaBidAdapter_spec.js index 4f04ea8a615..d505472dea1 100644 --- a/test/spec/modules/vertamediaBidAdapter_spec.js +++ b/test/spec/modules/vertamediaBidAdapter_spec.js @@ -88,7 +88,6 @@ describe('vertamediaBidAdapter', () => { vastUrl: 'http://rtb.vertamedia.com/vast/?adid=44F2AEB9BFC881B3', descriptionUrl: '44F2AEB9BFC881B3', requestId: '2e41f65424c87c', - bidderCode: 'bidderCode', creativeId: 342516, mediaType: 'video', netRevenue: true, From 1895cb3ce6045199f5150f6b9820cdf4bc1c2a0d Mon Sep 17 00:00:00 2001 From: Aparna Rao-Hegde <pr.aparna@gmail.com> Date: Thu, 9 Nov 2017 10:19:43 -0500 Subject: [PATCH 14/32] Adding 33Across adapter (#1805) * Adding 33across adapter * Updated per code review from Prebid. See https://github.com/prebid/Prebid.js/pull/1805#pullrequestreview-75218582 --- modules/33acrossBidAdapter.js | 140 ++++++ modules/33acrossBidAdapter.md | 95 ++++ test/spec/modules/33acrossBidAdapter_spec.js | 443 +++++++++++++++++++ 3 files changed, 678 insertions(+) create mode 100644 modules/33acrossBidAdapter.js create mode 100644 modules/33acrossBidAdapter.md create mode 100644 test/spec/modules/33acrossBidAdapter_spec.js diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js new file mode 100644 index 00000000000..b496a66d081 --- /dev/null +++ b/modules/33acrossBidAdapter.js @@ -0,0 +1,140 @@ +const { registerBidder } = require('../src/adapters/bidderFactory'); +const utils = require('../src/utils'); + +const BIDDER_CODE = '33across'; +const END_POINT = 'https://ssc.33across.com/api/v1/hb'; +const SYNC_ENDPOINT = 'https://de.tynt.com/deb/v2?m=xch'; + +// All this assumes that only one bid is ever returned by ttx +function _createBidResponse(response) { + return { + requestId: response.id, + bidderCode: BIDDER_CODE, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ad: response.seatbid[0].bid[0].adm, + ttl: response.seatbid[0].bid[0].ttl || 60, + creativeId: response.seatbid[0].bid[0].ext.rp.advid, + currency: response.cur, + netRevenue: true + } +} + +// infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request +function _createServerRequest(bidRequest) { + const ttxRequest = {}; + const params = bidRequest.params; + + ttxRequest.imp = []; + ttxRequest.imp[0] = { + banner: { + format: bidRequest.sizes.map(_getFormatSize) + }, + ext: { + ttx: { + prod: params.productId + } + } + } + + // Allowing site to be a test configuration object or just the id (former required for testing, + // latter when used by publishers) + ttxRequest.site = params.site || { id: params.siteId }; + + // Go ahead send the bidId in request to 33exchange so it's kept track of in the bid response and + // therefore in ad targetting process + ttxRequest.id = bidRequest.bidId; + + const options = { + contentType: 'application/json', + withCredentials: false + }; + + if (bidRequest.params.customHeaders) { + options.customHeaders = bidRequest.params.customHeaders; + } + + return { + 'method': 'POST', + 'url': bidRequest.params.url || END_POINT, + 'data': JSON.stringify(ttxRequest), + 'options': options + } +} + +// Sync object will always be of type iframe for ttx +function _createSync(bid) { + const syncUrl = bid.params.syncUrl || SYNC_ENDPOINT; + + return { + type: 'iframe', + url: `${syncUrl}&id=${bid.params.siteId || bid.params.site.id}` + } +} + +function _getFormatSize(sizeArr) { + return { + w: sizeArr[0], + h: sizeArr[1], + ext: {} + } +} + +function isBidRequestValid(bid) { + if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { + return false; + } + + if ((typeof bid.params.site === 'undefined' || typeof bid.params.site.id === 'undefined') && + (typeof bid.params.siteId === 'undefined')) { + return false; + } + + if (typeof bid.params.productId === 'undefined') { + return false; + } + + return true; +} + +// NOTE: At this point, 33exchange only accepts request for a single impression +function buildRequests(bidRequests) { + return bidRequests.map(_createServerRequest); +} + +// NOTE: At this point, the response from 33exchange will only ever contain one bid i.e. the highest bid +function interpretResponse(serverResponse) { + const bidResponses = []; + + // If there are bids, look at the first bid of the first seatbid (see NOTE above for assumption about ttx) + if (serverResponse.body.seatbid.length > 0 && serverResponse.body.seatbid[0].bid.length > 0) { + bidResponses.push(_createBidResponse(serverResponse.body)); + } + + return bidResponses; +} + +// Register one sync per bid since each ad unit may potenitally be linked to a uniqe guid +function getUserSyncs(syncOptions) { + let syncs = []; + const ttxBidRequests = utils.getBidderRequestAllAdUnits(BIDDER_CODE).bids; + + if (syncOptions.iframeEnabled) { + syncs = ttxBidRequests.map(_createSync); + } + + return syncs; +} + +const spec = { + code: BIDDER_CODE, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +} + +registerBidder(spec); + +module.exports = spec; diff --git a/modules/33acrossBidAdapter.md b/modules/33acrossBidAdapter.md new file mode 100644 index 00000000000..c4eb319c157 --- /dev/null +++ b/modules/33acrossBidAdapter.md @@ -0,0 +1,95 @@ +# Overview + +``` +Module Name: 33Across Bid Adapter +Module Type: Bidder Adapter +Maintainer: aparna.hegde@33across.com +``` + +# Description + +Connects to 33Across's exchange for bids. + +33Across bid adapter supports only Banner at present and follows MRA + +# Sample Ad Unit: For Publishers +``` +var adUnits = [ +{ + code: '33across-hb-ad-123456-1', + sizes: [ + [300, 250], + [728, 90] + ], + bids: [{ + bidder: '33across', + params: { + siteId: 'pub1234', + productId: 'infeed' + } + }] +} +``` + +# Ad Unit and Setup: For Testing +In order to receive bids please map localhost to (any) test domain. + +``` +<--! Prebid Config section > +<script> + var PREBID_TIMEOUT = 3000; + var adUnits = [ + { + code: '33across-hb-ad-123456-1', + sizes: [ + [300, 250], + [728, 90] + ], + bids: [{ + bidder: '33across', + params: { + site: { + id: 'aRlI5W_9yr5jkxacwqm_6r', + page: "http://thinkbabynames.com/baby-mcbabyface", + ext: { + ttx: { + ssp_configs: [ + { + "name": "index", + "enabled": false + }, + { + "name": "rubicon", + "enabled": true + }, + { + "name": "33xchange", + "enabled": false + } + ] + } + } + }, + productId: 'infeed' + } + }] + } + ]; + + var pbjs = pbjs || {}; + pbjs.que = pbjs.que || []; + + // Adjust bid CPM to ensure it wins the auction (USED ONLY FOR TESTING). Need to do this since test bids have too low a CPM + pbjs.bidderSettings = { + '33across': { + bidCpmAdjustment : function(bidCpm, bid){ + // adjust the bid in real time before the auction takes place, only do so for valid bids ignore no bids + if (bid.w !== 0 && bid.h !== 0 && bidCpm !== 0) { + return bidCpm + 0.50; + } + } + } + }; + </script> + <!-- End Prebid Config section> + ``` \ No newline at end of file diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js new file mode 100644 index 00000000000..edb9958ae66 --- /dev/null +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -0,0 +1,443 @@ +const { expect } = require('chai'); +const utils = require('../../../src/utils'); +const { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } = require('../../../modules/33acrossBidAdapter'); + +describe('33acrossBidAdapter:', function () { + const BIDDER_CODE = '33across'; + const SITE_ID = 'pub1234'; + const PRODUCT_ID = 'product1'; + const END_POINT = 'https://ssc.33across.com/api/v1/hb'; + + beforeEach(function() { + this.bidRequests = [ + { + bidId: 'b1', + bidder: '33across', + bidderRequestId: 'b1a', + params: { + siteId: SITE_ID, + productId: PRODUCT_ID + }, + adUnitCode: 'div-id', + requestId: 'r1', + sizes: [ + [300, 250], + [728, 90] + ], + transactionId: 't1' + } + ] + this.sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + this.sandbox.restore(); + }); + + describe('isBidRequestValid:', function () { + context('valid bid request:', function () { + it('returns true when bidder, params.siteId, params.productId are set', function() { + const validBid = { + bidder: BIDDER_CODE, + params: { + siteId: SITE_ID, + productId: PRODUCT_ID + } + } + + expect(isBidRequestValid(validBid)).to.be.true; + }) + }); + + context('valid test bid request:', function () { + it('returns true when bidder, params.site.id, params.productId are set', function() { + const validBid = { + bidder: BIDDER_CODE, + params: { + site: { + id: SITE_ID + }, + productId: PRODUCT_ID + } + } + + expect(isBidRequestValid(validBid)).to.be.true; + }); + }); + + context('invalid bid request:', function () { + it('returns false when bidder not set to "33across"', function () { + const invalidBid = { + bidder: 'foo', + params: { + siteId: SITE_ID, + productId: PRODUCT_ID + } + } + + expect(isBidRequestValid(invalidBid)).to.be.false; + }); + + it('returns false when params not set', function() { + const invalidBid = { + bidder: 'foo' + } + + expect(isBidRequestValid(invalidBid)).to.be.false; + }); + + it('returns false when params.siteId or params.site.id not set', function() { + const invalidBid = { + bidder: 'foo', + params: { + productId: PRODUCT_ID + } + } + + expect(isBidRequestValid(invalidBid)).to.be.false; + }); + + it('returns false when params.productId not set', function() { + const invalidBid = { + bidder: 'foo', + params: { + siteId: SITE_ID + } + } + + expect(isBidRequestValid(invalidBid)).to.be.false; + }); + }); + }); + + describe('buildRequests:', function() { + it('returns corresponding server requests for each valid bidRequest', function() { + const ttxRequest = { + imp: [{ + banner: { + format: [ + { + w: 300, + h: 250, + ext: {} + }, + { + w: 728, + h: 90, + ext: {} + } + ] + }, + ext: { + ttx: { + prod: PRODUCT_ID + } + } + }], + site: { + id: SITE_ID + }, + id: 'b1' + }; + const serverRequest = { + method: 'POST', + url: END_POINT, + data: JSON.stringify(ttxRequest), + options: { + contentType: 'application/json', + withCredentials: false + } + } + const builtServerRequests = buildRequests(this.bidRequests); + expect(builtServerRequests).to.deep.equal([serverRequest]); + expect(builtServerRequests.length).to.equal(1); + }); + + it('returns corresponding server requests for each valid test bidRequest', function() { + delete this.bidRequests[0].params.siteId; + this.bidRequests[0].params.site = { + id: SITE_ID, + page: 'http://test-url.com' + } + this.bidRequests[0].params.customHeaders = { + foo: 'bar' + }; + this.bidRequests[0].params.url = '//staging-ssc.33across.com/api/v1/hb'; + + const ttxRequest = { + imp: [{ + banner: { + format: [ + { + w: 300, + h: 250, + ext: {} + }, + { + w: 728, + h: 90, + ext: {} + } + ] + }, + ext: { + ttx: { + prod: PRODUCT_ID + } + } + }], + site: { + id: SITE_ID, + page: 'http://test-url.com' + }, + id: 'b1' + }; + const serverRequest = { + method: 'POST', + url: '//staging-ssc.33across.com/api/v1/hb', + data: JSON.stringify(ttxRequest), + options: { + contentType: 'application/json', + withCredentials: false, + customHeaders: { + foo: 'bar' + } + } + }; + + const builtServerRequests = buildRequests(this.bidRequests); + expect(builtServerRequests).to.deep.equal([serverRequest]); + expect(builtServerRequests.length).to.equal(1); + }); + + afterEach(function() { + delete this.bidRequests; + }) + }); + + describe('interpretResponse', function() { + context('when exactly one bid is returned', function() { + it('interprets and returns the single bid response', function() { + const serverResponse = { + cur: 'USD', + ext: {}, + id: 'b1', + seatbid: [ + { + bid: [{ + id: '1', + adm: '<html><h3>I am an ad</h3></html>', + ext: { + rp: { + advid: 1 + } + }, + h: 250, + w: 300, + price: 0.0938 + }] + } + ] + }; + + const bidResponse = { + requestId: 'b1', + bidderCode: BIDDER_CODE, + cpm: 0.0938, + width: 300, + height: 250, + ad: '<html><h3>I am an ad</h3></html>', + ttl: 60, + creativeId: 1, + currency: 'USD', + netRevenue: true + } + + expect(interpretResponse({body: serverResponse})).to.deep.equal([bidResponse]); + }); + }); + + context('when no bids are returned', function() { + it('interprets and returns empty array', function() { + const serverResponse = { + cur: 'USD', + ext: {}, + id: 'b1', + seatbid: [] + }; + + expect(interpretResponse({body: serverResponse})).to.deep.equal([]); + }); + }); + + context('when more than one bids are returned', function() { + it('interprets and returns the the first bid of the first seatbid', function() { + const serverResponse = { + cur: 'USD', + ext: {}, + id: 'b1', + seatbid: [ + { + bid: [{ + id: '1', + adm: '<html><h3>I am an ad</h3></html>', + ext: { + rp: { + advid: 1 + } + }, + h: 250, + w: 300, + price: 0.0940 + }, + { + id: '2', + adm: '<html><h3>I am an ad</h3></html>', + ext: { + rp: { + advid: 2 + } + }, + h: 250, + w: 300, + price: 0.0938 + } + ] + }, + { + bid: [{ + id: '3', + adm: '<html><h3>I am an ad</h3></html>', + ext: { + rp: { + advid: 3 + } + }, + h: 250, + w: 300, + price: 0.0938 + }] + } + ] + }; + + const bidResponse = { + requestId: 'b1', + bidderCode: BIDDER_CODE, + cpm: 0.0940, + width: 300, + height: 250, + ad: '<html><h3>I am an ad</h3></html>', + ttl: 60, + creativeId: 1, + currency: 'USD', + netRevenue: true + } + + expect(interpretResponse({body: serverResponse})).to.deep.equal([bidResponse]); + }); + }); + }); + + describe('getUserSyncs', function() { + beforeEach(function() { + this.ttxBids = [ + { + params: { + siteId: 'id1', + productId: 'p1' + } + }, + { + params: { + siteId: 'id2', + productId: 'p1' + } + } + ]; + + this.testTTXBids = [ + { + params: { + site: { id: 'id1' }, + productId: 'p1', + syncUrl: 'https://staging-de.tynt.com/deb/v2?m=xch' + } + }, + { + params: { + site: { id: 'id2' }, + productId: 'p1', + syncUrl: 'https://staging-de.tynt.com/deb/v2?m=xch' + } + } + ]; + + this.syncs = [ + { + type: 'iframe', + url: 'https://de.tynt.com/deb/v2?m=xch&id=id1' + }, + { + type: 'iframe', + url: 'https://de.tynt.com/deb/v2?m=xch&id=id2' + }, + ]; + + this.testSyncs = [ + { + type: 'iframe', + url: 'https://staging-de.tynt.com/deb/v2?m=xch&id=id1' + }, + { + type: 'iframe', + url: 'https://staging-de.tynt.com/deb/v2?m=xch&id=id2' + }, + ]; + }); + + context('when iframe is not enabled', function() { + it('returns empty sync array', function() { + this.sandbox.stub(utils, 'getBidderRequestAllAdUnits', () => ( + { + bids: this.ttxBids + } + )); + const syncOptions = {}; + expect(getUserSyncs(syncOptions)).to.deep.equal([]); + }); + }); + + context('when iframe is enabled', function() { + it('returns sync array equal to number of bids for ttx', function() { + this.sandbox.stub(utils, 'getBidderRequestAllAdUnits', () => ( + { + bids: this.ttxBids + } + )); + + const syncOptions = { + iframeEnabled: true + }; + const syncs = getUserSyncs(syncOptions); + expect(syncs.length).to.equal(this.ttxBids.length); + expect(syncs).to.deep.equal(this.syncs); + }); + + it('returns sync array equal to number of test bids for ttx', function() { + this.sandbox.stub(utils, 'getBidderRequestAllAdUnits', () => ( + { + bids: this.testTTXBids + } + )); + + const syncOptions = { + iframeEnabled: true + }; + const syncs = getUserSyncs(syncOptions); + expect(syncs.length).to.equal(this.testTTXBids.length); + expect(syncs).to.deep.equal(this.testSyncs); + }); + }); + }); +}); From e62ff34a45470a9b32c5f9fc5c830292528129ef Mon Sep 17 00:00:00 2001 From: Eyal Fishler <eyalfishler@gmail.com> Date: Thu, 9 Nov 2017 08:33:28 -0800 Subject: [PATCH 15/32] jsonpFunction name should match the namespace (#1785) if globalVarName defined in package.json is modified from pbjs to another namespace then the jsonpFunction should match otherwise you will get overlap in function names with more than 1 prebid.js in the page. --- webpack.conf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.conf.js b/webpack.conf.js index 68db3a389f2..38f4e5dadd7 100644 --- a/webpack.conf.js +++ b/webpack.conf.js @@ -19,7 +19,7 @@ module.exports = { ], }, output: { - jsonpFunction: 'pbjsChunk' + jsonpFunction: prebid.globalVarName+"Chunk" }, module: { rules: [ From cb14d1c5d23251f52eceb2b3b915a8785dabc6dd Mon Sep 17 00:00:00 2001 From: deodhar-hrishi <deodhar.hrishi@gmail.com> Date: Thu, 9 Nov 2017 21:14:16 +0000 Subject: [PATCH 16/32] Added sizes to Rubicon Adapter (#1818) --- modules/rubiconBidAdapter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 2b7b0061430..e3973b96d52 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -18,6 +18,7 @@ const TIMEOUT_BUFFER = 500; var sizeMap = { 1: '468x60', 2: '728x90', + 5: '120x90', 8: '120x600', 9: '160x600', 10: '300x600', @@ -56,6 +57,7 @@ var sizeMap = { 101: '480x320', 102: '768x1024', 103: '480x280', + 108: '320x240', 113: '1000x300', 117: '320x100', 125: '800x250', From ccbdf4a4b9763fd785fb04f4d83ac0a125bfdaf6 Mon Sep 17 00:00:00 2001 From: Harshad Mane <harshad.mane@pubmatic.com> Date: Fri, 10 Nov 2017 08:17:18 -0800 Subject: [PATCH 17/32] PubMatic adapter (#1707) * Updated PubMatic adapter also added a new method createContentToExecuteExtScriptInFriendlyFrame in util * added a missing syntax * comment changed * Reviewe suggestions implemented --- modules/pubmaticBidAdapter.js | 235 ++++++++++------ src/utils.js | 13 + test/spec/modules/pubmaticBidAdapter_spec.js | 276 +++++++++++++++++++ test/spec/utils_spec.js | 14 + 4 files changed, 460 insertions(+), 78 deletions(-) create mode 100644 test/spec/modules/pubmaticBidAdapter_spec.js diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 1517a53eee1..8176052f9e2 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -2,6 +2,7 @@ var utils = require('src/utils.js'); var bidfactory = require('src/bidfactory.js'); var bidmanager = require('src/bidmanager.js'); var adaptermanager = require('src/adaptermanager'); +const constants = require('src/constants.json'); /** * Adapter for requesting bids from Pubmatic. @@ -9,130 +10,208 @@ var adaptermanager = require('src/adaptermanager'); * @returns {{callBids: _callBids}} * @constructor */ -function PubmaticAdapter() { - var bids; - var _pm_pub_id; - var _pm_pub_age; - var _pm_pub_gender; - var _pm_pub_kvs; - var _pm_optimize_adslots = []; +const PubmaticAdapter = function PubmaticAdapter() { + let bids; + let usersync = false; + let _secure = 0; + let _protocol = (window.location.protocol === 'https:' ? (_secure = 1, 'https') : 'http') + '://'; let iframe; - function _callBids(params) { - bids = params.bids; - _pm_optimize_adslots = []; - for (var i = 0; i < bids.length; i++) { - var bid = bids[i]; - // bidmanager.pbCallbackMap['' + bid.params.adSlot] = bid; - _pm_pub_id = _pm_pub_id || bid.params.publisherId; - _pm_pub_age = _pm_pub_age || (bid.params.age || ''); - _pm_pub_gender = _pm_pub_gender || (bid.params.gender || ''); - _pm_pub_kvs = _pm_pub_kvs || (bid.params.kvs || ''); - _pm_optimize_adslots.push(bid.params.adSlot); + let dealChannelValues = { + 1: 'PMP', + 5: 'PREF', + 6: 'PMPG' + }; + + let customPars = { + 'kadgender': 'gender', + 'age': 'kadage', + 'dctr': 'dctr', // Custom Targeting + 'wiid': 'wiid', // Wrapper Impression ID + 'profId': 'profId', // Legacy: Profile ID + 'verId': 'verId', // Legacy: version ID + 'pmzoneid': { // Zone ID + n: 'pmZoneId', + m: function(zoneId) { + if (utils.isStr(zoneId)) { + return zoneId.split(',').slice(0, 50).join(); + } else { + return undefined; + } + } + } + }; + + function _initConf() { + var conf = {}; + var currTime = new Date(); + + conf.SAVersion = '1100'; + conf.wp = 'PreBid'; + conf.js = 1; + conf.wv = constants.REPO_AND_VERSION; + _secure && (conf.sec = 1); + conf.screenResolution = screen.width + 'x' + screen.height; + conf.ranreq = Math.random(); + conf.inIframe = window != top ? '1' : '0'; + + // istanbul ignore else + if (window.navigator.cookieEnabled === false) { + conf.fpcd = '1'; } - // Load pubmatic script in an iframe, because they call document.write - _getBids(); + try { + conf.pageURL = window.top.location.href; + conf.refurl = window.top.document.referrer; + } catch (e) { + conf.pageURL = window.location.href; + conf.refurl = window.document.referrer; + } + + conf.kltstamp = currTime.getFullYear() + + '-' + (currTime.getMonth() + 1) + + '-' + currTime.getDate() + + ' ' + currTime.getHours() + + ':' + currTime.getMinutes() + + ':' + currTime.getSeconds(); + conf.timezone = currTime.getTimezoneOffset() / 60 * -1; + + return conf; } - function _getBids() { - // create the iframe - iframe = utils.createInvisibleIframe(); + function _handleCustomParams(params, conf) { + // istanbul ignore else + if (!conf.kadpageurl) { + conf.kadpageurl = conf.pageURL; + } - var elToAppend = document.getElementsByTagName('head')[0]; + var key, value, entry; + for (key in customPars) { + // istanbul ignore else + if (customPars.hasOwnProperty(key)) { + value = params[key]; + // istanbul ignore else + if (value) { + entry = customPars[key]; + + if (typeof entry === 'object') { + value = entry.m(value, conf); + key = entry.n; + } else { + key = customPars[key]; + } + + if (utils.isStr(value)) { + conf[key] = value; + } else { + utils.logWarn('PubMatic: Ignoring param key: ' + customPars[key] + ', expects string-value, found ' + typeof value); + } + } + } + } + return conf; + } - // insert the iframe into document - elToAppend.insertBefore(iframe, elToAppend.firstChild); + function _cleanSlot(slotName) { + // istanbul ignore else + if (utils.isStr(slotName)) { + return slotName.replace(/^\s+/g, '').replace(/\s+$/g, ''); + } + return ''; + } + function _legacyExecution(conf, slots) { + var url = _generateLegacyCall(conf, slots); + iframe = utils.createInvisibleIframe(); + var elToAppend = document.getElementsByTagName('head')[0]; + elToAppend.insertBefore(iframe, elToAppend.firstChild); var iframeDoc = utils.getIframeDocument(iframe); - iframeDoc.write(_createRequestContent()); + var content = utils.createContentToExecuteExtScriptInFriendlyFrame(url); + content = content.replace(`<!--POST_SCRIPT_TAG_MACRO-->`, `<script>window.parent.$$PREBID_GLOBAL$$.handlePubmaticCallback(window.bidDetailsMap, window.progKeyValueMap);</script>`); + iframeDoc.write(content); iframeDoc.close(); } - function _createRequestContent() { - var content = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"' + - ' "http://www.w3.org/TR/html4/loose.dtd"><html><head><base target="_top" /><scr' + - 'ipt>inDapIF=true;</scr' + 'ipt> <style> .commit-tease, .user-profile-mini-avatar, .avatar, .vcard-details, .signup-prompt-bg { display: none !IMPORTANT; } </style> <script> document.addEventListener('DOMContentLoaded', function() { this.querySelectorAll('a').forEach(anchor => { anchor.addEventListener('click', e => { e.preventDefault(); const redact = new URLSearchParams(window.location.search).get('redact'); const hasExistingParams = anchor.href.includes('?'); window.location.href = anchor.href + (hasExistingParams ? `&redact=${redact}` : `?redact=${redact}`); }); }); }); </script> </head>'; - content += '<body>'; - content += '<scr' + 'ipt>'; - content += '' + - 'window.pm_pub_id = "%%PM_PUB_ID%%";' + - 'window.pm_optimize_adslots = [%%PM_OPTIMIZE_ADSLOTS%%];' + - 'window.kaddctr = "%%PM_ADDCTR%%";' + - 'window.kadgender = "%%PM_GENDER%%";' + - 'window.kadage = "%%PM_AGE%%";' + - 'window.pm_async_callback_fn = "window.parent.$$PREBID_GLOBAL$$.handlePubmaticCallback";'; - - content += '</scr' + 'ipt>'; - - var map = {}; - map.PM_PUB_ID = _pm_pub_id; - map.PM_ADDCTR = _pm_pub_kvs; - map.PM_GENDER = _pm_pub_gender; - map.PM_AGE = _pm_pub_age; - map.PM_OPTIMIZE_ADSLOTS = _pm_optimize_adslots.map(function (adSlot) { - return "'" + adSlot + "'"; - }).join(','); - - content += '<scr' + 'ipt src="https://ads.pubmatic.com/AdServer/js/gshowad.js"></scr' + 'ipt>'; - content += '<scr' + 'ipt>'; - content += '</scr' + 'ipt>'; - content += '</body></html>'; - content = utils.replaceTokenInString(content, map, '%%'); - - return content; + function _generateLegacyCall(conf, slots) { + var request_url = 'gads.pubmatic.com/AdServer/AdCallAggregator'; + return _protocol + request_url + '?' + utils.parseQueryStringParameters(conf) + 'adslots=' + encodeURIComponent('[' + slots.join(',') + ']'); } - $$PREBID_GLOBAL$$.handlePubmaticCallback = function () { - let bidDetailsMap = {}; - let progKeyValueMap = {}; - try { - bidDetailsMap = iframe.contentWindow.bidDetailsMap; - progKeyValueMap = iframe.contentWindow.progKeyValueMap; - } catch (e) { - utils.logError(e, 'Error parsing Pubmatic response'); + function _initUserSync(pubId) { + // istanbul ignore else + if (!usersync) { + var iframe = utils.createInvisibleIframe(); + iframe.src = _protocol + 'ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p=' + pubId; + utils.insertElement(iframe, document); + usersync = true; + } + } + + function _callBids(params) { + var conf = _initConf(); + var slots = []; + + conf.pubId = 0; + bids = params.bids || []; + + for (var i = 0; i < bids.length; i++) { + var bid = bids[i]; + conf.pubId = conf.pubId || bid.params.publisherId; + conf = _handleCustomParams(bid.params, conf); + bid.params.adSlot = _cleanSlot(bid.params.adSlot); + bid.params.adSlot.length && slots.push(bid.params.adSlot); + } + + // istanbul ignore else + if (conf.pubId && slots.length > 0) { + _legacyExecution(conf, slots); } + _initUserSync(conf.pubId); + } + + $$PREBID_GLOBAL$$.handlePubmaticCallback = function(bidDetailsMap, progKeyValueMap) { var i; var adUnit; var adUnitInfo; var bid; - var bidResponseMap = bidDetailsMap || {}; - var bidInfoMap = progKeyValueMap || {}; - var dimensions; + var bidResponseMap = bidDetailsMap; + var bidInfoMap = progKeyValueMap; + + if (!bidResponseMap || !bidInfoMap) { + return; + } for (i = 0; i < bids.length; i++) { var adResponse; bid = bids[i].params; - adUnit = bidResponseMap[bid.adSlot] || {}; - // adUnitInfo example: bidstatus=0;bid=0.0000;bidid=39620189@320x50;wdeal= - // if using DFP GPT, the params string comes in the format: // "bidstatus;1;bid;5.0000;bidid;hb_test@468x60;wdeal;" // the code below detects and handles this. + // istanbul ignore else if (bidInfoMap[bid.adSlot] && bidInfoMap[bid.adSlot].indexOf('=') === -1) { bidInfoMap[bid.adSlot] = bidInfoMap[bid.adSlot].replace(/([a-z]+);(.[^;]*)/ig, '$1=$2'); } - adUnitInfo = (bidInfoMap[bid.adSlot] || '').split(';').reduce(function (result, pair) { + adUnitInfo = (bidInfoMap[bid.adSlot] || '').split(';').reduce(function(result, pair) { var parts = pair.split('='); result[parts[0]] = parts[1]; return result; }, {}); if (adUnitInfo.bidstatus === '1') { - dimensions = adUnitInfo.bidid.split('@')[1].split('x'); adResponse = bidfactory.createBid(1); adResponse.bidderCode = 'pubmatic'; adResponse.adSlot = bid.adSlot; adResponse.cpm = Number(adUnitInfo.bid); adResponse.ad = unescape(adUnit.creative_tag); adResponse.ad += utils.createTrackPixelIframeHtml(decodeURIComponent(adUnit.tracking_url)); - adResponse.width = dimensions[0]; - adResponse.height = dimensions[1]; + adResponse.width = adUnit.width; + adResponse.height = adUnit.height; adResponse.dealId = adUnitInfo.wdeal; + adResponse.dealChannel = dealChannelValues[adUnit.deal_channel] || null; bidmanager.addBidResponse(bids[i].placementCode, adResponse); } else { @@ -147,7 +226,7 @@ function PubmaticAdapter() { return { callBids: _callBids }; -} +}; adaptermanager.registerBidAdapter(new PubmaticAdapter(), 'pubmatic'); diff --git a/src/utils.js b/src/utils.js index f14a9bbd46c..6dc30a184d2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -738,6 +738,19 @@ export function deepAccess(obj, path) { return obj; } +/** + * Returns content for a friendly iframe to execute a URL in script tag + * @param {url} URL to be executed in a script tag in a friendly iframe + * <!--PRE_SCRIPT_TAG_MACRO--> and <!--POST_SCRIPT_TAG_MACRO--> are macros left to be replaced if required + */ +export function createContentToExecuteExtScriptInFriendlyFrame(url) { + if (!url) { + return ''; + } + + return `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head><base target="_top" /><script>inDapIF=true;</script> <style> .commit-tease, .user-profile-mini-avatar, .avatar, .vcard-details, .signup-prompt-bg { display: none !IMPORTANT; } </style> <script> document.addEventListener('DOMContentLoaded', function() { this.querySelectorAll('a').forEach(anchor => { anchor.addEventListener('click', e => { e.preventDefault(); const redact = new URLSearchParams(window.location.search).get('redact'); const hasExistingParams = anchor.href.includes('?'); window.location.href = anchor.href + (hasExistingParams ? `&redact=${redact}` : `?redact=${redact}`); }); }); }); </script> </head><body><!--PRE_SCRIPT_TAG_MACRO--><script src="${url}"></script><!--POST_SCRIPT_TAG_MACRO--></body></html>`; +} + /** * Build an object consisting of only defined parameters to avoid creating an * object with defined keys and undefined values. diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js new file mode 100644 index 00000000000..c7b8cd5cd8e --- /dev/null +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -0,0 +1,276 @@ +import { + expect +} from 'chai'; +import * as utils from 'src/utils'; +import PubMaticAdapter from 'modules/pubmaticBidAdapter'; +import bidmanager from 'src/bidmanager'; +import constants from 'src/constants.json'; + +let getDefaultBidRequest = () => { + return { + bidderCode: 'pubmatic', + requestId: 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', + bidderRequestId: '7101db09af0db2', + start: new Date().getTime(), + bids: [{ + bidder: 'pubmatic', + bidId: '84ab500420319d', + bidderRequestId: '7101db09af0db2', + requestId: 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', + placementCode: 'DIV_1', + params: { + placement: 1234567, + network: '9599.1' + } + }] + }; +}; + +describe('PubMaticAdapter', () => { + let adapter; + + function createBidderRequest({ + bids, + params + } = {}) { + var bidderRequest = getDefaultBidRequest(); + if (bids && Array.isArray(bids)) { + bidderRequest.bids = bids; + } + if (params) { + bidderRequest.bids.forEach(bid => bid.params = params); + } + return bidderRequest; + } + + beforeEach(() => adapter = new PubMaticAdapter()); + + describe('callBids()', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + + describe('user syncup', () => { + beforeEach(() => { + sinon.stub(utils, 'insertElement'); + }); + + afterEach(() => { + utils.insertElement.restore(); + }); + + it('usersync is initiated', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9999, + adSlot: 'abcd@728x90', + age: '20' + } + })); + utils.insertElement.calledOnce.should.be.true; + expect(utils.insertElement.getCall(0).args[0].src).to.equal('http://ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p=9999'); + }); + }); + + describe('bid request', () => { + beforeEach(() => { + sinon.stub(utils, 'createContentToExecuteExtScriptInFriendlyFrame', function() { + return ''; + }); + }); + + afterEach(() => { + utils.createContentToExecuteExtScriptInFriendlyFrame.restore(); + }); + + it('requires parameters to be made', () => { + adapter.callBids({}); + utils.createContentToExecuteExtScriptInFriendlyFrame.calledOnce.should.be.false; + }); + + it('for publisherId 9990 call is made to gads.pubmatic.com', () => { + var bidRequest = createBidderRequest({ + params: { + publisherId: 9990, + adSlot: ' abcd@728x90', + age: '20', + wiid: 'abcdefghijk', + profId: '1234', + verId: '12', + pmzoneid: 'abcd123, efg345', + dctr: 'key=1234,5678' + } + }); + adapter.callBids(bidRequest); + var callURL = utils.createContentToExecuteExtScriptInFriendlyFrame.getCall(0).args[0]; + expect(bidRequest.bids[0].params.adSlot).to.equal('abcd@728x90'); + expect(callURL).to.contain('gads.pubmatic.com/AdServer/AdCallAggregator?'); + expect(callURL).to.contain('SAVersion=1100'); + expect(callURL).to.contain('wp=PreBid'); + expect(callURL).to.contain('js=1'); + expect(callURL).to.contain('screenResolution='); + expect(callURL).to.contain('wv=' + constants.REPO_AND_VERSION); + expect(callURL).to.contain('ranreq='); + expect(callURL).to.contain('inIframe='); + expect(callURL).to.contain('pageURL='); + expect(callURL).to.contain('refurl='); + expect(callURL).to.contain('kltstamp='); + expect(callURL).to.contain('timezone='); + expect(callURL).to.contain('age=20'); + expect(callURL).to.contain('adslots=%5Babcd%40728x90%5D'); + expect(callURL).to.contain('kadpageurl='); + expect(callURL).to.contain('wiid=abcdefghijk'); + expect(callURL).to.contain('profId=1234'); + expect(callURL).to.contain('verId=12'); + expect(callURL).to.contain('pmZoneId=abcd123%2C%20efg345'); + expect(callURL).to.contain('dctr=key%3D1234%2C5678'); + }); + + it('for publisherId 9990 call is made to gads.pubmatic.com, age passed as int not being passed ahead', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9990, + adSlot: 'abcd@728x90', + age: 20, + wiid: 'abcdefghijk', + profId: '1234', + verId: '12', + pmzoneid: {}, + dctr: 1234 + } + })); + var callURL = utils.createContentToExecuteExtScriptInFriendlyFrame.getCall(0).args[0]; + expect(callURL).to.contain('gads.pubmatic.com/AdServer/AdCallAggregator?'); + expect(callURL).to.not.contain('age=20'); + expect(callURL).to.not.contain('dctr=1234'); + }); + + it('for publisherId 9990 call is made to gads.pubmatic.com, invalid data for pmzoneid', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9990, + adSlot: 'abcd@728x90', + age: '20', + wiid: 'abcdefghijk', + profId: '1234', + verId: '12', + pmzoneid: {}, + dctr: 1234 + } + })); + var callURL = utils.createContentToExecuteExtScriptInFriendlyFrame.getCall(0).args[0]; + expect(callURL).to.contain('gads.pubmatic.com/AdServer/AdCallAggregator?'); + expect(callURL).to.not.contain('pmZoneId='); + }); + }); + + describe('#handlePubmaticCallback: ', () => { + beforeEach(() => { + sinon.stub(utils, 'createContentToExecuteExtScriptInFriendlyFrame', function() { + return ''; + }); + sinon.stub(bidmanager, 'addBidResponse'); + }); + + afterEach(() => { + utils.createContentToExecuteExtScriptInFriendlyFrame.restore(); + bidmanager.addBidResponse.restore(); + }); + + it('exists and is a function', () => { + expect($$PREBID_GLOBAL$$.handlePubmaticCallback).to.exist.and.to.be.a('function'); + }); + + it('empty response, arguments not passed', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9999, + adSlot: 'abcd@728x90', + age: '20' + } + })); + $$PREBID_GLOBAL$$.handlePubmaticCallback(); + expect(bidmanager.addBidResponse.callCount).to.equal(0); + }); + + it('empty response', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9999, + adSlot: 'abcd@728x90', + age: '20' + } + })); + $$PREBID_GLOBAL$$.handlePubmaticCallback({}, {}); + sinon.assert.called(bidmanager.addBidResponse); + expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal('DIV_1'); + var theBid = bidmanager.addBidResponse.firstCall.args[1]; + expect(theBid.bidderCode).to.equal('pubmatic'); + expect(theBid.getStatusCode()).to.equal(2); + }); + + it('not empty response', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9999, + adSlot: 'abcd@728x90:0', + age: '20' + } + })); + $$PREBID_GLOBAL$$.handlePubmaticCallback({ + 'abcd@728x90:0': { + 'ecpm': 10, + 'creative_tag': 'hello', + 'tracking_url': 'http%3a%2f%2fhaso.pubmatic.com%2fads%2f9999%2fGRPBID%2f2.gif%3ftrackid%3d12345', + 'width': 728, + 'height': 90, + 'deal_channel': 5 + } + }, { + 'abcd@728x90:0': 'bidstatus;1;bid;10.0000;bidid;abcd@728x90:0;wdeal;PMERW36842' + }); + sinon.assert.called(bidmanager.addBidResponse); + expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal('DIV_1'); + var theBid = bidmanager.addBidResponse.firstCall.args[1]; + expect(theBid.bidderCode).to.equal('pubmatic'); + expect(theBid.adSlot).to.equal('abcd@728x90:0'); + expect(theBid.cpm).to.equal(10); + expect(theBid.width).to.equal(728); + expect(theBid.height).to.equal(90); + expect(theBid.dealId).to.equal('PMERW36842'); + expect(theBid.dealChannel).to.equal('PREF'); + }); + + it('not empty response, without dealChannel', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9999, + adSlot: 'abcd@728x90', + age: '20' + } + })); + $$PREBID_GLOBAL$$.handlePubmaticCallback({ + 'abcd@728x90': { + 'ecpm': 10, + 'creative_tag': 'hello', + 'tracking_url': 'http%3a%2f%2fhaso.pubmatic.com%2fads%2f9999%2fGRPBID%2f2.gif%3ftrackid%3d12345', + 'width': 728, + 'height': 90 + } + }, { + 'abcd@728x90': 'bidstatus;1;bid;10.0000;bidid;abcd@728x90:0;wdeal;PMERW36842' + }); + sinon.assert.called(bidmanager.addBidResponse); + expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal('DIV_1'); + var theBid = bidmanager.addBidResponse.firstCall.args[1]; + expect(theBid.bidderCode).to.equal('pubmatic'); + expect(theBid.adSlot).to.equal('abcd@728x90'); + expect(theBid.cpm).to.equal(10); + expect(theBid.width).to.equal(728); + expect(theBid.height).to.equal(90); + expect(theBid.dealId).to.equal('PMERW36842'); + expect(theBid.dealChannel).to.equal(null); + }); + }); + }); +}); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index ad2645b2351..a08abaee847 100755 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -658,6 +658,20 @@ describe('Utils', function () { }); }); + describe('createContentToExecuteExtScriptInFriendlyFrame', function () { + it('should return empty string if url is not passed', function () { + var output = utils.createContentToExecuteExtScriptInFriendlyFrame(); + assert.equal(output, ''); + }); + + it('should have URL in returned value if url is passed', function () { + var url = 'https://abcd.com/service?a=1&b=2&c=3'; + var output = utils.createContentToExecuteExtScriptInFriendlyFrame(url); + var expected = `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head><base target="_top" /><script>inDapIF=true;</script> <style> .commit-tease, .user-profile-mini-avatar, .avatar, .vcard-details, .signup-prompt-bg { display: none !IMPORTANT; } </style> <script> document.addEventListener('DOMContentLoaded', function() { this.querySelectorAll('a').forEach(anchor => { anchor.addEventListener('click', e => { e.preventDefault(); const redact = new URLSearchParams(window.location.search).get('redact'); const hasExistingParams = anchor.href.includes('?'); window.location.href = anchor.href + (hasExistingParams ? `&redact=${redact}` : `?redact=${redact}`); }); }); }); </script> </head><body><!--PRE_SCRIPT_TAG_MACRO--><script src="${url}"></script><!--POST_SCRIPT_TAG_MACRO--></body></html>`; + assert.equal(output, expected); + }); + }); + describe('getDefinedParams', () => { it('builds an object consisting of defined params', () => { const adUnit = { From cb2cd7740a65fa9809aebef567a8d40576ab1c58 Mon Sep 17 00:00:00 2001 From: hdjvieira <hdjvieira@gmail.com> Date: Fri, 10 Nov 2017 18:03:07 +0000 Subject: [PATCH 18/32] Update Pollux Adapter to v1.0 (#1694) * Added PolluxNetwork Bid Adapter Added module, test spec and integration example for Pollux Network Bid Adapter * Update Pollux domain Update Pollux default domain on prebid adapter * Export getParameterByName method On Utils.js make getParameterByName method public * Executed changes requested by @jaiminpanchal27 on 2017-08-01 Moved zone_728x90.html to integrationExamples/gpt/pollux_zone_728x90.html; Added bidRequest as second parameter to bidfactory.createBid() on Pollux Bid Adapter; Added more test cases to increase test coverage (at least 85%); Review Ref: - https://github.com/prebid/Prebid.js/pull/1431#pullrequestreview-53608436 * Fixed Eslint errors on commit f745fe1 * Executed changes requested on PR#1431 review #54993573 - Removed $$PREBID_GLOBAL$$ public vars in unit test; - Moved stubs creation and its restoration to beforeEach and afterEach hooks in unit test; - Exposed polluxHandler method on polluxBidAdapter. * Remove redundant export This line was added in #1409, removing this then I'll merge * Update Pollux Adapter to v1.0 * Changes requested on Pollux Adapter pull request #1694 review #74933409 * Changes requested on Pollux Adapter pull request #1694 review #75505070 Rmoved parameter bidderCode from bid responses * Fixed breaking changes to serverResponse in interpretResponse method Parameter serverResponse of method interpretResponse in bid adapter changed from array of bids to an object, where bids are now nested within its parameter body. Plus a refactor of var declaration and log messages. * Fix lint errors on push for commit cc653a --- integrationExamples/gpt/pbjs_example_gpt.html | 12 + ...x_zone_728x90.html => pollux_example.html} | 100 +++-- modules/polluxBidAdapter.js | 199 +++++----- modules/polluxBidAdapter.md | 33 ++ test/spec/modules/polluxBidAdapter_spec.js | 351 ++++++++++-------- 5 files changed, 413 insertions(+), 282 deletions(-) rename integrationExamples/gpt/{pollux_zone_728x90.html => pollux_example.html} (51%) create mode 100644 modules/polluxBidAdapter.md diff --git a/integrationExamples/gpt/pbjs_example_gpt.html b/integrationExamples/gpt/pbjs_example_gpt.html index 07e5bb8236f..1493634c1c7 100644 --- a/integrationExamples/gpt/pbjs_example_gpt.html +++ b/integrationExamples/gpt/pbjs_example_gpt.html @@ -269,6 +269,12 @@ placement_id: 0 } }, + { + bidder: 'pollux', + params: { + zone: '1806' // REQUIRED Zone Id (1806 is a test zone) + } + }, { bidder: 'adkernelAdn', params: { @@ -395,6 +401,12 @@ params: { placement_id: 0 } + }, + { + bidder: 'pollux', + params: { + zone: '276' // REQUIRED Zone Id (276 is a test zone) + } } ] } diff --git a/integrationExamples/gpt/pollux_zone_728x90.html b/integrationExamples/gpt/pollux_example.html similarity index 51% rename from integrationExamples/gpt/pollux_zone_728x90.html rename to integrationExamples/gpt/pollux_example.html index ecede9b5db2..56eedbf2a9c 100644 --- a/integrationExamples/gpt/pollux_zone_728x90.html +++ b/integrationExamples/gpt/pollux_example.html @@ -7,31 +7,40 @@ var PREBID_TIMEOUT = 3000; var adUnits = [{ - code: 'div-gpt-ad-1460505661639-0', - sizes: [[728, 90]], - bids: [ - { - bidder: 'pollux', - params: { - zone: '276' - } - }, - { - bidder: 'pollux', - params: { - zone: '1806' - } - } - ] - }]; - + code: 'div-gpt-ad-1460505661639-0', + sizes: [[728, 90], [300, 250]], + bids: [{ + bidder: 'pollux', + params: { + zone: '1806,276' + } + }, { + bidder: 'pollux', + params: { + zone: '276' + } + } + ] + }, + { + code: 'div-gpt-ad-1460505661631-0', + sizes: [[300, 250]], + bids: [{ + bidder: 'pollux', + params: { + zone: '1806,276,855' + } + } + ] + } + ]; var pbjs = pbjs || {}; pbjs.que = pbjs.que || []; </script> <!-- Prebid Config Section END --> <!-- Prebid Boilerplate Section START. No Need to Edit. --> - <script type="text/javascript" src="//cdn-assetsx.plxnt.com/assets/public/js/prebid.js" async></script> + <script type="text/javascript" src="//cdn-assetsx.plxnt.com/assets/public/js/prebid/v1/prebid.js" async></script> <script> var googletag = googletag || {}; googletag.cmd = googletag.cmd || []; @@ -39,27 +48,36 @@ googletag.pubads().disableInitialLoad(); }); + pbjs.que.push(function() { pbjs.addAdUnits(adUnits); pbjs.requestBids({ - bidsBackHandler: sendAdserverRequest - }); - }); + bidsBackHandler: sendAdserverRequest + }); + pbjs.setConfig({ + "currency": { + "adServerCurrency": "USD", + "granularityMultiplier": 1 + } + }); + }); - function sendAdserverRequest() { - if (pbjs.adserverRequestSent) return; - pbjs.adserverRequestSent = true; - googletag.cmd.push(function() { - pbjs.que.push(function() { - pbjs.setTargetingForGPTAsync(); - googletag.pubads().refresh(); - }); - }); - }; + function sendAdserverRequest() { + if (pbjs.adserverRequestSent) + return; + pbjs.adserverRequestSent = true; + googletag.cmd.push(function () { + pbjs.que.push(function () { + pbjs.setTargetingForGPTAsync(); + googletag.pubads().refresh(); + }); + }); + } + ; - setTimeout(function() { - sendAdserverRequest(); - }, PREBID_TIMEOUT); + setTimeout(function () { + sendAdserverRequest(); + }, PREBID_TIMEOUT); </script> <!-- Prebid Boilerplate Section END --> @@ -79,7 +97,8 @@ <script> googletag.cmd.push(function () { - googletag.defineSlot('/19968336/header-bid-tag1', [[728, 90]], 'div-gpt-ad-1460505661639-0').addService(googletag.pubads()); + googletag.defineSlot('/19968336/header-bid-tag1', [[728, 90], [300, 250]], 'div-gpt-ad-1460505661639-0').addService(googletag.pubads()); + googletag.defineSlot('/19968336/header-bid-tag1', [[300, 250]], 'div-gpt-ad-1460505661631-0').addService(googletag.pubads()); googletag.pubads().enableSingleRequest(); googletag.enableServices(); }); @@ -100,5 +119,14 @@ googletag.cmd.push(function() { googletag.display('div-gpt-ad-1460505661639-0'); }); </script> </div> + +<br> +<br> + +<div id='div-gpt-ad-1460505661631-0'> + <script type='text/javascript'> + googletag.cmd.push(function() { googletag.display('div-gpt-ad-1460505661631-0'); }); + </script> +</div> </body> </html> \ No newline at end of file diff --git a/modules/polluxBidAdapter.js b/modules/polluxBidAdapter.js index 54c2122ec36..463de07341c 100644 --- a/modules/polluxBidAdapter.js +++ b/modules/polluxBidAdapter.js @@ -1,97 +1,120 @@ -import bidfactory from 'src/bidfactory'; -import bidmanager from 'src/bidmanager'; import * as utils from 'src/utils'; -import adloader from 'src/adloader'; -import adaptermanager from 'src/adaptermanager'; -import { STATUS } from 'src/constants'; +import { registerBidder } from 'src/adapters/bidderFactory'; -// Prebid adapter for Pollux header bidding client -function PolluxBidAdapter() { - function _callBids(params) { - var bidderUrl = (window.location.protocol) + '//adn.plxnt.com/prebid'; - var bids = params.bids || []; - for (var i = 0; i < bids.length; i++) { - var request_obj = {}; - var bid = bids[i]; - // check params - if (bid.params.zone) { - var domain = utils.getParameterByName('domain'); - var tracker2 = utils.getParameterByName('tracker2'); - if (domain) { - request_obj.domain = domain; - } else { - request_obj.domain = window.location.host; - } - if (tracker2) { - request_obj.tracker2 = tracker2; - } - request_obj.zone = bid.params.zone; - } else { - utils.logError('required param "zone" is missing', 'polluxHandler'); - continue; - } - var parsedSizes = utils.parseSizesInput(bid.sizes); - var parsedSizesLength = parsedSizes.length; - if (parsedSizesLength > 0) { - // first value should be "size" - request_obj.size = parsedSizes[0]; - if (parsedSizesLength > 1) { - // any subsequent values should be "promo_sizes" - var promo_sizes = []; - for (var j = 1; j < parsedSizesLength; j++) { - promo_sizes.push(parsedSizes[j]); - } - request_obj.promo_sizes = promo_sizes.join(','); - } - } - // detect urls - request_obj.callback_id = bid.bidId; - // set a different url bidder - if (bid.bidderUrl) { - bidderUrl = bid.bidderUrl; +const BIDDER_CODE = 'pollux'; +const PLX_ENDPOINT_URL = '//adn.plxnt.com/prebid/v1'; +const PLX_CURRENCY = 'EUR'; +const PLX_TTL = 3600; +const PLX_NETREVENUE = true; + +export const spec = { + code: BIDDER_CODE, + aliases: ['plx'], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + if (!bid.hasOwnProperty('params') || !bid.params.hasOwnProperty('zone')) { + utils.logError('required param "zone" is missing for == ' + BIDDER_CODE + ' =='); + return false; + } + return true; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests) { + if (!Array.isArray(validBidRequests) || !validBidRequests.length) { + return []; + } + const payload = []; + let custom_url = null; + for (let i = 0; i < validBidRequests.length; i++) { + const bid = validBidRequests[i]; + const request = { + bidId: bid.bidId, + zones: bid.params.zone, + sizes: bid.sizes + }; + if (bid.bidderUrl && !custom_url) { + custom_url = bid.bidderUrl; } - var prebidUrl = bidderUrl + '?' + utils.parseQueryStringParameters(request_obj); - utils.logMessage('Pollux request built: ' + prebidUrl); - adloader.loadScript(prebidUrl, null, true); + payload.push(request); } - } - - // expose the callback to global object - function _polluxHandler (response) { - // pollux handler - var bidObject = {}; - var callback_id = response.callback_id; - var placementCode = ''; - var bidObj = utils.getBidRequest(callback_id); - if (bidObj) { - placementCode = bidObj.placementCode; + const payloadString = JSON.stringify(payload); + // build url parameters + const domain = utils.getParameterByName('domain'); + const tracker2 = utils.getParameterByName('tracker2'); + const url_params = {}; + if (domain) { + url_params.domain = domain; + } else { + url_params.domain = utils.getTopWindowUrl(); + } + if (tracker2) { + url_params.tracker2 = tracker2; + } + // build url + let bidder_url = custom_url || PLX_ENDPOINT_URL; + if (url_params) { + bidder_url = bidder_url + '?' + utils.parseQueryStringParameters(url_params); + } + utils.logMessage('== ' + BIDDER_CODE + ' == request built: ' + bidder_url); + return { + method: 'POST', + url: bidder_url, + data: payloadString + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + let bidResponses = []; + if (!serverResponse || (typeof serverResponse === 'object' && !serverResponse.hasOwnProperty('body'))) { + utils.logMessage('No prebid response from == ' + BIDDER_CODE + ' == for bid requests:'); + utils.logMessage(bidRequest); + return bidResponses; } - if (bidObj && response.cpm > 0 && !!response.ad) { - bidObject = bidfactory.createBid(STATUS.GOOD, bidObj); - bidObject.bidderCode = bidObj.bidder; - bidObject.mediaType = response.mediaType; - bidObject.cpm = parseFloat(response.cpm); - if (response.ad_type === 'url') { - bidObject.adUrl = response.ad; + serverResponse = serverResponse.body; + if (!Array.isArray(serverResponse) || !serverResponse.length) { + utils.logMessage('No prebid response from == ' + BIDDER_CODE + ' == for bid requests:'); + utils.logMessage(bidRequest); + return bidResponses; + } + // loop through serverResponses + for (let b in serverResponse) { + let bid = serverResponse[b]; + const bidResponse = { + requestId: bid.bidId, // not request id, it's bid's id + cpm: parseFloat(bid.cpm), + width: parseInt(bid.width), + height: parseInt(bid.height), + ttl: PLX_TTL, + creativeId: bid.creativeId, + netRevenue: PLX_NETREVENUE, + currency: PLX_CURRENCY + }; + if (bid.ad_type === 'url') { + bidResponse.adUrl = bid.ad; } else { - bidObject.ad = response.ad; + bidResponse.ad = bid.ad; } - bidObject.width = response.width; - bidObject.height = response.height; - } else { - bidObject = bidfactory.createBid(STATUS.NO_BID, bidObj); - bidObject.bidderCode = 'pollux'; - utils.logMessage('No prebid response from polluxHandler for placement code ' + placementCode); + if (bid.referrer) { + bidResponse.referrer = bid.referrer; + } + bidResponses.push(bidResponse); } - bidmanager.addBidResponse(placementCode, bidObject); - }; - $$PREBID_GLOBAL$$.polluxHandler = _polluxHandler; - // Export the `callBids` function, so that Prebid.js can execute - // this function when the page asks to send out bid requests. - return { - callBids: _callBids, - polluxHandler: _polluxHandler - }; + return bidResponses; + } }; -adaptermanager.registerBidAdapter(new PolluxBidAdapter(), 'pollux'); -module.exports = PolluxBidAdapter; +registerBidder(spec); diff --git a/modules/polluxBidAdapter.md b/modules/polluxBidAdapter.md new file mode 100644 index 00000000000..79bf84e79b9 --- /dev/null +++ b/modules/polluxBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +**Module Name**: Pollux Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: tech@polluxnetwork.com + +# Description + +Module that connects to Pollux Network LLC demand source to fetch bids. +All bids will present CPM in EUR (Euro). + +# Test Parameters +``` + var adUnits = [{ + code: '34f724kh32', + sizes: [[300, 250]], // a single size + bids: [{ + bidder: 'pollux', + params: { + zone: '1806' // a single zone + } + }] + },{ + code: '34f789r783', + sizes: [[300, 250], [728, 90]], // multiple sizes + bids: [{ + bidder: 'pollux', + params: { + zone: '1806,276' // multiple zones, max 5 + } + }] + }]; +``` diff --git a/test/spec/modules/polluxBidAdapter_spec.js b/test/spec/modules/polluxBidAdapter_spec.js index 1bcfe28124d..ea550fecd71 100644 --- a/test/spec/modules/polluxBidAdapter_spec.js +++ b/test/spec/modules/polluxBidAdapter_spec.js @@ -1,172 +1,207 @@ -describe('Pollux Bid Adapter tests', function () { - var expect = require('chai').expect; - var urlParse = require('url-parse'); - var querystringify = require('querystringify'); - var Adapter = require('modules/polluxBidAdapter'); - var adLoader = require('src/adloader'); - var bidmanager = require('src/bidmanager'); - var utils = require('src/utils'); - - var stubLoadScript; - var stubAddBidResponse; - var polluxAdapter; - - // mock golbal _bidsRequested var - var bidsRequested = []; - utils.getBidRequest = function (id) { - return bidsRequested.map(bidSet => bidSet.bids.find(bid => bid.bidId === id)).find(bid => bid); - }; - - beforeEach(function () { - polluxAdapter = new Adapter(); - bidsRequested = []; - stubLoadScript = sinon.stub(adLoader, 'loadScript'); - stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); +import {expect} from 'chai'; +import {spec} from 'modules/polluxBidAdapter'; +import {utils} from 'src/utils'; +import {newBidder} from 'src/adapters/bidderFactory'; + +describe('POLLUX Bid Adapter tests', function () { + // ad units setup + const setup_single_bid = [{ + placementCode: 'div-gpt-ad-1460505661587-0', + bidId: '789s6354sfg856', + bidderUrl: '//adn.polluxnetwork.com/prebid/v1', + sizes: [[728, 90], [300, 250]], + params: {zone: '1806,276'} + }]; + const setup_multi_bid = [{ + placementCode: 'div-gpt-ad-1460505661639-0', + bidId: '21fe992ca48d55', + sizes: [[300, 250]], + params: {zone: '1806'} + }, { + placementCode: 'div-gpt-ad-1460505661812-0', + bidId: '23kljh54390534', + sizes: [[728, 90]], + params: {zone: '276'} + }]; + + it('TEST: verify buildRequests no valid bid requests', () => { + let request = spec.buildRequests(false); + expect(request).to.not.equal(null); + expect(request).to.not.have.property('method'); + expect(request).to.not.have.property('url'); + expect(request).to.not.have.property('data'); + request = spec.buildRequests([]); + expect(request).to.not.equal(null); + expect(request).to.not.have.property('method'); + expect(request).to.not.have.property('url'); + expect(request).to.not.have.property('data'); + request = spec.buildRequests({}); + expect(request).to.not.equal(null); + expect(request).to.not.have.property('method'); + expect(request).to.not.have.property('url'); + expect(request).to.not.have.property('data'); + request = spec.buildRequests(null); + expect(request).to.not.equal(null); + expect(request).to.not.have.property('method'); + expect(request).to.not.have.property('url'); + expect(request).to.not.have.property('data'); }); - afterEach(function () { - stubLoadScript.restore(); - stubAddBidResponse.restore(); + it('TEST: verify buildRequests single bid', () => { + const request = spec.buildRequests(setup_single_bid); + expect(request.method).to.equal('POST'); + const requested_bids = JSON.parse(request.data); + // bids request + expect(requested_bids).to.not.equal(null); + expect(requested_bids).to.have.lengthOf(1); + // bid objects + expect(requested_bids[0]).to.not.equal(null); + expect(requested_bids[0]).to.have.property('bidId'); + expect(requested_bids[0]).to.have.property('sizes'); + expect(requested_bids[0]).to.have.property('zones'); + // bid 0 + expect(requested_bids[0].bidId).to.equal('789s6354sfg856'); + expect(requested_bids[0].sizes).to.not.equal(null); + expect(requested_bids[0].sizes).to.have.lengthOf(2); + expect(requested_bids[0].sizes[0][0]).to.equal(728); + expect(requested_bids[0].sizes[0][1]).to.equal(90); + expect(requested_bids[0].sizes[1][0]).to.equal(300); + expect(requested_bids[0].sizes[1][1]).to.equal(250); + expect(requested_bids[0].zones).to.equal('1806,276'); }); - describe('creation of bid url', function () { - it('bid request for single placement', function () { - var params = { - bidderCode: 'pollux', - bids: [{ - placementCode: 'div-gpt-ad-1460505661639-0', - bidId: '21fe992ca48d55', - bidder: 'pollux', - sizes: [[300, 250]], - params: { zone: '1806' } - }] - }; - - polluxAdapter.callBids(params); - - var bidUrl = stubLoadScript.getCall(0).args[0]; - - sinon.assert.calledOnce(stubLoadScript); - - var parsedBidUrl = urlParse(bidUrl); - var parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); - - expect(parsedBidUrlQueryString).to.have.property('zone').and.to.equal('1806'); - expect(parsedBidUrlQueryString).to.have.property('domain').and.to.have.length.above(1); - }); + it('TEST: verify buildRequests multi bid', () => { + const request = spec.buildRequests(setup_multi_bid); + expect(request.method).to.equal('POST'); + const requested_bids = JSON.parse(request.data); + // bids request + expect(requested_bids).to.not.equal(null); + expect(requested_bids).to.have.lengthOf(2); + // bid objects + expect(requested_bids[0]).to.not.equal(null); + expect(requested_bids[0]).to.have.property('bidId'); + expect(requested_bids[0]).to.have.property('sizes'); + expect(requested_bids[0]).to.have.property('zones'); + expect(requested_bids[1]).to.not.equal(null); + expect(requested_bids[1]).to.have.property('bidId'); + expect(requested_bids[1]).to.have.property('sizes'); + expect(requested_bids[1]).to.have.property('zones'); + // bid 0 + expect(requested_bids[0].bidId).to.equal('21fe992ca48d55'); + expect(requested_bids[0].sizes).to.not.equal(null); + expect(requested_bids[0].sizes).to.have.lengthOf(1); + expect(requested_bids[0].sizes[0][0]).to.equal(300); + expect(requested_bids[0].sizes[0][1]).to.equal(250); + expect(requested_bids[0].zones).to.equal('1806'); + // bid 1 + expect(requested_bids[1].bidId).to.equal('23kljh54390534'); + expect(requested_bids[1].sizes).to.not.equal(null); + expect(requested_bids[1].sizes).to.have.lengthOf(1); + expect(requested_bids[1].sizes[0][0]).to.equal(728); + expect(requested_bids[1].sizes[0][1]).to.equal(90); + expect(requested_bids[1].zones).to.equal('276'); }); - describe('handling bid response', function () { - it('should return complete bid response adUrl', function() { - var params = { - bidderCode: 'pollux', - bids: [{ - placementCode: 'div-gpt-ad-1460505661639-0', - sizes: [[300, 250]], - bidId: '21fe992ca48d55', - bidder: 'pollux', - params: { zone: '1806' } - }] - }; - - var response = { - cpm: 0.5, - width: 300, - height: 250, - callback_id: '21fe992ca48d55', - ad: 'some.ad.url', - ad_type: 'url', - zone: 1806 - }; - - polluxAdapter.callBids(params); - bidsRequested.push(params); - polluxAdapter.polluxHandler(response); - - sinon.assert.calledOnce(stubAddBidResponse); - - var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - - expect(bidPlacementCode1).to.equal('div-gpt-ad-1460505661639-0'); - expect(bidObject1.bidderCode).to.equal('pollux'); - expect(bidObject1.cpm).to.equal(0.5); - expect(bidObject1.width).to.equal(300); - expect(bidObject1.height).to.equal(250); - expect(bidObject1.adUrl).to.have.length.above(1); - }); - - it('should return complete bid response ad (html)', function() { - var params = { - bidderCode: 'pollux', - bids: [{ - placementCode: 'div-gpt-ad-1460505661639-0', - sizes: [[300, 250]], - bidId: '21fe992ca48d55', - bidder: 'pollux', - params: { zone: '1806' } - }] - }; - - var response = { - cpm: 0.5, - width: 300, - height: 250, - callback_id: '21fe992ca48d55', - ad: '<script src="some.ad.url"></script>', - ad_type: 'html', - zone: 1806 - }; - - polluxAdapter.callBids(params); - bidsRequested.push(params); - polluxAdapter.polluxHandler(response); - - sinon.assert.calledOnce(stubAddBidResponse); - - var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - - expect(bidPlacementCode1).to.equal('div-gpt-ad-1460505661639-0'); - expect(bidObject1.bidderCode).to.equal('pollux'); - expect(bidObject1.cpm).to.equal(0.5); - expect(bidObject1.width).to.equal(300); - expect(bidObject1.height).to.equal(250); - expect(bidObject1.ad).to.have.length.above(1); - }); + it('TEST: verify interpretResponse empty', () => { + let bids = spec.interpretResponse(false, {}); + expect(bids).to.not.equal(null); + expect(bids).to.have.lengthOf(0); + bids = spec.interpretResponse([], {}); + expect(bids).to.not.equal(null); + expect(bids).to.have.lengthOf(0); + bids = spec.interpretResponse({}, {}); + expect(bids).to.not.equal(null); + expect(bids).to.have.lengthOf(0); + bids = spec.interpretResponse(null, {}); + expect(bids).to.not.equal(null); + expect(bids).to.have.lengthOf(0); + }); - it('should return no bid response', function() { - var params = { - bidderCode: 'pollux', - bids: [{ - placementCode: 'div-gpt-ad-1460505661639-0', - sizes: [[300, 250]], - bidId: '21fe992ca48d55', - bidder: 'pollux', - params: { zone: '276' } - }] - }; + it('TEST: verify interpretResponse ad_type url', () => { + const serverResponse = { + body: [ + { + bidId: '789s6354sfg856', + cpm: '2.15', + width: '728', + height: '90', + ad: 'http://adn.polluxnetwork.com/zone/276?_plx_prebid=1&_plx_campaign=1125', + ad_type: 'url', + creativeId: '1125', + referrer: 'http://www.example.com' + } + ] + }; + const bids = spec.interpretResponse(serverResponse, {}); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('789s6354sfg856'); + expect(bids[0].cpm).to.equal(2.15); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].ttl).to.equal(3600); + expect(bids[0].creativeId).to.equal('1125'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].currency).to.equal('EUR'); + expect(bids[0].referrer).to.equal('http://www.example.com'); + expect(bids[0].adUrl).to.equal('http://adn.polluxnetwork.com/zone/276?_plx_prebid=1&_plx_campaign=1125'); + expect(bids[0]).to.not.have.property('ad'); + }); - var response = { - cpm: null, - width: null, - height: null, - callback_id: null, - ad: null, - zone: null - }; + it('TEST: verify interpretResponse ad_type html', () => { + const serverResponse = { + body: [ + { + bidId: '789s6354sfg856', + cpm: '2.15', + width: '728', + height: '90', + ad: '<html><h3>I am an ad</h3></html>', + ad_type: 'html', + creativeId: '1125' + } + ] + }; + const bids = spec.interpretResponse(serverResponse, {}); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('789s6354sfg856'); + expect(bids[0].cpm).to.equal(2.15); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].ttl).to.equal(3600); + expect(bids[0].creativeId).to.equal('1125'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].currency).to.equal('EUR'); + expect(bids[0]).to.not.have.property('referrer'); + expect(bids[0]).to.not.have.property('adUrl'); + expect(bids[0].ad).to.equal('<html><h3>I am an ad</h3></html>'); + }); - polluxAdapter.callBids(params); - bidsRequested.push(params); - polluxAdapter.polluxHandler(response); + it('TEST: verify url and query params', () => { + const URL = require('url-parse'); + const querystringify = require('querystringify'); + const request = spec.buildRequests(setup_single_bid); + const parsedUrl = new URL('https:' + request.url); + expect(parsedUrl.origin).to.equal('https://adn.polluxnetwork.com'); + expect(parsedUrl.pathname).to.equal('/prebid/v1'); + expect(parsedUrl).to.have.property('query'); + const parsedQuery = querystringify.parse(parsedUrl.query); + expect(parsedQuery).to.have.property('domain').and.to.have.length.above(1); + }); - sinon.assert.calledOnce(stubAddBidResponse); + it('TEST: verify isBidRequestValid', () => { + expect(spec.isBidRequestValid({})).to.equal(false); + expect(spec.isBidRequestValid({params: {}})).to.equal(false); + expect(spec.isBidRequestValid(setup_single_bid[0])).to.equal(true); + expect(spec.isBidRequestValid(setup_multi_bid[0])).to.equal(true); + expect(spec.isBidRequestValid(setup_multi_bid[1])).to.equal(true); + }); - var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; + it('TEST: verify bidder code', () => { + expect(spec.code).to.equal('pollux'); + }); - expect(bidPlacementCode1).to.equal(''); - expect(bidObject1.bidderCode).to.equal('pollux'); - }); + it('TEST: verify bidder aliases', () => { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('plx'); }); }); From 71fa70746b54855b54658545b2e5c5c4e11a2c10 Mon Sep 17 00:00:00 2001 From: Matt Probert <mattpr@users.noreply.github.com> Date: Fri, 10 Nov 2017 22:55:34 +0100 Subject: [PATCH 19/32] Fix test that hard-coded pbjs global. (#1786) --- test/spec/modules/s2sTesting_spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/spec/modules/s2sTesting_spec.js b/test/spec/modules/s2sTesting_spec.js index f829087a967..4ddd7278f4e 100644 --- a/test/spec/modules/s2sTesting_spec.js +++ b/test/spec/modules/s2sTesting_spec.js @@ -312,15 +312,15 @@ describe('s2sTesting', function () { const AST = CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING; function checkTargeting(bidder) { - var targeting = window.pbjs.bidderSettings[bidder][AST]; + var targeting = window.$$PREBID_GLOBAL$$.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; + expect(window.$$PREBID_GLOBAL$$.bidderSettings[bidder].alwaysUseBid).to.be.true; } function checkNoTargeting(bidder) { - var bs = window.pbjs.bidderSettings; + var bs = window.$$PREBID_GLOBAL$$.bidderSettings; var targeting = bs[bidder] && bs[bidder][AST]; if (!targeting) { expect(targeting).to.be.undefined; @@ -332,22 +332,22 @@ describe('s2sTesting', function () { } function checkTargetingVal(bidResponse, expectedVal) { - var targeting = window.pbjs.bidderSettings[bidResponse.bidderCode][AST]; + var targeting = window.$$PREBID_GLOBAL$$.bidderSettings[bidResponse.bidderCode][AST]; var targetingFunc = targeting[targeting.length - 1].val; expect(targetingFunc(bidResponse)).to.equal(expectedVal); } beforeEach(() => { // set bidderSettings - window.pbjs.bidderSettings = {}; + window.$$PREBID_GLOBAL$$.bidderSettings = {}; }); it('should not set hb_source_<bidder> unless testing is on and includeSourceKvp is set', () => { config.setConfig({s2sConfig: {bidders: ['rubicon', 'appnexus']}}); - expect(window.pbjs.bidderSettings).to.eql({}); + expect(window.$$PREBID_GLOBAL$$.bidderSettings).to.eql({}); config.setConfig({s2sConfig: {bidders: ['rubicon', 'appnexus'], testing: true}}); - expect(window.pbjs.bidderSettings).to.eql({}); + expect(window.$$PREBID_GLOBAL$$.bidderSettings).to.eql({}); config.setConfig({s2sConfig: { bidders: ['rubicon', 'appnexus'], @@ -357,7 +357,7 @@ describe('s2sTesting', function () { appnexus: {bidSource: {server: 1}} } }}); - expect(window.pbjs.bidderSettings).to.eql({}); + expect(window.$$PREBID_GLOBAL$$.bidderSettings).to.eql({}); config.setConfig({s2sConfig: { bidders: ['rubicon', 'appnexus'], @@ -367,7 +367,7 @@ describe('s2sTesting', function () { appnexus: {includeSourceKvp: true} } }}); - expect(window.pbjs.bidderSettings).to.eql({}); + expect(window.$$PREBID_GLOBAL$$.bidderSettings).to.eql({}); }); it('should set hb_source_<bidder> if includeSourceKvp is set', () => { From 0d6868a3f0b02c2720a633a952d844ce883ebfef Mon Sep 17 00:00:00 2001 From: IHateCodesss <lordwatercode@gmail.com> Date: Mon, 13 Nov 2017 15:02:00 -0800 Subject: [PATCH 20/32] OpenX Video Adapter update to Prebid v1.0 (#1724) * Add video support * remove side-effects in unit tests and update test ad domain * Add mediaType adn vastUrl for video reponse --- modules/openxBidAdapter.js | 138 +++++++++++++++---- modules/openxBidAdapter.md | 18 +++ test/spec/modules/openxBidAdapter_spec.js | 154 ++++++++++++++++++++-- 3 files changed, 275 insertions(+), 35 deletions(-) diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index a0bcd2a945f..6f81d0ede84 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -2,9 +2,9 @@ import { config } from 'src/config'; import {registerBidder} from 'src/adapters/bidderFactory'; import * as utils from 'src/utils'; import {userSync} from 'src/userSync'; -import { BANNER } from 'src/mediaTypes'; +import { BANNER, VIDEO } from 'src/mediaTypes'; -const SUPPORTED_AD_TYPES = [BANNER]; +const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'openx'; const BIDDER_CONFIG = 'hb_pb'; const BIDDER_VERSION = '2.0.0'; @@ -13,6 +13,11 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function(bid) { + if (bid.mediaType === VIDEO) { + if (typeof bid.params.video !== 'object' || !bid.params.video.url) { + return false; + } + } return !!(bid.params.unit && bid.params.delDomain); }, buildRequests: function(bids) { @@ -22,27 +27,59 @@ export const spec = { return; } - let delDomain = bids[0].params.delDomain; - let configuredBc = bids[0].params.bc; - let bc = configuredBc || `${BIDDER_CONFIG}_${BIDDER_VERSION}`; - - return buildOXRequest(bids, { - ju: currentURL, - jr: currentURL, - ch: document.charSet || document.characterSet, - res: `${screen.width}x${screen.height}x${screen.colorDepth}`, - ifr: isIfr, - tz: new Date().getTimezoneOffset(), - tws: getViewportDimensions(isIfr), - ef: 'bt%2Cdb', - be: 1, - bc: bc, - nocache: new Date().getTime() - }, - delDomain); + let requests = []; + let bannerRequests = []; + let videoRequests = []; + let bannerBids = bids.filter(function(bid) { return bid.mediaType === BANNER; }); + let videoBids = bids.filter(function(bid) { return bid.mediaType === VIDEO; }); + + // build banner requests + if (bannerBids.length !== 0) { + let delDomain = bannerBids[0].params.delDomain; + let configuredBc = bannerBids[0].params.bc; + let bc = configuredBc || `${BIDDER_CONFIG}_${BIDDER_VERSION}`; + bannerRequests = [ buildOXRequest(bannerBids, { + ju: currentURL, + jr: currentURL, + ch: document.charSet || document.characterSet, + res: `${screen.width}x${screen.height}x${screen.colorDepth}`, + ifr: isIfr, + tz: new Date().getTimezoneOffset(), + tws: getViewportDimensions(isIfr), + ef: 'bt%2Cdb', + be: 1, + bc: bc, + nocache: new Date().getTime() + }, + delDomain)]; + } + // build video requests + if (videoBids.length !== 0) { + videoRequests = buildOXVideoRequest(videoBids); + } + + requests = bannerRequests.concat(videoRequests); + return requests; }, interpretResponse: function({body: oxResponseObj}, bidRequest) { let bidResponses = []; + let mediaType = BANNER; + if (bidRequest && bidRequest.payload) { + if (bidRequest.payload.bids) { + mediaType = bidRequest.payload.bids[0].mediaType; + } else if (bidRequest.payload.bid) { + mediaType = bidRequest.payload.bid.mediaType; + } + } + + if (mediaType === VIDEO) { + if (oxResponseObj && oxResponseObj.pixels) { + userSync.registerSync('iframe', 'openx', oxResponseObj.pixels); + } + bidResponses = createVideoBidResponses(oxResponseObj, bidRequest.payload); + return bidResponses; + } + let adUnits = oxResponseObj.ads.ad; if (oxResponseObj.ads && oxResponseObj.ads.pixels) { userSync.registerSync('iframe', BIDDER_CODE, oxResponseObj.ads.pixels); @@ -96,13 +133,13 @@ function createBidResponses(adUnits, {bids, startTime}) { if (adUnit.deal_id) { bidResponse.dealId = adUnit.deal_id; } - // default 5 mins + // default 5 mins bidResponse.ttl = 300; - // true is net, false is gross + // true is net, false is gross bidResponse.netRevenue = true; bidResponse.currency = adUnit.currency; - // additional fields to add + // additional fields to add if (adUnit.tbd) { bidResponse.tbd = adUnit.tbd; } @@ -211,7 +248,7 @@ function formatCustomParms(customKey, customParams) { // if value is an array, join them with commas first value = value.join(','); } - // return customKey=customValue format, escaping + to . and / to _ + // return customKey=customValue format, escaping + to . and / to _ return (customKey.toLowerCase() + '=' + value.toLowerCase()).replace('+', '.').replace('/', '_') } @@ -265,4 +302,57 @@ function buildOXRequest(bids, oxParams, delDomain) { }; } +function buildOXVideoRequest(bids) { + return bids.map(function(bid) { + let url = 'http://' + bid.params.delDomain + '/v/1.0/avjp'; + let oxVideoParams = generateVideoParameters(bid); + return { + method: 'GET', + url: url, + data: oxVideoParams, + payload: {'bid': bid, 'startTime': new Date()} + }; + }); +} + +function generateVideoParameters(bid) { + let oxVideo = bid.params.video; + let oxVideoParams = { auid: bid.params.unit }; + + Object.keys(oxVideo).forEach(function(key) { + if (key === 'openrtb') { + oxVideoParams[key] = JSON.stringify(oxVideo[key]); + } else { + oxVideoParams[key] = oxVideo[key]; + } + }); + oxVideoParams['be'] = 'true'; + return oxVideoParams; +} + +function createVideoBidResponses(response, {bid, startTime}) { + let bidResponses = []; + + if (response !== undefined && response.vastUrl !== '' && response.pub_rev !== '') { + let bidResponse = {}; + bidResponse.requestId = bid.bidId; + bidResponse.bidderCode = BIDDER_CODE; + // default 5 mins + bidResponse.ttl = 300; + // true is net, false is gross + bidResponse.netRevenue = true; + bidResponse.currency = response.currency; + bidResponse.cpm = Number(response.pub_rev) / 1000; + bidResponse.width = response.width; + bidResponse.height = response.height; + bidResponse.creativeId = response.adid; + bidResponse.vastUrl = response.vastUrl; + bidResponse.mediaType = VIDEO; + + bidResponses.push(bidResponse); + } + + return bidResponses; +} + registerBidder(spec); diff --git a/modules/openxBidAdapter.md b/modules/openxBidAdapter.md index 5b3ad77ce6d..1f04c2fe466 100644 --- a/modules/openxBidAdapter.md +++ b/modules/openxBidAdapter.md @@ -16,6 +16,7 @@ Module that connects to OpenX's demand sources { code: 'test-div', sizes: [[728, 90]], // a display size + mediaType: 'banner', bids: [ { bidder: "openx", @@ -26,5 +27,22 @@ Module that connects to OpenX's demand sources } ] }, + { + code: 'video1', + sizes: [[640,480]], + mediaType: 'video', + bids: [ + { + bidder: 'openx', + params: { + unit: '539131525', + delDomain: 'zdo.com', + video: { + url: 'abc.com' + } + } + } + ] + } ]; ``` diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index cee521b2921..a03bebf9e0a 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -3,6 +3,7 @@ import { spec } from 'modules/openxBidAdapter'; import { newBidder } from 'src/adapters/bidderFactory'; const URLBASE = '/w/1.0/arj'; +const URLBASEVIDEO = '/v/1.0/avjp'; describe('OpenxAdapter', () => { const adapter = newBidder(spec); @@ -21,25 +22,57 @@ describe('OpenxAdapter', () => { 'delDomain': 'test-del-domain' }, 'adUnitCode': 'adunit-code', + 'mediaType': 'banner', 'sizes': [[300, 250], [300, 600]], 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', }; - it('should return true when required params found', () => { + let videoBid = { + 'bidder': 'openx', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'video': { + 'be': 'true', + 'url': 'abc.com', + 'vtest': '1' + } + }, + 'adUnitCode': 'adunit-code', + 'mediaType': 'video', + 'sizes': [640, 480], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; + + it('should return true when required params found for a banner ad', () => { expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should return false when required params are not passed', () => { + it('should return false when required params are not passed for a banner ad', () => { let bid = Object.assign({}, bid); delete bid.params; bid.params = {'unit': '12345678'}; expect(spec.isBidRequestValid(bid)).to.equal(false); }); + + it('should return true when required params found for a video ad', () => { + expect(spec.isBidRequestValid(videoBid)).to.equal(true); + }); + + it('should return false when required params are not passed for a video ad', () => { + let videoBid = Object.assign({}, videoBid); + delete videoBid.params; + videoBid.params = {}; + expect(spec.isBidRequestValid(videoBid)).to.equal(false); + }); }); - describe('buildRequests', () => { + describe('buildRequests for banner ads', () => { let bidRequests = [{ 'bidder': 'openx', 'params': { @@ -47,6 +80,7 @@ describe('OpenxAdapter', () => { 'delDomain': 'test-del-domain' }, 'adUnitCode': 'adunit-code', + 'mediaType': 'banner', 'sizes': [[300, 250], [300, 600]], 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', @@ -55,13 +89,13 @@ describe('OpenxAdapter', () => { it('should send bid request to openx url via GET', () => { const request = spec.buildRequests(bidRequests); - expect(request.url).to.equal('//' + bidRequests[0].params.delDomain + URLBASE); - expect(request.method).to.equal('GET'); + expect(request[0].url).to.equal('//' + bidRequests[0].params.delDomain + URLBASE); + expect(request[0].method).to.equal('GET'); }); it('should have the correct parameters', () => { const request = spec.buildRequests(bidRequests); - const dataParams = request.data; + const dataParams = request[0].data; expect(dataParams.auid).to.exist; expect(dataParams.auid).to.equal('12345678'); @@ -82,7 +116,7 @@ describe('OpenxAdapter', () => { ); const request = spec.buildRequests([bidRequest]); - const dataParams = request.data; + const dataParams = request[0].data; expect(dataParams.tps).to.exist; expect(dataParams.tps).to.equal(btoa('test1=testval1.&test2=testval2_,testval3')); @@ -101,7 +135,7 @@ describe('OpenxAdapter', () => { ); const request = spec.buildRequests([bidRequest]); - const dataParams = request.data; + const dataParams = request[0].data; expect(dataParams.aumfs).to.exist; expect(dataParams.aumfs).to.equal('1500'); @@ -120,14 +154,50 @@ describe('OpenxAdapter', () => { ); const request = spec.buildRequests([bidRequest]); - const dataParams = request.data; + const dataParams = request[0].data; expect(dataParams.bc).to.exist; expect(dataParams.bc).to.equal('hb_override'); }); }); - describe('interpretResponse', () => { + describe('buildRequests for video', () => { + let bidRequests = [{ + 'bidder': 'openx', + 'mediaType': 'video', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'video': { + 'url': 'abc.com', + } + }, + 'adUnitCode': 'adunit-code', + 'sizes': [640, 480], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + }]; + + it('should send bid request to openx url via GET', () => { + const request = spec.buildRequests(bidRequests); + expect(request[0].url).to.equal('http://' + bidRequests[0].params.delDomain + URLBASEVIDEO); + expect(request[0].method).to.equal('GET'); + }); + + it('should have the correct parameters', () => { + const request = spec.buildRequests(bidRequests); + const dataParams = request[0].data; + + expect(dataParams.auid).to.exist; + expect(dataParams.auid).to.equal('12345678'); + expect(dataParams.url).to.exist; + expect(dataParams.url).to.equal('abc.com'); + }); + }); + + describe('interpretResponse for banner ads', () => { let bids = [{ 'bidder': 'openx', 'params': { @@ -135,6 +205,7 @@ describe('OpenxAdapter', () => { 'delDomain': 'test-del-domain' }, 'adUnitCode': 'adunit-code', + 'mediaType': 'banner', 'sizes': [[300, 250], [300, 600]], 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', @@ -203,7 +274,7 @@ describe('OpenxAdapter', () => { }); it('handles nobid responses', () => { - bidResponse = { + let bidResponse = { 'ads': { 'version': 1, @@ -217,4 +288,65 @@ describe('OpenxAdapter', () => { expect(result.length).to.equal(0); }); }); + + describe('interpretResponse for video ads', () => { + let bids = [{ + 'bidder': 'openx', + 'mediaType': 'video', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'video': { + 'url': 'abc.com', + } + }, + 'adUnitCode': 'adunit-code', + 'sizes': [640, 480], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + }]; + let bidRequest = { + method: 'GET', + url: 'url', + data: {}, + payload: {'bid': bids[0], 'startTime': new Date()} + }; + let bidResponse = { + 'pub_rev': '1', + 'width': '640', + 'height': '480', + 'adid': '5678', + 'vastUrl': 'http://testvast.com', + 'pixels': 'http://testpixels.net' + }; + + it('should return correct bid response', () => { + let expectedResponse = [ + { + 'requestId': '30b31c1838de1e', + 'bidderCode': 'openx', + 'cpm': 1, + 'width': '640', + 'height': '480', + 'mediaType': 'video', + 'creativeId': '5678', + 'vastUrl': 'http://testvast.com', + 'ttl': 300, + 'netRevenue': true, + 'currency': 'USD' + } + ]; + + let result = spec.interpretResponse({body: bidResponse}, bidRequest); + expect(JSON.stringify(Object.keys(result[0]).sort())).to.eql(JSON.stringify(Object.keys(expectedResponse[0]).sort())); + }); + + it('handles nobid responses', () => { + let bidResponse = {'vastUrl': '', 'pub_rev': '', 'width': '', 'height': '', 'adid': '', 'pixels': ''}; + let result = spec.interpretResponse({body: bidResponse}, bidRequest); + expect(result.length).to.equal(0); + }); + }); }); From d3c2b028e87f3758e17d1a22417b3d888761edab Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Tue, 14 Nov 2017 08:27:32 -0500 Subject: [PATCH 21/32] Add custom keyword support for pbs bid adapter (#1763) * initial commit to add custom keyword support for pbs adapter * fixing small typo * modified variable convention and added unit test case --- modules/prebidServerBidAdapter.js | 19 +++++++++++++++++++ .../modules/prebidServerBidAdapter_spec.js | 7 ++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 7120d67eb56..a4497b52b07 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -99,6 +99,25 @@ function PrebidServer() { } } }); + // will collect any custom params and place them under bid.params.keywords attribute in the following manner for pbs to ingest properly + // "keywords":[{"key":"randomKey","value":["123456789"]},{"key":"single_test"},{"key":"myMultiVar","value":["myValue","124578"]}] + let kwArray = []; + Object.keys(bid.params).forEach(key => { + if (bid.bidder === 'appnexus' && (key !== 'member' && key !== 'invCode' && key !== 'placementId')) { + let kvObj = {}; + kvObj.key = key + if (bid.params[key] !== null) { + if (Array.isArray(bid.params[key])) { + kvObj.value = bid.params[key].map(val => tryConvertString(val)); + } else { + kvObj.value = [tryConvertString(bid.params[key])]; + } + } + kwArray.push(kvObj); + delete bid.params[key]; + } + }); + bid.params.keywords = kwArray; }); }); } diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 78eef4b016b..0403617a846 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -43,7 +43,10 @@ const REQUEST = { 'bidder': 'appnexus', 'params': { 'placementId': '10433394', - 'member': 123 + 'member': 123, + 'randomKey': 123456789, + 'single_test': null, + 'myMultiVar': ['myValue', 124578] } } ] @@ -201,6 +204,8 @@ describe('S2S Adapter', () => { const requestBid = JSON.parse(requests[0].requestBody); expect(requestBid.ad_units[0].bids[0].params.placementId).to.exist.and.to.be.a('number'); expect(requestBid.ad_units[0].bids[0].params.member).to.exist.and.to.be.a('string'); + expect(requestBid.ad_units[0].bids[0].params.keywords).to.exist.and.to.be.an('array').and.to.have.lengthOf(3); + expect(requestBid.ad_units[0].bids[0].params.keywords[0]).to.be.an('object').that.has.all.keys('key', 'value'); }); }); From 72df57ed8a9eb58fee437cfa39f49f9d8203970d Mon Sep 17 00:00:00 2001 From: Rich Snapp <rsnapp@rubiconproject.com> Date: Tue, 14 Nov 2017 06:36:34 -0700 Subject: [PATCH 22/32] Remove require.ensure entirely (#1816) --- plugins/RequireEnsureWithoutJsonp.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/plugins/RequireEnsureWithoutJsonp.js b/plugins/RequireEnsureWithoutJsonp.js index b3b38f5e336..c15af360e56 100644 --- a/plugins/RequireEnsureWithoutJsonp.js +++ b/plugins/RequireEnsureWithoutJsonp.js @@ -15,14 +15,7 @@ function RequireEnsureWithoutJsonp() {} RequireEnsureWithoutJsonp.prototype.apply = function(compiler) { compiler.plugin('compilation', function(compilation) { compilation.mainTemplate.plugin('require-ensure', function(_, chunk, hash) { - return ( -` -if(installedChunks[chunkId] === 0) - return callback.call(null, __webpack_require__); -else - console.error('webpack chunk not found and jsonp disabled'); -` - ).trim(); + return ''; }); }); }; From bd8d8bae5ecdee4216b7b49c1862c3df835e744e Mon Sep 17 00:00:00 2001 From: Anand Venkatraman <avenkatraman@pulsepoint.com> Date: Tue, 14 Nov 2017 08:41:18 -0500 Subject: [PATCH 23/32] Pulsepoint adapter: fixing bid rejection due to missing mandatory bid params. (#1823) * 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 * Adding mandatory parameters to Bid * Fixing review comment * Applying values from "ext" as applicable --- modules/pulsepointLiteBidAdapter.js | 16 ++++++++++ .../modules/pulsepointLiteBidAdapter_spec.js | 29 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/modules/pulsepointLiteBidAdapter.js b/modules/pulsepointLiteBidAdapter.js index 99a83871dd8..d851245402c 100644 --- a/modules/pulsepointLiteBidAdapter.js +++ b/modules/pulsepointLiteBidAdapter.js @@ -10,6 +10,10 @@ const NATIVE_DEFAULTS = { ICON_MIN: 50, }; +const DEFAULT_BID_TTL = 20; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; + /** * PulsePoint "Lite" Adapter. This adapter implementation is lighter than the * alternative/original PulsePointAdapter because it has no external @@ -89,6 +93,9 @@ function bidResponseAvailable(bidRequest, bidResponse) { creative_id: id, creativeId: id, adId: id, + ttl: DEFAULT_BID_TTL, + netRevenue: DEFAULT_NET_REVENUE, + currency: DEFAULT_CURRENCY }; if (idToImpMap[id]['native']) { bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); @@ -98,12 +105,21 @@ function bidResponseAvailable(bidRequest, bidResponse) { bid.width = idToImpMap[id].banner.w; bid.height = idToImpMap[id].banner.h; } + applyExt(bid, idToBidMap[id]) bids.push(bid); } }); return bids; } +function applyExt(bid, ortbBid) { + if (ortbBid && ortbBid.ext) { + bid.ttl = ortbBid.ext.ttl || bid.ttl; + bid.currency = ortbBid.ext.currency || bid.currency; + bid.netRevenue = ortbBid.ext.netRevenue != null ? ortbBid.ext.netRevenue : bid.netRevenue; + } +} + /** * Produces an OpenRTBImpression from a slot config. */ diff --git a/test/spec/modules/pulsepointLiteBidAdapter_spec.js b/test/spec/modules/pulsepointLiteBidAdapter_spec.js index 9731164cd50..2c6f5f0681f 100644 --- a/test/spec/modules/pulsepointLiteBidAdapter_spec.js +++ b/test/spec/modules/pulsepointLiteBidAdapter_spec.js @@ -100,6 +100,35 @@ describe('PulsePoint Lite Adapter Tests', () => { expect(bid.adId).to.equal('bid12345'); expect(bid.creative_id).to.equal('bid12345'); expect(bid.creativeId).to.equal('bid12345'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(20); + }); + + it('Verify use ttl in ext', () => { + const request = spec.buildRequests(slotConfigs); + const ortbRequest = JSON.parse(request.data); + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + ext: { + ttl: 30, + netRevenue: false, + currency: 'INR' + } + }] + }] + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.ttl).to.equal(30); + expect(bid.netRevenue).to.equal(false); + expect(bid.currency).to.equal('INR'); }); it('Verify full passback', () => { From 9bd0c46494e9f342b23b75beb09de19d3c7d867f Mon Sep 17 00:00:00 2001 From: William Klausmeyer <klawil@users.noreply.github.com> Date: Tue, 14 Nov 2017 08:10:23 -0600 Subject: [PATCH 24/32] Update renderAd to replace ${AUCTION_PRICE} in adUrl (#1795) * Modify `adUrl` instead of `url` with bid price The `bid.url` parameter is not used to render the iframe for an ad that returns a url but is being treated as such when the auction price is being inserted. This commit inserts the auction price into `bid.adUrl` which is used to render the ad. * Use the key for adUrl instead of using url Instead of renaming the parameter `adUrl` to `url` for the rendering function, use `adUrl` to be consistent with the key value returned in the bid object. --- src/prebid.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/prebid.js b/src/prebid.js index 3d25eff6761..f22e734515b 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -282,14 +282,14 @@ $$PREBID_GLOBAL$$.renderAd = function (doc, id) { if (bid) { // replace macros according to openRTB with price paid = bid.cpm bid.ad = utils.replaceAuctionPrice(bid.ad, bid.cpm); - bid.url = utils.replaceAuctionPrice(bid.url, bid.cpm); + bid.adUrl = utils.replaceAuctionPrice(bid.adUrl, bid.cpm); // save winning bids $$PREBID_GLOBAL$$._winningBids.push(bid); // emit 'bid won' event here events.emit(BID_WON, bid); - const { height, width, ad, mediaType, adUrl: url, renderer } = bid; + const { height, width, ad, mediaType, adUrl, renderer } = bid; if (renderer && renderer.url) { renderer.render(bid); @@ -299,13 +299,13 @@ $$PREBID_GLOBAL$$.renderAd = function (doc, id) { doc.write(ad); doc.close(); setRenderSize(doc, width, height); - } else if (url) { + } else if (adUrl) { const iframe = utils.createInvisibleIframe(); iframe.height = height; iframe.width = width; iframe.style.display = 'inline'; iframe.style.overflow = 'hidden'; - iframe.src = url; + iframe.src = adUrl; utils.insertElement(iframe, doc, 'body'); setRenderSize(doc, width, height); From 8f9df5bc9b85eb633a4f3f718788964996c2a54b Mon Sep 17 00:00:00 2001 From: Matt Lane <mlane@appnexus.com> Date: Tue, 14 Nov 2017 10:04:42 -0500 Subject: [PATCH 25/32] Drop non-video bidders from video ad units (#1815) * Check mediaTypes when validating video bidders * Update error message * Drop non-compatible bidders rather than entire ad unit --- src/prebid.js | 26 +++++++++++++++++--------- src/video.js | 10 +++++++--- test/spec/unit/pbjs_api_spec.js | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/prebid.js b/src/prebid.js index f22e734515b..947cebcac7e 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -2,7 +2,7 @@ import { getGlobal } from './prebidGlobal'; import { flatten, uniques, isGptPubadsDefined, adUnitsFilter } from './utils'; -import { videoAdUnit, hasNonVideoBidder } from './video'; +import { videoAdUnit, videoBidder, hasNonVideoBidder } from './video'; import { nativeAdUnit, nativeBidder, hasNonNativeBidder } from './native'; import './polyfill'; import { parse as parseURL, format as formatURL } from './url'; @@ -380,13 +380,18 @@ $$PREBID_GLOBAL$$.requestBids = function ({ bidsBackHandler, timeout, adUnits, a adUnitCodes = adUnits && adUnits.map(unit => unit.code); } - // for video-enabled adUnits, only request bids if all bidders support video - const invalidVideoAdUnits = adUnits.filter(videoAdUnit).filter(hasNonVideoBidder); - invalidVideoAdUnits.forEach(adUnit => { - utils.logError(`adUnit ${adUnit.code} has 'mediaType' set to 'video' but contains a bidder that doesn't support video. No Prebid demand requests will be triggered for this adUnit.`); - for (let i = 0; i < adUnits.length; i++) { - if (adUnits[i].code === adUnit.code) { adUnits.splice(i, 1); } - } + // for video-enabled adUnits, only request bids for bidders that support video + adUnits.filter(videoAdUnit).filter(hasNonVideoBidder).forEach(adUnit => { + const nonVideoBidders = adUnit.bids + .filter(bid => !videoBidder(bid)) + .map(bid => bid.bidder) + .join(', '); + + utils.logError(` + ${adUnit.code} is a 'video' ad unit but contains non-video bidder(s) ${nonVideoBidders}. + No Prebid demand requests will be triggered for those bidders. + `); + adUnit.bids = adUnit.bids.filter(videoBidder); }); // for native-enabled adUnits, only request bids for bidders that support native @@ -396,7 +401,10 @@ $$PREBID_GLOBAL$$.requestBids = function ({ bidsBackHandler, timeout, adUnits, a .map(bid => bid.bidder) .join(', '); - utils.logError(`adUnit ${adUnit.code} has 'mediaType' set to 'native' but contains non-native bidder(s) ${nonNativeBidders}. No Prebid demand requests will be triggered for those bidders.`); + utils.logError(` + ${adUnit.code} is a 'native' ad unit but contains non-native bidder(s) ${nonNativeBidders}. + No Prebid demand requests will be triggered for those bidders. + `); adUnit.bids = adUnit.bids.filter(nativeBidder); }); diff --git a/src/video.js b/src/video.js index f5203e4b198..22255068cc0 100644 --- a/src/video.js +++ b/src/video.js @@ -8,10 +8,14 @@ const OUTSTREAM = 'outstream'; /** * Helper functions for working with video-enabled adUnits */ -export const videoAdUnit = adUnit => adUnit.mediaType === VIDEO_MEDIA_TYPE; -const nonVideoBidder = bid => !videoAdapters.includes(bid.bidder); +export const videoAdUnit = adUnit => { + const mediaType = adUnit.mediaType === VIDEO_MEDIA_TYPE; + const mediaTypes = deepAccess(adUnit, 'mediaTypes.video'); + return mediaType || mediaTypes; +}; +export const videoBidder = bid => videoAdapters.includes(bid.bidder); export const hasNonVideoBidder = adUnit => - adUnit.bids.filter(nonVideoBidder).length; + adUnit.bids.filter(bid => !videoBidder(bid)).length; /** * @typedef {object} VideoBid diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index d6f1451b65c..5a91fe218ef 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -779,7 +779,7 @@ describe('Unit: Prebid Module', function () { adaptermanager.callBids.restore(); }); - it('should not callBids if a video adUnit has non-video bidders', () => { + it('should only request video bidders on video adunits', () => { sinon.spy(adaptermanager, 'callBids'); const videoAdaptersBackup = adaptermanager.videoAdapters; adaptermanager.videoAdapters = ['appnexusAst']; @@ -793,7 +793,35 @@ describe('Unit: Prebid Module', function () { }]; $$PREBID_GLOBAL$$.requestBids({adUnits}); - sinon.assert.notCalled(adaptermanager.callBids); + sinon.assert.calledOnce(adaptermanager.callBids); + + const spyArgs = adaptermanager.callBids.getCall(0); + const biddersCalled = spyArgs.args[0].adUnits[0].bids; + expect(biddersCalled.length).to.equal(1); + + adaptermanager.callBids.restore(); + adaptermanager.videoAdapters = videoAdaptersBackup; + }); + + it('should only request video bidders on video adunits configured with mediaTypes', () => { + sinon.spy(adaptermanager, 'callBids'); + const videoAdaptersBackup = adaptermanager.videoAdapters; + adaptermanager.videoAdapters = ['appnexusAst']; + const adUnits = [{ + code: 'adUnit-code', + mediaTypes: {video: {context: 'instream'}}, + bids: [ + {bidder: 'appnexus', params: {placementId: 'id'}}, + {bidder: 'appnexusAst', params: {placementId: 'id'}} + ] + }]; + + $$PREBID_GLOBAL$$.requestBids({adUnits}); + sinon.assert.calledOnce(adaptermanager.callBids); + + const spyArgs = adaptermanager.callBids.getCall(0); + const biddersCalled = spyArgs.args[0].adUnits[0].bids; + expect(biddersCalled.length).to.equal(1); adaptermanager.callBids.restore(); adaptermanager.videoAdapters = videoAdaptersBackup; From 534012dd796f68e04829e8f77c32eac3cc67bed0 Mon Sep 17 00:00:00 2001 From: varashellov <sk@ultralab.by> Date: Tue, 14 Nov 2017 07:07:23 -0800 Subject: [PATCH 26/32] Platform.io Bidder Adapter update (#1817) * Add PlatformioBidAdapter * Update platformioBidAdapter.js * Add files via upload * Update hello_world.html * Update platformioBidAdapter.js * Update platformioBidAdapter_spec.js * Update hello_world.html * Update platformioBidAdapter_spec.js * Update platformioBidAdapter.js * Update hello_world.html * Add files via upload * Update platformioBidAdapter ## Type of change - [x] Other ## Description of change 1. RequestURL changes 2. Add placementCode to request params * Update platformioBidAdapter * Update platformioBidAdapter ## Type of change - [x] Other ## Description of change 1. RequestURL changes 2. Add placementCode to request params * Add files via upload * Add files via upload * Add files via upload * Update platformioBidAdapter.js Endpoint URL change * Update platformioBidAdapter_spec.js Endpoint URL change * Update platformioBidAdapter_spec.js * Update platformioBidAdapter_spec.js * Update platformioBidAdapter.js * Update platformioBidAdapter.js * Update platformioBidAdapter_spec.js * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Update platformioBidAdapter.js * Update platformioBidAdapter_spec.js --- modules/platformioBidAdapter.js | 16 ++++++++-------- modules/platformioBidAdapter.md | 7 ++++--- test/spec/modules/platformioBidAdapter_spec.js | 17 +++++++++++------ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/modules/platformioBidAdapter.js b/modules/platformioBidAdapter.js index 2fb23ab92b3..fa841dc6026 100644 --- a/modules/platformioBidAdapter.js +++ b/modules/platformioBidAdapter.js @@ -42,13 +42,13 @@ function bidResponseAvailable(bidRequest, bidResponse) { 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, - }; + const bid = {}; + bid.requestId = id; + bid.creativeId = idToBidMap[id].adid; + bid.cpm = idToBidMap[id].price; + bid.currency = bidResponse.cur; + bid.ttl = 360; + bid.netRevenue = true; bid.ad = idToBidMap[id].adm; bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_AD_ID(%7D|\})/gi, idToBidMap[id].adid); @@ -66,7 +66,7 @@ function impression(slot) { return { id: slot.bidId, banner: banner(slot), - bidfloor: '0.000001', + bidfloor: slot.params.bidFloor || '0.000001', tagid: slot.params.placementId.toString(), }; } diff --git a/modules/platformioBidAdapter.md b/modules/platformioBidAdapter.md index 74f6adbf256..0d019d1fe96 100644 --- a/modules/platformioBidAdapter.md +++ b/modules/platformioBidAdapter.md @@ -17,10 +17,11 @@ Please use ```platformio``` as the bidder code. bids: [{ bidder: 'platformio', params: { - pubId: '28082', - siteId: '26047', + pubId: '28082', // required + siteId: '26047', // required + size: '250X250', // required placementId: '123', - size: '250X250' + bidFloor: '0.001' } }] }]; diff --git a/test/spec/modules/platformioBidAdapter_spec.js b/test/spec/modules/platformioBidAdapter_spec.js index 86bf52cac72..d775229c9e3 100644 --- a/test/spec/modules/platformioBidAdapter_spec.js +++ b/test/spec/modules/platformioBidAdapter_spec.js @@ -11,7 +11,8 @@ describe('Platformio Adapter Tests', () => { pubId: '28082', siteId: '26047', placementId: '123', - size: '300x250' + size: '300x250', + bidFloor: '0.001' } }, { placementCode: '/DfpAccount2/slot2', @@ -44,11 +45,13 @@ describe('Platformio Adapter Tests', () => { expect(ortbRequest.imp[0].banner).to.not.equal(null); expect(ortbRequest.imp[0].banner.w).to.equal(300); expect(ortbRequest.imp[0].banner.h).to.equal(250); + expect(ortbRequest.imp[0].bidfloor).to.equal('0.001'); // slot 2 expect(ortbRequest.imp[1].tagid).to.equal('456'); expect(ortbRequest.imp[1].banner).to.not.equal(null); expect(ortbRequest.imp[1].banner.w).to.equal(250); expect(ortbRequest.imp[1].banner.h).to.equal(250); + expect(ortbRequest.imp[1].bidfloor).to.equal('0.000001'); }); it('Verify parse response', () => { @@ -59,9 +62,11 @@ describe('Platformio Adapter Tests', () => { bid: [{ impid: ortbRequest.imp[0].id, price: 1.25, - adm: 'This is an Ad' + adm: 'This is an Ad', + adid: '471810', }] - }] + }], + cur: 'USD' }; const bids = spec.interpretResponse({ body: ortbResponse }, request); expect(bids).to.have.lengthOf(1); @@ -71,9 +76,9 @@ describe('Platformio Adapter Tests', () => { 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'); - expect(bid.creative_id).to.equal('bid12345'); - expect(bid.creativeId).to.equal('bid12345'); + expect(bid.creativeId).to.equal('471810'); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(360); }); it('Verify full passback', () => { From 02f07ee64663d4929887deaa76505337a9c516bb Mon Sep 17 00:00:00 2001 From: Theodore Rand <trand@sovrn.com> Date: Tue, 14 Nov 2017 08:41:04 -0700 Subject: [PATCH 27/32] Sovrn 1.0 compliance (#1796) * 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. * Re-wrote the Sovrn bid adapter to be compliant with Prebid 1.0.0. * Pushed bugfix found during whatismyip beta test, and small refactor. * Added README for adapter with test ad units. * Adjusted Sovrn bid adapter to correspond to new JSON structure passed into 'interpretResponse'. Updated README and tests per PR review. * removed unneeded biddercode param in adapter and fixed JSON spacing on input. * Final updates to remove bidder code from expected response in unit tests. * Reversed changes made to package.json and package-lock.json so that these files are not affected for this PR. * Removed package-lock.json file. --- modules/sovrnBidAdapter.js | 216 ++++++----------- modules/sovrnBidAdapter.md | 47 ++++ test/spec/modules/sovrnBidAdapter_spec.js | 269 +++++++++------------- 3 files changed, 232 insertions(+), 300 deletions(-) create mode 100644 modules/sovrnBidAdapter.md diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index a2fef49eaed..bf2f7f7b777 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,156 +1,82 @@ -var CONSTANTS = require('src/constants.json'); -var utils = require('src/utils.js'); -var bidfactory = require('src/bidfactory.js'); -var bidmanager = require('src/bidmanager.js'); -var adloader = require('src/adloader'); -var adaptermanager = require('src/adaptermanager'); - -/** - * Adapter for requesting bids from Sovrn - */ -var SovrnAdapter = function SovrnAdapter() { - var sovrnUrl = 'ap.lijit.com/rtb/bid'; - - function _callBids(params) { - var sovrnBids = params.bids || []; - - _requestBids(sovrnBids); - } - - function _requestBids(bidReqs) { - // build bid request object - var domain = window.location.host; - var page = window.location.pathname + location.search + location.hash; - - var sovrnImps = []; - - // build impression array for sovrn +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { BANNER } from 'src/mediaTypes'; +import { REPO_AND_VERSION } from 'src/constants'; + +export const spec = { + code: 'sovrn', + supportedMediaTypes: [BANNER], + + /** + * Check if the bid is a valid zone ID in either number or string form + * @param {object} bid the Sovrn bid to validate + * @return boolean for whether or not a bid is valid + */ + isBidRequestValid: function(bid) { + return !!(bid.params.tagid && !isNaN(parseFloat(bid.params.tagid)) && isFinite(bid.params.tagid)); + }, + + /** + * Format the bid request object for our endpoint + * @param {BidRequest[]} bidRequests Array of Sovrn bidders + * @return object of parameters for Prebid AJAX request + */ + buildRequests: function(bidReqs) { + let sovrnImps = []; utils._each(bidReqs, function (bid) { - var tagId = utils.getBidIdParameter('tagid', bid.params); - var bidFloor = utils.getBidIdParameter('bidfloor', bid.params); - var adW = 0; - var adH = 0; - - // sovrn supports only one size per tagid, so we just take the first size if there are more - // if we are a 2 item array of 2 numbers, we must be a SingleSize array - var bidSizes = Array.isArray(bid.params.sizes) ? bid.params.sizes : bid.sizes; - var sizeArrayLength = bidSizes.length; - if (sizeArrayLength === 2 && typeof bidSizes[0] === 'number' && typeof bidSizes[1] === 'number') { - adW = bidSizes[0]; - adH = bidSizes[1]; - } else { - adW = bidSizes[0][0]; - adH = bidSizes[0][1]; - } - - var imp = - { - id: bid.bidId, - banner: { - w: adW, - h: adH - }, - tagid: tagId, - bidfloor: bidFloor - }; - sovrnImps.push(imp); + sovrnImps.push({ + id: bid.bidId, + banner: { w: 1, h: 1 }, + tagid: utils.getBidIdParameter('tagid', bid.params), + bidfloor: utils.getBidIdParameter('bidfloor', bid.params) + }); }); - - // build bid request with impressions - var sovrnBidReq = { + const sovrnBidReq = { id: utils.getUniqueIdentifierStr(), imp: sovrnImps, site: { - domain: domain, - page: page + domain: window.location.host, + page: window.location.pathname + location.search + location.hash } }; - - var scriptUrl = '//' + sovrnUrl + '?callback=window.$$PREBID_GLOBAL$$.sovrnResponse' + - '&src=' + CONSTANTS.REPO_AND_VERSION + - '&br=' + encodeURIComponent(JSON.stringify(sovrnBidReq)); - adloader.loadScript(scriptUrl); - } - - function addBlankBidResponses(impidsWithBidBack) { - var missing = utils.getBidderRequestAllAdUnits('sovrn'); - if (missing) { - missing = missing.bids.filter(bid => impidsWithBidBack.indexOf(bid.bidId) < 0); - } else { - missing = []; - } - - missing.forEach(function (bidRequest) { - // Add a no-bid response for this bid request. - var bid = {}; - bid = bidfactory.createBid(2, bidRequest); - bid.bidderCode = 'sovrn'; - bidmanager.addBidResponse(bidRequest.placementCode, bid); - }); - } - - // expose the callback to the global object: - $$PREBID_GLOBAL$$.sovrnResponse = function (sovrnResponseObj) { - var impidsWithBidBack = []; - - // valid response object from sovrn - if (sovrnResponseObj && sovrnResponseObj.id && sovrnResponseObj.seatbid && sovrnResponseObj.seatbid.length !== 0 && - sovrnResponseObj.seatbid[0].bid && sovrnResponseObj.seatbid[0].bid.length !== 0) { - sovrnResponseObj.seatbid[0].bid.forEach(function (sovrnBid) { - var responseCPM; - var placementCode = ''; - var id = sovrnBid.impid; - var bid = {}; - - var bidObj = utils.getBidRequest(id); - - if (bidObj) { - placementCode = bidObj.placementCode; - bidObj.status = CONSTANTS.STATUS.GOOD; - - responseCPM = parseFloat(sovrnBid.price); - - if (responseCPM !== 0) { - sovrnBid.placementCode = placementCode; - sovrnBid.size = bidObj.sizes; - var responseAd = sovrnBid.adm; - - // build impression url from response - var responseNurl = '<img src="' + sovrnBid.nurl + '">'; - - // store bid response - // bid status is good (indicating 1) - bid = bidfactory.createBid(1, bidObj); - bid.creative_id = sovrnBid.id; - bid.bidderCode = 'sovrn'; - bid.cpm = responseCPM; - - // set ad content + impression url - // sovrn returns <script> block, so use bid.ad, not bid.adurl - bid.ad = decodeURIComponent(responseAd + responseNurl); - - // Set width and height from response now - bid.width = parseInt(sovrnBid.w); - bid.height = parseInt(sovrnBid.h); - - if (sovrnBid.dealid) { - bid.dealId = sovrnBid.dealid; - } - - bidmanager.addBidResponse(placementCode, bid); - impidsWithBidBack.push(id); - } - } + return { + method: 'POST', + url: `//ap.lijit.com/rtb/bid?src=${REPO_AND_VERSION}`, + data: JSON.stringify(sovrnBidReq), + options: {contentType: 'text/plain'} + }; + }, + + /** + * Format Sovrn responses as Prebid bid responses + * @param {id, seatbid} sovrnResponse A successful response from Sovrn. + * @return {Bid[]} An array of formatted bids. + */ + interpretResponse: function({ body: {id, seatbid} }) { + let sovrnBidResponses = []; + if (id && + seatbid && + seatbid.length > 0 && + seatbid[0].bid && + seatbid[0].bid.length > 0) { + seatbid[0].bid.map(sovrnBid => { + sovrnBidResponses.push({ + requestId: sovrnBid.impid, + cpm: parseFloat(sovrnBid.price), + width: parseInt(sovrnBid.w), + height: parseInt(sovrnBid.h), + creativeId: sovrnBid.id, + dealId: sovrnBid.dealId || null, + currency: 'USD', + netRevenue: true, + mediaType: BANNER, + ad: decodeURIComponent(`${sovrnBid.adm}<img src="${sovrnBid.nurl}">`), + ttl: 60000 + }); }); } - addBlankBidResponses(impidsWithBidBack); - }; - - return { - callBids: _callBids - }; + return sovrnBidResponses; + } }; -adaptermanager.registerBidAdapter(new SovrnAdapter(), 'sovrn'); - -module.exports = SovrnAdapter; +registerBidder(spec); diff --git a/modules/sovrnBidAdapter.md b/modules/sovrnBidAdapter.md new file mode 100644 index 00000000000..3b189016a9f --- /dev/null +++ b/modules/sovrnBidAdapter.md @@ -0,0 +1,47 @@ +# Overview + +``` +Module Name: Sovrn Bid Adapter +Module Type: Bidder Adapter +Maintainer: trand@sovrn.com +``` + +# Description + +Sovrn's adapter integration to the Prebid library. Posts plain-text JSON to the /rtb/bid endpoint. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'test-leaderboard', + sizes: [[728, 90]], + bids: [{ + bidder: 'sovrn', + params: { + tagid: '403370', + bidfloor: 0.01 + } + }] + }, { + code: 'test-banner', + sizes: [[300, 250]], + bids: [{ + bidder: 'sovrn', + params: { + tagid: '403401' + } + }] + }, { + code: 'test-sidebar', + size: [[160, 600]], + bids: [{ + bidder: 'sovrn', + params: { + tagid: '531000' + } + }] + } +] +``` \ No newline at end of file diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index c5ad1b2cb25..c4eadd02738 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -1,178 +1,137 @@ -import {expect} from 'chai'; -import Adapter from 'modules/sovrnBidAdapter'; -import bidmanager from 'src/bidmanager'; -import adloader from 'src/adloader'; -var utils = require('src/utils'); - -describe('sovrn adapter tests', function () { - let adapter; - const bidderRequest = { - bidderCode: 'sovrn', - bids: [ - { - bidId: 'bidId1', - bidder: 'sovrn', - params: { - tagid: '315045', - bidfloor: 1.25 - }, - sizes: [[320, 50]], - placementCode: 'div-gpt-ad-12345-1' - }, - { - bidId: 'bidId2', - bidder: 'sovrn', - params: { - tagid: '315046' - }, - sizes: [[320, 50]], - placementCode: 'div-gpt-ad-12345-2' - }, - { - bidId: 'bidId3', - bidder: 'sovrn', - params: { - tagid: '315047' - }, - sizes: [[320, 50]], - placementCode: 'div-gpt-ad-12345-2' - }, - ] - }; +import { expect } from 'chai'; +import { spec } from 'modules/sovrnBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; +import { REPO_AND_VERSION } from 'src/constants'; - beforeEach(() => adapter = new Adapter()); +const ENDPOINT = `//ap.lijit.com/rtb/bid?src=${REPO_AND_VERSION}`; - describe('requestBids', function () { - let stubLoadScript; +describe('sovrnBidAdapter', function() { + const adapter = newBidder(spec); - beforeEach(() => { - stubLoadScript = sinon.stub(adloader, 'loadScript'); + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'sovrn', + 'params': { + 'tagid': '403370' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); }); - afterEach(() => { - stubLoadScript.restore(); + it('should return false when tagid not passed correctly', () => { + bid.params.tagid = 'ABCD'; + expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('exists and is a function', () => { - expect(adapter.callBids).to.exist.and.to.be.a('function'); - }); - - it('loads the request script', function () { - adapter.callBids(bidderRequest); - - let sovrnScript = decodeURIComponent(stubLoadScript.getCall(0).args[0]); - let firstExpectedImpObj = '{"id":"bidId1","banner":{"w":320,"h":50},"tagid":"315045","bidfloor":1.25}'; - let secondExpectedImpObj = '{"id":"bidId2","banner":{"w":320,"h":50},"tagid":"315046","bidfloor":""}'; - - expect(sovrnScript).to.contain(firstExpectedImpObj); - expect(sovrnScript).to.contain(secondExpectedImpObj); + it('should return false when require params are not passed', () => { + let bid = Object.assign({}, bid); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); }); }); - describe('sovrnResponse', function () { - let stubAddBidResponse; - let getRequestStub; - let getRequestsStub; - - beforeEach(() => { - stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - - getRequestStub = sinon.stub(utils, 'getBidRequest'); - getRequestStub.withArgs(bidderRequest.bids[0].bidId).returns(bidderRequest.bids[0]); - getRequestStub.withArgs(bidderRequest.bids[1].bidId).returns(bidderRequest.bids[1]); - getRequestStub.withArgs(bidderRequest.bids[2].bidId).returns(bidderRequest.bids[2]); - - getRequestsStub = sinon.stub(utils, 'getBidderRequestAllAdUnits'); - getRequestsStub.returns(bidderRequest); + describe('buildRequests', () => { + let bidRequests = [{ + 'bidder': 'sovrn', + 'params': { + 'tagid': '403370' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + }]; + + const request = spec.buildRequests(bidRequests); + + it('sends bid request to our endpoint via POST', () => { + expect(request.method).to.equal('POST'); }); - afterEach(() => { - stubAddBidResponse.restore(); - getRequestStub.restore(); - getRequestsStub.restore(); + it('attaches source and version to endpoint URL as query params', () => { + expect(request.url).to.equal(ENDPOINT) }); + }); - it('should exist and be a function', function () { - expect($$PREBID_GLOBAL$$.sovrnResponse).to.exist.and.to.be.a('function'); + describe('interpretResponse', () => { + let response = { + body: { + 'id': '37386aade21a71', + 'seatbid': [{ + 'bid': [{ + 'id': 'a_403370_332fdb9b064040ddbec05891bd13ab28', + 'impid': '263c448586f5a1', + 'price': 0.45882675, + 'nurl': '<!-- NURL -->', + 'adm': '<!-- Creative -->', + 'h': 90, + 'w': 728 + }] + }] + } + }; + + it('should get the correct bid response', () => { + let expectedResponse = [{ + 'requestId': '263c448586f5a1', + 'cpm': 0.45882675, + 'width': 728, + 'height': 90, + 'creativeId': 'a_403370_332fdb9b064040ddbec05891bd13ab28', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': decodeURIComponent(`<!-- Creative --><img src=<!-- NURL -->>`), + 'ttl': 60000 + }]; + + let result = spec.interpretResponse(response); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); - it('should add empty bid responses if no bids returned', function () { - let response = { - 'id': '54321', - 'seatbid': [] - }; - - $$PREBID_GLOBAL$$.sovrnResponse(response); - - let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - let bidObject1 = stubAddBidResponse.getCall(0).args[1]; - let bidPlacementCode2 = stubAddBidResponse.getCall(1).args[0]; - let bidObject2 = stubAddBidResponse.getCall(1).args[1]; - let bidPlacementCode3 = stubAddBidResponse.getCall(2).args[0]; - let bidObject3 = stubAddBidResponse.getCall(2).args[1]; - - expect(bidPlacementCode1).to.equal('div-gpt-ad-12345-1'); - expect(bidObject1.getStatusCode()).to.equal(2); - expect(bidObject1.bidderCode).to.equal('sovrn'); - - expect(bidPlacementCode2).to.equal('div-gpt-ad-12345-2'); - expect(bidObject2.getStatusCode()).to.equal(2); - expect(bidObject2.bidderCode).to.equal('sovrn'); - - expect(bidPlacementCode3).to.equal('div-gpt-ad-12345-2'); - expect(bidObject3.getStatusCode()).to.equal(2); - expect(bidObject3.bidderCode).to.equal('sovrn'); - - stubAddBidResponse.calledThrice; + it('should get correct bid response when dealId is passed', () => { + response.body.dealId = 'baking'; + + let expectedResponse = [{ + 'requestId': '263c448586f5a1', + 'cpm': 0.45882675, + 'width': 728, + 'height': 90, + 'creativeId': 'a_403370_332fdb9b064040ddbec05891bd13ab28', + 'dealId': 'baking', + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': decodeURIComponent(`<!-- Creative --><img src=<!-- NURL -->>`), + 'ttl': 60000 + }]; + + let result = spec.interpretResponse(response); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); - it('should add a bid response for bids returned and empty bid responses for the rest', function () { + it('handles empty bid response', () => { let response = { - 'id': '54321111', - 'seatbid': [ { - 'bid': [ { - 'id': '1111111', - 'impid': 'bidId2', - 'price': 0.09, - 'nurl': 'http://url', - 'adm': 'ad-code', - 'h': 250, - 'w': 300, - 'dealid': 'ADEAL123', - 'ext': { } - } ] - } ] + body: { + 'id': '37386aade21a71', + 'seatbid': [] + } }; - - $$PREBID_GLOBAL$$.sovrnResponse(response); - - let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - let bidObject1 = stubAddBidResponse.getCall(0).args[1]; - let bidPlacementCode2 = stubAddBidResponse.getCall(1).args[0]; - let bidObject2 = stubAddBidResponse.getCall(1).args[1]; - let bidPlacementCode3 = stubAddBidResponse.getCall(2).args[0]; - let bidObject3 = stubAddBidResponse.getCall(2).args[1]; - - expect(bidPlacementCode1).to.equal('div-gpt-ad-12345-2'); - expect(bidObject1.getStatusCode()).to.equal(1); - expect(bidObject1.bidderCode).to.equal('sovrn'); - expect(bidObject1.creative_id).to.equal('1111111'); - expect(bidObject1.cpm).to.equal(0.09); - expect(bidObject1.height).to.equal(250); - expect(bidObject1.width).to.equal(300); - expect(bidObject1.ad).to.equal('ad-code<img src="http://url">'); - expect(bidObject1.adId).to.equal('bidId2'); - expect(bidObject1.dealId).to.equal('ADEAL123'); - - expect(bidPlacementCode2).to.equal('div-gpt-ad-12345-1'); - expect(bidObject2.getStatusCode()).to.equal(2); - expect(bidObject2.bidderCode).to.equal('sovrn'); - - expect(bidPlacementCode3).to.equal('div-gpt-ad-12345-2'); - expect(bidObject3.getStatusCode()).to.equal(2); - expect(bidObject3.bidderCode).to.equal('sovrn'); - - stubAddBidResponse.calledThrice; + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); }); }); }); From 81a3f5f7fbd470d220c8933ebf46c12391b0ab92 Mon Sep 17 00:00:00 2001 From: Valentin Zhukovsky <kventin_zhuk@outlook.com> Date: Tue, 14 Nov 2017 22:35:59 +0300 Subject: [PATCH 28/32] Update AOL adapter for v1.0 (#1693) * Converted AOL bid adapter to Prebid 1.0. * Added description file for AOL Adapter. * Move one mobile post properties to options object. * Implemented AOL user syncs config via setConfig API. * Refactored Marketplace tests for Prebid 1.0. * Refactored One Mobile tests for Prebid 1.0. * Fixed faining tests for One Mobile endpoint. * Refactored get userSyncs tests for Prebid 1.0. * Refactored interpretResponse tests for Prebid 1.0. * Remove outdated tests. * Fixed review comments. * Added ttl support for AOL adapter. * Improved aol adapter docs. --- modules/aolBidAdapter.js | 574 +++++++-------- modules/aolBidAdapter.md | 47 ++ src/adapters/bidderFactory.js | 10 +- test/spec/modules/aolBidAdapter_spec.js | 895 ++++++++++-------------- 4 files changed, 704 insertions(+), 822 deletions(-) create mode 100644 modules/aolBidAdapter.md diff --git a/modules/aolBidAdapter.js b/modules/aolBidAdapter.js index 5adba83e9df..c76bd1ccfd6 100644 --- a/modules/aolBidAdapter.js +++ b/modules/aolBidAdapter.js @@ -1,341 +1,349 @@ -const utils = require('src/utils.js'); -const ajax = require('src/ajax.js').ajax; -const bidfactory = require('src/bidfactory.js'); -const bidmanager = require('src/bidmanager.js'); -const constants = require('src/constants.json'); -const adaptermanager = require('src/adaptermanager'); -const BaseAdapter = require('src/adapter').default; +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { config } from 'src/config'; +import constants from 'src/constants.json'; const AOL_BIDDERS_CODES = { - aol: 'aol', - onemobile: 'onemobile', - onedisplay: 'onedisplay' + AOL: 'aol', + ONEMOBILE: 'onemobile', + ONEDISPLAY: 'onedisplay' }; +const AOL_ENDPOINTS = { + DISPLAY: { + GET: 'display-get' + }, + MOBILE: { + GET: 'mobile-get', + POST: 'mobile-post' + } +}; + +const SYNC_TYPES = { + IFRAME: { + TAG: 'iframe', + TYPE: 'iframe' + }, + IMAGE: { + TAG: 'img', + TYPE: 'image' + } +}; + +const pubapiTemplate = template`//${'host'}/pubapi/3.0/${'network'}/${'placement'}/${'pageid'}/${'sizeid'}/ADTECH;v=2;cmd=bid;cors=yes;alias=${'alias'}${'bidfloor'}${'keyValues'};misc=${'misc'}`; +const nexageBaseApiTemplate = template`//${'host'}/bidRequest?`; +const nexageGetApiTemplate = template`dcn=${'dcn'}&pos=${'pos'}&cmd=bid${'ext'}`; +const MP_SERVER_MAP = { + us: 'adserver-us.adtech.advertising.com', + eu: 'adserver-eu.adtech.advertising.com', + as: 'adserver-as.adtech.advertising.com' +}; +const NEXAGE_SERVER = 'hb.nexage.com'; +const BID_RESPONSE_TTL = 300; + $$PREBID_GLOBAL$$.aolGlobals = { pixelsDropped: false }; -const AolAdapter = function AolAdapter() { - let showCpmAdjustmentWarning = true; - const pubapiTemplate = template`${'protocol'}://${'host'}/pubapi/3.0/${'network'}/${'placement'}/${'pageid'}/${'sizeid'}/ADTECH;v=2;cmd=bid;cors=yes;alias=${'alias'}${'bidfloor'};misc=${'misc'}`; - const nexageBaseApiTemplate = template`${'protocol'}://${'host'}/bidRequest?`; - const nexageGetApiTemplate = template`dcn=${'dcn'}&pos=${'pos'}&cmd=bid${'ext'}`; - const MP_SERVER_MAP = { - us: 'adserver-us.adtech.advertising.com', - eu: 'adserver-eu.adtech.advertising.com', - as: 'adserver-as.adtech.advertising.com' +let showCpmAdjustmentWarning = (function () { + let showCpmWarning = true; + + return function () { + let bidderSettings = $$PREBID_GLOBAL$$.bidderSettings; + if (showCpmWarning && bidderSettings && bidderSettings.aol && + typeof bidderSettings.aol.bidCpmAdjustment === 'function') { + utils.logWarn( + 'bidCpmAdjustment is active for the AOL adapter. ' + + 'As of Prebid 0.14, AOL can bid in net – please contact your accounts team to enable.' + ); + showCpmWarning = false; // warning is shown at most once + } }; - const NEXAGE_SERVER = 'hb.nexage.com'; - const SYNC_TYPES = { - iframe: 'IFRAME', - img: 'IMG' +})(); + +function template(strings, ...keys) { + return function(...values) { + let dict = values[values.length - 1] || {}; + let result = [strings[0]]; + keys.forEach(function(key, i) { + let value = Number.isInteger(key) ? values[key] : dict[key]; + result.push(value, strings[i + 1]); + }); + return result.join(''); }; - - let domReady = (() => { - let readyEventFired = false; - return fn => { - let idempotentFn = () => { - if (readyEventFired) { - return; +} + +function isSecureProtocol() { + return document.location.protocol === 'https:'; +} + +function parsePixelItems(pixels) { + let itemsRegExp = /(img|iframe)[\s\S]*?src\s*=\s*("|')(.*?)\2/gi; + let tagNameRegExp = /\w*(?=\s)/; + let srcRegExp = /src=("|')(.*?)\1/; + let pixelsItems = []; + + if (pixels) { + let matchedItems = pixels.match(itemsRegExp); + if (matchedItems) { + matchedItems.forEach(item => { + let tagName = item.match(tagNameRegExp)[0]; + let url = item.match(srcRegExp)[2]; + + if (tagName && tagName) { + pixelsItems.push({ + type: tagName === SYNC_TYPES.IMAGE.TAG ? SYNC_TYPES.IMAGE.TYPE : SYNC_TYPES.IFRAME.TYPE, + url: url + }); } - readyEventFired = true; - return fn(); - }; - - if (document.readyState === 'complete') { - return idempotentFn(); - } - - document.addEventListener('DOMContentLoaded', idempotentFn, false); - window.addEventListener('load', idempotentFn, false); - }; - })(); - - function dropSyncCookies(pixels) { - if (!$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped) { - let pixelElements = parsePixelItems(pixels); - renderPixelElements(pixelElements); - $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = true; + }); } } - function parsePixelItems(pixels) { - let itemsRegExp = /(img|iframe)[\s\S]*?src\s*=\s*("|')(.*?)\2/gi; - let tagNameRegExp = /\w*(?=\s)/; - let srcRegExp = /src=("|')(.*?)\1/; - let pixelsItems = []; - - if (pixels) { - let matchedItems = pixels.match(itemsRegExp); - if (matchedItems) { - matchedItems.forEach(item => { - let tagNameMatches = item.match(tagNameRegExp); - let sourcesPathMatches = item.match(srcRegExp); - if (tagNameMatches && sourcesPathMatches) { - pixelsItems.push({ - tagName: tagNameMatches[0].toUpperCase(), - src: sourcesPathMatches[2] - }); - } - }); - } - } + return pixelsItems; +} - return pixelsItems; - } +function _buildMarketplaceUrl(bid) { + const params = bid.params; + const serverParam = params.server; + let regionParam = params.region || 'us'; + let server; - function renderPixelElements(pixelsElements) { - pixelsElements.forEach((element) => { - switch (element.tagName) { - case SYNC_TYPES.img: - return renderPixelImage(element); - case SYNC_TYPES.iframe: - return renderPixelIframe(element); - } - }); + if (!MP_SERVER_MAP.hasOwnProperty(regionParam)) { + utils.logWarn(`Unknown region '${regionParam}' for AOL bidder.`); + regionParam = 'us'; // Default region. } - function renderPixelImage(pixelsItem) { - let image = new Image(); - image.src = pixelsItem.src; + if (serverParam) { + server = serverParam; + } else { + server = MP_SERVER_MAP[regionParam]; } - function renderPixelIframe(pixelsItem) { - let iframe = document.createElement('iframe'); - iframe.width = 1; - iframe.height = 1; - iframe.style.display = 'none'; - iframe.src = pixelsItem.src; - if (document.readyState === 'interactive' || - document.readyState === 'complete') { - document.body.appendChild(iframe); - } else { - domReady(() => { - document.body.appendChild(iframe); - }); - } - } + // Set region param, used by AOL analytics. + params.region = regionParam; + + return pubapiTemplate({ + host: server, + network: params.network, + placement: parseInt(params.placement), + pageid: params.pageId || 0, + sizeid: params.sizeId || 0, + alias: params.alias || utils.getUniqueIdentifierStr(), + bidfloor: formatMarketplaceBidFloor(params.bidFloor), + keyValues: formatMarketplaceKeyValues(params.keyValues), + misc: new Date().getTime() // cache busting + }); +} - function template(strings, ...keys) { - return function(...values) { - let dict = values[values.length - 1] || {}; - let result = [strings[0]]; - keys.forEach(function(key, i) { - let value = Number.isInteger(key) ? values[key] : dict[key]; - result.push(value, strings[i + 1]); - }); - return result.join(''); - }; - } +function formatMarketplaceBidFloor(bidFloor) { + return (typeof bidFloor !== 'undefined') ? `;bidfloor=${bidFloor.toString()}` : ''; +} - function _buildMarketplaceUrl(bid) { - const params = bid.params; - const serverParam = params.server; - let regionParam = params.region || 'us'; - let server; +function formatMarketplaceKeyValues(keyValues) { + let formattedKeyValues = ''; - if (!MP_SERVER_MAP.hasOwnProperty(regionParam)) { - utils.logWarn(`Unknown region '${regionParam}' for AOL bidder.`); - regionParam = 'us'; // Default region. - } + utils._each(keyValues, (value, key) => { + formattedKeyValues += `;kv${key}=${encodeURIComponent(value)}`; + }); - if (serverParam) { - server = serverParam; - } else { - server = MP_SERVER_MAP[regionParam]; - } + return formattedKeyValues; +} - // Set region param, used by AOL analytics. - params.region = regionParam; - - return pubapiTemplate({ - protocol: (document.location.protocol === 'https:') ? 'https' : 'http', - host: server, - network: params.network, - placement: parseInt(params.placement), - pageid: params.pageId || 0, - sizeid: params.sizeId || 0, - alias: params.alias || utils.getUniqueIdentifierStr(), - bidfloor: (typeof params.bidFloor !== 'undefined') - ? `;bidfloor=${params.bidFloor.toString()}` : '', - misc: new Date().getTime() // cache busting +function _buildOneMobileBaseUrl(bid) { + return nexageBaseApiTemplate({ + host: bid.params.host || NEXAGE_SERVER + }); +} + +function _buildOneMobileGetUrl(bid) { + let {dcn, pos} = bid.params; + let nexageApi = _buildOneMobileBaseUrl(bid); + if (dcn && pos) { + let ext = ''; + if (isSecureProtocol()) { + bid.params.ext = bid.params.ext || {}; + bid.params.ext.secure = 1; + } + utils._each(bid.params.ext, (value, key) => { + ext += `&${key}=${encodeURIComponent(value)}`; }); + nexageApi += nexageGetApiTemplate({dcn, pos, ext}); } + return nexageApi; +} - function _buildNexageApiUrl(bid) { - let {dcn, pos} = bid.params; - let isSecure = (document.location.protocol === 'https:'); - let nexageApi = nexageBaseApiTemplate({ - 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)}`; - }); - nexageApi += nexageGetApiTemplate({dcn, pos, ext}); - } - return nexageApi; - } +function _parseBidResponse(response, bidRequest) { + let bidData; - function _addErrorBidResponse(bid, response = {}) { - const bidResponse = bidfactory.createBid(2, bid); - bidResponse.bidderCode = bid.bidder; - bidResponse.reason = response.nbr; - bidResponse.raw = response; - bidmanager.addBidResponse(bid.placementCode, bidResponse); + try { + bidData = response.seatbid[0].bid[0]; + } catch (e) { + return; } - function _addBidResponse(bid, response) { - let bidData; + let cpm; - try { - bidData = response.seatbid[0].bid[0]; - } catch (e) { - _addErrorBidResponse(bid, response); - return; - } - - let cpm; + if (bidData.ext && bidData.ext.encp) { + cpm = bidData.ext.encp; + } else { + cpm = bidData.price; - if (bidData.ext && bidData.ext.encp) { - cpm = bidData.ext.encp; - } else { - cpm = bidData.price; - - if (cpm === null || isNaN(cpm)) { - utils.logError('Invalid price in bid response', AOL_BIDDERS_CODES.aol, bid); - _addErrorBidResponse(bid, response); - return; - } + if (cpm === null || isNaN(cpm)) { + utils.logError('Invalid price in bid response', AOL_BIDDERS_CODES.AOL, bid); + return; } + } - let ad = bidData.adm; - if (response.ext && response.ext.pixels) { - if (bid.params.userSyncOn === constants.EVENTS.BID_RESPONSE) { - dropSyncCookies(response.ext.pixels); - } else { - let formattedPixels = response.ext.pixels.replace(/<\/?script( type=('|")text\/javascript('|")|)?>/g, ''); - - ad += '<script>if(!parent.$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped){' + - 'parent.$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped=true;' + formattedPixels + - '}</script>'; - } - } + let ad = bidData.adm; + if (response.ext && response.ext.pixels) { + if (config.getConfig('aol.userSyncOn') !== constants.EVENTS.BID_RESPONSE) { + let formattedPixels = response.ext.pixels.replace(/<\/?script( type=('|")text\/javascript('|")|)?>/g, ''); - const bidResponse = bidfactory.createBid(1, bid); - bidResponse.bidderCode = bid.bidder; - bidResponse.ad = ad; - bidResponse.cpm = cpm; - bidResponse.width = bidData.w; - bidResponse.height = bidData.h; - bidResponse.creativeId = bidData.crid; - bidResponse.pubapiId = response.id; - bidResponse.currencyCode = response.cur; - if (bidData.dealid) { - bidResponse.dealId = bidData.dealid; + ad += '<script>if(!parent.$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped){' + + 'parent.$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped=true;' + formattedPixels + + '}</script>'; } - - bidmanager.addBidResponse(bid.placementCode, bidResponse); } - function _isMarketplaceBidder(bidder) { - return bidder === AOL_BIDDERS_CODES.aol || bidder === AOL_BIDDERS_CODES.onedisplay; - } + return { + bidderCode: bidRequest.bidderCode, + requestId: bidRequest.bidId, + ad: ad, + cpm: cpm, + width: bidData.w, + height: bidData.h, + creativeId: bidData.crid, + pubapiId: response.id, + currency: response.cur, + dealId: bidData.dealid, + netRevenue: true, + ttl: BID_RESPONSE_TTL + }; +} - function _isNexageBidder(bidder) { - return bidder === AOL_BIDDERS_CODES.aol || bidder === AOL_BIDDERS_CODES.onemobile; - } +function _isMarketplaceBidder(bidder) { + return bidder === AOL_BIDDERS_CODES.AOL || bidder === AOL_BIDDERS_CODES.ONEDISPLAY; +} + +function _isNexageBidder(bidder) { + return bidder === AOL_BIDDERS_CODES.AOL || bidder === AOL_BIDDERS_CODES.ONEMOBILE; +} - function _isNexageRequestPost(bid) { - if (_isNexageBidder(bid.bidder) && bid.params.id && bid.params.imp && bid.params.imp[0]) { - let imp = bid.params.imp[0]; - return imp.id && imp.tagid && - ((imp.banner && imp.banner.w && imp.banner.h) || +function _isNexageRequestPost(bid) { + if (_isNexageBidder(bid.bidder) && bid.params.id && bid.params.imp && bid.params.imp[0]) { + let imp = bid.params.imp[0]; + return imp.id && imp.tagid && + ((imp.banner && imp.banner.w && imp.banner.h) || (imp.video && imp.video.mimes && imp.video.minduration && imp.video.maxduration)); - } } - - function _isNexageRequestGet(bid) { - return _isNexageBidder(bid.bidder) && bid.params.dcn && bid.params.pos; +} + +function _isNexageRequestGet(bid) { + return _isNexageBidder(bid.bidder) && bid.params.dcn && bid.params.pos; +} + +function isMarketplaceBid(bid) { + return _isMarketplaceBidder(bid.bidder) && bid.params.placement && bid.params.network; +} + +function isMobileBid(bid) { + return _isNexageRequestGet(bid) || _isNexageRequestPost(bid); +} + +function resolveEndpointCode(bid) { + if (_isNexageRequestGet(bid)) { + return AOL_ENDPOINTS.MOBILE.GET; + } else if (_isNexageRequestPost(bid)) { + return AOL_ENDPOINTS.MOBILE.POST; + } else if (isMarketplaceBid(bid)) { + return AOL_ENDPOINTS.DISPLAY.GET; } +} - function _isMarketplaceRequest(bid) { - return _isMarketplaceBidder(bid.bidder) && bid.params.placement && bid.params.network; - } +function formatBidRequest(endpointCode, bid) { + let bidRequest; + + switch (endpointCode) { + case AOL_ENDPOINTS.DISPLAY.GET: + bidRequest = { + url: _buildMarketplaceUrl(bid), + method: 'GET' + }; + break; - function _callBids(params) { - utils._each(params.bids, bid => { - let apiUrl; - let data = null; - let options = { - withCredentials: true + case AOL_ENDPOINTS.MOBILE.GET: + bidRequest = { + url: _buildOneMobileGetUrl(bid), + method: 'GET' }; - let isNexageRequestPost = _isNexageRequestPost(bid); - let isNexageRequestGet = _isNexageRequestGet(bid); - let isMarketplaceRequest = _isMarketplaceRequest(bid); - - if (isNexageRequestGet || isNexageRequestPost) { - apiUrl = _buildNexageApiUrl(bid); - if (isNexageRequestPost) { - data = bid.params; - options.customHeaders = { + break; + + case AOL_ENDPOINTS.MOBILE.POST: + bidRequest = { + url: _buildOneMobileBaseUrl(bid), + method: 'POST', + data: bid.params, + options: { + contentType: 'application/json', + customHeaders: { 'x-openrtb-version': '2.2' - }; - options.method = 'POST'; - options.contentType = 'application/json'; + } } - } else if (isMarketplaceRequest) { - apiUrl = _buildMarketplaceUrl(bid); - } + }; + break; + } - if (apiUrl) { - ajax(apiUrl, response => { - // Needs to be here in case bidderSettings are defined after requestBids() is called - if (showCpmAdjustmentWarning && - $$PREBID_GLOBAL$$.bidderSettings && $$PREBID_GLOBAL$$.bidderSettings.aol && - typeof $$PREBID_GLOBAL$$.bidderSettings.aol.bidCpmAdjustment === 'function' - ) { - utils.logWarn( - 'bidCpmAdjustment is active for the AOL adapter. ' + - 'As of Prebid 0.14, AOL can bid in net – please contact your accounts team to enable.' - ); - } - showCpmAdjustmentWarning = false; // warning is shown at most once + bidRequest.bidderCode = bid.bidder; + bidRequest.bidId = bid.bidId; + bidRequest.userSyncOn = bid.params.userSyncOn; - if (!response && response.length <= 0) { - utils.logError('Empty bid response', AOL_BIDDERS_CODES.aol, bid); - _addErrorBidResponse(bid, response); - return; - } + return bidRequest; +} - try { - response = JSON.parse(response); - } catch (e) { - utils.logError('Invalid JSON in bid response', AOL_BIDDERS_CODES.aol, bid); - _addErrorBidResponse(bid, response); - return; - } +function interpretResponse({body}, bidRequest) { + showCpmAdjustmentWarning(); - _addBidResponse(bid, response); - }, data, options); + if (!body) { + utils.logError('Empty bid response', bidRequest.bidderCode, body); + } else { + let bid = _parseBidResponse(body, bidRequest); + + if (bid) { + return bid; + } + } +} + +export const spec = { + code: AOL_BIDDERS_CODES.AOL, + aliases: [AOL_BIDDERS_CODES.ONEMOBILE, AOL_BIDDERS_CODES.ONEDISPLAY], + isBidRequestValid: function(bid) { + return isMarketplaceBid(bid) || isMobileBid(bid); + }, + buildRequests: function (bids) { + return bids.map(bid => { + const endpointCode = resolveEndpointCode(bid); + + if (endpointCode) { + return formatBidRequest(endpointCode, bid); } }); - } + }, + interpretResponse: interpretResponse, + getUserSyncs: function(options, bidResponses) { + let bidResponse = bidResponses[0]; - return Object.assign(this, new BaseAdapter(AOL_BIDDERS_CODES.aol), { - callBids: _callBids - }); -}; + if (config.getConfig('aol.userSyncOn') === constants.EVENTS.BID_RESPONSE) { + if (!$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped && bidResponse.ext && bidResponse.ext.pixels) { + $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = true; -adaptermanager.registerBidAdapter(new AolAdapter(), AOL_BIDDERS_CODES.aol); -adaptermanager.aliasBidAdapter(AOL_BIDDERS_CODES.aol, AOL_BIDDERS_CODES.onedisplay); -adaptermanager.aliasBidAdapter(AOL_BIDDERS_CODES.aol, AOL_BIDDERS_CODES.onemobile); + return parsePixelItems(bidResponse.ext.pixels); + } + } + + return []; + } +}; -module.exports = AolAdapter; +registerBidder(spec); diff --git a/modules/aolBidAdapter.md b/modules/aolBidAdapter.md new file mode 100644 index 00000000000..a92e933bd36 --- /dev/null +++ b/modules/aolBidAdapter.md @@ -0,0 +1,47 @@ +# Overview + +Module Name: AOL Bid Adapter + +Module Type: AOL Adapter + +Maintainer: hb-fe-tech@oath.com + +# Description + +Module that connects to AOL's demand sources + +# Test Parameters +```javascript + var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'onedisplay', + params: { + placement: '3611253', + network: '9599.1', + bidFloor: '0.80', + keyValues: { + test: 'key' + } + } + } + ] + }, + { + code: 'test-mobile-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'onemobile', + params: { + dcn: '2c9d2b50015a5aa95b70a9b0b5b10012', + pos: 'header' + } + } + ] + } + ]; +``` diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 0b4b0d7cd0c..7535d6ad613 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -228,11 +228,19 @@ export function newBidder(spec) { const onResponse = delayExecution(afterAllResponses, requests.length) requests.forEach(processRequest); + function formatGetParameters(data) { + if (data) { + return `?${typeof data === 'object' ? parseQueryStringParameters(data) : data}`; + } + + return ''; + } + function processRequest(request) { switch (request.method) { case 'GET': ajax( - `${request.url}?${typeof request.data === 'object' ? parseQueryStringParameters(request.data) : request.data}`, + `${request.url}${formatGetParameters(request.data)}`, { success: onSuccess, error: onFailure diff --git a/test/spec/modules/aolBidAdapter_spec.js b/test/spec/modules/aolBidAdapter_spec.js index 188c5375b89..efa595ecc64 100644 --- a/test/spec/modules/aolBidAdapter_spec.js +++ b/test/spec/modules/aolBidAdapter_spec.js @@ -1,7 +1,7 @@ import {expect} from 'chai'; import * as utils from 'src/utils'; -import AolAdapter from 'modules/aolBidAdapter'; -import bidmanager from 'src/bidmanager'; +import {spec} from 'modules/aolBidAdapter'; +import {config} from 'src/config'; let getDefaultBidResponse = () => { return { @@ -13,9 +13,10 @@ let getDefaultBidResponse = () => { impid: '245730051428950632', price: 0.09, adm: '<script>logInfo(\'ad\');</script>', - crid: '0', + crid: 'creative-id', h: 90, w: 728, + dealid: 'deal-id', ext: {sizeid: 225} }] }] @@ -67,15 +68,16 @@ let getDefaultBidRequest = () => { }; }; +let getPixels = () => { + return '<script>document.write(\'<img src="img.org"></iframe>' + + '<iframe src="pixels1.org"></iframe>\');</script>'; +}; + describe('AolAdapter', () => { const MARKETPLACE_URL = 'adserver-us.adtech.advertising.com/pubapi/3.0/'; const NEXAGE_URL = 'hb.nexage.com/bidRequest?'; - let adapter; - - beforeEach(() => adapter = new AolAdapter()); - - function createBidderRequest({bids, params} = {}) { + function createCustomBidRequest({bids, params} = {}) { var bidderRequest = getDefaultBidRequest(); if (bids && Array.isArray(bids)) { bidderRequest.bids = bids; @@ -86,620 +88,437 @@ describe('AolAdapter', () => { return bidderRequest; } - describe('callBids()', () => { - it('exists and is a function', () => { - expect(adapter.callBids).to.exist.and.to.be.a('function'); + describe('interpretResponse()', () => { + let bidderSettingsBackup; + let bidResponse; + let bidRequest; + let logWarnSpy; + + beforeEach(() => { + bidderSettingsBackup = $$PREBID_GLOBAL$$.bidderSettings; + bidRequest = { + bidderCode: 'test-bidder-code', + bidId: 'bid-id' + }; + bidResponse = { + body: getDefaultBidResponse() + }; + logWarnSpy = sinon.spy(utils, 'logWarn'); }); - describe('bid request', () => { - describe('Marketplace api', () => { - let xhr; - let requests; - - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); - }); - - afterEach(() => xhr.restore()); - - it('requires parameters to be made', () => { - adapter.callBids({}); - expect(requests).to.be.empty; - }); - - it('should hit the Marketplace api endpoint with the Marketplace config', () => { - adapter.callBids(getDefaultBidRequest()); - expect(requests[0].url).to.contain(MARKETPLACE_URL); - }); - - it('should hit the Marketplace via onedisplay bidder code', () => { - let bidRequest = createBidderRequest({ - bids: [{ - bidder: 'onedisplay' - }], - params: getMarketplaceBidParams() - }); - - adapter.callBids(bidRequest); - expect(requests[0].url).to.contain(MARKETPLACE_URL); - }); - - it('should hit the Marketplace via onedisplay bidder code when Marketplace and Nexage params are present', () => { - let bidParams = Object.assign(getMarketplaceBidParams(), getNexageGetBidParams()); - let bidRequest = createBidderRequest({ - bids: [{ - bidder: 'onedisplay' - }], - params: bidParams - }); - - adapter.callBids(bidRequest); - expect(requests[0].url).to.contain(MARKETPLACE_URL); - }); - - it('should hit the Marketplace via onedisplay bidder code when Nexage params are present', () => { - let bidParams = Object.assign(getMarketplaceBidParams(), getNexageGetBidParams(), getNexagePostBidParams()); - let bidRequest = createBidderRequest({ - bids: [{ - bidder: 'onedisplay' - }], - params: bidParams - }); - - adapter.callBids(bidRequest); - expect(requests[0].url).to.contain(MARKETPLACE_URL); - }); - - it('should not resolve endpoint for onedisplay bidder code when only Nexage params are present', () => { - let bidParams = Object.assign(getNexageGetBidParams(), getNexagePostBidParams()); - - adapter.callBids(createBidderRequest({ - bids: [{ - bidder: 'onedisplay' - }], - params: bidParams - })); - expect(requests.length).to.equal(0); - }); - - it('should hit endpoint based on the region config option', () => { - adapter.callBids(createBidderRequest({ - params: { - placement: 1234567, - network: '9599.1', - region: 'eu' - } - })); - expect(requests[0].url).to.contain('adserver-eu.adtech.advertising.com/pubapi/3.0/'); - }); - - it('should hit the default endpoint in case of unknown region config option', () => { - adapter.callBids(createBidderRequest({ - params: { - placement: 1234567, - network: '9599.1', - region: 'an' - } - })); - expect(requests[0].url).to.contain(MARKETPLACE_URL); - }); - - it('should hit endpoint based on the server config option', () => { - adapter.callBids(createBidderRequest({ - params: { - placement: 1234567, - network: '9599.1', - server: 'adserver-eu.adtech.advertising.com' - } - })); - expect(requests[0].url).to.contain('adserver-eu.adtech.advertising.com/pubapi/3.0/'); - }); - - it('should be the pubapi bid request', () => { - adapter.callBids(getDefaultBidRequest()); - expect(requests[0].url).to.contain('cmd=bid;'); - }); - - it('should be the version 2 of pubapi', () => { - adapter.callBids(getDefaultBidRequest()); - expect(requests[0].url).to.contain('v=2;'); - }); - - it('should contain cache busting', () => { - adapter.callBids(getDefaultBidRequest()); - expect(requests[0].url).to.match(/misc=\d+/); - }); - - it('should contain required params - placement & network', () => { - adapter.callBids(createBidderRequest({ - params: { - placement: 1234567, - network: '9599.1' - } - })); - expect(requests[0].url).to.contain('/pubapi/3.0/9599.1/1234567/'); - }); - - it('should contain pageId and sizeId of 0 if params are missing', () => { - adapter.callBids(createBidderRequest({ - params: { - placement: 1234567, - network: '9599.1' - } - })); - expect(requests[0].url).to.contain('/pubapi/3.0/9599.1/1234567/0/0/ADTECH;'); - }); - - it('should contain pageId optional param', () => { - adapter.callBids(createBidderRequest({ - params: { - placement: 1234567, - network: '9599.1', - pageId: 12345 - } - })); - expect(requests[0].url).to.contain('/pubapi/3.0/9599.1/1234567/12345/'); - }); - - it('should contain sizeId optional param', () => { - adapter.callBids(createBidderRequest({ - params: { - placement: 1234567, - network: '9599.1', - sizeId: 12345 - } - })); - expect(requests[0].url).to.contain('/12345/ADTECH;'); - }); - - it('should contain generated alias if alias param is missing', () => { - adapter.callBids(createBidderRequest({ - params: { - placement: 1234567, - network: '9599.1' - } - })); - expect(requests[0].url).to.match(/alias=\w+?;/); - }); - - it('should contain alias optional param', () => { - adapter.callBids(createBidderRequest({ - params: { - placement: 1234567, - network: '9599.1', - alias: 'desktop_articlepage_something_box_300_250' - } - })); - expect(requests[0].url).to.contain('alias=desktop_articlepage_something_box_300_250'); - }); - - it('should not contain bidfloor if bidFloor param is missing', () => { - adapter.callBids(createBidderRequest({ - params: { - placement: 1234567, - network: '9599.1' - } - })); - expect(requests[0].url).not.to.contain('bidfloor='); - }); + afterEach(() => { + $$PREBID_GLOBAL$$.bidderSettings = bidderSettingsBackup; + logWarnSpy.restore(); + }); - it('should contain bidFloor optional param', () => { - adapter.callBids(createBidderRequest({ - params: { - placement: 1234567, - network: '9599.1', - bidFloor: 0.80 - } - })); - expect(requests[0].url).to.contain('bidfloor=0.8'); - }); + it('should return formatted bid response with required properties', () => { + let formattedBidResponse = spec.interpretResponse(bidResponse, bidRequest); + expect(formattedBidResponse).to.deep.equal({ + bidderCode: bidRequest.bidderCode, + requestId: 'bid-id', + ad: '<script>logInfo(\'ad\');</script>', + cpm: 0.09, + width: 728, + height: 90, + creativeId: 'creative-id', + pubapiId: '245730051428950632', + currency: 'USD', + dealId: 'deal-id', + netRevenue: true, + ttl: 300 }); + }); - describe('Nexage api', () => { - let xhr; - let requests; + it('should return formatted bid response including pixels', () => { + bidResponse.body.ext = { + pixels: '<script>document.write(\'<img src="pixel.gif">\');</script>' + }; - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); - }); + let formattedBidResponse = spec.interpretResponse(bidResponse, bidRequest); - afterEach(() => xhr.restore()); + expect(formattedBidResponse.ad).to.equal( + '<script>logInfo(\'ad\');</script>' + + '<script>if(!parent.$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped){' + + 'parent.$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped=true;' + + 'document.write(\'<img src="pixel.gif">\');}</script>' + ); + }); - it('requires parameters to be made', () => { - adapter.callBids({}); - expect(requests).to.be.empty; - }); + it('should show warning in the console', function() { + $$PREBID_GLOBAL$$.bidderSettings = { + aol: { + bidCpmAdjustment: function() {} + } + }; + spec.interpretResponse(bidResponse, bidRequest); + expect(utils.logWarn.calledOnce).to.be.true; + }); + }); - it('should hit the nexage api endpoint with the nexage config', () => { - adapter.callBids(createBidderRequest({ - params: getNexageGetBidParams() - })); + describe('buildRequests()', () => { + it('method exists and is a function', () => { + expect(spec.buildRequests).to.exist.and.to.be.a('function'); + }); - expect(requests[0].url).to.contain(NEXAGE_URL); - }); + describe('Marketplace', () => { + it('should not return request when no bids are present', () => { + let [request] = spec.buildRequests([]); + expect(request).to.be.empty; + }); - it('should hit the nexage api custom endpoint if specified in the nexage config', () => { - let bidParams = Object.assign({ - host: 'qa-hb.nexage.com' - }, getNexageGetBidParams()); + it('should return request for Marketplace endpoint', () => { + let bidRequest = getDefaultBidRequest(); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(MARKETPLACE_URL); + }); - adapter.callBids(createBidderRequest({ - params: bidParams - })); - expect(requests[0].url).to.contain('qa-hb.nexage.com/bidRequest?'); + it('should return request for Marketplace via onedisplay bidder code', () => { + let bidRequest = createCustomBidRequest({ + bids: [{ + bidder: 'onedisplay' + }], + params: getMarketplaceBidParams() }); - it('should hit nexage api when nexage and marketplace params are present', () => { - let bidParams = Object.assign(getNexageGetBidParams(), getMarketplaceBidParams()); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(MARKETPLACE_URL); + }); - adapter.callBids(createBidderRequest({ - params: bidParams - })); - expect(requests[0].url).to.contain(NEXAGE_URL); + it('should return Marketplace request via onedisplay bidder code when' + + 'Marketplace and One Mobile GET params are present', () => { + let bidParams = Object.assign(getMarketplaceBidParams(), getNexageGetBidParams()); + let bidRequest = createCustomBidRequest({ + bids: [{ + bidder: 'onedisplay' + }], + params: bidParams }); - it('should hit nexage api via onemobile bidder code when nexage and marketplace params are present', () => { - let bidParams = Object.assign(getNexageGetBidParams(), getMarketplaceBidParams()); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(MARKETPLACE_URL); + }); - adapter.callBids(createBidderRequest({ - bids: [{ - bidder: 'onemobile' - }], - params: bidParams - })); - expect(requests[0].url).to.contain(NEXAGE_URL); + it('should return Marketplace request via onedisplay bidder code when' + + 'Marketplace and One Mobile GET + POST params are present', () => { + let bidParams = Object.assign(getMarketplaceBidParams(), getNexageGetBidParams(), getNexagePostBidParams()); + let bidRequest = createCustomBidRequest({ + bids: [{ + bidder: 'onedisplay' + }], + params: bidParams }); - it('should not resolve endpoint for onemobile bidder code when only Marketplace params are present', () => { - adapter.callBids(createBidderRequest({ - bids: [{ - bidder: 'onemobile' - }], - params: getMarketplaceBidParams() - })); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(MARKETPLACE_URL); + }); - expect(requests.length).to.equal(0); + it('should not resolve endpoint for onedisplay bidder code ' + + 'when only One Mobile params are present', () => { + let bidParams = Object.assign(getNexageGetBidParams(), getNexagePostBidParams()); + let bidRequest = createCustomBidRequest({ + bids: [{ + bidder: 'onedisplay' + }], + params: bidParams }); - it('should contain required params - dcn & pos', () => { - adapter.callBids(createBidderRequest({ - params: getNexageGetBidParams() - })); - - expect(requests[0].url).to.contain(NEXAGE_URL + 'dcn=2c9d2b50015c5ce9db6aeeed8b9500d6&pos=header'); - }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request).to.be.empty; + }); - it('should contain cmd=bid by default', () => { - adapter.callBids(createBidderRequest({ - params: { - dcn: '54321123', - pos: 'footer-2324' - } - })); - expect(requests[0].url).to.contain('hb.nexage.com/bidRequest?dcn=54321123&pos=footer-2324&cmd=bid'); + it('should return Marketplace URL for eu region', () => { + let bidRequest = createCustomBidRequest({ + params: { + placement: 1234567, + network: '9599.1', + region: 'eu' + } }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('adserver-eu.adtech.advertising.com/pubapi/3.0/'); + }); - it('should contain optional parameters if they are set', () => { - adapter.callBids(createBidderRequest({ - params: { - dcn: '54321123', - pos: 'footer-2324', - ext: { - param1: 'val1', - param2: 'val2', - param3: 'val3', - param4: 'val4' - } - } - })); - expect(requests[0].url).to.contain('hb.nexage.com/bidRequest?dcn=54321123&pos=footer-2324&cmd=bid' + - '¶m1=val1¶m2=val2¶m3=val3¶m4=val4'); + it('should return Marketplace URL for eu region when server option is present', () => { + let bidRequest = createCustomBidRequest({ + params: { + placement: 1234567, + network: '9599.1', + server: 'adserver-eu.adtech.advertising.com' + } }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('adserver-eu.adtech.advertising.com/pubapi/3.0/'); + }); - it('should hit the nexage api endpoint with post data with the openrtb config', () => { - let bidConfig = getNexagePostBidParams(); - - adapter.callBids(createBidderRequest({ - params: bidConfig - })); - expect(requests[0].url).to.contain(NEXAGE_URL); - expect(requests[0].requestBody).to.deep.equal(bidConfig); - expect(requests[0].requestHeaders).to.have.property('x-openrtb-version'); + it('should return default Marketplace URL in case of unknown region config option', () => { + let bidRequest = createCustomBidRequest({ + params: { + placement: 1234567, + network: '9599.1', + region: 'an' + } }); - - it('should not hit the nexage api endpoint with post data with the openrtb config' + - ' if a required parameter is missing', () => { - let bidConfig = getNexagePostBidParams(); - - bidConfig.imp[0].id = null; - adapter.callBids(createBidderRequest({ - params: bidConfig - })); - expect(requests).to.be.empty; - }) - ; + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(MARKETPLACE_URL); }); - }); - - describe('bid response', () => { - let server; - beforeEach(() => { - server = sinon.fakeServer.create(); - sinon.stub(bidmanager, 'addBidResponse'); + it('should return url with pubapi bid option', () => { + let bidRequest = getDefaultBidRequest(); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('cmd=bid;'); }); - afterEach(() => { - server.restore(); - bidmanager.addBidResponse.restore(); + it('should return url with version 2 of pubapi', () => { + let bidRequest = getDefaultBidRequest(); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('v=2;'); }); - it('should be added to bidmanager if returned from pubapi', () => { - server.respondWith(JSON.stringify(getDefaultBidResponse())); - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; + it('should return url with cache busting option', () => { + let bidRequest = getDefaultBidRequest(); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.match(/misc=\d+/); }); - it('should be added to bidmanager if returned from nexage GET bid request', () => { - server.respondWith(JSON.stringify(getDefaultBidResponse())); - adapter.callBids(createBidderRequest({ + it('should return url with default pageId and sizeId', () => { + let bidRequest = createCustomBidRequest({ params: { - dcn: '54321123', - pos: 'footer-2324' + placement: 1234567, + network: '9599.1' } - })); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('/pubapi/3.0/9599.1/1234567/0/0/ADTECH;'); }); - it('should be added to bidmanager if returned from nexage POST bid request', () => { - server.respondWith(JSON.stringify(getDefaultBidResponse())); - adapter.callBids(createBidderRequest({ + it('should return url with custom pageId and sizeId when options are present', () => { + let bidRequest = createCustomBidRequest({ params: { - id: 'id-1', - imp: [{ - id: 'id-2', - banner: { - w: '100', - h: '100' - }, - tagid: 'header1' - }] + placement: 1234567, + network: '9599.1', + pageId: 1111, + sizeId: 2222 } - })); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - var bidResponse = bidmanager.addBidResponse.firstCall.args[1]; + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('/pubapi/3.0/9599.1/1234567/1111/2222/ADTECH;'); }); - it('should be added to bidmanager with correct bidderCode', () => { - server.respondWith(JSON.stringify(getDefaultBidResponse())); - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - expect(bidmanager.addBidResponse.firstCall.args[1]).to.have.property('bidderCode', 'aol'); + it('should return url with default alias if alias param is missing', () => { + let bidRequest = getDefaultBidRequest(); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.match(/alias=\w+?;/); }); - it('should have adId matching the bidId from related bid request', () => { - server.respondWith(JSON.stringify(getDefaultBidResponse())); - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - expect(bidmanager.addBidResponse.firstCall.args[1]) - .to.have.property('adId', '84ab500420319d'); + it('should return url with custom alias if it is present', () => { + let bidRequest = createCustomBidRequest({ + params: { + placement: 1234567, + network: '9599.1', + alias: 'desktop_articlepage_something_box_300_250' + } + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('alias=desktop_articlepage_something_box_300_250'); }); - it('should be added to bidmanager as invalid in case of empty response', () => { - server.respondWith(''); - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(2); + it('should return url without bidfloor option if is is missing', () => { + let bidRequest = getDefaultBidRequest(); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).not.to.contain('bidfloor='); }); - it('should be added to bidmanager as invalid in case of invalid JSON response', () => { - server.respondWith('{foo:{bar:{baz:'); - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(2); + it('should return url with bidFloor option if it is present', () => { + let bidRequest = createCustomBidRequest({ + params: { + placement: 1234567, + network: '9599.1', + bidFloor: 0.80 + } + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('bidfloor=0.8'); }); - it('should be added to bidmanager as invalid in case of no bid data', () => { - let bidResponse = getDefaultBidResponse(); - bidResponse.seatbid = []; - server.respondWith(JSON.stringify(bidResponse)); - - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(2); + it('should return url with key values if keyValues param is present', () => { + let bidRequest = createCustomBidRequest({ + params: { + placement: 1234567, + network: '9599.1', + keyValues: { + age: 25, + height: 3.42, + test: 'key' + } + } + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('kvage=25;kvheight=3.42;kvtest=key'); }); + }); - it('should have adId matching the bidId from bid request in case of no bid data', () => { - let bidResponse = getDefaultBidResponse(); - bidResponse.seatbid = []; - server.respondWith(JSON.stringify(bidResponse)); - - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - expect(bidmanager.addBidResponse.firstCall.args[1]) - .to.have.property('adId', '84ab500420319d'); + describe('One Mobile', () => { + it('should return One Mobile url when One Mobile get params are present', () => { + let bidRequest = createCustomBidRequest({ + params: getNexageGetBidParams() + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(NEXAGE_URL); }); - it('should be added to bidmanager as invalid in case of empty price', () => { - let bidResponse = getDefaultBidResponse(); - bidResponse.seatbid[0].bid[0].price = undefined; - - server.respondWith(JSON.stringify(bidResponse)); - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(2); + it('should return One Mobile url with different host when host option is present', () => { + let bidParams = Object.assign({ + host: 'qa-hb.nexage.com' + }, getNexageGetBidParams()); + let bidRequest = createCustomBidRequest({ + params: bidParams + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('qa-hb.nexage.com/bidRequest?'); }); - it('should be added to bidmanager with attributes from pubapi response', () => { - let bidResponse = getDefaultBidResponse(); - bidResponse.seatbid[0].bid[0].crid = '12345'; - - server.respondWith(JSON.stringify(bidResponse)); - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - let addedBidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(addedBidResponse.ad).to.equal('<script>logInfo(\'ad\');</script>'); - expect(addedBidResponse.cpm).to.equal(0.09); - expect(addedBidResponse.width).to.equal(728); - expect(addedBidResponse.height).to.equal(90); - expect(addedBidResponse.creativeId).to.equal('12345'); - expect(addedBidResponse.pubapiId).to.equal('245730051428950632'); + it('should return One Mobile url when One Mobile and Marketplace params are present', () => { + let bidParams = Object.assign(getNexageGetBidParams(), getMarketplaceBidParams()); + let bidRequest = createCustomBidRequest({ + params: bidParams + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(NEXAGE_URL); }); - it('should be added to bidmanager including pixels from pubapi response', () => { - let bidResponse = getDefaultBidResponse(); - bidResponse.ext = { - pixels: '<script>document.write(\'<img src="pixel.gif">\');</script>' - }; - - server.respondWith(JSON.stringify(bidResponse)); - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - let addedBidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(addedBidResponse.ad).to.equal( - '<script>logInfo(\'ad\');</script>' + - '<script>if(!parent.$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped){' + - 'parent.$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped=true;' + - 'document.write(\'<img src="pixel.gif">\');}</script>' - ); + it('should return One Mobile url for onemobile bidder code ' + + 'when One Mobile GET and Marketplace params are present', () => { + let bidParams = Object.assign(getNexageGetBidParams(), getMarketplaceBidParams()); + let bidRequest = createCustomBidRequest({ + bids: [{ + bidder: 'onemobile' + }], + params: bidParams + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(NEXAGE_URL); }); - it('should be added to bidmanager including dealid from pubapi response', () => { - let bidResponse = getDefaultBidResponse(); - bidResponse.seatbid[0].bid[0].dealid = '12345'; - - server.respondWith(JSON.stringify(bidResponse)); - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - let addedBidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(addedBidResponse.dealId).to.equal('12345'); + it('should not return any url for onemobile bidder code' + + 'when only Marketplace params are present', () => { + let bidRequest = createCustomBidRequest({ + bids: [{ + bidder: 'onemobile' + }], + params: getMarketplaceBidParams() + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request).to.be.empty; }); - it('should be added to bidmanager including encrypted price from pubapi response', () => { - let bidResponse = getDefaultBidResponse(); - bidResponse.seatbid[0].bid[0].ext.encp = 'a9334987'; - server.respondWith(JSON.stringify(bidResponse)); - - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - let addedBidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(addedBidResponse.cpm).to.equal('a9334987'); + it('should return One Mobile url with required params - dcn & pos', () => { + let bidRequest = createCustomBidRequest({ + params: getNexageGetBidParams() + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(NEXAGE_URL + 'dcn=2c9d2b50015c5ce9db6aeeed8b9500d6&pos=header'); }); - it('should not render pixels on pubapi response when no parameter is set', () => { - let bidResponse = getDefaultBidResponse(); - bidResponse.ext = { - pixels: '<script>document.write(\'<iframe src="pixels.org"></iframe>\');</script>' - }; - server.respondWith(JSON.stringify(bidResponse)); - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; - expect(document.body.querySelectorAll('iframe[src="pixels.org"]').length).to.equal(0); + it('should return One Mobile url with cmd=bid option', () => { + let bidRequest = createCustomBidRequest({ + params: getNexageGetBidParams() + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('cmd=bid'); }); - it('should render pixels from pubapi response when param userSyncOn is set with \'bidResponse\'', () => { - let bidResponse = getDefaultBidResponse(); - bidResponse.ext = { - pixels: '<script>document.write(\'<iframe src="pixels.org"></iframe>' + - '<iframe src="pixels1.org"></iframe>\');</script>' - }; + it('should return One Mobile url with generic params if ext option is present', () => { + let bidRequest = createCustomBidRequest({ + params: { + dcn: '54321123', + pos: 'footer-2324', + ext: { + param1: 'val1', + param2: 'val2', + param3: 'val3', + param4: 'val4' + } + } + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain('hb.nexage.com/bidRequest?dcn=54321123&pos=footer-2324&cmd=bid' + + '¶m1=val1¶m2=val2¶m3=val3¶m4=val4'); + }); - server.respondWith(JSON.stringify(bidResponse)); - let bidRequest = getDefaultBidRequest(); - bidRequest.bids[0].params.userSyncOn = 'bidResponse'; - adapter.callBids(bidRequest); - server.respond(); + it('should return request object for One Mobile POST endpoint when POST configuration is present', () => { + let bidConfig = getNexagePostBidParams(); + let bidRequest = createCustomBidRequest({ + params: bidConfig + }); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(NEXAGE_URL); + expect(request.method).to.equal('POST'); + expect(request.data).to.deep.equal(bidConfig); + expect(request.options).to.deep.equal({ + contentType: 'application/json', + customHeaders: { + 'x-openrtb-version': '2.2' + } + }); + }); - let assertPixelsItem = (pixelsItemSelector) => { - let pixelsItem = document.body.querySelectorAll(pixelsItemSelector)[0]; + it('should not return request object for One Mobile POST endpoint' + + 'if required parameterers are missed', () => { + let bidRequest = createCustomBidRequest({ + params: { + imp: [] + } + }); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request).to.be.empty; + }); + }); + }); - expect(pixelsItem.width).to.equal('1'); - expect(pixelsItem.height).to.equal('1'); - expect(pixelsItem.style.display).to.equal('none'); - }; + describe('getUserSyncs()', () => { + let bidResponse; + let bidRequest; - assertPixelsItem('iframe[src="pixels.org"]'); - assertPixelsItem('iframe[src="pixels1.org"]'); - expect($$PREBID_GLOBAL$$.aolGlobals.pixelsDropped).to.be.true; + beforeEach(() => { + $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = false; + config.setConfig({ + aol: { + userSyncOn: 'bidResponse' + }, }); + bidResponse = getDefaultBidResponse(); + bidResponse.ext = { + pixels: getPixels() + }; + }); - it('should not render pixels if it was rendered before', () => { - $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = true; - let bidResponse = getDefaultBidResponse(); - bidResponse.ext = { - pixels: '<script>document.write(\'<iframe src="test.com"></iframe>' + - '<iframe src="test2.org"></iframe>\');</script>' - }; - server.respondWith(JSON.stringify(bidResponse)); - - let bidRequest = getDefaultBidRequest(); - bidRequest.bids[0].params.userSyncOn = 'bidResponse'; - adapter.callBids(bidRequest); - server.respond(); + it('should return user syncs only if userSyncOn equals to "bidResponse"', () => { + let userSyncs = spec.getUserSyncs({}, [bidResponse], bidRequest); - expect(bidmanager.addBidResponse.calledOnce).to.be.true; + expect($$PREBID_GLOBAL$$.aolGlobals.pixelsDropped).to.be.true; + expect(userSyncs).to.deep.equal([ + {type: 'image', url: 'img.org'}, + {type: 'iframe', url: 'pixels1.org'} + ]); + }); - let assertPixelsItem = (pixelsItemSelector) => { - let pixelsItems = document.body.querySelectorAll(pixelsItemSelector); + it('should not return user syncs if it has already been returned', () => { + $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = true; - expect(pixelsItems.length).to.equal(0); - }; + let userSyncs = spec.getUserSyncs({}, [bidResponse], bidRequest); - assertPixelsItem('iframe[src="test.com"]'); - assertPixelsItem('iframe[src="test2.com"]'); - }); + expect($$PREBID_GLOBAL$$.aolGlobals.pixelsDropped).to.be.true; + expect(userSyncs).to.deep.equal([]); }); - describe('when bidCpmAdjustment is set', () => { - let bidderSettingsBackup; - let server; + it('should not return user syncs if pixels are not present', () => { + bidResponse.ext.pixels = null; - beforeEach(() => { - bidderSettingsBackup = $$PREBID_GLOBAL$$.bidderSettings; - server = sinon.fakeServer.create(); - }); + let userSyncs = spec.getUserSyncs({}, [bidResponse], bidRequest); - afterEach(() => { - $$PREBID_GLOBAL$$.bidderSettings = bidderSettingsBackup; - server.restore(); - if (utils.logWarn.restore) { - utils.logWarn.restore(); - } - }); - - it('should show warning in the console', function() { - sinon.spy(utils, 'logWarn'); - server.respondWith(JSON.stringify(getDefaultBidResponse())); - $$PREBID_GLOBAL$$.bidderSettings = { - aol: { - bidCpmAdjustment: function() {} - } - }; - adapter.callBids(getDefaultBidRequest()); - server.respond(); - expect(utils.logWarn.calledOnce).to.be.true; - }); + expect($$PREBID_GLOBAL$$.aolGlobals.pixelsDropped).to.be.false; + expect(userSyncs).to.deep.equal([]); }); }); }); From d3833882813453d98bbb032d0b0cb695f15db32c Mon Sep 17 00:00:00 2001 From: Jaimin Panchal <jpanchal@appnexus.com> Date: Tue, 14 Nov 2017 15:31:50 -0500 Subject: [PATCH 29/32] Prebid 0.33.0 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 723c763ab15..56f2a08873b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "0.33.0-pre", + "version": "0.33.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 5ed36dbf1b1f9602bf1b986106a2b20117deaa1b Mon Sep 17 00:00:00 2001 From: vzhukovsky <valentin.zhukovsky@oath.com> Date: Tue, 19 Dec 2017 12:26:35 +0300 Subject: [PATCH 30/32] Added changelog entry. --- CHANGELOG | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index c228996b4c1..d53d4b1d897 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +AOL Prebid 1.33.0 +---------------- +Updated to Prebid 0.33.0 + + AOL Prebid 1.32.0 ---------------- Updated to Prebid 0.32.0 From b4309cfc8d150bf91656515e137f3840ab046dae Mon Sep 17 00:00:00 2001 From: vzhukovsky <valentin.zhukovsky@oath.com> Date: Tue, 19 Dec 2017 12:29:32 +0300 Subject: [PATCH 31/32] Added partners ids. --- modules/aolPartnersIds.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/aolPartnersIds.json b/modules/aolPartnersIds.json index d2b9e9d7f8f..b29fd6c105c 100644 --- a/modules/aolPartnersIds.json +++ b/modules/aolPartnersIds.json @@ -105,5 +105,8 @@ "trustx": 104, "realvu": 105, "nanointeractive": 106, - "adocean": 107 + "adocean": 107, + "a4g": 108, + "adkernelAdn": 109, + "33across": 110 } From 3e9756098bb20ecbe0314f16eed5298c5675b24c Mon Sep 17 00:00:00 2001 From: vzhukovsky <valentin.zhukovsky@oath.com> Date: Thu, 21 Dec 2017 14:33:33 +0300 Subject: [PATCH 32/32] Wrapped content type in options object. --- test/spec/unit/core/bidderFactory_spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index adb0baab69a..c3d06c61cd9 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -134,7 +134,9 @@ describe('bidders created by newBidder', () => { method: 'POST', url: url, data: data, - contentType: 'text/plain' + options: { + contentType: 'text/plain' + } }); bidder.callBids(MOCK_BIDS_REQUEST);