From 75f6dc53cf900b0292025b1d58488f7514d38d7f Mon Sep 17 00:00:00 2001 From: Jaimin Panchal <jpanchal@appnexus.com> Date: Tue, 17 Oct 2017 16:51:16 -0400 Subject: [PATCH 01/44] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c757a971847..062e0943815 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "0.31.0", + "version": "0.32.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 89a8ed6cfc509a4020dafe8d625ea021884e6d47 Mon Sep 17 00:00:00 2001 From: Dan Harton <dan@sparklit.com> Date: Wed, 18 Oct 2017 11:26:58 -0700 Subject: [PATCH 02/44] Update AdButler adapter for Prebid v1.0 (#1664) * Adding AdButler Adapter * Prevent AdButler TypeError Only attempt to build a bid response if we have the information of which bid to respond to. * Refactor AdButler Testing Now stubbing adLoader instead of spying. Additional changes to ensure all tests still passed. * Prevent AdButler TypeErrors Prevent AdButler TypeErrors and pass bid request object into the bid response. * Add optional domain parameter. Add optional domain parameter to AdButler adapter. * Update AdButler adapter to Prebid 1.0 * Code Style updates based on lint warnings. * Removed mutable global, simplified tests, and added markdown file. * Update c1x adapter tests & remove old adbutler_spec file. --- modules/adbutlerBidAdapter.js | 229 +++---- modules/adbutlerBidAdapter.md | 31 + test/spec/modules/adbutlerBidAdapter_spec.js | 685 +++++-------------- test/spec/modules/c1xBidAdapter_spec.js | 17 + 4 files changed, 343 insertions(+), 619 deletions(-) create mode 100644 modules/adbutlerBidAdapter.md diff --git a/modules/adbutlerBidAdapter.js b/modules/adbutlerBidAdapter.js index d6492a72e1c..f633eba98a3 100644 --- a/modules/adbutlerBidAdapter.js +++ b/modules/adbutlerBidAdapter.js @@ -1,130 +1,122 @@ -/** - * @overview AdButler Prebid.js adapter. - * @author dkharton - */ - 'use strict'; -var utils = require('src/utils.js'); -var adloader = require('src/adloader.js'); -var bidmanager = require('src/bidmanager.js'); -var bidfactory = require('src/bidfactory.js'); -var adaptermanager = require('src/adaptermanager'); - -var AdButlerAdapter = function AdButlerAdapter() { - function _callBids(params) { - var bids = params.bids || []; - var callbackData = {}; - var zoneCount = {}; - var pageID = Math.floor(Math.random() * 10e6); - - // Build and send bid requests - for (var i = 0; i < bids.length; i++) { - var bid = bids[i]; - var zoneID = utils.getBidIdParameter('zoneID', bid.params); - var callbackID; - - if (!(zoneID in zoneCount)) { - zoneCount[zoneID] = 0; +import * as utils from 'src/utils'; +import {config} from 'src/config'; +import {registerBidder} from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'adbutler'; + +export const spec = { + code: BIDDER_CODE, + pageID: Math.floor(Math.random() * 10e6), + + isBidRequestValid: function (bid) { + return !!(bid.params.accountID && bid.params.zoneID); + }, + + buildRequests: function (validBidRequests) { + var i; + var zoneID; + var bidRequest; + var accountID; + var keyword; + var domain; + var requestURI; + var serverRequests = []; + var zoneCounters = {}; + + for (i = 0; i < validBidRequests.length; i++) { + bidRequest = validBidRequests[i]; + zoneID = utils.getBidIdParameter('zoneID', bidRequest.params); + accountID = utils.getBidIdParameter('accountID', bidRequest.params); + keyword = utils.getBidIdParameter('keyword', bidRequest.params); + domain = utils.getBidIdParameter('domain', bidRequest.params); + + if (!(zoneID in zoneCounters)) { + zoneCounters[zoneID] = 0; } - // build callbackID to get placementCode later - callbackID = zoneID + '_' + zoneCount[zoneID]; + if (typeof domain === 'undefined' || domain.length === 0) { + domain = 'servedbyadbutler.com'; + } - callbackData[callbackID] = {}; - callbackData[callbackID].bidId = bid.bidId; + requestURI = location.protocol + '//' + domain + '/adserve/;type=hbr;'; + requestURI += 'ID=' + encodeURIComponent(accountID) + ';'; + requestURI += 'setID=' + encodeURIComponent(zoneID) + ';'; + requestURI += 'pid=' + encodeURIComponent(spec.pageID) + ';'; + requestURI += 'place=' + encodeURIComponent(zoneCounters[zoneID]) + ';'; - var adRequest = buildRequest(bid, zoneCount[zoneID], pageID); - zoneCount[zoneID]++; + // append the keyword for targeting if one was passed in + if (keyword !== '') { + requestURI += 'kw=' + encodeURIComponent(keyword) + ';'; + } - adloader.loadScript(adRequest); + zoneCounters[zoneID]++; + serverRequests.push({ + method: 'GET', + url: requestURI, + data: {}, + bidRequest: bidRequest + }); } + return serverRequests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + var bidObj = bidRequest.bidRequest; + var bidResponses = []; + var bidResponse = {}; + var isCorrectSize = false; + var isCorrectCPM = true; + var CPM; + var minCPM; + var maxCPM; + var width; + var height; + + if (serverResponse && serverResponse.status === 'SUCCESS' && bidObj) { + CPM = serverResponse.cpm; + minCPM = utils.getBidIdParameter('minCPM', bidObj.params); + maxCPM = utils.getBidIdParameter('maxCPM', bidObj.params); + width = parseInt(serverResponse.width); + height = parseInt(serverResponse.height); + + // Ensure response CPM is within the given bounds + if (minCPM !== '' && CPM < parseFloat(minCPM)) { + isCorrectCPM = false; + } + if (maxCPM !== '' && CPM > parseFloat(maxCPM)) { + isCorrectCPM = false; + } - // Define callback function for bid responses - $$PREBID_GLOBAL$$.adbutlerCB = function(aBResponseObject) { - var bidResponse = {}; - var callbackID = aBResponseObject.zone_id + '_' + aBResponseObject.place; - var width = parseInt(aBResponseObject.width); - var height = parseInt(aBResponseObject.height); - var isCorrectSize = false; - var isCorrectCPM = true; - var CPM; - var minCPM; - var maxCPM; - var bidObj = callbackData[callbackID] ? utils.getBidRequest(callbackData[callbackID].bidId) : null; - - if (bidObj) { - if (aBResponseObject.status === 'SUCCESS') { - CPM = aBResponseObject.cpm; - minCPM = utils.getBidIdParameter('minCPM', bidObj.params); - maxCPM = utils.getBidIdParameter('maxCPM', bidObj.params); - - // Ensure response CPM is within the given bounds - if (minCPM !== '' && CPM < parseFloat(minCPM)) { - isCorrectCPM = false; - } - if (maxCPM !== '' && CPM > parseFloat(maxCPM)) { - isCorrectCPM = false; - } - - // Ensure that response ad matches one of the placement sizes. - utils._each(bidObj.sizes, function(size) { - if (width === size[0] && height === size[1]) { - isCorrectSize = true; - } - }); - - if (isCorrectCPM && isCorrectSize) { - bidResponse = bidfactory.createBid(1, bidObj); - bidResponse.bidderCode = 'adbutler'; - bidResponse.cpm = CPM; - bidResponse.width = width; - bidResponse.height = height; - bidResponse.ad = aBResponseObject.ad_code; - bidResponse.ad += addTrackingPixels(aBResponseObject.tracking_pixels); - } else { - bidResponse = bidfactory.createBid(2, bidObj); - bidResponse.bidderCode = 'adbutler'; - } - } else { - bidResponse = bidfactory.createBid(2, bidObj); - bidResponse.bidderCode = 'adbutler'; + // Ensure that response ad matches one of the placement sizes. + utils._each(bidObj.sizes, function (size) { + if (width === size[0] && height === size[1]) { + isCorrectSize = true; } - - bidmanager.addBidResponse(bidObj.placementCode, bidResponse); + }); + if (isCorrectCPM && isCorrectSize) { + bidResponse.requestId = bidObj.bidId; + bidResponse.bidderCode = spec.code; + bidResponse.creativeId = serverResponse.placement_id; + bidResponse.cpm = CPM; + bidResponse.width = width; + bidResponse.height = height; + bidResponse.ad = serverResponse.ad_code; + bidResponse.ad += spec.addTrackingPixels(serverResponse.tracking_pixels); + bidResponse.currency = 'USD'; + bidResponse.netRevenue = true; + bidResponse.ttl = config.getConfig('_bidderTimeout'); + bidResponse.referrer = utils.getTopWindowUrl(); + bidResponses.push(bidResponse); } - }; - } - - function buildRequest(bid, adIndex, pageID) { - var accountID = utils.getBidIdParameter('accountID', bid.params); - var zoneID = utils.getBidIdParameter('zoneID', bid.params); - var keyword = utils.getBidIdParameter('keyword', bid.params); - var domain = utils.getBidIdParameter('domain', bid.params); - - if (typeof domain === 'undefined' || domain.length === 0) { - domain = 'servedbyadbutler.com'; } + return bidResponses; + }, - var requestURI = location.protocol + '//' + domain + '/adserve/;type=hbr;'; - requestURI += 'ID=' + encodeURIComponent(accountID) + ';'; - requestURI += 'setID=' + encodeURIComponent(zoneID) + ';'; - requestURI += 'pid=' + encodeURIComponent(pageID) + ';'; - requestURI += 'place=' + encodeURIComponent(adIndex) + ';'; - - // append the keyword for targeting if one was passed in - if (keyword !== '') { - requestURI += 'kw=' + encodeURIComponent(keyword) + ';'; - } - requestURI += 'jsonpfunc=$$PREBID_GLOBAL$$.adbutlerCB;'; - requestURI += 'click=CLICK_MACRO_PLACEHOLDER'; - - return requestURI; - } - - function addTrackingPixels(trackingPixels) { + addTrackingPixels: function (trackingPixels) { var trackingPixelMarkup = ''; - utils._each(trackingPixels, function(pixelURL) { + utils._each(trackingPixels, function (pixelURL) { var trackingPixel = '<img height="0" width="0" border="0" style="display:none;" src="'; trackingPixel += pixelURL; trackingPixel += '">'; @@ -133,14 +125,5 @@ var AdButlerAdapter = function AdButlerAdapter() { }); return trackingPixelMarkup; } - - // Export the callBids function, so that prebid.js can execute this function - // when the page asks to send out bid requests. - return { - callBids: _callBids - }; }; - -adaptermanager.registerBidAdapter(new AdButlerAdapter(), 'adbutler'); - -module.exports = AdButlerAdapter; +registerBidder(spec); diff --git a/modules/adbutlerBidAdapter.md b/modules/adbutlerBidAdapter.md new file mode 100644 index 00000000000..5905074270a --- /dev/null +++ b/modules/adbutlerBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +**Module Name**: AdButler Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: dan@sparklit.com + +# Description + +Module that connects to an AdButler zone to fetch bids. + +# Test Parameters +``` + var adUnits = [ + { + code: 'display-div', + sizes: [[300, 250]], // a display size + bids: [ + { + bidder: "adbutler", + params: { + accountID: '167283', + zoneID: '210093', + keyword: 'red', //optional + minCPM: '1.00', //optional + maxCPM: '5.00' //optional + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/test/spec/modules/adbutlerBidAdapter_spec.js b/test/spec/modules/adbutlerBidAdapter_spec.js index d026ac8de98..352358be8d0 100644 --- a/test/spec/modules/adbutlerBidAdapter_spec.js +++ b/test/spec/modules/adbutlerBidAdapter_spec.js @@ -1,516 +1,209 @@ -describe('adbutler adapter tests', function () { - var expect = require('chai').expect; - var adapter = require('modules/adbutlerBidAdapter'); - var adLoader = require('src/adloader'); - var bidmanager = require('src/bidmanager'); +import {expect} from 'chai'; +import {spec} from 'modules/adbutlerBidAdapter'; - describe('creation of bid url', function () { - var stubLoadScript; +describe('AdButler adapter', () => { + let bidRequests; - beforeEach(function () { - stubLoadScript = sinon.stub(adLoader, 'loadScript'); - }); - - afterEach(function () { - stubLoadScript.restore(); - }); - - if (typeof ($$PREBID_GLOBAL$$._bidsReceived) === 'undefined') { - $$PREBID_GLOBAL$$._bidsReceived = []; - } - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = []; - } - if (typeof ($$PREBID_GLOBAL$$._adsReceived) === 'undefined') { - $$PREBID_GLOBAL$$._adsReceived = []; - } - - it('should be called', function () { - var params = { - bidderCode: 'adbutler', - bids: [ - { - bidId: '3c9408cdbf2f68', - sizes: [[300, 250]], - bidder: 'adbutler', - params: { - accountID: '167283', - zoneID: '210093' - }, - requestId: '10b327aa396609', - placementCode: '/123456/header-bid-tag-1' - } - - ] - }; - - adapter().callBids(params); - - sinon.assert.called(stubLoadScript); - }); - - it('should populate the keyword', function() { - var params = { - bidderCode: 'adbutler', - bids: [ - { - bidId: '3c9408cdbf2f68', - sizes: [[300, 250]], - bidder: 'adbutler', - params: { - accountID: '167283', - zoneID: '210093', - keyword: 'fish' - }, - requestId: '10b327aa396609', - placementCode: '/123456/header-bid-tag-1' - } - - ] - }; - - adapter().callBids(params); - - var requestURI = stubLoadScript.getCall(0).args[0]; - - expect(requestURI).to.have.string(';kw=fish;'); - }); - - it('should use custom domain string', function() { - var params = { - bidderCode: 'adbutler', - bids: [ - { - bidId: '3c9408cdbf2f68', - sizes: [[300, 250]], - bidder: 'adbutler', - params: { - accountID: '107878', - zoneID: '86133', - domain: 'servedbyadbutler.com.dan.test' - }, - requestId: '10b327aa396609', - placementCode: '/123456/header-bid-tag-1' - } - ] - }; - - adapter().callBids(params); - - var requestURI = stubLoadScript.getCall(0).args[0]; - - expect(requestURI).to.have.string('.dan.test'); - }); - }); - describe('bid responses', function() { - it('should return complete bid response', function() { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - - var params = { - bidderCode: 'adbutler', + beforeEach(() => { + bidRequests = [ + { bidder: 'adbutler', - bids: [ - { - bidId: '3c94018cdbf2f68-1', - sizes: [[300, 250]], - bidder: 'adbutler', - params: { - accountID: '167283', - zoneID: '210093', - }, - requestId: '10b327aa396609', - placementCode: '/123456/header-bid-tag-1' - } - - ] - }; - - var response = { - status: 'SUCCESS', - account_id: 167283, - zone_id: 210093, - cpm: 1.5, - width: 300, - height: 250, - place: 0 - }; - - adapter().callBids(params); - - var adUnits = new Array(); - var unit = new Object(); - unit.bids = params.bids; - unit.code = '/123456/header-bid-tag-1'; - unit.sizes = [[300, 250]]; - adUnits.push(unit); - - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = [params]; - } else { - $$PREBID_GLOBAL$$._bidsRequested.push(params); + params: { + accountID: '167283', + zoneID: '210093', + keyword: 'red', + minCPM: '1.00', + maxCPM: '5.00' + }, + placementCode: '/19968336/header-bid-tag-1', + sizes: [[300, 250], [300, 600]], + bidId: '23acc48ad47af5', + requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' } - - $$PREBID_GLOBAL$$.adUnits = adUnits; - - $$PREBID_GLOBAL$$.adbutlerCB(response); - - var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - - expect(bidPlacementCode1).to.equal('/123456/header-bid-tag-1'); - expect(bidObject1.getStatusCode()).to.equal(1); - expect(bidObject1.bidderCode).to.equal('adbutler'); - expect(bidObject1.cpm).to.equal(1.5); - expect(bidObject1.width).to.equal(300); - expect(bidObject1.height).to.equal(250); - - stubAddBidResponse.restore(); - }); - - it('should return empty bid response', function() { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - var params = { - bidderCode: 'adbutler', - bids: [ - { - bidId: '3c9408cdbf2f68-2', - sizes: [[300, 250]], - bidder: 'adbutler', - params: { - accountID: '167283', - zoneID: '210085', - }, - requestId: '10b327aa396609', - placementCode: '/123456/header-bid-tag-1' - } - - ] - }; - - var response = { - status: 'NO_ELIGIBLE_ADS', - zone_id: 210085, - width: 728, - height: 90, - place: 0 - }; - - adapter().callBids(params); - - var adUnits = new Array(); - var unit = new Object(); - unit.bids = params.bids; - unit.code = '/123456/header-bid-tag-1'; - unit.sizes = [[300, 250]]; - adUnits.push(unit); - - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = [params]; - } else { - $$PREBID_GLOBAL$$._bidsRequested.push(params); - } - - $$PREBID_GLOBAL$$.adUnits = adUnits; - - $$PREBID_GLOBAL$$.adbutlerCB(response); - - var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - - expect(bidPlacementCode1).to.equal('/123456/header-bid-tag-1'); - expect(bidObject1.getStatusCode()).to.equal(2); - expect(bidObject1.bidderCode).to.equal('adbutler'); - - stubAddBidResponse.restore(); - }); - - it('should return empty bid response on incorrect size', function() { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - var params = { - bidderCode: 'adbutler', - bids: [ - { - bidId: '3c9408cdbf2f68-3', - sizes: [[300, 250]], - bidder: 'adbutler', - params: { - accountID: '167283', - zoneID: '210085', - }, - requestId: '10b327aa396609', - placementCode: '/123456/header-bid-tag-1' - } - - ] - }; - - var response = { - status: 'SUCCESS', - account_id: 167283, - zone_id: 210085, - cpm: 1.5, - width: 728, - height: 90, - place: 0 - }; - - adapter().callBids(params); - - var adUnits = new Array(); - var unit = new Object(); - unit.bids = params.bids; - unit.code = '/123456/header-bid-tag-1'; - unit.sizes = [[300, 250]]; - adUnits.push(unit); - - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = [params]; - } else { - $$PREBID_GLOBAL$$._bidsRequested.push(params); - } - - $$PREBID_GLOBAL$$.adUnits = adUnits; - - $$PREBID_GLOBAL$$.adbutlerCB(response); - - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - expect(bidObject1.getStatusCode()).to.equal(2); - - stubAddBidResponse.restore(); - }); - - it('should return empty bid response with CPM too low', function() { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - var params = { - bidderCode: 'adbutler', - bids: [ - { - bidId: '3c9408cdbf2f68-4', - sizes: [[300, 250]], - bidder: 'adbutler', - params: { - accountID: '167283', - zoneID: '210093', - minCPM: '5.00' - }, - requestId: '10b327aa396609', - placementCode: '/123456/header-bid-tag-1' - } - - ] - }; - - var response = { - status: 'SUCCESS', - account_id: 167283, - zone_id: 210093, - cpm: 1.5, - width: 300, - height: 250, - place: 0 - }; - - adapter().callBids(params); - - var adUnits = new Array(); - var unit = new Object(); - unit.bids = params.bids; - unit.code = '/123456/header-bid-tag-1'; - unit.sizes = [[300, 250]]; - adUnits.push(unit); - - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = [params]; - } else { - $$PREBID_GLOBAL$$._bidsRequested.push(params); - } - - $$PREBID_GLOBAL$$.adUnits = adUnits; - - $$PREBID_GLOBAL$$.adbutlerCB(response); - - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - expect(bidObject1.getStatusCode()).to.equal(2); - - stubAddBidResponse.restore(); - }); - - it('should return empty bid response with CPM too high', function() { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - - var params = { - bidderCode: 'adbutler', - bids: [ - { - bidId: '3c9408cdbf2f68-5', - sizes: [[300, 250]], - bidder: 'adbutler', - params: { - accountID: '167283', - zoneID: '210093', - maxCPM: '1.00' - }, - requestId: '10b327aa396609', - placementCode: '/123456/header-bid-tag-1' - } - - ] - }; - - var response = { - status: 'SUCCESS', - account_id: 167283, - zone_id: 210093, - cpm: 1.5, - width: 300, - height: 250, - place: 0 - }; - - adapter().callBids(params); - - var adUnits = new Array(); - var unit = new Object(); - unit.bids = params.bids; - unit.code = '/123456/header-bid-tag-1'; - unit.sizes = [[300, 250]]; - adUnits.push(unit); - - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = [params]; - } else { - $$PREBID_GLOBAL$$._bidsRequested.push(params); - } - - $$PREBID_GLOBAL$$.adUnits = adUnits; - - $$PREBID_GLOBAL$$.adbutlerCB(response); - - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - expect(bidObject1.getStatusCode()).to.equal(2); - - stubAddBidResponse.restore(); - }); + ]; }); - describe('ad code', function() { - it('should be populated', function() { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - - var params = { - bidderCode: 'adbutler', - bids: [ - { - bidId: '3c9408cdbf2f68-6', - sizes: [[300, 250]], + describe('implementation', () => { + describe('for requests', () => { + it('should accept valid bid', () => { + let validBid = { bidder: 'adbutler', params: { accountID: '167283', zoneID: '210093' - }, - requestId: '10b327aa396609', - placementCode: '/123456/header-bid-tag-1' - } - - ] - }; - - var response = { - status: 'SUCCESS', - account_id: 167283, - zone_id: 210093, - cpm: 1.5, - width: 300, - height: 250, - place: 0, - ad_code: '<img src="http://image.source.com/img" alt="" title="" border="0" width="300" height="250">' - }; - - adapter().callBids(params); - - var adUnits = new Array(); - var unit = new Object(); - unit.bids = params.bids; - unit.code = '/123456/header-bid-tag-1'; - unit.sizes = [[300, 250]]; - adUnits.push(unit); - - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = [params]; - } else { - $$PREBID_GLOBAL$$._bidsRequested.push(params); - } - - $$PREBID_GLOBAL$$.adUnits = adUnits; - - $$PREBID_GLOBAL$$.adbutlerCB(response); + } + }, + isValid = spec.isBidRequestValid(validBid); - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - expect(bidObject1.getStatusCode()).to.equal(1); - expect(bidObject1.ad).to.have.length.above(1); - - stubAddBidResponse.restore(); - }); + expect(isValid).to.equal(true); + }); - it('should contain tracking pixels', function() { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - - var params = { - bidderCode: 'adbutler', - bids: [ - { - bidId: '3c9408cdbf2f68-7', - sizes: [[300, 250]], + it('should reject invalid bid', () => { + let invalidBid = { bidder: 'adbutler', params: { accountID: '167283', - zoneID: '210093' + } + }, + isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should use custom domain string', () => { + let bidRequests = [ + { + bidId: '3c9408cdbf2f68', + sizes: [[300, 250]], + bidder: 'adbutler', + params: { + accountID: '107878', + zoneID: '86133', + domain: 'servedbyadbutler.com.dan.test' + }, + requestId: '10b327aa396609', + placementCode: '/123456/header-bid-tag-1' + } + ], + requests = spec.buildRequests(bidRequests), + requestURL = requests[0].url; + + expect(requestURL).to.have.string('.dan.test'); + }); + + it('should set default domain', () => { + let requests = spec.buildRequests(bidRequests), + request = requests[0]; + + let [domain] = request.url.split('/adserve/'); + + expect(domain).to.equal('http://servedbyadbutler.com'); + }); + + it('should set the keyword parameter', () => { + let requests = spec.buildRequests(bidRequests), + requestURL = requests[0].url; + + expect(requestURL).to.have.string(';kw=red;'); + }); + + it('should increment the count for the same zone', () => { + let bidRequests = [ + { + sizes: [[300, 250]], + bidder: 'adbutler', + params: { + accountID: '107878', + zoneID: '86133', + domain: 'servedbyadbutler.com.dan.test' + } + }, { + sizes: [[300, 250]], + bidder: 'adbutler', + params: { + accountID: '107878', + zoneID: '86133', + domain: 'servedbyadbutler.com.dan.test' + } }, - requestId: '10b327aa396609', - placementCode: '/123456/header-bid-tag-1' - } - - ] - }; - - var response = { - status: 'SUCCESS', - account_id: 167283, - zone_id: 210093, - cpm: 1.5, - width: 300, - height: 250, - place: 0, - ad_code: '<img src="http://image.source.com/img" alt="" title="" border="0" width="300" height="250">', - tracking_pixels: [ - 'http://tracking.pixel.com/params=info' - ] - }; - - adapter().callBids(params); - - var adUnits = new Array(); - var unit = new Object(); - unit.bids = params.bids; - unit.code = '/123456/header-bid-tag-1'; - unit.sizes = [[300, 250]]; - adUnits.push(unit); - - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = [params]; - } else { - $$PREBID_GLOBAL$$._bidsRequested.push(params); - } - - $$PREBID_GLOBAL$$.adUnits = adUnits; - - $$PREBID_GLOBAL$$.adbutlerCB(response); - - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - expect(bidObject1.getStatusCode()).to.equal(1); - expect(bidObject1.ad).to.have.string('http://tracking.pixel.com/params=info'); + ], + requests = spec.buildRequests(bidRequests), + firstRequest = requests[0].url, + secondRequest = requests[1].url; + + expect(firstRequest).to.have.string(';place=0;'); + expect(secondRequest).to.have.string(';place=1;'); + }); + }); - stubAddBidResponse.restore(); + describe('bid responses', () => { + it('should return complete bid response', () => { + let serverResponse = { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210093, + cpm: 1.5, + width: 300, + height: 250, + place: 0, + ad_code: '<img src="http://image.source.com/img" alt="" title="" border="0" width="300" height="250">', + tracking_pixels: [ + 'http://tracking.pixel.com/params=info' + ] + }, + bids = spec.interpretResponse(serverResponse, {'bidRequest': bidRequests[0]}); + + expect(bids).to.be.lengthOf(1); + + expect(bids[0].bidderCode).to.equal('adbutler'); + expect(bids[0].cpm).to.equal(1.5); + expect(bids[0].width).to.equal(300); + expect(bids[0].height).to.equal(250); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].ad).to.have.length.above(1); + expect(bids[0].ad).to.have.string('http://tracking.pixel.com/params=info'); + }); + + it('should return empty bid response', () => { + let serverResponse = { + status: 'NO_ELIGIBLE_ADS', + zone_id: 210083, + width: 300, + height: 250, + place: 0 + }, + bids = spec.interpretResponse(serverResponse, {'bidRequest': bidRequests[0]}); + + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response on incorrect size', () => { + let serverResponse = { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210083, + cpm: 1.5, + width: 728, + height: 90, + place: 0 + }, + bids = spec.interpretResponse(serverResponse, {'bidRequest': bidRequests[0]}); + + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response with CPM too low', () => { + let serverResponse = { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210093, + cpm: 0.75, + width: 300, + height: 250, + place: 0 + }, + bids = spec.interpretResponse(serverResponse, {'bidRequest': bidRequests[0]}); + + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response with CPM too high', () => { + let serverResponse = { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210093, + cpm: 7.00, + width: 300, + height: 250, + place: 0 + }, + bids = spec.interpretResponse(serverResponse, {'bidRequest': bidRequests[0]}); + + expect(bids).to.be.lengthOf(0); + }); }); }); }); diff --git a/test/spec/modules/c1xBidAdapter_spec.js b/test/spec/modules/c1xBidAdapter_spec.js index 3e482dfaae9..e1a48a5b701 100644 --- a/test/spec/modules/c1xBidAdapter_spec.js +++ b/test/spec/modules/c1xBidAdapter_spec.js @@ -177,6 +177,23 @@ describe('c1x adapter tests: ', () => { }); it('should show error when bidder sends invalid bid responses', () => { let responses; + let adUnits = []; + let unit = {}; + let params = getDefaultBidRequest(); + + unit.bids = params.bids; + unit.code = '/123456/header-bid-tag-1'; + unit.sizes = [[300, 250]]; + adUnits.push(unit); + + if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { + $$PREBID_GLOBAL$$._bidsRequested = [params]; + } else { + $$PREBID_GLOBAL$$._bidsRequested.push(params); + } + + $$PREBID_GLOBAL$$.adUnits = adUnits; + pbjs._c1xResponse(responses); let bidObject = stubAddBidResponse.getCall(0).args[1]; expect(bidObject.statusMessage).to.equal('Bid returned empty or error response'); From bd883523f02186336d64c4486018510dd8bba810 Mon Sep 17 00:00:00 2001 From: John Salis <jsalis531@gmail.com> Date: Wed, 18 Oct 2017 18:54:30 -0400 Subject: [PATCH 03/44] Update Beachfront adapter for v1.0 (#1675) * Update Beachfront adapter for v1.0 * Revert Beachfront test endpoint * Add Beachfront markdown file * Add mediaTypes to example * Fix formatting * Remove descriptionUrl from bid response --- modules/beachfrontBidAdapter.js | 201 ++++++---------- modules/beachfrontBidAdapter.md | 35 +++ .../spec/modules/beachfrontBidAdapter_spec.js | 220 ++++++++++-------- 3 files changed, 231 insertions(+), 225 deletions(-) create mode 100644 modules/beachfrontBidAdapter.md diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 0193df6a3ac..2729dcdd324 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -1,135 +1,88 @@ -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 = '//reachms.bfmio.com/bid.json?exchange_id='; - -function BeachfrontAdapter() { - var baseAdapter = new Adapter('beachfront'); - - baseAdapter.callBids = function (bidRequests) { - const bids = bidRequests.bids || []; - bids.forEach(function(bid) { - var bidRequest = getBidRequest(bid); - var RTBDataParams = prepareAndSaveRTBRequestParams(bid); - if (!RTBDataParams) { - var error = 'No bid params'; - utils.logError(error); - if (bid && bid.placementCode) { - bidmanager.addBidResponse(bid.placementCode, createBid(bid, STATUS.NO_BID)); - } - return; - } - var BID_URL = ENDPOINT + RTBDataParams.appId; - ajax(BID_URL, handleResponse(bidRequest), JSON.stringify(RTBDataParams), { - contentType: 'text/plain', - withCredentials: true - }); +import { registerBidder } from 'src/adapters/bidderFactory'; + +export const ENDPOINT = '//reachms.bfmio.com/bid.json?exchange_id='; + +export const spec = { + code: 'beachfront', + supportedMediaTypes: ['video'], + + isBidRequestValid(bid) { + return !!(bid && bid.params && bid.params.appId && bid.params.bidfloor); + }, + + buildRequests(bids) { + return bids.map(bid => { + return { + method: 'POST', + url: ENDPOINT + bid.params.appId, + data: createRequestParams(bid), + bidRequest: bid + }; }); - }; - - function getBidRequest(bid) { - if (!bid || !bid.params || !bid.params.appId) { - return; - } - - var bidRequest = bid; - bidRequest.width = parseInt(bid.sizes[0], 10) || undefined; - bidRequest.height = parseInt(bid.sizes[1], 10) || undefined; - return bidRequest; - } - - function prepareAndSaveRTBRequestParams(bid) { - if (!bid || !bid.params || !bid.params.appId || !bid.params.bidfloor) { - return; - } + }, - function fetchDeviceType() { - return ((/(ios|ipod|ipad|iphone|android)/i).test(global.navigator.userAgent) ? 1 : ((/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(global.navigator.userAgent) ? 1 : 2)); + interpretResponse(response, { bidRequest }) { + if (!response || !response.url || !response.bidPrice) { + utils.logWarn(`No valid bids from ${spec.code} bidder`); + return []; } - - var bidRequestObject = { - isPrebid: true, - appId: bid.params.appId, - domain: document.location.hostname, - imp: [{ - video: { - w: bid.width, - h: bid.height - }, - bidfloor: bid.params.bidfloor - }], - site: { - page: utils.getTopWindowLocation().host - }, - device: { - ua: navigator.userAgent, - devicetype: fetchDeviceType() - }, - cur: ['USD'] - }; - return bidRequestObject; - } - - /* Notify Prebid of bid responses so bids can get in the auction */ - function handleResponse(bidRequest) { - return function(response) { - var parsed; - if (response) { - try { - parsed = JSON.parse(response); - } catch (error) { - utils.logError(error); - } - } else { - utils.logWarn('No bid response'); - } - - if (!parsed || parsed.error || !parsed.url || !parsed.bidPrice) { - utils.logWarn('No Valid Bid'); - bidmanager.addBidResponse(bidRequest.placementCode, createBid(bidRequest, STATUS.NO_BID)); - return; - } - - var newBid = {}; - newBid.price = parsed.bidPrice; - newBid.url = parsed.url; - newBid.bidId = bidRequest.bidId; - bidmanager.addBidResponse(bidRequest.placementCode, createBid(bidRequest, STATUS.GOOD, newBid)); + let size = getSize(bidRequest.sizes); + return { + requestId: bidRequest.bidId, + bidderCode: spec.code, + cpm: response.bidPrice, + creativeId: response.cmpId, + vastUrl: response.url, + width: size.width, + height: size.height, + mediaType: 'video', + currency: 'USD', + ttl: 300, + netRevenue: true }; } +}; + +function getSize(sizes) { + let parsedSizes = utils.parseSizesInput(sizes); + let [ width, height ] = parsedSizes.length ? parsedSizes[0].split('x') : []; + return { + width: parseInt(width, 10) || undefined, + height: parseInt(height, 10) || undefined + }; +} - function createBid(bidRequest, status, tag) { - var bid = bidfactory.createBid(status, tag); - bid.code = baseAdapter.getBidderCode(); - bid.bidderCode = bidRequest.bidder; - if (!tag || status !== STATUS.GOOD) { - return bid; - } - - bid.cpm = tag.price; - bid.creative_id = tag.cmpId; - bid.width = bidRequest.width; - bid.height = bidRequest.height; - bid.descriptionUrl = tag.url; - bid.vastUrl = tag.url; - bid.mediaType = 'video'; - - return bid; - } +function isMobile() { + return (/(ios|ipod|ipad|iphone|android)/i).test(global.navigator.userAgent); +} - return Object.assign(this, { - callBids: baseAdapter.callBids, - setBidderCode: baseAdapter.setBidderCode - }); +function isConnectedTV() { + return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(global.navigator.userAgent); } -adaptermanager.registerBidAdapter(new BeachfrontAdapter(), 'beachfront', { - supportedMediaTypes: ['video'] -}); +function createRequestParams(bid) { + let size = getSize(bid.sizes); + return { + isPrebid: true, + appId: bid.params.appId, + domain: document.location.hostname, + imp: [{ + video: { + w: size.width, + h: size.height + }, + bidfloor: bid.params.bidfloor + }], + site: { + page: utils.getTopWindowLocation().host + }, + device: { + ua: global.navigator.userAgent, + devicetype: isMobile() ? 1 : isConnectedTV() ? 3 : 2 + }, + cur: ['USD'] + }; +} -module.exports = BeachfrontAdapter; +registerBidder(spec); diff --git a/modules/beachfrontBidAdapter.md b/modules/beachfrontBidAdapter.md new file mode 100644 index 00000000000..27c28d6c86e --- /dev/null +++ b/modules/beachfrontBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +Module Name: Beachfront Bid Adapter + +Module Type: Bidder Adapter + +Maintainer: johnsalis@beachfront.com + +# Description + +Module that connects to Beachfront's demand sources + +# Test Parameters +```javascript + var adUnits = [ + { + code: 'test-video', + sizes: [[640, 360]], + mediaTypes: { + video: { + context: 'instream' + } + }, + bids: [ + { + bidder: 'beachfront', + params: { + bidfloor: 0.01, + appId: '11bc5dd5-7421-4dd8-c926-40fa653bec76' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index 3c9b6d47e9c..43df639613f 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -1,131 +1,149 @@ import { expect } from 'chai'; -import BeachfrontAdapter from 'modules/beachfrontBidAdapter'; -import bidmanager from 'src/bidmanager'; - -const ENDPOINT = '//reachms.bfmio.com/bid.json?exchange_id=11bc5dd5-7421-4dd8-c926-40fa653bec76'; - -const REQUEST = { - 'width': 640, - 'height': 480, - 'bidId': '2a1444be20bb2c', - 'bidder': 'beachfront', - 'bidderRequestId': '7101db09af0db2', - 'params': { - 'appId': 'whatever', - 'video': {}, - 'placementCode': 'video', - 'sizes': [ - 640, 480 - ] - }, - 'bids': [ - { - 'bidFloor': 0.01, - 'bidder': 'beachfront', - 'params': { - 'appId': '11bc5dd5-7421-4dd8-c926-40fa653bec76', - 'bidfloor': 0.01, - 'dev': true - }, - 'placementCode': 'video', - 'sizes': [640, 480], - 'bidId': '2a1444be20bb2c', - 'bidderRequestId': '7101db09af0db2', - 'requestId': '979b659e-ecff-46b8-ae03-7251bae4b725' - } - ], - 'requestId': '979b659e-ecff-46b8-ae03-7251bae4b725', -}; -var RESPONSE = { - 'bidPrice': 5.00, - 'url': 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da:0a47f4ce-d91f-48d0-bd1c-64fa2c196f13:2.90&dsp=58bf26882aba5e6ad608beda,0.612&i_type=pre' -}; +import { spec, ENDPOINT } from 'modules/beachfrontBidAdapter'; +import * as utils from 'src/utils'; describe('BeachfrontAdapter', () => { - let adapter; - - beforeEach(() => adapter = new BeachfrontAdapter()); + let bidRequest; + + beforeEach(() => { + bidRequest = { + bidder: 'beachfront', + params: { + bidfloor: 5.00, + appId: '11bc5dd5-7421-4dd8-c926-40fa653bec76' + }, + adUnitCode: 'adunit-code', + sizes: [ 640, 480 ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475' + }; + }); - describe('request function', () => { - let xhr; - let requests; - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + describe('spec.isBidRequestValid', () => { + it('should return true when the required params are passed', () => { + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); }); - afterEach(() => xhr.restore()); + it('should return false when the "bidfloor" param is missing', () => { + bidRequest.params = { + appId: '11bc5dd5-7421-4dd8-c926-40fa653bec76' + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); - it('exists and is a function', () => { - expect(adapter.callBids).to.exist.and.to.be.a('function'); + it('should return false when the "appId" param is missing', () => { + bidRequest.params = { + bidfloor: 5.00 + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('requires parameters to make request', () => { - adapter.callBids({}); - expect(requests).to.be.empty; + it('should return false when no bid params are passed', () => { + bidRequest.params = {}; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('sends bid request to ENDPOINT via POST', () => { - adapter.callBids(REQUEST); - expect(requests[0].url).to.equal(ENDPOINT); - expect(requests[0].method).to.equal('POST'); + it('should return false when a bid request is not passed', () => { + expect(spec.isBidRequestValid()).to.equal(false); + expect(spec.isBidRequestValid({})).to.equal(false); }); }); - describe('response handler', () => { - let server; - - beforeEach(() => { - server = sinon.fakeServer.create(); - sinon.stub(bidmanager, 'addBidResponse'); + describe('spec.buildRequests', () => { + it('should create a POST request for every bid', () => { + const requests = spec.buildRequests([ bidRequest ]); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.equal(ENDPOINT + bidRequest.params.appId); }); - afterEach(() => { - server.restore(); - bidmanager.addBidResponse.restore(); + it('should attach the bid request object', () => { + const requests = spec.buildRequests([ bidRequest ]); + expect(requests[0].bidRequest).to.equal(bidRequest); }); - it('registers bids', () => { - server.respondWith(JSON.stringify(RESPONSE)); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + it('should attach request data', () => { + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + const [ width, height ] = bidRequest.sizes; + expect(data.isPrebid).to.equal(true); + expect(data.appId).to.equal(bidRequest.params.appId); + expect(data.domain).to.equal(document.location.hostname); + expect(data.imp[0].video).to.deep.equal({ w: width, h: height }); + expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); + expect(data.site).to.deep.equal({ page: utils.getTopWindowLocation().host }); + expect(data.device).to.deep.contain({ ua: navigator.userAgent }); + expect(data.cur).to.deep.equal(['USD']); + }); - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property('statusMessage', 'Bid available'); - expect(response).to.have.property('cpm', 5.00); + it('must parse bid size from a nested array', () => { + const width = 640; + const height = 480; + bidRequest.sizes = [[ width, height ]]; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.imp[0].video).to.deep.equal({ w: width, h: height }); }); - it('handles nobid responses', () => { - server.respondWith(JSON.stringify({ - 'bidPrice': 5.00 - })); + it('must parse bid size from a string', () => { + const width = 640; + const height = 480; + bidRequest.sizes = `${width}x${height}`; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.imp[0].video).to.deep.equal({ w: width, h: height }); + }); - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + it('must handle an empty bid size', () => { + bidRequest.sizes = []; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.imp[0].video).to.deep.equal({ w: undefined, h: undefined }); + }); + }); - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property( - 'statusMessage', - 'Bid returned empty or error response' - ); + describe('spec.interpretResponse', () => { + it('should return no bids if the response is not valid', () => { + const bidResponse = spec.interpretResponse(null, { bidRequest }); + expect(bidResponse.length).to.equal(0); }); - it('handles JSON.parse errors', () => { - server.respondWith(''); + it('should return no bids if the response "url" is missing', () => { + const serverResponse = { + bidPrice: 5.00 + }; + const bidResponse = spec.interpretResponse(serverResponse, { bidRequest }); + expect(bidResponse.length).to.equal(0); + }); - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + it('should return no bids if the response "bidPrice" is missing', () => { + const serverResponse = { + url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da' + }; + const bidResponse = spec.interpretResponse(serverResponse, { bidRequest }); + expect(bidResponse.length).to.equal(0); + }); - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property( - 'statusMessage', - 'Bid returned empty or error response' - ); + it('should return a valid bid response', () => { + const serverResponse = { + bidPrice: 5.00, + url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', + cmpId: '123abc' + }; + const bidResponse = spec.interpretResponse(serverResponse, { bidRequest }); + expect(bidResponse).to.deep.equal({ + requestId: bidRequest.bidId, + bidderCode: spec.code, + cpm: serverResponse.bidPrice, + creativeId: serverResponse.cmpId, + vastUrl: serverResponse.url, + width: 640, + height: 480, + mediaType: 'video', + currency: 'USD', + ttl: 300, + netRevenue: true + }); }); }); }); From b721d6fd7d5b8b63d3fdcae69b740f60a2cb3f83 Mon Sep 17 00:00:00 2001 From: Rich Loveland <loveland.richard@gmail.com> Date: Thu, 19 Oct 2017 17:04:55 -0400 Subject: [PATCH 04/44] Update JSDoc to call the module `pbjs` (#1572) * Update JSDoc to call the module `pbjs` (instead of `$$PREBID_GLOBAL$$``) * Add alias to moar functions, per feedback --- src/prebid.js | 69 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/src/prebid.js b/src/prebid.js index 5e3168a829d..54dc9c55118 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -1,4 +1,4 @@ -/** @module $$PREBID_GLOBAL$$ */ +/** @module pbjs */ import { getGlobal } from './prebidGlobal'; import { flatten, uniques, isGptPubadsDefined, adUnitsFilter } from './utils'; @@ -117,7 +117,7 @@ function setRenderSize(doc, width, height) { /** * This function returns the query string targeting parameters available at this moment for a given ad unit. Note that some bidder's response may not have been received if you call this function too quickly after the requests are sent. * @param {string} [adunitCode] adUnitCode to get the bid responses for - * @alias module:$$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCodeStr + * @alias module:pbjs.getAdserverTargetingForAdUnitCodeStr * @return {Array} returnObj return bids array */ $$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCodeStr = function (adunitCode) { @@ -135,6 +135,7 @@ $$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCodeStr = function (adunitCode) { /** * This function returns the query string targeting parameters available at this moment for a given ad unit. Note that some bidder's response may not have been received if you call this function too quickly after the requests are sent. * @param adUnitCode {string} adUnitCode to get the bid responses for + * @alias module:pbjs.getAdserverTargetingForAdUnitCode * @returns {Object} returnObj return bids */ $$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCode = function(adUnitCode) { @@ -144,7 +145,7 @@ $$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCode = function(adUnitCode) { /** * returns all ad server targeting for all ad units * @return {Object} Map of adUnitCodes and targeting values [] - * @alias module:$$PREBID_GLOBAL$$.getAdserverTargeting + * @alias module:pbjs.getAdserverTargeting */ $$PREBID_GLOBAL$$.getAdserverTargeting = function (adUnitCode) { @@ -169,7 +170,7 @@ $$PREBID_GLOBAL$$.getAdserverTargeting = function (adUnitCode) { /** * This function returns the bid responses at the given moment. - * @alias module:$$PREBID_GLOBAL$$.getBidResponses + * @alias module:pbjs.getBidResponses * @return {Object} map | object that contains the bidResponses */ @@ -196,7 +197,7 @@ $$PREBID_GLOBAL$$.getBidResponses = function () { /** * Returns bidResponses for the specified adUnitCode * @param {string} adUnitCode adUnitCode - * @alias module:$$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode + * @alias module:pbjs.getBidResponsesForAdUnitCode * @return {Object} bidResponse object */ @@ -210,7 +211,7 @@ $$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode = function (adUnitCode) { /** * Set query string targeting on one or more GPT ad units. * @param {(string|string[])} adUnit a single `adUnit.code` or multiple. - * @alias module:$$PREBID_GLOBAL$$.setTargetingForGPTAsync + * @alias module:pbjs.setTargetingForGPTAsync */ $$PREBID_GLOBAL$$.setTargetingForGPTAsync = function (adUnit) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.setTargetingForGPTAsync', arguments); @@ -232,6 +233,10 @@ $$PREBID_GLOBAL$$.setTargetingForGPTAsync = function (adUnit) { events.emit(SET_TARGETING); }; +/** + * Set query string targeting on all AST (AppNexus Seller Tag) ad units. Note that this function has to be called after all ad units on page are defined. For working example code, see [Using Prebid.js with AppNexus Publisher Ad Server](http://prebid.org/dev-docs/examples/use-prebid-with-appnexus-ad-server.html). + * @alias module:pbjs.setTargetingForAst + */ $$PREBID_GLOBAL$$.setTargetingForAst = function() { utils.logInfo('Invoking $$PREBID_GLOBAL$$.setTargetingForAn', arguments); if (!targeting.isApntagDefined()) { @@ -247,7 +252,7 @@ $$PREBID_GLOBAL$$.setTargetingForAst = function() { /** * Returns a bool if all the bids have returned or timed out - * @alias module:$$PREBID_GLOBAL$$.allBidsAvailable + * @alias module:pbjs.allBidsAvailable * @return {bool} all bids available * * @deprecated This function will be removed in Prebid 1.0 @@ -265,7 +270,7 @@ $$PREBID_GLOBAL$$.allBidsAvailable = function () { * Note that doc SHOULD NOT be the parent document page as we can't doc.write() asynchronously * @param {HTMLDocument} doc document * @param {string} id bid id to locate the ad - * @alias module:$$PREBID_GLOBAL$$.renderAd + * @alias module:pbjs.renderAd */ $$PREBID_GLOBAL$$.renderAd = function (doc, id) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.renderAd', arguments); @@ -321,7 +326,7 @@ $$PREBID_GLOBAL$$.renderAd = function (doc, id) { /** * Remove adUnit from the $$PREBID_GLOBAL$$ configuration * @param {string} adUnitCode the adUnitCode to remove - * @alias module:$$PREBID_GLOBAL$$.removeAdUnit + * @alias module:pbjs.removeAdUnit */ $$PREBID_GLOBAL$$.removeAdUnit = function (adUnitCode) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.removeAdUnit', arguments); @@ -334,6 +339,9 @@ $$PREBID_GLOBAL$$.removeAdUnit = function (adUnitCode) { } }; +/** + * @alias module:pbjs.clearAuction + */ $$PREBID_GLOBAL$$.clearAuction = function() { auctionRunning = false; // Only automatically sync if the publisher has not chosen to "enableOverride" @@ -355,6 +363,7 @@ $$PREBID_GLOBAL$$.clearAuction = function() { * @param {number} requestOptions.timeout * @param {Array} requestOptions.adUnits * @param {Array} requestOptions.adUnitCodes + * @alias module:pbjs.requestBids */ $$PREBID_GLOBAL$$.requestBids = function ({ bidsBackHandler, timeout, adUnits, adUnitCodes } = {}) { events.emit('requestBids'); @@ -434,7 +443,7 @@ $$PREBID_GLOBAL$$.requestBids = function ({ bidsBackHandler, timeout, adUnits, a * * Add adunit(s) * @param {Array|Object} adUnitArr Array of adUnits or single adUnit Object. - * @alias module:$$PREBID_GLOBAL$$.addAdUnits + * @alias module:pbjs.addAdUnits */ $$PREBID_GLOBAL$$.addAdUnits = function (adUnitArr) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.addAdUnits', arguments); @@ -456,6 +465,7 @@ $$PREBID_GLOBAL$$.addAdUnits = function (adUnitArr) { * @param {string} event the name of the event * @param {Function} handler a callback to set on event * @param {string} id an identifier in the context of the event + * @alias module:pbjs.onEvent * * This API call allows you to register a callback to handle a Prebid.js event. * An optional `id` parameter provides more finely-grained event callback registration. @@ -486,6 +496,7 @@ $$PREBID_GLOBAL$$.onEvent = function (event, handler, id) { * @param {string} event the name of the event * @param {Function} handler a callback to remove from the event * @param {string} id an identifier in the context of the event (see `$$PREBID_GLOBAL$$.onEvent`) + * @alias module:pbjs.offEvent */ $$PREBID_GLOBAL$$.offEvent = function (event, handler, id) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.offEvent', arguments); @@ -500,7 +511,7 @@ $$PREBID_GLOBAL$$.offEvent = function (event, handler, id) { * Add a callback event * @param {string} eventStr event to attach callback to Options: "allRequestedBidsBack" | "adUnitBidsBack" * @param {Function} func function to execute. Parameters passed into the function: (bidResObj), [adUnitCode]); - * @alias module:$$PREBID_GLOBAL$$.addCallback + * @alias module:pbjs.addCallback * @returns {string} id for callback * * @deprecated This function will be removed in Prebid 1.0 @@ -523,7 +534,7 @@ $$PREBID_GLOBAL$$.addCallback = function (eventStr, func) { /** * Remove a callback event * //@param {string} cbId id of the callback to remove - * @alias module:$$PREBID_GLOBAL$$.removeCallback + * @alias module:pbjs.removeCallback * @returns {string} id for callback * * @deprecated This function will be removed in Prebid 1.0 @@ -539,6 +550,7 @@ $$PREBID_GLOBAL$$.removeCallback = function (/* cbId */) { * Wrapper to register bidderAdapter externally (adaptermanager.registerBidAdapter()) * @param {Function} bidderAdaptor [description] * @param {string} bidderCode [description] + * @alias module:pbjs.registerBidAdapter */ $$PREBID_GLOBAL$$.registerBidAdapter = function (bidderAdaptor, bidderCode) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.registerBidAdapter', arguments); @@ -552,6 +564,7 @@ $$PREBID_GLOBAL$$.registerBidAdapter = function (bidderAdaptor, bidderCode) { /** * Wrapper to register analyticsAdapter externally (adaptermanager.registerAnalyticsAdapter()) * @param {Object} options [description] + * @alias module:pbjs.registerAnalyticsAdapter */ $$PREBID_GLOBAL$$.registerAnalyticsAdapter = function (options) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.registerAnalyticsAdapter', arguments); @@ -562,6 +575,9 @@ $$PREBID_GLOBAL$$.registerAnalyticsAdapter = function (options) { } }; +/** + * @alias module:pbjs.bidsAvailableForAdapter +*/ $$PREBID_GLOBAL$$.bidsAvailableForAdapter = function (bidderCode) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.bidsAvailableForAdapter', arguments); @@ -578,6 +594,7 @@ $$PREBID_GLOBAL$$.bidsAvailableForAdapter = function (bidderCode) { /** * Wrapper to bidfactory.createBid() * @param {string} statusCode [description] + * @alias module:pbjs.createBid * @return {Object} bidResponse [description] */ $$PREBID_GLOBAL$$.createBid = function (statusCode) { @@ -589,7 +606,7 @@ $$PREBID_GLOBAL$$.createBid = function (statusCode) { * Wrapper to bidmanager.addBidResponse * @param {string} adUnitCode [description] * @param {Object} bid [description] - * + * @alias module:pbjs.addBidResponse * @deprecated This function will be removed in Prebid 1.0 * Each bidder will be passed a reference to addBidResponse function in callBids as an argument. * See https://github.com/prebid/Prebid.js/issues/1087 for more details. @@ -604,6 +621,7 @@ $$PREBID_GLOBAL$$.addBidResponse = function (adUnitCode, bid) { * Wrapper to adloader.loadScript * @param {string} tagSrc [description] * @param {Function} callback [description] + * @alias module:pbjs.loadScript */ $$PREBID_GLOBAL$$.loadScript = function (tagSrc, callback, useCache) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.loadScript', arguments); @@ -622,6 +640,7 @@ $$PREBID_GLOBAL$$.loadScript = function (tagSrc, callback, useCache) { * @param {Object} config * @param {string} config.provider The name of the provider, e.g., `"ga"` for Google Analytics. * @param {Object} config.options The options for this particular analytics adapter. This will likely vary between adapters. + * @alias module:pbjs.enableAnalytics */ $$PREBID_GLOBAL$$.enableAnalytics = function (config) { if (config && !utils.isEmpty(config)) { @@ -632,6 +651,9 @@ $$PREBID_GLOBAL$$.enableAnalytics = function (config) { } }; +/** + * @alias module:pbjs.aliasBidder + */ $$PREBID_GLOBAL$$.aliasBidder = function (bidderCode, alias) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.aliasBidder', arguments); if (bidderCode && alias) { @@ -644,6 +666,7 @@ $$PREBID_GLOBAL$$.aliasBidder = function (bidderCode, alias) { /** * Sets a default price granularity scheme. * @param {string|Object} granularity - the granularity scheme. + * @alias module:pbjs.setPriceGranularity * @deprecated - use pbjs.setConfig({ priceGranularity: <granularity> }) * "low": $0.50 increments, capped at $5 CPM * "medium": $0.10 increments, capped at $20 CPM (the default) @@ -661,12 +684,16 @@ $$PREBID_GLOBAL$$.setPriceGranularity = function (granularity) { config.setConfig({ priceGranularity: granularity }); }; -/** @deprecated - use pbjs.setConfig({ enableSendAllBids: <true|false> }) */ +/** + * @alias module:pbjs.enableSendAllBids + * @deprecated - use pbjs.setConfig({ enableSendAllBids: <true|false> }) +*/ $$PREBID_GLOBAL$$.enableSendAllBids = function () { config.setConfig({ enableSendAllBids: true }); }; /** + * @alias module:pbjs.getAllWinningBids * The bid response object returned by an external bidder adapter during the auction. * @typedef {Object} AdapterBidResponse * @property {string} pbAg Auto granularity price bucket; CPM <= 5 ? increment = 0.05 : CPM > 5 && CPM <= 10 ? increment = 0.10 : CPM > 10 && CPM <= 20 ? increment = 0.50 : CPM > 20 ? priceCap = 20.00. Example: `"0.80"`. @@ -713,7 +740,7 @@ $$PREBID_GLOBAL$$.getAllWinningBids = function () { * Build master video tag from publishers adserver tag * @param {string} adserverTag default url * @param {Object} options options for video tag - * + * @alias module:pbjs.buildMasterVideoTagFromAdserverTag * @deprecated Include the dfpVideoSupport module in your build, and use the $$PREBID_GLOBAL$$.adservers.dfp.buildVideoAdUrl function instead. * This function will be removed in Prebid 1.0. */ @@ -751,6 +778,7 @@ $$PREBID_GLOBAL$$.buildMasterVideoTagFromAdserverTag = function (adserverTag, op * If never called, Prebid will use "random" as the default. * * @param {string} order One of the valid orders, described above. + * @alias module:pbjs.setBidderSequence * @deprecated - use pbjs.setConfig({ bidderSequence: <order> }) */ $$PREBID_GLOBAL$$.setBidderSequence = adaptermanager.setBidderSequence; @@ -759,6 +787,7 @@ $$PREBID_GLOBAL$$.setBidderSequence = adaptermanager.setBidderSequence; * Get array of highest cpm bids for all adUnits, or highest cpm bid * object for the given adUnit * @param {string} adUnitCode - optional ad unit code + * @alias module:pbjs.getHighestCpmBids * @return {Array} array containing highest cpm bid object(s) */ $$PREBID_GLOBAL$$.getHighestCpmBids = function (adUnitCode) { @@ -777,6 +806,7 @@ $$PREBID_GLOBAL$$.getHighestCpmBids = function (adUnitCode) { * @property {string} [adapter] adapter code to use for S2S * @property {string} [syncEndpoint] endpoint URL for syncing cookies * @property {boolean} [cookieSet] enables cookieSet functionality + * @alias module:pbjs.setS2SConfig */ $$PREBID_GLOBAL$$.setS2SConfig = function(options) { if (!utils.contains(Object.keys(options), 'accountId')) { @@ -805,6 +835,7 @@ $$PREBID_GLOBAL$$.setS2SConfig = function(options) { /** * Get Prebid config options * @param {Object} options + * @alias module:pbjs.getConfig */ $$PREBID_GLOBAL$$.getConfig = config.getConfig; @@ -841,6 +872,7 @@ $$PREBID_GLOBAL$$.getConfig = config.getConfig; * @param {string} options.publisherDomain The publisher's domain where Prebid is running, for cross-domain iFrame communication. Example: `pbjs.setConfig({ publisherDomain: "https://www.theverge.com" })`. * @param {number} options.cookieSyncDelay A delay (in milliseconds) for requesting cookie sync to stay out of the critical path of page load. Example: `pbjs.setConfig({ cookieSyncDelay: 100 })`. * @param {Object} options.s2sConfig The configuration object for [server-to-server header bidding](http://prebid.org/dev-docs/get-started-with-prebid-server.html). Example: + * @alias module:pbjs.setConfig * ``` * pbjs.setConfig({ * s2sConfig: { @@ -873,10 +905,10 @@ $$PREBID_GLOBAL$$.que.push(() => listenMessagesFromCreative()); * by prebid once it's done loading. If it runs after prebid loads, then this monkey-patch causes their * function to execute immediately. * - * @memberof $$PREBID_GLOBAL$$ + * @memberof pbjs * @param {function} command A function which takes no arguments. This is guaranteed to run exactly once, and only after * the Prebid script has been fully loaded. - * @alias module:$$PREBID_GLOBAL$$.cmd.push + * @alias module:pbjs.cmd.push */ $$PREBID_GLOBAL$$.cmd.push = function(command) { if (typeof command === 'function') { @@ -905,6 +937,9 @@ function processQueue(queue) { }); } +/** + * @alias module:pbjs.processQueue + */ $$PREBID_GLOBAL$$.processQueue = function() { processQueue($$PREBID_GLOBAL$$.que); processQueue($$PREBID_GLOBAL$$.cmd); From 37b218a65bd3cb2903dbd002382a9dc751423622 Mon Sep 17 00:00:00 2001 From: Igor Tchibirev <it@realvu.com> Date: Fri, 20 Oct 2017 09:53:56 -0400 Subject: [PATCH 05/44] realvuBidAdapter (#1571) * init commit realvuAnalyticsAdapter.js * realvuBidAdapter.js initial commit * realvuBidAdapter fixed for latest version * realvuBidAdapter unit_id:bid.code * tabs and spaces * test implemented * postpone analytics * try-catch for top * appnexusBidAdapter inline * var replaced with const in require statements * unwanted code removed, test fixed * lint and test-coverage fix * lint fix --- modules/realvuBidAdapter.js | 239 +++++++++++++++++++++ test/spec/modules/realvuBidAdapter_spec.js | 61 ++++++ yarn.lock | 4 + 3 files changed, 304 insertions(+) create mode 100644 modules/realvuBidAdapter.js create mode 100644 test/spec/modules/realvuBidAdapter_spec.js diff --git a/modules/realvuBidAdapter.js b/modules/realvuBidAdapter.js new file mode 100644 index 00000000000..b3e0e4f043f --- /dev/null +++ b/modules/realvuBidAdapter.js @@ -0,0 +1,239 @@ +import { getBidRequest } from 'src/utils'; +import adaptermanager from 'src/adaptermanager'; + +const CONSTANTS = require('src/constants'); +const utils = require('src/utils.js'); +const adloader = require('src/adloader.js'); +const bidmanager = require('src/bidmanager.js'); +const bidfactory = require('src/bidfactory.js'); +const Adapter = require('src/adapter.js').default; + +var RealVuAdapter = function RealVuAdapter() { + var baseAdapter = new Adapter('realvu'); + baseAdapter.callBids = function (params) { + var pbids = params.bids; + var boost_back = function() { + var top1 = window; + realvu_frm = 0; + try { + var wnd = window; + while ((top1 != top) && (typeof (wnd.document) != 'undefined')) { + top1 = wnd; + wnd = wnd.parent; + } + } catch (e) { }; + for (var i = 0; i < pbids.length; i++) { + var bid_rq = pbids[i]; + var sizes = utils.parseSizesInput(bid_rq.sizes); + top1.realvu_boost.addUnitById({ + partner_id: bid_rq.params.partnerId, + unit_id: bid_rq.placementCode, + callback: baseAdapter.boostCall, + pbjs_bid: bid_rq, + size: sizes[0], + mode: 'kvp' + }); + } + }; + adloader.loadScript('//ac.realvu.net/realvu_boost.js', boost_back, 1); + }; + + baseAdapter.boostCall = function(rez) { + var bid_request = rez.pin.pbjs_bid; + var callbackId = bid_request.bidId; + if (rez.realvu === 'yes') { + var adap = new RvAppNexusAdapter(); + adloader.loadScript(adap.buildJPTCall(bid_request, callbackId)); + } else { // not in view - respond with no bid. + var adResponse = bidfactory.createBid(2); + adResponse.bidderCode = 'realvu'; + bidmanager.addBidResponse(bid_request.placementCode, adResponse); + } + }; + + // +copy/pasted appnexusBidAdapter, "handleAnCB" replaced with "handleRvAnCB" + var RvAppNexusAdapter = function RvAppNexusAdapter() { + var usersync = false; + + this.buildJPTCall = function (bid, callbackId) { + // determine tag params + var placementId = utils.getBidIdParameter('placementId', bid.params); + + // memberId will be deprecated, use member instead + var memberId = utils.getBidIdParameter('memberId', bid.params); + var member = utils.getBidIdParameter('member', bid.params); + var inventoryCode = utils.getBidIdParameter('invCode', bid.params); + var query = utils.getBidIdParameter('query', bid.params); + var referrer = utils.getBidIdParameter('referrer', bid.params); + var altReferrer = utils.getBidIdParameter('alt_referrer', bid.params); + var jptCall = '//ib.adnxs.com/jpt?'; + + jptCall = utils.tryAppendQueryString(jptCall, 'callback', '$$PREBID_GLOBAL$$.handleRvAnCB'); + jptCall = utils.tryAppendQueryString(jptCall, 'callback_uid', callbackId); + jptCall = utils.tryAppendQueryString(jptCall, 'psa', '0'); + jptCall = utils.tryAppendQueryString(jptCall, 'id', placementId); + if (member) { + jptCall = utils.tryAppendQueryString(jptCall, 'member', member); + } else if (memberId) { + jptCall = utils.tryAppendQueryString(jptCall, 'member', memberId); + utils.logMessage('appnexus.callBids: "memberId" will be deprecated soon. Please use "member" instead'); + } + + jptCall = utils.tryAppendQueryString(jptCall, 'code', inventoryCode); + jptCall = utils.tryAppendQueryString(jptCall, 'traffic_source_code', (utils.getBidIdParameter('trafficSourceCode', bid.params))); + + // sizes takes a bit more logic + var sizeQueryString = ''; + var parsedSizes = utils.parseSizesInput(bid.sizes); + + // combine string into proper querystring for impbus + var parsedSizesLength = parsedSizes.length; + if (parsedSizesLength > 0) { + // first value should be "size" + sizeQueryString = 'size=' + parsedSizes[0]; + if (parsedSizesLength > 1) { + // any subsequent values should be "promo_sizes" + sizeQueryString += '&promo_sizes='; + for (var j = 1; j < parsedSizesLength; j++) { + sizeQueryString += parsedSizes[j] += ','; + } + + // remove trailing comma + if (sizeQueryString && sizeQueryString.charAt(sizeQueryString.length - 1) === ',') { + sizeQueryString = sizeQueryString.slice(0, sizeQueryString.length - 1); + } + } + } + + if (sizeQueryString) { + jptCall += sizeQueryString + '&'; + } + + // this will be deprecated soon + var targetingParams = utils.parseQueryStringParameters(query); + + if (targetingParams) { + // don't append a & here, we have already done it in parseQueryStringParameters + jptCall += targetingParams; + } + + // append custom attributes: + var paramsCopy = Object.assign({}, bid.params); + + // delete attributes already used + delete paramsCopy.placementId; + delete paramsCopy.memberId; + delete paramsCopy.invCode; + delete paramsCopy.query; + delete paramsCopy.referrer; + delete paramsCopy.alt_referrer; + delete paramsCopy.member; + + // get the reminder + var queryParams = utils.parseQueryStringParameters(paramsCopy); + + // append + if (queryParams) { + jptCall += queryParams; + } + + // append referrer + if (referrer === '') { + referrer = utils.getTopWindowUrl(); + } + + jptCall = utils.tryAppendQueryString(jptCall, 'referrer', referrer); + jptCall = utils.tryAppendQueryString(jptCall, 'alt_referrer', altReferrer); + + // remove the trailing "&" + if (jptCall.lastIndexOf('&') === jptCall.length - 1) { + jptCall = jptCall.substring(0, jptCall.length - 1); + } + + // @if NODE_ENV='debug' + utils.logMessage('jpt request built: ' + jptCall); + // @endif + + // append a timer here to track latency + bid.startTime = new Date().getTime(); + + return jptCall; + } + + // expose the callback to the global object: + $$PREBID_GLOBAL$$.handleRvAnCB = function (jptResponseObj) { + var bidCode; + + if (jptResponseObj && jptResponseObj.callback_uid) { + var responseCPM; + var id = jptResponseObj.callback_uid; + var placementCode = ''; + var bidObj = getBidRequest(id); + if (bidObj) { + bidCode = bidObj.bidder; + + placementCode = bidObj.placementCode; + + // set the status + bidObj.status = CONSTANTS.STATUS.GOOD; + } + + // @if NODE_ENV='debug' + utils.logMessage('JSONP callback function called for ad ID: ' + id); + + // @endif + var bid = []; + if (jptResponseObj.result && jptResponseObj.result.cpm && jptResponseObj.result.cpm !== 0) { + responseCPM = parseInt(jptResponseObj.result.cpm, 10); + + // CPM response from /jpt is dollar/cent multiplied by 10000 + // in order to avoid using floats + // switch CPM to "dollar/cent" + responseCPM = responseCPM / 10000; + + // store bid response + // bid status is good (indicating 1) + var adId = jptResponseObj.result.creative_id; + bid = bidfactory.createBid(1, bidObj); + bid.creative_id = adId; + bid.bidderCode = bidCode; + bid.cpm = responseCPM; + bid.adUrl = jptResponseObj.result.ad; + bid.width = jptResponseObj.result.width; + bid.height = jptResponseObj.result.height; + bid.dealId = jptResponseObj.result.deal_id; + + bidmanager.addBidResponse(placementCode, bid); + } else { + // no bid + bid = bidfactory.createBid(2, bidObj); + bid.bidderCode = bidCode; + bidmanager.addBidResponse(placementCode, bid); + } + + if (!usersync) { + var iframe = utils.createInvisibleIframe(); + iframe.src = '//acdn.adnxs.com/ib/static/usersync/v3/async_usersync.html'; + try { + document.body.appendChild(iframe); + } catch (error) { + utils.logError(error); + } + usersync = true; + } + } else { + utils.logMessage('No prebid response for placement %%PLACEMENT%%'); + } + }; + }; + // -copy/pasted appnexusBidAdapter + return Object.assign(this, { + callBids: baseAdapter.callBids, + setBidderCode: baseAdapter.setBidderCode, + boostCall: baseAdapter.boostCall + }); +}; + +adaptermanager.registerBidAdapter(new RealVuAdapter(), 'realvu'); + +module.exports = RealVuAdapter; diff --git a/test/spec/modules/realvuBidAdapter_spec.js b/test/spec/modules/realvuBidAdapter_spec.js new file mode 100644 index 00000000000..36517fa723e --- /dev/null +++ b/test/spec/modules/realvuBidAdapter_spec.js @@ -0,0 +1,61 @@ +import {expect} from 'chai'; +import RealVuAdapter from '../../../modules/realvuBidAdapter'; +import bidmanager from '../../../src/bidmanager'; +import adloader from '../../../src/adloader'; + +describe('RealVu Adapter Test', () => { + let adapter; + + const REQUEST = { + bidderCode: 'realvu', + requestId: '0d67ddab-1502-4897-a7bf-e8078e983405', + bidderRequestId: '1b5e314fe79b1d', + bids: [ + { + bidId: '2d86a04312d95d', + bidder: 'realvu', + bidderRequestId: '1b5e314fe79b1d', + // mediaType:undefined, + params: { + partnerId: '1Y', + placementId: '9339508', + }, + placementCode: 'ad_container_1', + // renderer:undefined, + sizes: [[300, 250]], + transactionId: '0d67ddab-1502-4897-a7bf-e8078e983405' + } + ], + start: 1504628062271 + }; + + var bidResponseStub; + var adloaderStub; + + beforeEach(function() { + bidResponseStub = sinon.stub(bidmanager, 'addBidResponse'); + adloaderStub = sinon.stub(adloader, 'loadScript'); + }); + + afterEach(function() { + adloaderStub.restore(); + bidResponseStub.restore(); + }); + + adapter = new RealVuAdapter(); + + it('load boost', () => { + adapter.callBids(REQUEST); + expect(adloaderStub.getCall(0).args[0]).to.contain('realvu_boost.js'); + }); + + it('callBid "yes"', () => { + adapter.boostCall({realvu: 'yes', pin: {pbjs_bid: REQUEST.bids[0]}}); + expect(adloaderStub.getCall(0).args[0]).to.contain('id=9339508'); + }); + + it('callBid "no"', () => { + adapter.boostCall({realvu: 'no', pin: {pbjs_bid: REQUEST.bids[0]}}); + expect(bidResponseStub.getCall(0).args[1].getStatusCode()).to.equal(2); + }); +}); diff --git a/yarn.lock b/yarn.lock index f6efe31c6a5..30e6b93b544 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4249,6 +4249,10 @@ ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" +ignore-loader@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ignore-loader/-/ignore-loader-0.1.2.tgz#d81f240376d0ba4f0d778972c3ad25874117a463" + ignore@^3.3.3: version "3.3.5" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.5.tgz#c4e715455f6073a8d7e5dae72d2fc9d71663dba6" From a7a73fd8228c84c2f377c7ccdbccbbbeba66b67f Mon Sep 17 00:00:00 2001 From: Pieter de Zwart <pdezwart@gmail.com> Date: Fri, 20 Oct 2017 14:41:45 -0700 Subject: [PATCH 06/44] Updating license (#1717) --- LICENSE | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/LICENSE b/LICENSE index c8daf0f8942..f5028c63317 100644 --- a/LICENSE +++ b/LICENSE @@ -176,18 +176,7 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2013 APPNEXUS INC + Copyright 2017 PREBID.ORG, INC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -199,4 +188,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. From be21952ee1ffa8f183f718f63d612b6cfdb84908 Mon Sep 17 00:00:00 2001 From: Sebastian Widelak <sebakpl@gmail.com> Date: Fri, 20 Oct 2017 23:42:49 +0200 Subject: [PATCH 07/44] Justpremium Adapter bugfix (#1716) * Justpremium adapter and unit tests. * Fix test suit. * Performance improvements. * Changes requested in pull request review. * Register justpremium adapter in adaptermanager * pass through bid from request * fix linting errors * Load polyfills for older browsers * Load polyfills if older browser * Remove package-lock.json --- modules/justpremiumBidAdapter.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/justpremiumBidAdapter.js b/modules/justpremiumBidAdapter.js index b608461946c..d1ca62cd8e4 100644 --- a/modules/justpremiumBidAdapter.js +++ b/modules/justpremiumBidAdapter.js @@ -49,6 +49,12 @@ const JustpremiumAdapter = function JustpremiumAdapter() { return null; } + function isOldBrowser() { + const isPromisse = typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]') !== -1; + const isWeakMap = typeof WeakMap !== 'undefined' && WeakMap.toString().indexOf('[native code]') !== -1; + return (!Array.prototype.find || !Array.prototype.sort || !Array.prototype.map || !Array.prototype.filter || !Array.prototype.keys || !isPromisse || !isWeakMap); + } + function setupVar() { d = top.document; jPAM = top.jPAM = top.jPAM || window.jPAM || {}; @@ -58,7 +64,7 @@ const JustpremiumAdapter = function JustpremiumAdapter() { server: null }; const libVer = readCookie('jpxhbjs') || null; - toLoad = dConfig.toLoad || [d.location.protocol + '//cdn-cf.justpremium.com/js/' + (libVer ? libVer + '/' : '') + 'jpx.js']; + toLoad = dConfig.toLoad || [d.location.protocol + '//cdn-cf.justpremium.com/js/' + (libVer ? libVer + '/' : '') + (isOldBrowser() ? 'jpxp.js' : 'jpx.js')]; server = dConfig.server || d.location.protocol + '//pre.ads.justpremium.com/v/1.4'; } @@ -114,7 +120,6 @@ const JustpremiumAdapter = function JustpremiumAdapter() { return rec.length ? rec.pop() : false; } } - return false; } From a45387dc9d6bdcfbf1937ab8f42c0871c7c3c284 Mon Sep 17 00:00:00 2001 From: FeatureForward <production@featureforward.com> Date: Mon, 23 Oct 2017 19:03:42 +0300 Subject: [PATCH 08/44] Allow more than one placement from one page (#1692) --- modules/featureforwardBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/featureforwardBidAdapter.js b/modules/featureforwardBidAdapter.js index 34d7dddba49..53a3a7c5a08 100644 --- a/modules/featureforwardBidAdapter.js +++ b/modules/featureforwardBidAdapter.js @@ -14,8 +14,8 @@ function FeatureForwardAdapter() { }; function _callBids(bidderRequest) { - var i = 0; bidderRequest.bids.forEach(bidRequest => { + var i = 0; try { while (bidRequest.sizes[i] !== undefined) { var params = Object.assign({}, environment(), bidRequest.params, {'size': bidRequest.sizes[i]}); From 505f7f3ecdc79a5c8fcfbf8fbd123f3848bebd94 Mon Sep 17 00:00:00 2001 From: Rich Snapp <rsnapp@rubiconproject.com> Date: Mon, 23 Oct 2017 11:03:18 -0600 Subject: [PATCH 09/44] fix log message not displaying when referencing missing bidder (#1737) * fix log message not displaying when referencing missing bidder * add test for missing bidder log message * move test to adaptermanager_spec --- src/adaptermanager.js | 14 ++++++------- test/spec/unit/core/adapterManager_spec.js | 24 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 2204e997084..37ab5dffafb 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -197,20 +197,18 @@ exports.callBids = ({adUnits, cbTimeout}) => { $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); _bidderRequests.push(bidderRequest); } + } else { + utils.logError(`Adapter trying to be called which does not exist: ${bidderCode} adaptermanager.callBids`); } }); _bidderRequests.forEach(bidRequest => { bidRequest.start = new Date().getTime(); const adapter = _bidderRegistry[bidRequest.bidderCode]; - if (adapter) { - if (bidRequest.bids && bidRequest.bids.length !== 0) { - utils.logMessage(`CALLING BIDDER ======= ${bidRequest.bidderCode}`); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); - adapter.callBids(bidRequest); - } - } else { - utils.logError(`Adapter trying to be called which does not exist: ${bidRequest.bidderCode} adaptermanager.callBids`); + if (bidRequest.bids && bidRequest.bids.length !== 0) { + utils.logMessage(`CALLING BIDDER ======= ${bidRequest.bidderCode}`); + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + adapter.callBids(bidRequest); } }) }; diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 6da22ed8984..8476d53af7b 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -34,6 +34,30 @@ var appnexusAdapterMock = { }; describe('adapterManager tests', () => { + describe('callBids', () => { + beforeEach(() => { + sinon.stub(utils, 'logError'); + }); + + afterEach(() => { + utils.logError.restore(); + }); + + it('should log an error if a bidder is used that does not exist', () => { + const adUnits = [{ + code: 'adUnit-code', + bids: [ + {bidder: 'appnexus', params: {placementId: 'id'}}, + {bidder: 'fakeBidder', params: {placementId: 'id'}} + ] + }]; + + AdapterManager.callBids({adUnits}); + + sinon.assert.called(utils.logError); + }); + }); + describe('S2S tests', () => { beforeEach(() => { AdapterManager.setS2SConfig(CONFIG); From f8bf19790a243d0d5be340d856da559c35b25d07 Mon Sep 17 00:00:00 2001 From: Nick Narbone <bansawbanchee@users.noreply.github.com> Date: Mon, 23 Oct 2017 13:07:51 -0400 Subject: [PATCH 10/44] Fix window.top.host cross origin issue when in nested iframes. (#1730) --- modules/sonobiBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index 81745427742..689de8635c9 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -12,7 +12,7 @@ var SonobiAdapter = function SonobiAdapter() { var trinity = 'https://apex.go.sonobi.com/trinity.js?key_maker='; var adSlots = request.bids || []; var bidderRequestId = request.bidderRequestId; - var ref = (window.frameElement) ? '&ref=' + encodeURI(top.location.host || document.referrer) : ''; + var ref = '&ref=' + encodeURI(utils.getTopWindowLocation().host); adloader.loadScript(trinity + JSON.stringify(_keymaker(adSlots)) + '&cv=' + _operator(bidderRequestId) + ref); } From 409fbc5be453a15ad0104db7a755ab9595214206 Mon Sep 17 00:00:00 2001 From: varashellov <sk@ultralab.by> Date: Mon, 23 Oct 2017 12:32:37 -0700 Subject: [PATCH 11/44] Platform.io Bidder Adapter update. Prebid v1.0. (#1705) * 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 --- modules/platformioBidAdapter.js | 188 ++++++++----- modules/platformioBidAdapter.md | 27 ++ .../spec/modules/platformioBidAdapter_spec.js | 250 +++++++----------- 3 files changed, 246 insertions(+), 219 deletions(-) create mode 100644 modules/platformioBidAdapter.md diff --git a/modules/platformioBidAdapter.js b/modules/platformioBidAdapter.js index 87ebb2b61c4..c33551ed396 100644 --- a/modules/platformioBidAdapter.js +++ b/modules/platformioBidAdapter.js @@ -1,63 +1,125 @@ -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 CONSTANTS = require('src/constants.json'); -var adaptermanager = require('src/adaptermanager'); - -var PlatformIOAdapter = function PlatformIOAdapter() { - function _callBids(params) { - var bidURL; - var bids = params.bids || []; - var requestURL = window.location.protocol + '//js.adx1.com/pb_ortb.js?cb=' + new Date().getTime() + '&ver=1&'; - - for (var i = 0; i < bids.length; i++) { - var requestParams = {}; - var bid = bids[i]; - - requestParams.pub_id = bid.params.pubId; - requestParams.site_id = bid.params.siteId; - requestParams.placement_id = bid.placementCode; - - var parseSized = utils.parseSizesInput(bid.sizes); - var arrSize = parseSized[0].split('x'); - - requestParams.width = arrSize[0]; - requestParams.height = arrSize[1]; - requestParams.callback = '$$PREBID_GLOBAL$$._doPlatformIOCallback'; - requestParams.callback_uid = bid.bidId; - bidURL = requestURL + utils.parseQueryStringParameters(requestParams); - - utils.logMessage('PlatformIO.prebid, Bid ID: ' + bid.bidId + ', Pub ID: ' + bid.params.pubId); - adloader.loadScript(bidURL); - } - } - - $$PREBID_GLOBAL$$._doPlatformIOCallback = function (response) { - var bidObject; - var bidRequest; - var callbackID; - callbackID = response.callback_uid; - bidRequest = utils.getBidRequest(callbackID); - if (response.cpm > 0) { - bidObject = bidfactory.createBid(CONSTANTS.STATUS.GOOD, bidRequest); - bidObject.bidderCode = 'platformio'; - bidObject.cpm = response.cpm; - bidObject.ad = response.tag; - bidObject.width = response.width; - bidObject.height = response.height; - } else { - bidObject = bidfactory.createBid(CONSTANTS.STATUS.NO_BID, bidRequest); - bidObject.bidderCode = 'platformio'; - utils.logMessage('No Bid response from Platformio request: ' + callbackID); - } - bidmanager.addBidResponse(bidRequest.placementCode, bidObject); - }; - - return { - callBids: _callBids - }; -}; -adaptermanager.registerBidAdapter(new PlatformIOAdapter(), 'platformio'); - -module.exports = PlatformIOAdapter; + +import {logError, getTopWindowLocation} from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +export const spec = { + + code: 'platformio', + + isBidRequestValid: bid => ( + !!(bid && bid.params && bid.params.pubId && bid.params.siteId) + ), + buildRequests: bidRequests => { + const request = { + id: bidRequests[0].bidderRequestId, + at: 2, + imp: bidRequests.map(slot => impression(slot)), + site: site(bidRequests), + device: device(), + }; + return { + method: 'POST', + url: '//piohbdisp.hb.adx1.com/', + data: JSON.stringify(request), + }; + }, + interpretResponse: (response, request) => ( + bidResponseAvailable(request, response) + ), +}; +function bidResponseAvailable(bidRequest, bidResponse) { + const idToImpMap = {}; + const idToBidMap = {}; + const ortbRequest = parse(bidRequest.data); + ortbRequest.imp.forEach(imp => { + idToImpMap[imp.id] = imp; + }); + if (bidResponse) { + bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => { + idToBidMap[bid.impid] = bid; + })); + } + const bids = []; + Object.keys(idToImpMap).forEach(id => { + if (idToBidMap[id]) { + const bid = { + requestId: id, + cpm: idToBidMap[id].price, + creative_id: id, + creativeId: id, + adId: id, + }; + 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); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); + bid.width = idToImpMap[id].banner.w; + bid.height = idToImpMap[id].banner.h; + bids.push(bid); + } + }); + return bids; +} +function impression(slot) { + return { + id: slot.bidId, + banner: banner(slot), + bidfloor: '0.000001', + tagid: slot.params.placementId.toString(), + }; +} +function banner(slot) { + const size = slot.params.size.toUpperCase().split('X'); + const width = parseInt(size[0]); + const height = parseInt(size[1]); + return { + w: width, + h: height, + }; +} +function site(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.pubId : '0'; + const siteId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.siteId : '0'; + const appParams = bidderRequest[0].params.app; + if (!appParams) { + return { + publisher: { + id: pubId.toString(), + domain: getTopWindowLocation().hostname, + }, + id: siteId.toString(), + ref: referrer(), + page: getTopWindowLocation().href, + } + } + return null; +} +function referrer() { + try { + return window.top.document.referrer; + } catch (e) { + return document.referrer; + } +} +function device() { + return { + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + w: (window.screen.width || window.innerWidth), + h: (window.screen.height || window.innerHeigh), + }; +} +function parse(rawResponse) { + try { + if (rawResponse) { + return JSON.parse(rawResponse); + } + } catch (ex) { + logError('platformio.parse', 'ERROR', ex); + } + return null; +} + +registerBidder(spec); diff --git a/modules/platformioBidAdapter.md b/modules/platformioBidAdapter.md new file mode 100644 index 00000000000..74f6adbf256 --- /dev/null +++ b/modules/platformioBidAdapter.md @@ -0,0 +1,27 @@ +# Overview + +**Module Name**: Platform.io Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: sk@ultralab.by + +# Description + +Connects to Platform.io demand source to fetch bids. +Please use ```platformio``` as the bidder code. + +# Test Parameters +``` + var adUnits = [{ + code: 'banner-ad-div', + sizes: [[300, 250]], + bids: [{ + bidder: 'platformio', + params: { + pubId: '28082', + siteId: '26047', + placementId: '123', + size: '250X250' + } + }] + }]; +``` diff --git a/test/spec/modules/platformioBidAdapter_spec.js b/test/spec/modules/platformioBidAdapter_spec.js index 01a6176c58d..1b07b4049d4 100644 --- a/test/spec/modules/platformioBidAdapter_spec.js +++ b/test/spec/modules/platformioBidAdapter_spec.js @@ -1,156 +1,94 @@ -describe('platformio adapter tests', function () { - var expect = require('chai').expect; - var urlParse = require('url-parse'); - var querystringify = require('querystringify'); - var adapter = require('modules/platformioBidAdapter'); - var adLoader = require('src/adloader'); - var bidmanager = require('src/bidmanager'); - - var stubLoadScript; - - beforeEach(function () { - stubLoadScript = sinon.stub(adLoader, 'loadScript'); - }); - - afterEach(function () { - stubLoadScript.restore(); - }); - - describe('creation of bid url', function () { - if (typeof ($$PREBID_GLOBAL$$._bidsReceived) === 'undefined') { - $$PREBID_GLOBAL$$._bidsReceived = []; - } - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = []; - } - - it('bid request for single placement', function () { - var params = { - bids: [{ - placementCode: '/19968336/header-bid-tag-0', - sizes: [[300, 250]], - bidId: 'bid1111', - bidder: 'platformio', - params: { pubId: '37054', siteId: '123' } - }] - }; - - adapter().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('pub_id').and.to.equal('37054'); - expect(parsedBidUrlQueryString).to.have.property('site_id').and.to.equal('123'); - expect(parsedBidUrlQueryString).to.have.property('width').and.to.equal('300'); - expect(parsedBidUrlQueryString).to.have.property('height').and.to.equal('250'); - }); - }); - - describe('handling bid response', function () { - it('should return complete bid response', function() { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - - var params = { - bids: [{ - placementCode: '/19968336/header-bid-tag-0', - sizes: [[300, 250]], - bidId: 'bid1111', - bidder: 'platformio', - params: { pubId: '37054', siteId: '123' } - }] - }; - - var response = { - cpm: 1, - width: 300, - height: 250, - callback_uid: 'bid1111', - tag: '<script>document.write("campaign banner");<\/script>' - }; - - adapter().callBids(params); - - var adUnits = []; - var unit = {}; - unit.bids = params.bids; - unit.code = '/123456/header-bid-tag-1'; - unit.sizes = [[300, 250]]; - adUnits.push(unit); - - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = [params]; - } else { - $$PREBID_GLOBAL$$._bidsRequested.push(params); - } - - $$PREBID_GLOBAL$$.adUnits = adUnits; - - $$PREBID_GLOBAL$$._doPlatformIOCallback(response); - - var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - - expect(bidPlacementCode1).to.equal('/19968336/header-bid-tag-0'); - expect(bidObject1.bidderCode).to.equal('platformio'); - expect(bidObject1.cpm).to.equal(1); - expect(bidObject1.width).to.equal(300); - expect(bidObject1.height).to.equal(250); - expect(bidObject1.ad).to.have.length.above(1); - - stubAddBidResponse.restore(); - }); - - it('should return no bid response', function() { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - - var params = { - bids: [{ - placementCode: '/19968336/header-bid-tag-0', - sizes: [[300, 250]], - bidId: 'bid1111', - bidder: 'platformio', - params: { pubId: '37054', siteId: '123' } - }] - }; - - var response = { - cpm: 0, - width: 300, - height: 250, - callback_uid: 'bid1111', - tag: '<script>document.write("default banner");<\/script>' - }; - - adapter().callBids(params); - - var adUnits = []; - var unit = {}; - unit.bids = params.bids; - unit.code = '/123456/header-bid-tag-1'; - unit.sizes = [[300, 250]]; - adUnits.push(unit); - - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = [params]; - } else { - $$PREBID_GLOBAL$$._bidsRequested.push(params); - } - - $$PREBID_GLOBAL$$.adUnits = adUnits; - - $$PREBID_GLOBAL$$._doPlatformIOCallback(response); - - var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - - expect(bidPlacementCode1).to.equal('/19968336/header-bid-tag-0'); - expect(bidObject1.bidderCode).to.equal('platformio'); - - stubAddBidResponse.restore(); - }); - }); -}); +import {expect} from 'chai'; +import {spec} from 'modules/platformioBidAdapter'; +import {getTopWindowLocation} from 'src/utils'; + +describe('Platformio Adapter Tests', () => { + const slotConfigs = [{ + placementCode: '/DfpAccount1/slot1', + sizes: [[300, 250]], + bidId: 'bid12345', + params: { + pubId: '28082', + siteId: '26047', + placementId: '123', + size: '300x250' + } + }, { + placementCode: '/DfpAccount2/slot2', + sizes: [[250, 250]], + bidId: 'bid23456', + params: { + pubId: '28082', + siteId: '26047', + placementId: '456', + size: '250x250' + } + }]; + it('Verify build request', () => { + const request = spec.buildRequests(slotConfigs); + expect(request.url).to.equal('//piohbdisp.hb.adx1.com/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + // site object + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site.publisher).to.not.equal(null); + expect(ortbRequest.site.publisher.id).to.equal('28082'); + expect(ortbRequest.site.ref).to.equal(window.top.document.referrer); + expect(ortbRequest.site.page).to.equal(getTopWindowLocation().href); + expect(ortbRequest.imp).to.have.lengthOf(2); + // device object + expect(ortbRequest.device).to.not.equal(null); + expect(ortbRequest.device.ua).to.equal(navigator.userAgent); + // slot 1 + expect(ortbRequest.imp[0].tagid).to.equal('123'); + 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); + // 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); + }); + + it('Verify parse response', () => { + 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' + }] + }] + }; + const bids = spec.interpretResponse(ortbResponse, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.ad).to.equal('This is an Ad'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.adId).to.equal('bid12345'); + expect(bid.creative_id).to.equal('bid12345'); + expect(bid.creativeId).to.equal('bid12345'); + }); + + it('Verify full passback', () => { + const request = spec.buildRequests(slotConfigs); + const bids = spec.interpretResponse(null, request) + expect(bids).to.have.lengthOf(0); + }); + + it('Verifies bidder code', () => { + expect(spec.code).to.equal('platformio'); + }); + + it('Verifies if bid request valid', () => { + expect(spec.isBidRequestValid(slotConfigs[0])).to.equal(true); + expect(spec.isBidRequestValid({})).to.equal(false); + expect(spec.isBidRequestValid({ params: {} })).to.equal(false); + }); +}); From 755f1939541a70d079202c9ed994447fd4e3cf0f Mon Sep 17 00:00:00 2001 From: dbemiller <dbemiller@appnexus.com> Date: Mon, 23 Oct 2017 16:37:08 -0400 Subject: [PATCH 12/44] Finished an unfinished comment. (#1749) --- src/adapters/bidderFactory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index e490e17c60d..12b7b931a9b 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -76,7 +76,7 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @typedef {object} Bid * * @property {string} requestId The specific BidRequest which this bid is aimed at. - * This should correspond to one of the + * This should match the BidRequest.bidId which this Bid targets. * @property {string} ad A URL which can be used to load this ad, if it's chosen by the publisher. * @property {string} currency The currency code for the cpm value * @property {number} cpm The bid price, in US cents per thousand impressions. From 923adfbcb7eeea88049005f2858d7519cde89ee2 Mon Sep 17 00:00:00 2001 From: JCarterGw <george@jcartermarketing.com> Date: Tue, 24 Oct 2017 05:54:04 -0700 Subject: [PATCH 13/44] Update JCM Adapter to 1.0 (#1715) * Add files via upload * Update package.json * Add files via upload Test for jcm adapter * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Delete jcm.js * Add files via upload * Add files via upload * Add files via upload * Delete jcm_spec.js * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Delete adapters.json * Update package.json --- modules/jcmBidAdapter.js | 115 ++++---- modules/jcmBidAdapter.md | 40 +++ test/spec/modules/jcmBidAdapter_spec.js | 331 ++++++++---------------- 3 files changed, 220 insertions(+), 266 deletions(-) create mode 100644 modules/jcmBidAdapter.md diff --git a/modules/jcmBidAdapter.js b/modules/jcmBidAdapter.js index 7666dae2f5b..3e0e46b8b77 100644 --- a/modules/jcmBidAdapter.js +++ b/modules/jcmBidAdapter.js @@ -1,45 +1,23 @@ -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'); +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; +const BIDDER_CODE = 'jcm'; +const URL = '//media.adfrontiers.com/pq' -var JCMAdapter = function JCMAdapter() { - window.pbjs = window.pbjs || {}; - window.pbjs.processJCMResponse = function(JCMResponse) { - if (JCMResponse) { - var JCMRespObj = JSON.parse(JCMResponse); - if (JCMRespObj) { - var bids = JCMRespObj.bids; - for (var i = 0; i < bids.length; i++) { - var bid = bids[i]; - var bidObject; - if (bid.cpm > 0) { - bidObject = bidfactory.createBid(1); - bidObject.bidderCode = 'jcm'; - bidObject.cpm = bid.cpm; - bidObject.ad = decodeURIComponent(bid.ad.replace(/\+/g, '%20')); - bidObject.width = bid.width; - bidObject.height = bid.height; - bidmanager.addBidResponse(utils.getBidRequest(bid.callbackId).placementCode, bidObject); - } else { - bidObject = bidfactory.createBid(2); - bidObject.bidderCode = 'jcm'; - bidmanager.addBidResponse(utils.getBidRequest(bid.callbackId).placementCode, bidObject); - } - } - } - } - }; +export const spec = { + code: BIDDER_CODE, + aliases: ['jcarter'], + isBidRequestValid: function(bid) { + return !!(bid.params && bid.params.siteId && bid.bidId); + }, - function _callBids(params) { - var BidRequest = { + buildRequests: function(validBidRequests) { + var BidRequestStr = { bids: [] }; - for (var i = 0; i < params.bids.length; i++) { + for (var i = 0; i < validBidRequests.length; i++) { var adSizes = ''; - var bid = params.bids[i]; + var bid = validBidRequests[i]; for (var x = 0; x < bid.sizes.length; x++) { adSizes += utils.parseGPTSingleSizeArray(bid.sizes[x]); if (x !== (bid.sizes.length - 1)) { @@ -47,23 +25,64 @@ var JCMAdapter = function JCMAdapter() { } } - BidRequest.bids.push({ + BidRequestStr.bids.push({ 'callbackId': bid.bidId, 'siteId': bid.params.siteId, - 'adSizes': adSizes + 'adSizes': adSizes, }); } - var JSONStr = JSON.stringify(BidRequest); - var reqURL = document.location.protocol + '//media.adfrontiers.com/pq?t=hb&bids=' + encodeURIComponent(JSONStr); - adloader.loadScript(reqURL); - } + var JSONStr = JSON.stringify(BidRequestStr); + var dataStr = 't=hb&ver=1.0&compact=true&bids=' + encodeURIComponent(JSONStr); + + return { + method: 'GET', + url: URL, + data: dataStr + } + }, - return { - callBids: _callBids - }; -}; + interpretResponse: function(serverResponse) { + const bidResponses = []; + // loop through serverResponses + if (serverResponse) { + if (serverResponse.bids) { + var bids = serverResponse.bids; + for (var i = 0; i < bids.length; i++) { + var bid = bids[i]; + const bidResponse = { + requestId: bid.callbackId, + bidderCode: spec.code, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.creativeId, + currency: 'USD', + netRevenue: bid.netRevenue, + ttl: bid.ttl, + ad: decodeURIComponent(bid.ad.replace(/\+/g, '%20')) + }; + bidResponses.push(bidResponse); + }; + }; + } + return bidResponses; + }, -adaptermanager.registerBidAdapter(new JCMAdapter(), 'jcm'); + getUserSyncs: function(syncOptions) { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: '//media.adfrontiers.com/hb/jcm_usersync.html' + }]; + } + if (syncOptions.image) { + return [{ + type: 'image', + url: '//media.adfrontiers.com/hb/jcm_usersync.png' + }]; + } + } +} -module.exports = JCMAdapter; +registerBidder(spec); diff --git a/modules/jcmBidAdapter.md b/modules/jcmBidAdapter.md new file mode 100644 index 00000000000..53a2356df2f --- /dev/null +++ b/modules/jcmBidAdapter.md @@ -0,0 +1,40 @@ +#Overview + +``` +Module Name: JCM Bidder Adapter +Module Type: Bidder Adapter +Maintainer: george@jcartermarketing.com +``` + +# Description + +Module that connects to J Carter Marketing demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div1', + sizes: [[300, 250]], // display 300x250 + bids: [ + { + bidder: 'jcm', + params: { + siteId: '3608' + } + } + ] + },{ + code: 'test-div2', + sizes: [[728, 90]], // display 728x90 + bids: [ + { + bidder: 'jcm', + params: { + siteId: '3608' + } + } + ] + } + ]; + diff --git a/test/spec/modules/jcmBidAdapter_spec.js b/test/spec/modules/jcmBidAdapter_spec.js index b34141869d8..a063d6b9805 100644 --- a/test/spec/modules/jcmBidAdapter_spec.js +++ b/test/spec/modules/jcmBidAdapter_spec.js @@ -1,244 +1,139 @@ -describe('jcm adapter tests', function () { - var expect = require('chai').expect; - var urlParse = require('url-parse'); +import { expect } from 'chai'; +import { spec } from 'modules/jcmBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; - // FYI: querystringify will perform encoding/decoding - var querystringify = require('querystringify'); +const ENDPOINT = '//media.adfrontiers.com/'; - var adapter = require('modules/jcmBidAdapter'); - var adLoader = require('src/adloader'); - var bidmanager = require('src/bidmanager'); +describe('jcmAdapter', () => { + const adapter = newBidder(spec); - let stubLoadScript; - - beforeEach(function () { - stubLoadScript = sinon.stub(adLoader, 'loadScript'); + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); }); - afterEach(function () { - stubLoadScript.restore(); - }); + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'jcm', + 'params': { + 'siteId': '3608' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; - describe('creation of bid url', function () { - if (typeof ($$PREBID_GLOBAL$$._bidsReceived) === 'undefined') { - $$PREBID_GLOBAL$$._bidsReceived = []; - } - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = []; - } - if (typeof ($$PREBID_GLOBAL$$._adsReceived) === 'undefined') { - $$PREBID_GLOBAL$$._adsReceived = []; - } - - it('should be called only once', function () { - var params = { - bidderCode: 'jcm', - bidder: 'jcm', - bids: [ - { - bidId: '3c9408cdbf2f68', - sizes: [[300, 250], [300, 300]], - bidder: 'jcm', - params: { siteId: '3608', adSizes: '300x250' }, - requestId: '10b327aa396609', - placementCode: '/19968336/header-bid-tag-0' - } - - ] - }; - - adapter().callBids(params); - - sinon.assert.calledOnce(stubLoadScript); + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should fix parameter name', function () { - var params = { - bidderCode: 'jcm', - bidder: 'jcm', - bids: [ - { - bidId: '3c9408cdbf2f68', - sizes: [[300, 250]], - bidder: 'jcm', - params: { siteId: '3608', adSizes: '300x250' }, - requestId: '10b327aa396609', - placementCode: '/19968336/header-bid-tag-0' - } - - ] - }; - - adapter().callBids(params); - var bidUrl = stubLoadScript.getCall(0).args[0]; - - sinon.assert.calledWith(stubLoadScript, bidUrl); - - var parsedBidUrl = urlParse(bidUrl); - var parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); - expect(parsedBidUrl.hostname).to.equal('media.adfrontiers.com'); - expect(parsedBidUrl.pathname).to.equal('/pq'); + describe('buildRequests', () => { + let bidRequests = [ + { + 'bidder': 'jcm', + 'params': { + 'siteId': '3608' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } - expect(parsedBidUrlQueryString).to.have.property('t').and.to.equal('hb'); - expect(parsedBidUrlQueryString).to.have.property('bids'); + ]; - var bidObjArr = JSON.parse(parsedBidUrlQueryString.bids); - expect(bidObjArr).to.have.property('bids'); - var bidObj = bidObjArr.bids[0]; + const request = spec.buildRequests(bidRequests); - expect(bidObj).to.have.property('adSizes').and.to.equal('300x250'); - expect(bidObj).to.have.property('siteId').and.to.equal('3608'); - expect(bidObj).to.have.property('callbackId').and.to.equal('3c9408cdbf2f68'); + it('sends bid request to ENDPOINT via GET', () => { + expect(request.method).to.equal('GET'); }); - }); - describe('placement by size', function () { - if (typeof ($$PREBID_GLOBAL$$._bidsReceived) === 'undefined') { - $$PREBID_GLOBAL$$._bidsReceived = []; - } - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = []; - } - if (typeof ($$PREBID_GLOBAL$$._adsReceived) === 'undefined') { - $$PREBID_GLOBAL$$._adsReceived = []; - } - - it('should be called with specific parameters for two bids', function () { - var params = { - bidderCode: 'jcm', - bidder: 'jcm', - bids: [ - { - bidId: '3c9408cdbf2f68', - sizes: [[300, 250]], - bidder: 'jcm', - params: { siteId: '3608', adSizes: '300x250' }, - requestId: '10b327aa396609', - placementCode: '/19968336/header-bid-tag-0' - }, - { - bidId: '3c9408cdbf2f69', - sizes: [[728, 90]], - bidder: 'jcm', - params: { siteId: '3608', adSizes: '728x90' }, - requestId: '10b327aa396610', - placementCode: '/19968336/header-bid-tag-1' - } - - ] - }; - - adapter().callBids(params); - var bidUrl = stubLoadScript.getCall(0).args[0]; - - sinon.assert.calledWith(stubLoadScript, bidUrl); - - var parsedBidUrl = urlParse(bidUrl); - var parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); - - expect(parsedBidUrl.hostname).to.equal('media.adfrontiers.com'); - expect(parsedBidUrl.pathname).to.equal('/pq'); - - expect(parsedBidUrlQueryString).to.have.property('t').and.to.equal('hb'); - expect(parsedBidUrlQueryString).to.have.property('bids'); - - var bidObjArr = JSON.parse(parsedBidUrlQueryString.bids); - expect(bidObjArr).to.have.property('bids'); - var bidObj1 = bidObjArr.bids[0]; - - expect(bidObj1).to.have.property('adSizes').and.to.equal('300x250'); - expect(bidObj1).to.have.property('siteId').and.to.equal('3608'); - expect(bidObj1).to.have.property('callbackId').and.to.equal('3c9408cdbf2f68'); - - var bidObj2 = bidObjArr.bids[1]; - - expect(bidObj2).to.have.property('adSizes').and.to.equal('728x90'); - expect(bidObj2).to.have.property('siteId').and.to.equal('3608'); - expect(bidObj2).to.have.property('callbackId').and.to.equal('3c9408cdbf2f69'); + it('sends correct bid parameters', () => { + const payloadArr = request.data.split('&'); + expect(request.method).to.equal('GET'); + expect(payloadArr.length).to.equal(4); + expect(payloadArr[0]).to.equal('t=hb'); + expect(payloadArr[1]).to.equal('ver=1.0'); + expect(payloadArr[2]).to.equal('compact=true'); + const adReqStr = request.data.split('&bids=')[1]; + const adReq = JSON.parse(decodeURIComponent(adReqStr)); + const adReqBid = JSON.parse(decodeURIComponent(adReqStr)).bids[0]; + expect(adReqBid.siteId).to.equal('3608'); + expect(adReqBid.callbackId).to.equal('30b31c1838de1e'); + expect(adReqBid.adSizes).to.equal('300x250,300x600'); }); }); - describe('handling of the callback response', function () { - if (typeof ($$PREBID_GLOBAL$$._bidsReceived) === 'undefined') { - $$PREBID_GLOBAL$$._bidsReceived = []; - } - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = []; - } - if (typeof ($$PREBID_GLOBAL$$._adsReceived) === 'undefined') { - $$PREBID_GLOBAL$$._adsReceived = []; - } - - var params = { - bidderCode: 'jcm', - bidder: 'jcm', - bidderRequestId: '2068db3c904101', - bids: [ - { - bidId: '3c9408cdbf2f68', - sizes: [[300, 250]], - bidder: 'jcm', - params: { siteId: '3608', adSizes: '300x250' }, - requestId: '10b327aa396609', - placementCode: '/19968336/header-bid-tag-0' - }, + describe('interpretResponse', () => { + it('should get correct bid response', () => { + let serverResponse = {'bids': [{'width': 300, 'height': 250, 'creativeId': '29681110', 'ad': '<!-- Creative -->', 'cpm': 0.5, 'callbackId': '30b31c1838de1e'}]}; + + let expectedResponse = [ { - bidId: '3c9408cdbf2f69', - sizes: [[728, 90]], - bidder: 'jcm', - params: { siteId: '3608', adSizes: '728x90' }, - requestId: '10b327aa396610', - placementCode: '/19968336/header-bid-tag-1' + 'requestId': '30b31c1838de1e', + 'bidderCode': 'jcm', + 'cpm': 0.5, + 'creativeId': '29681110', + 'width': 300, + 'height': 250, + 'ttl': 60, + 'currency': 'USA', + 'netRevenue': true, + 'ad': '<!-- Creative -->', } - - ] - }; - - var response = '{"bids":[{"width":300,"cpm":3,"ad":"%3Cimg+src%3D%22http%3A%2F%2Fmedia.adfrontiers.com%2Fimgs%2Fpartnership_300x250.png%22%3E","callbackId":"3c9408cdbf2f68","height":250},{"width":728,"cpm":0,"ad":"%3Cimg+src%3D%22http%3A%2F%2Fmedia.adfrontiers.com%2Fimgs%2Fpartnership_728x90.png%22%3E","callbackId":"3c9408cdbf2f69","height":90}]}'; - - it('callback function should exist', function () { - expect(pbjs.processJCMResponse).to.exist.and.to.be.a('function'); + ]; + + let result = spec.interpretResponse(serverResponse); + expect(Object.keys(result[0]).length).to.equal(Object.keys(expectedResponse[0]).length); + expect(Object.keys(result[0]).requestId).to.equal(Object.keys(expectedResponse[0]).requestId); + expect(Object.keys(result[0]).bidderCode).to.equal(Object.keys(expectedResponse[0]).bidderCode); + expect(Object.keys(result[0]).cpm).to.equal(Object.keys(expectedResponse[0]).cpm); + expect(Object.keys(result[0]).creativeId).to.equal(Object.keys(expectedResponse[0]).creativeId); + expect(Object.keys(result[0]).width).to.equal(Object.keys(expectedResponse[0]).width); + expect(Object.keys(result[0]).height).to.equal(Object.keys(expectedResponse[0]).height); + expect(Object.keys(result[0]).ttl).to.equal(Object.keys(expectedResponse[0]).ttl); + expect(Object.keys(result[0]).currency).to.equal(Object.keys(expectedResponse[0]).currency); + expect(Object.keys(result[0]).netRevenue).to.equal(Object.keys(expectedResponse[0]).netRevenue); + + expect(Object.keys(result[0]).ad).to.equal(Object.keys(expectedResponse[0]).ad); }); - it('bidmanager.addBidResponse should be called twice with correct arguments', function () { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - - adapter().callBids(params); + it('handles nobid responses', () => { + let serverResponse = {'bids': []}; - var adUnits = new Array(); - var unit = new Object(); - unit.bids = [params]; - unit.code = '/19968336/header-bid-tag'; - unit.sizes = [[300, 250], [728, 90]]; - adUnits.push(unit); + let result = spec.interpretResponse(serverResponse); + expect(result.length).to.equal(0); + }); + }); + describe('getUserSyncs', () => { + it('Verifies sync iframe option', () => { + expect(spec.getUserSyncs({})).to.be.undefined; + expect(spec.getUserSyncs({ iframeEnabled: false})).to.be.undefined; + const options = spec.getUserSyncs({ iframeEnabled: true}); + expect(options).to.not.be.undefined; + expect(options).to.have.lengthOf(1); + expect(options[0].type).to.equal('iframe'); + expect(options[0].url).to.equal('//media.adfrontiers.com/hb/jcm_usersync.html'); + }); - if (typeof ($$PREBID_GLOBAL$$._bidsRequested) === 'undefined') { - $$PREBID_GLOBAL$$._bidsRequested = [params]; - } else { - $$PREBID_GLOBAL$$._bidsRequested.push(params); - } - $$PREBID_GLOBAL$$.adUnits = adUnits; - pbjs.processJCMResponse(response); - - var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - var bidPlacementCode2 = stubAddBidResponse.getCall(1).args[0]; - var bidObject2 = stubAddBidResponse.getCall(1).args[1]; - - expect(bidPlacementCode1).to.equal('/19968336/header-bid-tag-0'); - expect(bidObject1.cpm).to.equal(3); - expect(bidObject1.ad).to.equal('<img src="http://media.adfrontiers.com/imgs/partnership_300x250.png">'); - expect(bidObject1.width).to.equal(300); - expect(bidObject1.height).to.equal(250); - expect(bidObject1.getStatusCode()).to.equal(1); - expect(bidObject1.bidderCode).to.equal('jcm'); - - expect(bidPlacementCode2).to.equal('/19968336/header-bid-tag-1'); - expect(bidObject2.getStatusCode()).to.equal(2); - - sinon.assert.calledTwice(stubAddBidResponse); - stubAddBidResponse.restore(); + it('Verifies sync image option', () => { + expect(spec.getUserSyncs({ image: false})).to.be.undefined; + const options = spec.getUserSyncs({ image: true}); + expect(options).to.not.be.undefined; + expect(options).to.have.lengthOf(1); + expect(options[0].type).to.equal('image'); + expect(options[0].url).to.equal('//media.adfrontiers.com/hb/jcm_usersync.png'); }); }); }); From 19deb8eb37bd95603c2a16cd435859a822b23beb Mon Sep 17 00:00:00 2001 From: Oz Weiss <thewizarodofoz@gmail.com> Date: Tue, 24 Oct 2017 20:00:20 +0300 Subject: [PATCH 14/44] add option to run tests in a specific file (#1727) * add option to run tests in a specific file * add option to run tests in a specific file * Update gulpfile.js * Update karma.conf.maker.js --- gulpfile.js | 6 ++++-- karma.conf.maker.js | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index f80cfe91d34..d7a151ce357 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -173,10 +173,11 @@ gulp.task('webpack', ['clean'], function () { // By default, this runs in headless chrome. // // If --watch is given, the task will re-run unit tests whenever the source code changes +// If --file "<path-to-test-file>" is given, the task will only run tests in the specified file. // If --browserstack is given, it will run the full suite of currently supported browsers. // If --browsers is given, browsers can be chosen explicitly. e.g. --browsers=chrome,firefox,ie9 gulp.task('test', ['clean'], function (done) { - var karmaConf = karmaConfMaker(false, argv.browserstack, argv.watch); + var karmaConf = karmaConfMaker(false, argv.browserstack, argv.watch, argv.file); var browserOverride = helpers.parseBrowserArgs(argv).map(helpers.toCapitalCase); if (browserOverride.length > 0) { @@ -186,8 +187,9 @@ gulp.task('test', ['clean'], function (done) { new KarmaServer(karmaConf, newKarmaCallback(done)).start(); }); +// If --file "<path-to-test-file>" is given, the task will only run tests in the specified file. gulp.task('test-coverage', ['clean'], function(done) { - new KarmaServer(karmaConfMaker(true, false), newKarmaCallback(done)).start(); + new KarmaServer(karmaConfMaker(true, false, argv.file), newKarmaCallback(done)).start(); }); // View the code coverage report in the browser. diff --git a/karma.conf.maker.js b/karma.conf.maker.js index aad6a7d7c1d..d2b1d49e081 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -92,12 +92,12 @@ function setBrowsers(karmaConf, browserstack) { } } -module.exports = function(codeCoverage, browserstack, watchMode) { +module.exports = function(codeCoverage, browserstack, watchMode, file) { var webpackConfig = newWebpackConfig(codeCoverage); var plugins = newPluginsArray(browserstack); var files = [ 'test/helpers/prebidGlobal.js', - 'test/**/*_spec.js' + file ? file : 'test/**/*_spec.js' ]; // This file opens the /debug.html tab automatically. // It has no real value unless you're running --watch, and intend to do some debugging in the browser. From 88af47b51029a2e277a7c643fe7683afb0ca9caa Mon Sep 17 00:00:00 2001 From: dbemiller <dbemiller@appnexus.com> Date: Tue, 24 Oct 2017 15:16:10 -0400 Subject: [PATCH 15/44] Make response headers available to the specs (#1748) * Updated the bidderFactory to make headers accessible to the spec files. * Updated the platform.io adapter to handle the new format. * Updated the jcm adapter. --- modules/adbutlerBidAdapter.js | 1 + modules/appnexusAstBidAdapter.js | 1 + modules/beachfrontBidAdapter.js | 1 + modules/jcmBidAdapter.js | 1 + modules/platformioBidAdapter.js | 2 +- modules/pulsepointLiteBidAdapter.js | 1 + modules/rubiconBidAdapter.js | 1 + src/adapters/bidderFactory.js | 25 +++++++++++++++++-- test/spec/modules/adbutlerBidAdapter_spec.js | 24 ++++++++++-------- .../modules/appnexusAstBidAdapter_spec.js | 8 +++--- .../spec/modules/beachfrontBidAdapter_spec.js | 8 +++--- test/spec/modules/jcmBidAdapter_spec.js | 4 +-- .../spec/modules/platformioBidAdapter_spec.js | 4 +-- .../modules/pulsepointLiteBidAdapter_spec.js | 6 ++--- test/spec/modules/rubiconBidAdapter_spec.js | 12 ++++----- test/spec/unit/core/bidderFactory_spec.js | 15 ++++++++--- 16 files changed, 75 insertions(+), 39 deletions(-) diff --git a/modules/adbutlerBidAdapter.js b/modules/adbutlerBidAdapter.js index f633eba98a3..44a2ef49f51 100644 --- a/modules/adbutlerBidAdapter.js +++ b/modules/adbutlerBidAdapter.js @@ -74,6 +74,7 @@ export const spec = { var width; var height; + serverResponse = serverResponse.body; if (serverResponse && serverResponse.status === 'SUCCESS' && bidObj) { CPM = serverResponse.cpm; minCPM = utils.getBidIdParameter('minCPM', bidObj.params); diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index c4e2686db15..9a2cea8a229 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -87,6 +87,7 @@ export const spec = { * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function(serverResponse, {bidderRequest}) { + serverResponse = serverResponse.body; const bids = []; if (!serverResponse || serverResponse.error) { let errorMessage = `in response for ${bidderRequest.bidderCode} adapter`; diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 2729dcdd324..d4fcc3e3578 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -23,6 +23,7 @@ export const spec = { }, interpretResponse(response, { bidRequest }) { + response = response.body; if (!response || !response.url || !response.bidPrice) { utils.logWarn(`No valid bids from ${spec.code} bidder`); return []; diff --git a/modules/jcmBidAdapter.js b/modules/jcmBidAdapter.js index 3e0e46b8b77..4c9792a00f2 100644 --- a/modules/jcmBidAdapter.js +++ b/modules/jcmBidAdapter.js @@ -44,6 +44,7 @@ export const spec = { interpretResponse: function(serverResponse) { const bidResponses = []; + serverResponse = serverResponse.body; // loop through serverResponses if (serverResponse) { if (serverResponse.bids) { diff --git a/modules/platformioBidAdapter.js b/modules/platformioBidAdapter.js index c33551ed396..2fb23ab92b3 100644 --- a/modules/platformioBidAdapter.js +++ b/modules/platformioBidAdapter.js @@ -24,7 +24,7 @@ export const spec = { }; }, interpretResponse: (response, request) => ( - bidResponseAvailable(request, response) + bidResponseAvailable(request, response.body) ), }; function bidResponseAvailable(bidRequest, bidResponse) { diff --git a/modules/pulsepointLiteBidAdapter.js b/modules/pulsepointLiteBidAdapter.js index 00b5c014e98..99a83871dd8 100644 --- a/modules/pulsepointLiteBidAdapter.js +++ b/modules/pulsepointLiteBidAdapter.js @@ -69,6 +69,7 @@ export const spec = { function bidResponseAvailable(bidRequest, bidResponse) { const idToImpMap = {}; const idToBidMap = {}; + bidResponse = bidResponse.body // extract the request bids and the response bids, keyed by impr-id const ortbRequest = parse(bidRequest.data); ortbRequest.imp.forEach(imp => { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 69981ba2b56..866e02bc258 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -230,6 +230,7 @@ export const spec = { * @return {Bid[]} An array of bids which */ interpretResponse: function(responseObj, {bidRequest}) { + responseObj = responseObj.body let ads = responseObj.ads; // check overall response diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 12b7b931a9b..87dfd372b9c 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -44,7 +44,7 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @property {function(BidRequest[], bidderRequest): ServerRequest|ServerRequest[]} buildRequests Build the request to the Server * which requests Bids for the given array of Requests. Each BidRequest in the argument array is guaranteed to have * passed the isBidRequestValid() test. - * @property {function(*, BidRequest): Bid[]} interpretResponse Given a successful response from the Server, + * @property {function(ServerResponse, BidRequest): Bid[]} interpretResponse Given a successful response from the Server, * interpret it and return the Bid objects. This function will be run inside a try/catch. * If it throws any errors, your bids will be discarded. * @property {function(SyncOptions, Array): UserSync[]} [getUserSyncs] Given an array of all the responses @@ -72,6 +72,15 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * JSON-serialized into the Request body. */ +/** + * @typedef {object} ServerResponse + * + * @property {*} body The response body. If this is legal JSON, then it will be parsed. Otherwise it'll be a + * string with the body's content. + * @property {{get: function(string): string} headers The response headers. + * Call this like `ServerResponse.headers.get("Content-Type")` + */ + /** * @typedef {object} Bid * @@ -263,10 +272,16 @@ export function newBidder(spec) { // If the server responds successfully, use the adapter code to unpack the Bids from it. // If the adapter code fails, no bids should be added. After all the bids have been added, make // sure to call the `onResponse` function so that we're one step closer to calling fillNoBids(). - function onSuccess(response) { + function onSuccess(response, responseObj) { try { response = JSON.parse(response); } catch (e) { /* response might not be JSON... that's ok. */ } + + // Make response headers available for #1742. These are lazy-loaded because most adapters won't need them. + response = { + body: response, + headers: headerParser(responseObj) + }; responses.push(response); let bids; @@ -296,6 +311,12 @@ export function newBidder(spec) { logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bid.requestId}. Ignoring.`); } } + + function headerParser(xmlHttpResponse) { + return { + get: responseObj.getResponseHeader.bind(responseObj) + }; + } } // If the server responds with an error, there's not much we can do. Log it, and make sure to diff --git a/test/spec/modules/adbutlerBidAdapter_spec.js b/test/spec/modules/adbutlerBidAdapter_spec.js index 352358be8d0..de40f72073b 100644 --- a/test/spec/modules/adbutlerBidAdapter_spec.js +++ b/test/spec/modules/adbutlerBidAdapter_spec.js @@ -121,17 +121,19 @@ describe('AdButler adapter', () => { describe('bid responses', () => { it('should return complete bid response', () => { let serverResponse = { - status: 'SUCCESS', - account_id: 167283, - zone_id: 210093, - cpm: 1.5, - width: 300, - height: 250, - place: 0, - ad_code: '<img src="http://image.source.com/img" alt="" title="" border="0" width="300" height="250">', - tracking_pixels: [ - 'http://tracking.pixel.com/params=info' - ] + body: { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210093, + cpm: 1.5, + width: 300, + height: 250, + place: 0, + ad_code: '<img src="http://image.source.com/img" alt="" title="" border="0" width="300" height="250">', + tracking_pixels: [ + 'http://tracking.pixel.com/params=info' + ] + } }, bids = spec.interpretResponse(serverResponse, {'bidRequest': bidRequests[0]}); diff --git a/test/spec/modules/appnexusAstBidAdapter_spec.js b/test/spec/modules/appnexusAstBidAdapter_spec.js index d07ee6df543..d76cc08810f 100644 --- a/test/spec/modules/appnexusAstBidAdapter_spec.js +++ b/test/spec/modules/appnexusAstBidAdapter_spec.js @@ -306,7 +306,7 @@ describe('AppNexusAdapter', () => { ]; let bidderRequest; - let result = spec.interpretResponse(response, {bidderRequest}); + let result = spec.interpretResponse({ body: response }, {bidderRequest}); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); @@ -322,7 +322,7 @@ describe('AppNexusAdapter', () => { }; let bidderRequest; - let result = spec.interpretResponse(response, {bidderRequest}); + let result = spec.interpretResponse({ body: response }, {bidderRequest}); expect(result.length).to.equal(0); }); @@ -343,7 +343,7 @@ describe('AppNexusAdapter', () => { }; let bidderRequest; - let result = spec.interpretResponse(response, {bidderRequest}); + let result = spec.interpretResponse({ body: response }, {bidderRequest}); expect(result[0]).to.have.property('vastUrl'); expect(result[0]).to.have.property('descriptionUrl'); expect(result[0]).to.have.property('mediaType', 'video'); @@ -376,7 +376,7 @@ describe('AppNexusAdapter', () => { }; let bidderRequest; - let result = spec.interpretResponse(response1, {bidderRequest}); + let result = spec.interpretResponse({ body: response1 }, {bidderRequest}); expect(result[0].native.title).to.equal('Native Creative'); expect(result[0].native.body).to.equal('Cool description great stuff'); expect(result[0].native.cta).to.equal('Do it'); diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index 43df639613f..92e16573972 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -104,7 +104,7 @@ describe('BeachfrontAdapter', () => { describe('spec.interpretResponse', () => { it('should return no bids if the response is not valid', () => { - const bidResponse = spec.interpretResponse(null, { bidRequest }); + const bidResponse = spec.interpretResponse({ body: null }, { bidRequest }); expect(bidResponse.length).to.equal(0); }); @@ -112,7 +112,7 @@ describe('BeachfrontAdapter', () => { const serverResponse = { bidPrice: 5.00 }; - const bidResponse = spec.interpretResponse(serverResponse, { bidRequest }); + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); expect(bidResponse.length).to.equal(0); }); @@ -120,7 +120,7 @@ describe('BeachfrontAdapter', () => { const serverResponse = { url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da' }; - const bidResponse = spec.interpretResponse(serverResponse, { bidRequest }); + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); expect(bidResponse.length).to.equal(0); }); @@ -130,7 +130,7 @@ describe('BeachfrontAdapter', () => { url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', cmpId: '123abc' }; - const bidResponse = spec.interpretResponse(serverResponse, { bidRequest }); + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); expect(bidResponse).to.deep.equal({ requestId: bidRequest.bidId, bidderCode: spec.code, diff --git a/test/spec/modules/jcmBidAdapter_spec.js b/test/spec/modules/jcmBidAdapter_spec.js index a063d6b9805..95356a9658e 100644 --- a/test/spec/modules/jcmBidAdapter_spec.js +++ b/test/spec/modules/jcmBidAdapter_spec.js @@ -94,7 +94,7 @@ describe('jcmAdapter', () => { } ]; - let result = spec.interpretResponse(serverResponse); + let result = spec.interpretResponse({ body: serverResponse }); expect(Object.keys(result[0]).length).to.equal(Object.keys(expectedResponse[0]).length); expect(Object.keys(result[0]).requestId).to.equal(Object.keys(expectedResponse[0]).requestId); expect(Object.keys(result[0]).bidderCode).to.equal(Object.keys(expectedResponse[0]).bidderCode); @@ -112,7 +112,7 @@ describe('jcmAdapter', () => { it('handles nobid responses', () => { let serverResponse = {'bids': []}; - let result = spec.interpretResponse(serverResponse); + let result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); }); diff --git a/test/spec/modules/platformioBidAdapter_spec.js b/test/spec/modules/platformioBidAdapter_spec.js index 1b07b4049d4..86bf52cac72 100644 --- a/test/spec/modules/platformioBidAdapter_spec.js +++ b/test/spec/modules/platformioBidAdapter_spec.js @@ -63,7 +63,7 @@ describe('Platformio Adapter Tests', () => { }] }] }; - const bids = spec.interpretResponse(ortbResponse, request); + const bids = spec.interpretResponse({ body: ortbResponse }, request); expect(bids).to.have.lengthOf(1); // verify first bid const bid = bids[0]; @@ -78,7 +78,7 @@ describe('Platformio Adapter Tests', () => { it('Verify full passback', () => { const request = spec.buildRequests(slotConfigs); - const bids = spec.interpretResponse(null, request) + const bids = spec.interpretResponse({ body: null }, request) expect(bids).to.have.lengthOf(0); }); diff --git a/test/spec/modules/pulsepointLiteBidAdapter_spec.js b/test/spec/modules/pulsepointLiteBidAdapter_spec.js index 96f5c7a8d1f..9731164cd50 100644 --- a/test/spec/modules/pulsepointLiteBidAdapter_spec.js +++ b/test/spec/modules/pulsepointLiteBidAdapter_spec.js @@ -89,7 +89,7 @@ describe('PulsePoint Lite Adapter Tests', () => { }] }] }; - const bids = spec.interpretResponse(ortbResponse, request); + const bids = spec.interpretResponse({ body: ortbResponse }, request); expect(bids).to.have.lengthOf(1); // verify first bid const bid = bids[0]; @@ -104,7 +104,7 @@ describe('PulsePoint Lite Adapter Tests', () => { it('Verify full passback', () => { const request = spec.buildRequests(slotConfigs); - const bids = spec.interpretResponse(null, request) + const bids = spec.interpretResponse({ body: null }, request) expect(bids).to.have.lengthOf(0); }); @@ -171,7 +171,7 @@ describe('PulsePoint Lite Adapter Tests', () => { }] }] }; - const bids = spec.interpretResponse(ortbResponse, request); + const bids = spec.interpretResponse({ body: ortbResponse }, request); // verify bid const bid = bids[0]; expect(bid.cpm).to.equal(1.25); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 620fc56e516..6c08c66f485 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -604,7 +604,7 @@ describe('the rubicon adapter', () => { ] }; - let bids = spec.interpretResponse(response, { + let bids = spec.interpretResponse({ body: response }, { bidRequest: bidderRequest.bids[0] }); @@ -654,7 +654,7 @@ describe('the rubicon adapter', () => { }] }; - let bids = spec.interpretResponse(response, { + let bids = spec.interpretResponse({ body: response }, { bidRequest: bidderRequest.bids[0] }); @@ -677,7 +677,7 @@ describe('the rubicon adapter', () => { 'ads': [] }; - let bids = spec.interpretResponse(response, { + let bids = spec.interpretResponse({ body: response }, { bidRequest: bidderRequest.bids[0] }); @@ -701,7 +701,7 @@ describe('the rubicon adapter', () => { }] }; - let bids = spec.interpretResponse(response, { + let bids = spec.interpretResponse({ body: response }, { bidRequest: bidderRequest.bids[0] }); @@ -711,7 +711,7 @@ describe('the rubicon adapter', () => { it('should handle an error because of malformed json response', () => { let response = '{test{'; - let bids = spec.interpretResponse(response, { + let bids = spec.interpretResponse({ body: response }, { bidRequest: bidderRequest.bids[0] }); @@ -752,7 +752,7 @@ describe('the rubicon adapter', () => { 'account_id': 7780 }; - let bids = spec.interpretResponse(response, { + let bids = spec.interpretResponse({ body: response }, { bidRequest: bidderRequest.bids[0] }); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index e91ddcf39a4..3260b66da28 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -265,7 +265,9 @@ describe('bidders created by newBidder', () => { beforeEach(() => { ajaxStub = sinon.stub(ajax, 'ajax', function(url, callbacks) { - callbacks.success('response body'); + const fakeResponse = sinon.stub(); + fakeResponse.returns('headerContent'); + callbacks.success('response body', { getResponseHeader: fakeResponse }); }); userSyncStub = sinon.stub(userSync, 'registerSync') }); @@ -275,7 +277,7 @@ describe('bidders created by newBidder', () => { userSyncStub.restore(); }); - it('should call spec.interpretResponse() with the response body content', () => { + it('should call spec.interpretResponse() with the response content', () => { const bidder = newBidder(spec); spec.isBidRequestValid.returns(true); @@ -289,7 +291,9 @@ describe('bidders created by newBidder', () => { bidder.callBids(MOCK_BIDS_REQUEST); expect(spec.interpretResponse.calledOnce).to.equal(true); - expect(spec.interpretResponse.firstCall.args[0]).to.equal('response body'); + const response = spec.interpretResponse.firstCall.args[0] + expect(response.body).to.equal('response body') + expect(response.headers.get('some-header')).to.equal('headerContent'); expect(spec.interpretResponse.firstCall.args[1]).to.deep.equal({ method: 'POST', url: 'test.url.com', @@ -364,7 +368,10 @@ describe('bidders created by newBidder', () => { bidder.callBids(MOCK_BIDS_REQUEST); expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal(['response body']); + expect(spec.getUserSyncs.firstCall.args[1].length).to.equal(1); + expect(spec.getUserSyncs.firstCall.args[1][0].body).to.equal('response body'); + expect(spec.getUserSyncs.firstCall.args[1][0].headers).to.have.property('get'); + expect(spec.getUserSyncs.firstCall.args[1][0].headers.get).to.be.a('function'); }); it('should register usersync pixels', () => { From 403a991615271612ab26b5b915f9654972234571 Mon Sep 17 00:00:00 2001 From: Lovell Fuller <lovell@users.noreply.github.com> Date: Thu, 26 Oct 2017 13:18:29 +0100 Subject: [PATCH 16/44] Upgrade Audience Network adapter for Prebid 1.0 (#1750) --- modules/audienceNetworkBidAdapter.js | 290 +++---- modules/audienceNetworkBidAdapter.md | 49 ++ .../modules/audienceNetworkBidAdapter_spec.js | 808 +++++++----------- 3 files changed, 456 insertions(+), 691 deletions(-) create mode 100644 modules/audienceNetworkBidAdapter.md diff --git a/modules/audienceNetworkBidAdapter.js b/modules/audienceNetworkBidAdapter.js index de2e3aaff6b..5dfc65aaee2 100644 --- a/modules/audienceNetworkBidAdapter.js +++ b/modules/audienceNetworkBidAdapter.js @@ -1,23 +1,25 @@ /** * @file AudienceNetwork adapter. */ -import { ajax } from 'src/ajax'; -import { createBid } from 'src/bidfactory'; -import { addBidResponse } from 'src/bidmanager'; -import { STATUS } from 'src/constants.json'; -import { format } from 'src/url'; -import { logError } from 'src/utils'; -import Adapter from 'src/adapter'; -import adaptermanager from 'src/adaptermanager'; - -const { setBidderCode, getBidderCode } = new Adapter('audienceNetwork'); +import { registerBidder } from 'src/adapters/bidderFactory'; +import { config } from 'src/config'; +import { formatQS } from 'src/url'; +import { getTopWindowUrl } from 'src/utils'; + +const code = 'audienceNetwork'; +const currency = 'USD'; +const method = 'GET'; +const url = 'https://an.facebook.com/v2/placementbid.json'; +const supportedMediaTypes = ['video']; +const netRevenue = true; +const hb_bidder = 'fan'; /** * Does this bid request contain valid parameters? * @param {Object} bid * @returns {Boolean} */ -const validateBidRequest = bid => +const isBidRequestValid = bid => typeof bid.params === 'object' && typeof bid.params.placementId === 'string' && bid.params.placementId.length > 0 && @@ -33,13 +35,6 @@ const validateBidRequest = bid => const flattenSize = size => (Array.isArray(size) && size.length === 2) ? `${size[0]}x${size[1]}` : size; -/** - * Expands a 'WxH' string as a 2-element [W, H] array - * @param {String} size - * @returns {Array} - */ -const expandSize = size => size.split('x').map(Number); - /** * Is this a valid slot size? * @param {String} size @@ -72,19 +67,6 @@ const isTestmode = () => Boolean( window.location.search.indexOf('anhb_testmode') !== -1 ).toString(); -/** - * Parse JSON-as-string into an Object, default to empty. - * @param {String} JSON-as-string - * @returns {Object} - */ -const parseJson = jsonAsString => { - let data = {}; - try { - data = JSON.parse(jsonAsString); - } catch (err) {} - return data; -}; - /** * Generate ad HTML for injection into an iframe * @param {String} placementId @@ -103,155 +85,121 @@ ${nativeContainer}</div></body></html>`; }; /** - * Creates a "good" Bid object with the given bid ID and CPM. - * @param {String} placementId - * @param {String} size - * @param {String} format - * @param {String} bidId - * @param {Number} cpmCents - * @param {String} pageurl - * @returns {Object} Bid - */ -const createSuccessBidResponse = (placementId, size, format, bidId, cpmCents, pageurl) => { - const bid = createBid(STATUS.GOOD, { bidId }); - // Prebid attributes - bid.bidderCode = getBidderCode(); - bid.cpm = cpmCents / 100; - bid.ad = createAdHtml(placementId, format, bidId); - [bid.width, bid.height] = expandSize(size); - - // Audience Network attributes - bid.hb_bidder = 'fan'; - bid.fb_bidid = bidId; - bid.fb_format = format; - bid.fb_placementid = placementId; - // Video attributes - if (isVideo(format)) { - const vast = `https://an.facebook.com/v1/instream/vast.xml?placementid=${placementId}&pageurl=${pageurl}&playerwidth=${bid.width}&playerheight=${bid.height}&bidid=${bidId}`; - bid.mediaType = 'video'; - bid.vastUrl = vast; - bid.descriptionUrl = vast; - } - return bid; -}; - -/** - * Creates a "no bid" Bid object. - * @returns {Object} Bid - */ -const createFailureBidResponse = () => { - const bid = createBid(STATUS.NO_BID); - bid.bidderCode = getBidderCode(); - return bid; -}; - -/** - * Fetch bids for given parameters. - * @param {Object} bidRequest - * @param {Array} params.bids - list of bids - * @param {String} params.bids[].placementCode - Prebid placement identifier - * @param {Object} params.bids[].params - * @param {String} params.bids[].params.placementId - Audience Network placement identifier - * @param {String} params.bids[].params.format - Optional format, one of 'video', 'native' or 'fullwidth' if set - * @param {Array} params.bids[].sizes - list of desired advert sizes - * @param {Array} params.bids[].sizes[] - Size arrays [h,w]: should include one of [300, 250], [320, 50]: first matched size is used - * @returns {void} + * Convert each bid request to a single URL to fetch those bids. + * @param {Array} bids - list of bids + * @param {String} bids[].placementCode - Prebid placement identifier + * @param {Object} bids[].params + * @param {String} bids[].params.placementId - Audience Network placement identifier + * @param {String} bids[].params.format - Optional format, one of 'video', 'native' or 'fullwidth' if set + * @param {Array} bids[].sizes - list of desired advert sizes + * @param {Array} bids[].sizes[] - Size arrays [h,w]: should include one of [300, 250], [320, 50]: first matched size is used + * @returns {Array<Object>} List of URLs to fetch, plus formats and sizes for later use with interpretResponse */ -const callBids = bidRequest => { - // Build lists of adUnitCodes, placementids, adformats and sizes - const adUnitCodes = []; +const buildRequests = bids => { + // Build lists of placementids, adformats, sizes and SDK versions const placementids = []; const adformats = []; const sizes = []; const sdk = []; - - bidRequest.bids - .filter(validateBidRequest) - .forEach(bid => bid.sizes - .map(flattenSize) - .filter(size => isValidSize(size) || isVideo(bid.params.format)) - .slice(0, 1) - .forEach(size => { - adUnitCodes.push(bid.placementCode); - placementids.push(bid.params.placementId); - adformats.push(bid.params.format || size); - sizes.push(size); - sdk.push(sdkVersion(bid.params.format)); - }) - ); - - if (placementids.length) { - // Build URL - const testmode = isTestmode(); - const pageurl = encodeURIComponent(location.href); - const search = { - placementids, - adformats, - testmode, - pageurl, - sdk - }; - const video = adformats.findIndex(isVideo); - if (video !== -1) { - [search.playerwidth, search.playerheight] = expandSize(sizes[video]); - } - const url = format({ - protocol: 'https', - host: 'an.facebook.com', - pathname: '/v2/placementbid.json', - search - }); - // Request - ajax(url, res => { - // Handle response - const data = parseJson(res); - if (data.errors && data.errors.length) { - const noBid = createFailureBidResponse(); - adUnitCodes.forEach(adUnitCode => addBidResponse(adUnitCode, noBid)); - data.errors.forEach(logError); - } else { - // For each placementId in bids Object - Object.keys(data.bids) - // extract Array of bid responses - .map(placementId => data.bids[placementId]) - // flatten - .reduce((a, b) => a.concat(b), []) - // call addBidResponse - .forEach((bid, i) => - addBidResponse(adUnitCodes[i], createSuccessBidResponse( - bid.placement_id, - sizes[i], - adformats[i], - bid.bid_id, - bid.bid_price_cents, - pageurl - )) - ); - } - }, null, { withCredentials: true }); - } else { - // No valid bids - logError('No valid bids requested'); + const requestIds = []; + + bids.forEach(bid => bid.sizes + .map(flattenSize) + .filter(size => isValidSize(size) || isVideo(bid.params.format)) + .slice(0, 1) + .forEach(size => { + placementids.push(bid.params.placementId); + adformats.push(bid.params.format || size); + sizes.push(size); + sdk.push(sdkVersion(bid.params.format)); + requestIds.push(bid.bidId); + }) + ); + + // Build URL + const testmode = isTestmode(); + const pageurl = getTopWindowUrl(); + const search = { + placementids, + adformats, + testmode, + pageurl, + sdk + }; + const video = adformats.findIndex(isVideo); + if (video !== -1) { + [search.playerwidth, search.playerheight] = sizes[video].split('x').map(Number) } + const data = formatQS(search); + + return [{ adformats, data, method, requestIds, sizes, url }]; }; /** - * @class AudienceNetwork - * @type {Object} - * @property {Function} callBids - fetch bids for given parameters - * @property {Function} setBidderCode - used for bidder aliasing - * @property {Function} getBidderCode - unique 'audienceNetwork' identifier + * Convert a server response to a bid response. + * @param {Object} response - object representing the response + * @param {Object} response.body - response body, already converted from JSON + * @param {Object} bidRequests - the original bid requests + * @param {Array} bidRequest.adformats - list of formats for the original bid requests + * @param {Array} bidRequest.sizes - list of sizes fot the original bid requests + * @returns {Array<Object>} A list of bid response objects */ -function AudienceNetwork() { - return Object.assign(this, { - callBids, - setBidderCode, - getBidderCode - }); -} +const interpretResponse = ({ body }, { adformats, requestIds, sizes }) => { + const ttl = Number(config.getConfig().bidderTimeout); + + return body.errors && body.errors.length + ? [] + : Object.keys(body.bids) + // extract Array of bid responses + .map(placementId => body.bids[placementId]) + // flatten + .reduce((a, b) => a.concat(b), []) + // transform to bidResponse + .map((bid, i) => { + const { + bid_id: fb_bidid, + placement_id: creativeId, + bid_price_cents: cpm + } = bid; + + const format = adformats[i]; + const [width, height] = sizes[i]; + const ad = createAdHtml(creativeId, format, fb_bidid); + const requestId = requestIds[i]; + + const bidResponse = { + // Prebid attributes + requestId, + cpm: cpm / 100, + width, + height, + ad, + ttl, + creativeId, + netRevenue, + currency, + // Audience Network attributes + hb_bidder, + fb_bidid, + fb_format: format, + fb_placementid: creativeId + }; + // Video attributes + if (isVideo(format)) { + const pageurl = getTopWindowUrl(); + bidResponse.mediaType = 'video'; + bidResponse.vastUrl = `https://an.facebook.com/v1/instream/vast.xml?placementid=${creativeId}&pageurl=${encodeURIComponent(pageurl)}&playerwidth=${width}&playerheight=${height}&bidid=${fb_bidid}`; + } + return bidResponse; + }); +}; -adaptermanager.registerBidAdapter(new AudienceNetwork(), 'audienceNetwork', { - supportedMediaTypes: ['video'] -}); +export const spec = { + code, + supportedMediaTypes, + isBidRequestValid, + buildRequests, + interpretResponse +}; -module.exports = AudienceNetwork; +registerBidder(spec); diff --git a/modules/audienceNetworkBidAdapter.md b/modules/audienceNetworkBidAdapter.md new file mode 100644 index 00000000000..72013c8610b --- /dev/null +++ b/modules/audienceNetworkBidAdapter.md @@ -0,0 +1,49 @@ +# Overview + +Module Name: Audience Network Bid Adapter + +Module Type: Bidder Adapter + +Maintainer: Lovell Fuller + +# Parameters + +| Name | Scope | Description | Example | +| :------------ | :------- | :---------------------------------------------- | :--------------------------------- | +| `placementId` | required | The Placement ID from Audience Network | "555555555555555\_555555555555555" | +| `format` | optional | Format, one of "native", "fullwidth" or "video" | "native" | + +# Example ad units + +```javascript +const adUnits = [{ + code: "test-iab", + sizes: [[300, 250]], + bids: [{ + bidder: "audienceNetwork", + params: { + placementId: "555555555555555_555555555555555" + } + }] +}, { + code: "test-native", + sizes: [[300, 250]], + bids: [{ + bidder: "audienceNetwork", + params: { + format: "native", + placementId: "555555555555555_555555555555555" + } + }] +}, { + code: "test-video", + sizes: [[640, 360]], + bids: [{ + bidder: "audienceNetwork", + params: { + format: "video", + placementId: "555555555555555_555555555555555" + } + }] +}]; +``` diff --git a/test/spec/modules/audienceNetworkBidAdapter_spec.js b/test/spec/modules/audienceNetworkBidAdapter_spec.js index 3dcd4833871..604c3bf0e06 100644 --- a/test/spec/modules/audienceNetworkBidAdapter_spec.js +++ b/test/spec/modules/audienceNetworkBidAdapter_spec.js @@ -3,542 +3,346 @@ */ import { expect } from 'chai'; -import bidmanager from 'src/bidmanager'; -import { STATUS } from 'src/constants.json'; -import * as utils from 'src/utils'; +import { spec } from 'modules/audienceNetworkBidAdapter'; -import AudienceNetwork from 'modules/audienceNetworkBidAdapter'; +const { + code, + supportedMediaTypes, + isBidRequestValid, + buildRequests, + interpretResponse +} = spec; -const bidderCode = 'audienceNetwork'; +const bidder = 'audienceNetwork'; const placementId = 'test-placement-id'; -const placementCode = '/test/placement/code'; const playerwidth = 320; const playerheight = 180; - -/** - * Expect haystack string to contain needle n times. - * @param {String} haystack - * @param {String} needle - * @param {String} [n=1] - * @throws {Error} - */ -const expectToContain = (haystack, needle, n = 1) => - expect(haystack.split(needle)).to.have.lengthOf(n + 1, - `expected ${n} occurrence(s) of '${needle}' in '${haystack}'`); +const requestId = 'test-request-id'; describe('AudienceNetwork adapter', () => { describe('Public API', () => { - const adapter = new AudienceNetwork(); - it('getBidderCode', () => { - expect(adapter.getBidderCode).to.be.a('function'); - expect(adapter.getBidderCode()).to.equal(bidderCode); + it('code', () => { + expect(code).to.equal(bidder); }); - it('setBidderCode', () => { - expect(adapter.setBidderCode).to.be.a('function'); + it('supportedMediaTypes', () => { + expect(supportedMediaTypes).to.deep.equal(['video']); }); - it('callBids', () => { - expect(adapter.setBidderCode).to.be.a('function'); + it('isBidRequestValid', () => { + expect(isBidRequestValid).to.be.a('function'); }); - }); - - describe('callBids parameter parsing', () => { - let xhr; - let requests; - let addBidResponse; - let logError; - - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - xhr.onCreate = request => requests.push(request); - requests = []; - addBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - logError = sinon.stub(utils, 'logError'); + it('buildRequests', () => { + expect(buildRequests).to.be.a('function'); }); - - afterEach(() => { - xhr.restore(); - bidmanager.addBidResponse.restore(); - utils.logError.restore(); + it('interpretResponse', () => { + expect(interpretResponse).to.be.a('function'); }); + }); + describe('isBidRequestValid', () => { it('missing placementId parameter', () => { - // Invalid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - sizes: ['native'] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify no attempt to fetch response - expect(requests).to.have.lengthOf(0); - // Verify no attempt to add a response as no placement was provided - expect(addBidResponse.calledOnce).to.equal(false); - // Verify attempt to log error - expect(logError.calledOnce).to.equal(true); + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250]] + })).to.equal(false); }); it('invalid sizes parameter', () => { - // Invalid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { placementId }, - sizes: ['', undefined, null, '300x100', [300, 100], [300], {}] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify no attempt to fetch response - expect(requests).to.have.lengthOf(0); - // Verify attempt to log error - expect(logError.calledOnce).to.equal(true); + expect(isBidRequestValid({ + bidder, + sizes: ['', undefined, null, '300x100', [300, 100], [300], {}], + params: { placementId } + })).to.equal(false); }); - it('filter valid sizes', () => { - // Valid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { placementId }, - sizes: [[1, 1], [300, 250]] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify attempt to fetch response - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('GET'); - expect(requests[0].url) - .to.contain('https://an.facebook.com/v2/placementbid.json?') - .and.to.contain('placementids[]=test-placement-id') - .and.to.contain('adformats[]=300x250') - .and.to.contain('pageurl=http%3A%2F%2F'); - // Verify no attempt to log error - expect(logError.called).to.equal(false); + it('valid when at least one valid size', () => { + expect(isBidRequestValid({ + bidder, + sizes: [[1, 1], [300, 250]], + params: { placementId } + })).to.equal(true); }); it('valid parameters', () => { - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { placementId }, - sizes: [[300, 250], [320, 50]] - }, - { - bidder: bidderCode, - params: { placementId }, - sizes: [[320, 50], [300, 250]] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify attempt to fetch response - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('GET'); - expect(requests[0].url) - .to.contain('https://an.facebook.com/v2/placementbid.json?') - .and.to.contain('placementids[]=test-placement-id&placementids[]=test-placement-id') - .and.to.contain('adformats[]=320x50') - .and.to.contain('adformats[]=300x250') - .and.to.contain('pageurl=http%3A%2F%2F'); - // Verify no attempt to log error - expect(logError.called).to.equal(false); + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250], [320, 50]], + params: { placementId } + })).to.equal(true); }); it('fullwidth', () => { - // Valid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { - placementId, - format: 'fullwidth' - }, - sizes: [[300, 250]] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify attempt to fetch response - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('GET'); - expect(requests[0].url) - .to.contain('https://an.facebook.com/v2/placementbid.json?') - .and.to.contain('placementids[]=test-placement-id') - .and.to.contain('adformats[]=fullwidth') - .and.to.contain('pageurl=http%3A%2F%2F'); - // Verify no attempt to log error - expect(logError.called).to.equal(false); + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250]], + params: { + placementId, + format: 'fullwidth' + } + })).to.equal(true); }); it('native', () => { - // Valid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { - placementId, - format: 'native' - }, - sizes: [[300, 250]] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify attempt to fetch response - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('GET'); - expect(requests[0].url) - .to.contain('https://an.facebook.com/v2/placementbid.json?') - .and.to.contain('placementids[]=test-placement-id') - .and.to.contain('adformats[]=native') - .and.to.contain('pageurl=http%3A%2F%2F'); - // Verify no attempt to log error - expect(logError.called).to.equal(false); + expect(isBidRequestValid({ + bidder, + sizes: [[300, 250]], + params: { + placementId, + format: 'native' + } + })).to.equal(true); }); it('video', () => { - // Valid parameters - const params = { - bidderCode, - bids: [{ - bidder: bidderCode, - params: { - placementId, - format: 'video' - }, - sizes: [[playerwidth, playerheight]] - }] - }; - // Request bids - new AudienceNetwork().callBids(params); - // Verify attempt to fetch response - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('GET'); - expect(requests[0].url) - .to.contain('https://an.facebook.com/v2/placementbid.json?') - .and.to.contain('placementids[]=test-placement-id') - .and.to.contain('adformats[]=video') - .and.to.contain('sdk[]=') - .and.to.contain('pageurl=http%3A%2F%2F'); - // Verify no attempt to log error - expect(logError.called).to.equal(false); + expect(isBidRequestValid({ + bidder, + sizes: [[playerwidth, playerheight]], + params: { + placementId, + format: 'video' + } + })).to.equal(true); }); }); - describe('callBids response handling', () => { - let server; - let addBidResponse; - let logError; - - beforeEach(() => { - server = sinon.fakeServer.create(); - addBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - logError = sinon.stub(utils, 'logError'); + describe('buildRequests', () => { + it('can build URL for IAB unit', () => { + expect(buildRequests([{ + bidder, + bidId: requestId, + sizes: [[300, 250], [320, 50]], + params: { placementId } + }])).to.deep.equal([{ + adformats: ['300x250'], + method: 'GET', + requestIds: [requestId], + sizes: ['300x250'], + url: 'https://an.facebook.com/v2/placementbid.json', + data: 'placementids[]=test-placement-id&adformats[]=300x250&testmode=false&pageurl=&sdk[]=5.5.web' + }]); }); - afterEach(() => { - server.restore(); - bidmanager.addBidResponse.restore(); - utils.logError.restore(); + it('can build URL for video unit', () => { + expect(buildRequests([{ + bidder, + bidId: requestId, + sizes: [[640, 480]], + params: { + placementId, + format: 'video' + } + }])).to.deep.equal([{ + adformats: ['video'], + method: 'GET', + requestIds: [requestId], + sizes: ['640x480'], + url: 'https://an.facebook.com/v2/placementbid.json', + data: 'placementids[]=test-placement-id&adformats[]=video&testmode=false&pageurl=&sdk[]=&playerwidth=640&playerheight=480' + }]); }); + }); + describe('interpretResponse', () => { it('error in response', () => { - // Error response - const error = 'test-error-message'; - server.respondWith(JSON.stringify({ - errors: [error] - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - params: { placementId }, - sizes: [[300, 250]] - }] - }); - server.respond(); - // Verify attempt to call addBidResponse - expect(addBidResponse.calledOnce).to.equal(true); - expect(addBidResponse.args[0]).to.have.lengthOf(2); - expect(addBidResponse.args[0][1].getStatusCode()).to.equal(STATUS.NO_BID); - expect(addBidResponse.args[0][1].bidderCode).to.equal(bidderCode); - // Verify attempt to log error - expect(logError.calledOnce).to.equal(true); - expect(logError.calledWith(error)).to.equal(true); + expect(interpretResponse({ + body: { + errors: ['test-error-message'] + } + }, {})).to.deep.equal([]); }); it('valid native bid in response', () => { - // Valid response - server.respondWith(JSON.stringify({ - errors: [], - bids: { - [placementId]: [{ - placement_id: placementId, - bid_id: 'test-bid-id', - bid_price_cents: 123, - bid_price_currency: 'usd', - bid_price_model: 'cpm' - }] + const [bidResponse] = interpretResponse({ + body: { + errors: [], + bids: { + [placementId]: [{ + placement_id: placementId, + bid_id: 'test-bid-id', + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode, - params: { - placementId, - format: 'native' - }, - sizes: [[300, 250]] - }] + }, { + adformats: ['native'], + requestIds: [requestId], + sizes: [[300, 250]] }); - server.respond(); - // Verify attempt to call addBidResponse - expect(addBidResponse.calledOnce).to.equal(true); - expect(addBidResponse.args[0]).to.have.lengthOf(2); - expect(addBidResponse.args[0][0]).to.equal(placementCode); - // Verify Prebid attributes in bid response - const bidResponse = addBidResponse.args[0][1]; - expect(bidResponse.getStatusCode()).to.equal(STATUS.GOOD); + expect(bidResponse.cpm).to.equal(1.23); - expect(bidResponse.bidderCode).to.equal(bidderCode); + expect(bidResponse.requestId).to.equal(requestId); expect(bidResponse.width).to.equal(300); expect(bidResponse.height).to.equal(250); expect(bidResponse.ad) .to.contain(`placementid:'${placementId}',format:'native',bidid:'test-bid-id'`, 'ad missing parameters') .and.to.contain('getElementsByTagName("style")', 'ad missing native styles') .and.to.contain('<div class="thirdPartyRoot"><a class="fbAdLink">', 'ad missing native container'); - // Verify Audience Network attributes in bid response + expect(bidResponse.creativeId).to.equal(placementId); + expect(bidResponse.netRevenue).to.equal(true); + expect(bidResponse.currency).to.equal('USD'); + expect(bidResponse.hb_bidder).to.equal('fan'); expect(bidResponse.fb_bidid).to.equal('test-bid-id'); expect(bidResponse.fb_format).to.equal('native'); expect(bidResponse.fb_placementid).to.equal(placementId); - // Verify no attempt to log error - expect(logError.called).to.equal(false, 'logError called'); }); it('valid IAB bid in response', () => { - // Valid response - server.respondWith(JSON.stringify({ - errors: [], - bids: { - [placementId]: [{ - placement_id: placementId, - bid_id: 'test-bid-id', - bid_price_cents: 123, - bid_price_currency: 'usd', - bid_price_model: 'cpm' - }] + const [bidResponse] = interpretResponse({ + body: { + errors: [], + bids: { + [placementId]: [{ + placement_id: placementId, + bid_id: 'test-bid-id', + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode, - params: { placementId }, - sizes: [[300, 250]] - }] + }, { + adformats: ['300x250'], + requestIds: [requestId], + sizes: [[300, 250]] }); - server.respond(); - // Verify attempt to call addBidResponse - expect(addBidResponse.calledOnce).to.equal(true); - expect(addBidResponse.args[0]).to.have.lengthOf(2); - expect(addBidResponse.args[0][0]).to.equal(placementCode); - // Verify bidResponse Object - const bidResponse = addBidResponse.args[0][1]; - expect(bidResponse.getStatusCode()).to.equal(STATUS.GOOD); + expect(bidResponse.cpm).to.equal(1.23); - expect(bidResponse.bidderCode).to.equal(bidderCode); + expect(bidResponse.requestId).to.equal(requestId); expect(bidResponse.width).to.equal(300); expect(bidResponse.height).to.equal(250); expect(bidResponse.ad) .to.contain(`placementid:'${placementId}',format:'300x250',bidid:'test-bid-id'`, 'ad missing parameters') .and.not.to.contain('getElementsByTagName("style")', 'ad should not contain native styles') .and.not.to.contain('<div class="thirdPartyRoot"><a class="fbAdLink">', 'ad should not contain native container'); - // Verify no attempt to log error - expect(logError.called).to.equal(false, 'logError called'); + expect(bidResponse.creativeId).to.equal(placementId); + expect(bidResponse.netRevenue).to.equal(true); + expect(bidResponse.currency).to.equal('USD'); + expect(bidResponse.hb_bidder).to.equal('fan'); + expect(bidResponse.fb_bidid).to.equal('test-bid-id'); + expect(bidResponse.fb_format).to.equal('300x250'); + expect(bidResponse.fb_placementid).to.equal(placementId); }); it('filters invalid slot sizes', () => { - // Valid response - server.respondWith(JSON.stringify({ - errors: [], - bids: { - [placementId]: [{ - placement_id: placementId, - bid_id: 'test-bid-id', - bid_price_cents: 123, - bid_price_currency: 'usd', - bid_price_model: 'cpm' - }] + const [bidResponse] = interpretResponse({ + body: { + errors: [], + bids: { + [placementId]: [{ + placement_id: placementId, + bid_id: 'test-bid-id', + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode, - params: { placementId }, - sizes: ['350x200'] - }, { - bidder: bidderCode, - placementCode, - params: { placementId }, - sizes: [[300, 250]] - }] + }, { + adformats: ['300x250'], + requestIds: [requestId], + sizes: [[300, 250]] }); - server.respond(); - // Verify attempt to call addBidResponse - expect(addBidResponse.calledOnce).to.equal(true); - expect(addBidResponse.args[0]).to.have.lengthOf(2); - expect(addBidResponse.args[0][0]).to.equal(placementCode); - // Verify bidResponse Object - const bidResponse = addBidResponse.args[0][1]; - expect(bidResponse.getStatusCode()).to.equal(STATUS.GOOD); + expect(bidResponse.cpm).to.equal(1.23); - expect(bidResponse.bidderCode).to.equal(bidderCode); + expect(bidResponse.requestId).to.equal(requestId); expect(bidResponse.width).to.equal(300); expect(bidResponse.height).to.equal(250); - // Verify no attempt to log error - expect(logError.called).to.equal(false, 'logError called'); + expect(bidResponse.creativeId).to.equal(placementId); + expect(bidResponse.netRevenue).to.equal(true); + expect(bidResponse.currency).to.equal('USD'); + expect(bidResponse.hb_bidder).to.equal('fan'); + expect(bidResponse.fb_bidid).to.equal('test-bid-id'); + expect(bidResponse.fb_format).to.equal('300x250'); + expect(bidResponse.fb_placementid).to.equal(placementId); }); it('valid multiple bids in response', () => { const placementIdNative = 'test-placement-id-native'; const placementIdIab = 'test-placement-id-iab'; - const placementCodeNative = 'test-placement-code-native'; - const placementCodeIab = 'test-placement-code-iab'; - // Valid response - server.respondWith(JSON.stringify({ - errors: [], - bids: { - [placementIdNative]: [{ - placement_id: placementIdNative, - bid_id: 'test-bid-id-native', - bid_price_cents: 123, - bid_price_currency: 'usd', - bid_price_model: 'cpm' - }], - [placementIdIab]: [{ - placement_id: placementIdIab, - bid_id: 'test-bid-id-iab', - bid_price_cents: 456, - bid_price_currency: 'usd', - bid_price_model: 'cpm' - }] + + const [bidResponseNative, bidResponseIab] = interpretResponse({ + body: { + errors: [], + bids: { + [placementIdNative]: [{ + placement_id: placementIdNative, + bid_id: 'test-bid-id-native', + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }], + [placementIdIab]: [{ + placement_id: placementIdIab, + bid_id: 'test-bid-id-iab', + bid_price_cents: 456, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode: placementCodeNative, - params: { - placementId: placementIdNative, - format: 'native' - }, - sizes: [[300, 250]] - }, { - bidder: bidderCode, - placementCode: placementCodeIab, - params: { placementId: placementIdIab }, - sizes: [[300, 250]] - }] + }, { + adformats: ['native', '300x250'], + requestIds: [requestId, requestId], + sizes: [[300, 250], [300, 250]] }); - server.respond(); - // Verify multiple attempts to call addBidResponse - expect(addBidResponse.calledTwice).to.equal(true); - // Verify native - const addBidResponseNativeCall = addBidResponse.args[0]; - expect(addBidResponseNativeCall).to.have.lengthOf(2); - expect(addBidResponseNativeCall[0]).to.equal(placementCodeNative); - expect(addBidResponseNativeCall[1].getStatusCode()).to.equal(STATUS.GOOD); - expect(addBidResponseNativeCall[1].cpm).to.equal(1.23); - expect(addBidResponseNativeCall[1].bidderCode).to.equal(bidderCode); - expect(addBidResponseNativeCall[1].width).to.equal(300); - expect(addBidResponseNativeCall[1].height).to.equal(250); - expect(addBidResponseNativeCall[1].ad).to.contain(`placementid:'${placementIdNative}',format:'native',bidid:'test-bid-id-native'`, 'ad missing parameters'); - // Verify IAB - const addBidResponseIabCall = addBidResponse.args[1]; - expect(addBidResponseIabCall).to.have.lengthOf(2); - expect(addBidResponseIabCall[0]).to.equal(placementCodeIab); - expect(addBidResponseIabCall[1].getStatusCode()).to.equal(STATUS.GOOD); - expect(addBidResponseIabCall[1].cpm).to.equal(4.56); - expect(addBidResponseIabCall[1].bidderCode).to.equal(bidderCode); - expect(addBidResponseIabCall[1].width).to.equal(300); - expect(addBidResponseIabCall[1].height).to.equal(250); - expect(addBidResponseIabCall[1].ad).to.contain(`placementid:'${placementIdIab}',format:'300x250',bidid:'test-bid-id-iab'`, 'ad missing parameters'); - // Verify no attempt to log error - expect(logError.called).to.equal(false, 'logError called'); + + expect(bidResponseNative.cpm).to.equal(1.23); + expect(bidResponseNative.requestId).to.equal(requestId); + expect(bidResponseNative.width).to.equal(300); + expect(bidResponseNative.height).to.equal(250); + expect(bidResponseNative.ad).to.contain(`placementid:'${placementIdNative}',format:'native',bidid:'test-bid-id-native'`, 'ad missing parameters'); + expect(bidResponseNative.creativeId).to.equal(placementIdNative); + expect(bidResponseNative.netRevenue).to.equal(true); + expect(bidResponseNative.currency).to.equal('USD'); + expect(bidResponseNative.hb_bidder).to.equal('fan'); + expect(bidResponseNative.fb_bidid).to.equal('test-bid-id-native'); + expect(bidResponseNative.fb_format).to.equal('native'); + expect(bidResponseNative.fb_placementid).to.equal(placementIdNative); + + expect(bidResponseIab.cpm).to.equal(4.56); + expect(bidResponseIab.requestId).to.equal(requestId); + expect(bidResponseIab.width).to.equal(300); + expect(bidResponseIab.height).to.equal(250); + expect(bidResponseIab.ad).to.contain(`placementid:'${placementIdIab}',format:'300x250',bidid:'test-bid-id-iab'`, 'ad missing parameters'); + expect(bidResponseIab.creativeId).to.equal(placementIdIab); + expect(bidResponseIab.netRevenue).to.equal(true); + expect(bidResponseIab.currency).to.equal('USD'); + expect(bidResponseIab.hb_bidder).to.equal('fan'); + expect(bidResponseIab.fb_bidid).to.equal('test-bid-id-iab'); + expect(bidResponseIab.fb_format).to.equal('300x250'); + expect(bidResponseIab.fb_placementid).to.equal(placementIdIab); }); it('valid video bid in response', () => { const bidId = 'test-bid-id-video'; - // Valid response - server.respondWith(JSON.stringify({ - errors: [], - bids: { - [placementId]: [{ - placement_id: placementId, - bid_id: bidId, - bid_price_cents: 123, - bid_price_currency: 'usd', - bid_price_model: 'cpm' - }] + + const [bidResponse] = interpretResponse({ + body: { + errors: [], + bids: { + [placementId]: [{ + placement_id: placementId, + bid_id: bidId, + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode, - params: { - placementId, - format: 'video' - }, - sizes: [[playerwidth, playerheight]] - }] + }, { + adformats: ['video'], + requestIds: [requestId], + sizes: [[playerwidth, playerheight]] }); - server.respond(); - // Verify addBidResponse call - expect(addBidResponse.calledOnce).to.equal(true); - const addBidResponseArgs = addBidResponse.args[0]; - expect(addBidResponseArgs).to.have.lengthOf(2); - expect(addBidResponseArgs[0]).to.equal(placementCode); - expect(addBidResponseArgs[1].getStatusCode()).to.equal(STATUS.GOOD); - expect(addBidResponseArgs[1].cpm).to.equal(1.23); - expect(addBidResponseArgs[1].bidderCode).to.equal(bidderCode); - // Video-specific properties - expect(addBidResponseArgs[1].mediaType).to.equal('video'); - expect(addBidResponseArgs[1].vastUrl) - .to.equal(addBidResponseArgs[1].descriptionUrl) - .and.to.contain('https://an.facebook.com/v1/instream/vast.xml?') - .and.to.contain(`placementid=${placementId}`) - .and.to.contain('pageurl=http%3A%2F%2F') - .and.to.contain(`playerwidth=${playerwidth}`) - .and.to.contain(`playerheight=${playerheight}`) - .and.to.contain(`bidid=${bidId}`); - expect(addBidResponseArgs[1].width).to.equal(playerwidth); - expect(addBidResponseArgs[1].height).to.equal(playerheight); - // Verify no attempt to log error - expect(logError.called).to.equal(false, 'logError called'); + + expect(bidResponse.cpm).to.equal(1.23); + expect(bidResponse.requestId).to.equal(requestId); + expect(bidResponse.mediaType).to.equal('video'); + expect(bidResponse.vastUrl).to.equal(`https://an.facebook.com/v1/instream/vast.xml?placementid=${placementId}&pageurl=&playerwidth=${playerwidth}&playerheight=${playerheight}&bidid=${bidId}`); + expect(bidResponse.width).to.equal(playerwidth); + expect(bidResponse.height).to.equal(playerheight); }); it('mixed video and native bids', () => { @@ -546,81 +350,45 @@ describe('AudienceNetwork adapter', () => { const videoBidId = 'test-video-bid-id'; const nativePlacementId = 'test-native-placement-id'; const nativeBidId = 'test-native-bid-id'; - // Valid response - server.respondWith(JSON.stringify({ - errors: [], - bids: { - [videoPlacementId]: [{ - placement_id: videoPlacementId, - bid_id: videoBidId, - bid_price_cents: 123, - bid_price_currency: 'usd', - bid_price_model: 'cpm' - }], - [nativePlacementId]: [{ - placement_id: nativePlacementId, - bid_id: nativeBidId, - bid_price_cents: 456, - bid_price_currency: 'usd', - bid_price_model: 'cpm' - }] + + const [bidResponseVideo, bidResponseNative] = interpretResponse({ + body: { + errors: [], + bids: { + [videoPlacementId]: [{ + placement_id: videoPlacementId, + bid_id: videoBidId, + bid_price_cents: 123, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }], + [nativePlacementId]: [{ + placement_id: nativePlacementId, + bid_id: nativeBidId, + bid_price_cents: 456, + bid_price_currency: 'usd', + bid_price_model: 'cpm' + }] + } } - })); - // Request bids - new AudienceNetwork().callBids({ - bidderCode, - bids: [{ - bidder: bidderCode, - placementCode, - params: { - placementId: videoPlacementId, - format: 'video' - }, - sizes: [[playerwidth, playerheight]] - }, { - bidder: bidderCode, - placementCode, - params: { - placementId: nativePlacementId, - format: 'native' - }, - sizes: [[300, 250]] - }] + }, { + adformats: ['video', 'native'], + requestIds: [requestId, requestId], + sizes: [[playerwidth, playerheight], [300, 250]] }); - server.respond(); - // Verify multiple attempts to call addBidResponse - expect(addBidResponse.calledTwice).to.equal(true); - // Verify video - const addBidResponseVideoCall = addBidResponse.args[0]; - expect(addBidResponseVideoCall).to.have.lengthOf(2); - expect(addBidResponseVideoCall[0]).to.equal(placementCode); - expect(addBidResponseVideoCall[1].getStatusCode()).to.equal(STATUS.GOOD); - expect(addBidResponseVideoCall[1].cpm).to.equal(1.23); - expect(addBidResponseVideoCall[1].bidderCode).to.equal(bidderCode); - // Video-specific properties - expect(addBidResponseVideoCall[1].mediaType).to.equal('video'); - expect(addBidResponseVideoCall[1].vastUrl) - .to.equal(addBidResponseVideoCall[1].descriptionUrl) - .and.to.contain('https://an.facebook.com/v1/instream/vast.xml?') - .and.to.contain(`placementid=${videoPlacementId}`) - .and.to.contain('pageurl=http%3A%2F%2F') - .and.to.contain(`playerwidth=${playerwidth}`) - .and.to.contain(`playerheight=${playerheight}`) - .and.to.contain(`bidid=${videoBidId}`); - expect(addBidResponseVideoCall[1].width).to.equal(playerwidth); - expect(addBidResponseVideoCall[1].height).to.equal(playerheight); - // Verify native - const addBidResponseNativeCall = addBidResponse.args[1]; - expect(addBidResponseNativeCall).to.have.lengthOf(2); - expect(addBidResponseNativeCall[0]).to.equal(placementCode); - expect(addBidResponseNativeCall[1].getStatusCode()).to.equal(STATUS.GOOD); - expect(addBidResponseNativeCall[1].cpm).to.equal(4.56); - expect(addBidResponseNativeCall[1].bidderCode).to.equal(bidderCode); - expect(addBidResponseNativeCall[1].width).to.equal(300); - expect(addBidResponseNativeCall[1].height).to.equal(250); - expect(addBidResponseNativeCall[1].ad).to.contain(`placementid:'${nativePlacementId}',format:'native',bidid:'${nativeBidId}'`); - // Verify no attempt to log error - expect(logError.called).to.equal(false, 'logError called'); + + expect(bidResponseVideo.cpm).to.equal(1.23); + expect(bidResponseVideo.requestId).to.equal(requestId); + expect(bidResponseVideo.mediaType).to.equal('video'); + expect(bidResponseVideo.vastUrl).to.equal(`https://an.facebook.com/v1/instream/vast.xml?placementid=${videoPlacementId}&pageurl=&playerwidth=${playerwidth}&playerheight=${playerheight}&bidid=${videoBidId}`); + expect(bidResponseVideo.width).to.equal(playerwidth); + expect(bidResponseVideo.height).to.equal(playerheight); + + expect(bidResponseNative.cpm).to.equal(4.56); + expect(bidResponseNative.requestId).to.equal(requestId); + expect(bidResponseNative.width).to.equal(300); + expect(bidResponseNative.height).to.equal(250); + expect(bidResponseNative.ad).to.contain(`placementid:'${nativePlacementId}',format:'native',bidid:'${nativeBidId}'`); }); }); }); From 32196ae40ca9a709e13766982fcc6178313bc358 Mon Sep 17 00:00:00 2001 From: Matt Lane <mlane@appnexus.com> Date: Thu, 26 Oct 2017 06:16:22 -0700 Subject: [PATCH 17/44] Fix TypeError condition when bid is empty (#1769) --- modules/dfpAdServerVideo.js | 6 ++++-- test/spec/modules/dfpAdServerVideo_spec.js | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index f2008b806ca..4f56355a70c 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -64,9 +64,11 @@ export default function buildDfpVideoUrl(options) { url: location.href, }; + const adserverTargeting = (bid && bid.adserverTargeting) || {}; + const customParams = Object.assign({}, - bid.adserverTargeting, - { hb_uuid: bid.videoCacheKey }, + adserverTargeting, + { hb_uuid: bid && bid.videoCacheKey }, options.params.cust_params); const queryParams = Object.assign({}, diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 81c83baa65c..3156c628abd 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -91,4 +91,13 @@ describe('The DFP video support module', () => { expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('my_targeting', 'foo'); }); + + it('should work with nobid responses', () => { + const url = buildDfpVideoUrl({ + adUnit: adUnit, + params: { 'iu': 'my/adUnit' } + }); + + expect(url).to.be.a('string'); + }); }); From 1b0439f92075b9bfe70bc9599761d4f3b289f288 Mon Sep 17 00:00:00 2001 From: Jaimin Panchal <jaiminpanchal27@gmail.com> Date: Thu, 26 Oct 2017 10:42:04 -0400 Subject: [PATCH 18/44] Validating bid response params (#1738) * validating bid response params * added all required params * replaced id with correct param requestid * keeping only common keys to validate --- modules/appnexusAstBidAdapter.js | 8 +++- src/adapters/bidderFactory.js | 23 +++++++--- .../modules/appnexusAstBidAdapter_spec.js | 10 +++-- test/spec/unit/core/bidderFactory_spec.js | 42 +++++++++++++++++-- 4 files changed, 69 insertions(+), 14 deletions(-) diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index 9a2cea8a229..3c900e15312 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -182,8 +182,11 @@ function newBid(serverBid, rtbBid) { const bid = { requestId: serverBid.uuid, cpm: rtbBid.cpm, - creative_id: rtbBid.creative_id, + creativeId: rtbBid.creative_id, dealId: rtbBid.deal_id, + currency: 'USD', + netRevenue: true, + ttl: 300 }; if (rtbBid.rtb.video) { @@ -191,7 +194,8 @@ function newBid(serverBid, rtbBid) { width: rtbBid.rtb.video.player_width, height: rtbBid.rtb.video.player_height, vastUrl: rtbBid.rtb.video.asset_url, - descriptionUrl: rtbBid.rtb.video.asset_url + descriptionUrl: rtbBid.rtb.video.asset_url, + ttl: 3600 }); // This supports Outstream Video if (rtbBid.renderer_url) { diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 87dfd372b9c..87d92cbfb92 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -116,6 +116,9 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @property {string} url The URL which makes the sync happen. */ +// common params for all mediaTypes +const COMMON_BID_RESPONSE_KEYS = ['requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency']; + /** * Register a bidder with prebid, using the given spec. * @@ -303,12 +306,17 @@ export function newBidder(spec) { onResponse(); function addBidUsingRequestMap(bid) { - const bidRequest = bidRequestMap[bid.requestId]; - if (bidRequest) { - const prebidBid = Object.assign(bidfactory.createBid(STATUS.GOOD, bidRequest), bid); - addBidWithCode(bidRequest.placementCode, prebidBid); + // In Prebid 1.0 all the validation logic from bidmanager will move here, as of now we are only validating new params so that adapters dont miss adding them. + if (hasValidKeys(bid)) { + const bidRequest = bidRequestMap[bid.requestId]; + if (bidRequest) { + const prebidBid = Object.assign(bidfactory.createBid(STATUS.GOOD, bidRequest), bid); + addBidWithCode(bidRequest.placementCode, prebidBid); + } else { + logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bid.requestId}. Ignoring.`); + } } else { - logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bid.requestId}. Ignoring.`); + logError(`Bidder ${spec.code} is missing required params. Check http://prebid.org/dev-docs/bidder-adapter-1.html for list of params.`); } } @@ -337,6 +345,11 @@ export function newBidder(spec) { return true; } + function hasValidKeys(bid) { + let bidKeys = Object.keys(bid); + return COMMON_BID_RESPONSE_KEYS.every(key => bidKeys.includes(key)); + } + function newEmptyBid() { const bid = bidfactory.createBid(STATUS.NO_BID); bid.code = spec.code; diff --git a/test/spec/modules/appnexusAstBidAdapter_spec.js b/test/spec/modules/appnexusAstBidAdapter_spec.js index d76cc08810f..83cbcc38a2b 100644 --- a/test/spec/modules/appnexusAstBidAdapter_spec.js +++ b/test/spec/modules/appnexusAstBidAdapter_spec.js @@ -296,18 +296,20 @@ describe('AppNexusAdapter', () => { { 'requestId': '3db3773286ee59', 'cpm': 0.5, - 'creative_id': 29681110, + 'creativeId': 29681110, 'dealId': undefined, 'width': 300, 'height': 250, 'ad': '<!-- Creative -->', - 'mediaType': 'banner' + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'netRevenue': true } ]; let bidderRequest; - let result = spec.interpretResponse({ body: response }, {bidderRequest}); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); it('handles nobid responses', () => { diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 3260b66da28..e621f1fb329 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -5,6 +5,7 @@ import * as ajax from 'src/ajax'; import { expect } from 'chai'; import { STATUS } from 'src/constants'; import { userSync } from 'src/userSync' +import * as utils from 'src/utils'; const CODE = 'sampleBidder'; const MOCK_BIDS_REQUEST = { @@ -113,7 +114,7 @@ describe('bidders created by newBidder', () => { expect(spec.buildRequests.firstCall.args[0]).to.deep.equal([MOCK_BIDS_REQUEST.bids[0]]); }); - it("should make no server requests if the spec doesn't return any", () => { + it('should make no server requests if the spec doesn\'t return any', () => { const bidder = newBidder(spec); spec.isBidRequestValid.returns(true); @@ -262,6 +263,7 @@ describe('bidders created by newBidder', () => { describe('when the ajax call succeeds', () => { let ajaxStub; let userSyncStub; + let logErrorSpy; beforeEach(() => { ajaxStub = sinon.stub(ajax, 'ajax', function(url, callbacks) { @@ -270,11 +272,13 @@ describe('bidders created by newBidder', () => { callbacks.success('response body', { getResponseHeader: fakeResponse }); }); userSyncStub = sinon.stub(userSync, 'registerSync') + logErrorSpy = sinon.spy(utils, 'logError'); }); afterEach(() => { ajaxStub.restore(); userSyncStub.restore(); + utils.logError.restore(); }); it('should call spec.interpretResponse() with the response content', () => { @@ -324,16 +328,21 @@ describe('bidders created by newBidder', () => { expect(spec.interpretResponse.calledTwice).to.equal(true); }); - it("should add bids for each placement code into the bidmanager, even if the bidder doesn't bid on all of them", () => { + it('should add bids for each placement code into the bidmanager, even if the bidder doesn\'t bid on all of them', () => { const bidder = newBidder(spec); const bid = { + creativeId: 'creative-id', + bidderCode: 'code', requestId: 'some-id', ad: 'ad-url.com', cpm: 0.5, height: 200, width: 300, - placementCode: 'mock/placement' + placementCode: 'mock/placement', + currency: 'USD', + netRevenue: true, + ttl: 300 }; spec.isBidRequestValid.returns(true); spec.buildRequests.returns({ @@ -352,6 +361,7 @@ describe('bidders created by newBidder', () => { [bidmanager.addBidResponse.firstCall.args[0], bidmanager.addBidResponse.secondCall.args[0]]; expect(placementsWithBids).to.contain('mock/placement'); expect(placementsWithBids).to.contain('mock/placement2'); + expect(logErrorSpy.callCount).to.equal(0); }); it('should call spec.getUserSyncs() with the response', () => { @@ -391,6 +401,32 @@ describe('bidders created by newBidder', () => { expect(userSyncStub.firstCall.args[1]).to.equal(spec.code); expect(userSyncStub.firstCall.args[2]).to.equal('usersync.com'); }); + + it('should logError when required bid response params are missing', () => { + const bidder = newBidder(spec); + + const bid = { + requestId: 'some-id', + ad: 'ad-url.com', + cpm: 0.5, + height: 200, + width: 300, + placementCode: 'mock/placement' + }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + spec.interpretResponse.returns(bid); + + bidder.callBids(MOCK_BIDS_REQUEST); + + expect(logErrorSpy.calledOnce).to.equal(true); + }); }); describe('when the ajax call fails', () => { From 698c3300754ac3abb233f5cbe9e56b6303ddf4a1 Mon Sep 17 00:00:00 2001 From: Rade Popovic <32302052+nanointeractive@users.noreply.github.com> Date: Thu, 26 Oct 2017 16:46:44 +0200 Subject: [PATCH 19/44] nanointeractive bid adapter (#1627) * nanointeractive bid adapter * nanointeractive bid adapter * - using utils.getParameterByName instead of utils.getTopWindowLocation().href - bidderCode is removed - Default ALG changed to 'ihr' - added protocol to 'cors' param * markdown file * enabling localhost for bid requests --- modules/nanointeractiveBidAdapter.js | 88 ++++++++++++++ modules/nanointeractiveBidAdapter.md | 69 +++++++++++ .../modules/nanointeractiveBidAdapter_spec.js | 109 ++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 modules/nanointeractiveBidAdapter.js create mode 100644 modules/nanointeractiveBidAdapter.md create mode 100644 test/spec/modules/nanointeractiveBidAdapter_spec.js diff --git a/modules/nanointeractiveBidAdapter.js b/modules/nanointeractiveBidAdapter.js new file mode 100644 index 00000000000..9781e36edb6 --- /dev/null +++ b/modules/nanointeractiveBidAdapter.js @@ -0,0 +1,88 @@ +import * as utils from 'src/utils'; +import { registerBidder } from '../src/adapters/bidderFactory'; +import { BANNER } from '../src/mediaTypes'; + +export const BIDDER_CODE = 'nanointeractive'; +export const ENGINE_BASE_URL = 'http://tmp.audiencemanager.de/hb'; + +export const SECURITY = 'sec'; +export const DATA_PARTNER_ID = 'dpid'; +export const DATA_PARTNER_PIXEL_ID = 'pid'; +export const ALG = 'alg'; +export const NQ = 'nq'; +export const NQ_NAME = 'name'; +export const CATEGORY = 'category'; + +const DEFAULT_ALG = 'ihr'; + +export const spec = { + + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid(bid) { + const sec = bid.params[SECURITY]; + const dpid = bid.params[DATA_PARTNER_ID]; + const pid = bid.params[DATA_PARTNER_PIXEL_ID]; + return !!(sec && dpid && pid); + }, + buildRequests(bidRequests) { + let payload = []; + bidRequests.forEach(bid => payload.push(createSingleBidRequest(bid))); + return { + method: 'POST', + url: ENGINE_BASE_URL, + data: JSON.stringify(payload) + }; + }, + interpretResponse(serverResponse) { + const bids = []; + serverResponse.forEach(serverBid => { + if (isEngineResponseValid(serverBid)) { + bids.push(createSingleBidResponse(serverBid)); + } + }); + return bids; + } +}; + +function createSingleBidRequest(bid) { + return { + [SECURITY]: bid.params[SECURITY], + [DATA_PARTNER_ID]: bid.params[DATA_PARTNER_ID], + [DATA_PARTNER_PIXEL_ID]: bid.params[DATA_PARTNER_PIXEL_ID], + [ALG]: bid.params[ALG] || DEFAULT_ALG, + [NQ]: [createNqParam(bid), createCategoryParam(bid)], + sizes: bid.sizes.map(value => value[0] + 'x' + value[1]), + bidId: bid.bidId, + cors: location.origin + }; +} + +function createSingleBidResponse(serverBid) { + return { + requestId: serverBid.id, + cpm: serverBid.cpm, + width: serverBid.width, + height: serverBid.height, + ad: serverBid.ad, + ttl: serverBid.ttl, + creativeId: serverBid.creativeId, + netRevenue: serverBid.netRevenue || true, + currency: serverBid.currency, + }; +} + +function createNqParam(bid) { + return bid.params[NQ_NAME] ? utils.getParameterByName(bid.params[NQ_NAME]) : bid.params[NQ] || null; +} + +function createCategoryParam(bid) { + return bid.params[CATEGORY] || null; +} + +function isEngineResponseValid(response) { + return !!response.cpm && !!response.ad; +} + +registerBidder(spec); diff --git a/modules/nanointeractiveBidAdapter.md b/modules/nanointeractiveBidAdapter.md new file mode 100644 index 00000000000..0159353750a --- /dev/null +++ b/modules/nanointeractiveBidAdapter.md @@ -0,0 +1,69 @@ +# Overview + +``` +Module Name: NanoInteractive Bid Adapter +Module Type: Bidder Adapter +Maintainer: rade@nanointeractive.com +``` + +# Description + +Connects to NanoInteractive search retargeting Ad Server for bids. + +Besides standard params, please provide, if exist, user search params. + +Three examples calling the Ad Server. + +**First** is basic + +**Second** is with hardcoded nq (user search) params + +**Third** is with the search query param name of the current url + + +# Test Parameters +``` +var adUnits = [ + // Basic call + { + code: 'basic-div', + sizes: [[300, 250], [300,600]], + bids: [{ + bidder: 'nanointeractive', + params: { + sec: '04a0cb7fb9ac02840f7f33d68a883780', + dpid: '58bfec94eb0a1916fa380162', + pid: '58bfec94eb0a1916fa380163' + } + }] + }, + // Hardcoded user search + { + code: 'nq-div', + sizes: [[300, 250], [300,600]], + bids: [{ + bidder: 'nanointeractive', + params: { + sec: '04a0cb7fb9ac02840f7f33d68a883780', + dpid: '58bfec94eb0a1916fa380162', + pid: '58bfec94eb0a1916fa380163', + nq: 'user search' + } + }] + }, + // URL user search + { + code: 'url-div', + sizes: [[300, 250], [300,600]], + bids: [{ + bidder: 'nanointeractive', + params: { + sec: '04a0cb7fb9ac02840f7f33d68a883780', + dpid: '58bfec94eb0a1916fa380162', + pid: '58bfec94eb0a1916fa380163', + name: 'search' + } + }] + } +]; +``` diff --git a/test/spec/modules/nanointeractiveBidAdapter_spec.js b/test/spec/modules/nanointeractiveBidAdapter_spec.js new file mode 100644 index 00000000000..b9b9207aca2 --- /dev/null +++ b/test/spec/modules/nanointeractiveBidAdapter_spec.js @@ -0,0 +1,109 @@ +import { expect } from 'chai'; +import { + ALG, + BIDDER_CODE, CATEGORY, DATA_PARTNER_ID, DATA_PARTNER_PIXEL_ID, ENGINE_BASE_URL, NQ, NQ_NAME, SECURITY, + spec +} from '../../../modules/nanointeractiveBidAdapter'; + +describe('nanointeractive adapter tests', function () { + const SEARCH_QUERY = 'rumpelstiltskin'; + const WIDTH = 300; + const HEIGHT = 250; + const SIZES = [[WIDTH, HEIGHT]]; + const AD = '<script type="text/javascript" src="https://trc.audiencemanager.de/ad/?pl=58c2829beb0a193456047a27&cb=${CACHEBUSTER}&tc=${CLICK_URL_ENC}"></script> <noscript> <a href="https://trc.audiencemanager.de/ad/?t=c&pl=58c2829beb0a193456047a27&cb=${CACHEBUSTER}&tc=${CLICK_URL_ENC}"> <img src="https://trc.audiencemanager.de/ad/?t=i&pl=58c2829beb0a193456047a27&cb=${CACHEBUSTER}" alt="Click Here" border="0"> </a> </noscript>'; + const CPM = 1; + + function getBid(isValid) { + return { + bidder: BIDDER_CODE, + params: (function () { + return { + [SECURITY]: isValid === true ? 'sec1' : null, + [DATA_PARTNER_ID]: 'dpid1', + [DATA_PARTNER_PIXEL_ID]: 'pid1', + [ALG]: 'ihr', + [NQ]: SEARCH_QUERY, + [NQ_NAME]: null, + [CATEGORY]: null, + } + })(), + placementCode: 'div-gpt-ad-1460505748561-0', + transactionId: 'ee335735-ddd3-41f2-b6c6-e8aa99f81c0f', + sizes: SIZES, + bidId: '24a1c9ec270973', + bidderRequestId: '189135372acd55', + requestId: 'ac15bb68-4ef0-477f-93f4-de91c47f00a9' + } + } + + const SINGlE_BID_REQUEST = { + [SECURITY]: 'sec1', + [DATA_PARTNER_ID]: 'dpid1', + [DATA_PARTNER_PIXEL_ID]: 'pid1', + [ALG]: 'ihr', + [NQ]: [SEARCH_QUERY, null], + sizes: [WIDTH + 'x' + HEIGHT], + bidId: '24a1c9ec270973', + cors: 'http://localhost:9876' + }; + + function getSingleBidResponse(isValid) { + return { + id: '24a1c9ec270973', + cpm: isValid === true ? CPM : null, + width: WIDTH, + height: HEIGHT, + ad: AD, + ttl: 360, + creativeId: 'TEST_ID', + netRevenue: false, + currency: 'EUR', + } + } + + const VALID_BID = { + requestId: '24a1c9ec270973', + cpm: CPM, + width: WIDTH, + height: HEIGHT, + ad: AD, + ttl: 360, + creativeId: 'TEST_ID', + netRevenue: false, + currency: 'EUR', + }; + + describe('NanoAdapter', () => { + let nanoBidAdapter = spec; + + describe('Methods', () => { + it('Test isBidRequestValid() with valid param', function () { + expect(nanoBidAdapter.isBidRequestValid(getBid(true))).to.equal(true); + }); + it('Test isBidRequestValid() with invalid param', function () { + expect(nanoBidAdapter.isBidRequestValid(getBid(false))).to.equal(false); + }); + it('Test buildRequests()', function () { + let request = nanoBidAdapter.buildRequests([getBid(true)]); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENGINE_BASE_URL); + expect(request.data).to.equal(JSON.stringify([SINGlE_BID_REQUEST])); + }); + it('Test interpretResponse() length', function () { + let bids = nanoBidAdapter.interpretResponse([getSingleBidResponse(true), getSingleBidResponse(false)]); + expect(bids.length).to.equal(1); + }); + it('Test interpretResponse() bids', function () { + let bid = nanoBidAdapter.interpretResponse([getSingleBidResponse(true), getSingleBidResponse(false)])[0]; + expect(bid.requestId).to.equal(VALID_BID.requestId); + expect(bid.cpm).to.equal(VALID_BID.cpm); + expect(bid.width).to.equal(VALID_BID.width); + expect(bid.height).to.equal(VALID_BID.height); + expect(bid.ad).to.equal(VALID_BID.ad); + expect(bid.ttl).to.equal(VALID_BID.ttl); + expect(bid.creativeId).to.equal(VALID_BID.creativeId); + expect(bid.currency).to.equal(VALID_BID.currency); + }); + }); + }); +}); From 15b2798924700abbb6b81c71d626ba8db5be96a6 Mon Sep 17 00:00:00 2001 From: dbemiller <dbemiller@appnexus.com> Date: Thu, 26 Oct 2017 11:52:22 -0400 Subject: [PATCH 20/44] Fixed the argument type on getUserSyncs. (#1767) --- src/adapters/bidderFactory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 87d92cbfb92..241891640c5 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -47,7 +47,7 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @property {function(ServerResponse, BidRequest): Bid[]} interpretResponse Given a successful response from the Server, * interpret it and return the Bid objects. This function will be run inside a try/catch. * If it throws any errors, your bids will be discarded. - * @property {function(SyncOptions, Array): UserSync[]} [getUserSyncs] Given an array of all the responses + * @property {function(SyncOptions, ServerResponse[]): UserSync[]} [getUserSyncs] Given an array of all the responses * from the server, determine which user syncs should occur. The argument array will contain every element * which has been sent through to interpretResponse. The order of syncs in this array matters. The most * important ones should come first, since publishers may limit how many are dropped on their page. From c730f95444aa344a68dfdf2508c75ef83dcb6ca4 Mon Sep 17 00:00:00 2001 From: jbartek-improve <31618107+jbartek-improve@users.noreply.github.com> Date: Thu, 26 Oct 2017 17:53:47 +0200 Subject: [PATCH 21/44] Update Improve Digital adapter for Prebid 1.0 (#1728) * Update Improve Digital adapter for Prebid 1.0 * Removed bidderCode from bids * Added creativeId to bid response; updated format of the first argument of interpretResponse --- modules/improvedigitalBidAdapter.js | 379 ++++----- modules/improvedigitalBidAdapter.md | 47 + .../modules/improvedigitalBidAdapter_spec.js | 804 ++++++------------ 3 files changed, 493 insertions(+), 737 deletions(-) create mode 100644 modules/improvedigitalBidAdapter.md diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 51fb61b03e2..bc00127b269 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -1,176 +1,150 @@ -const LIB_VERSION_GLOBAL = '3.0.5'; - -const CONSTANTS = require('src/constants'); -const utils = require('src/utils'); -const bidfactory = require('src/bidfactory'); -const bidmanager = require('src/bidmanager'); -const adloader = require('src/adloader'); -const Adapter = require('src/adapter').default; -const adaptermanager = require('src/adaptermanager'); - -const IMPROVE_DIGITAL_BIDDER_CODE = 'improvedigital'; - -const ImproveDigitalAdapter = function () { - let baseAdapter = new Adapter(IMPROVE_DIGITAL_BIDDER_CODE); - baseAdapter.idClient = new ImproveDigitalAdServerJSClient('hb'); - - const LIB_VERSION = LIB_VERSION_GLOBAL; +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { userSync } from 'src/userSync'; + +const BIDDER_CODE = 'improvedigital'; + +export const spec = { + version: '4.0.0', + code: BIDDER_CODE, + aliases: ['id'], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid && bid.params && (bid.params.placementId || (bid.params.placementKey && bid.params.publisherId))); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (bidRequests) { + let normalizedBids = bidRequests.map((bidRequest) => { + return getNormalizedBidRequest(bidRequest); + }); - // Ad server needs to implement JSONP using this function as the callback - const CALLBACK_FUNCTION = '$$PREBID_GLOBAL$$' + '.improveDigitalResponse'; + let idClient = new ImproveDigitalAdServerJSClient('hb'); + let requestParameters = { + singleRequestMode: false, + httpRequestType: idClient.CONSTANTS.HTTP_REQUEST_TYPE.GET, + returnObjType: idClient.CONSTANTS.RETURN_OBJ_TYPE.PREBID, + libVersion: this.version + }; - baseAdapter.getNormalizedBidRequest = function(bid) { - let adUnitId = utils.getBidIdParameter('placementCode', bid) || null; - let placementId = utils.getBidIdParameter('placementId', bid.params) || null; - let publisherId = null; - let placementKey = null; + let requestObj = idClient.createRequest( + normalizedBids, // requestObject + requestParameters + ); - if (placementId === null) { - publisherId = utils.getBidIdParameter('publisherId', bid.params) || null; - placementKey = utils.getBidIdParameter('placementKey', bid.params) || null; + if (requestObj.errors && requestObj.errors.length > 0) { + utils.logError('ID WARNING 0x01'); } - let keyValues = utils.getBidIdParameter('keyValues', bid.params) || null; - let localSize = utils.getBidIdParameter('size', bid.params) || null; - let bidId = utils.getBidIdParameter('bidId', bid); - let normalizedBidRequest = {}; - if (placementId) { - normalizedBidRequest.placementId = placementId; - } else { - if (publisherId) { - normalizedBidRequest.publisherId = publisherId; - } - if (placementKey) { - normalizedBidRequest.placementKey = placementKey; + return requestObj.requests; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, request) { + const bids = []; + utils._each(serverResponse.body.bid, function (bidObject) { + if (!bidObject.price || bidObject.price === null || + bidObject.hasOwnProperty('errorCode') || + typeof bidObject.adm !== 'string') { + return; } - } - if (keyValues) { - normalizedBidRequest.keyValues = keyValues; - } - if (localSize && localSize.w && localSize.h) { - normalizedBidRequest.size = {}; - normalizedBidRequest.size.h = localSize.h; - normalizedBidRequest.size.w = localSize.w; - } - if (bidId) { - normalizedBidRequest.id = bidId; - } - if (adUnitId) { - normalizedBidRequest.adUnitId = adUnitId; - } - return normalizedBidRequest; - } - - let submitNoBidResponse = function(bidRequest) { - let bid = bidfactory.createBid(CONSTANTS.STATUS.NO_BID, bidRequest); - bid.bidderCode = IMPROVE_DIGITAL_BIDDER_CODE; - bidmanager.addBidResponse(bidRequest.placementCode, bid); - }; - - $$PREBID_GLOBAL$$.improveDigitalResponse = function(response) { - let bidRequests = utils.getBidderRequestAllAdUnits(IMPROVE_DIGITAL_BIDDER_CODE); - if (bidRequests && bidRequests.bids && bidRequests.bids.length > 0) { - utils._each(bidRequests.bids, function (bidRequest) { - let bidObjects = response.bid || []; - utils._each(bidObjects, function (bidObject) { - if (bidObject.id === bidRequest.bidId) { - if (!bidObject.price || bidObject.price === null) { - submitNoBidResponse(bidRequest); - return; - } - if (bidObject.errorCode && bidObject.errorCode !== 0) { - submitNoBidResponse(bidRequest); - return; - } - if (!bidObject.adm || bidObject.adm === null || typeof bidObject.adm !== 'string') { - submitNoBidResponse(bidRequest); - return; - } - - let bid = bidfactory.createBid(CONSTANTS.STATUS.GOOD, bidRequest); - - let syncString = ''; - let syncArray = (bidObject.sync && bidObject.sync.length > 0) ? bidObject.sync : []; + let bid = {}; + let nurl = ''; + if (bidObject.nurl && bidObject.nurl.length > 0) { + nurl = `<img src="${bidObject.nurl}" width="0" height="0" style="display:none">`; + } + bid.ad = `${nurl}<script>${bidObject.adm}</script>`; + bid.adId = bidObject.id; + bid.cpm = parseFloat(bidObject.price); + bid.creativeId = bidObject.crid; + bid.currency = bidObject.currency ? bidObject.currency.toUpperCase() : 'USD'; + if (utils.isNumber(bidObject.lid)) { + bid.dealId = bidObject.lid; + } else if (typeof bidObject.lid === 'object' && bidObject.lid['1']) { + bid.dealId = bidObject.lid['1']; + } + bid.height = bidObject.h; + bid.netRevenue = bidObject.isNet ? bidObject.isNet : false; + bid.requestId = bidObject.id; + bid.width = bidObject.w; - utils._each(syncArray, function (syncElement) { - let syncInd = syncElement.replace(/\//g, '\\\/'); - syncString = `${syncString}${(syncString === '') ? 'document.writeln(\"' : ''}<img src=\\\"${syncInd}\\\" style=\\\"display:none\\\"\/>`; - }); - syncString = `${syncString}${(syncString === '') ? '' : '\")'}`; + bids.push(bid); - let nurl = ''; - if (bidObject.nurl && bidObject.nurl.length > 0) { - nurl = `<img src=\"${bidObject.nurl}\" width=\"0\" height=\"0\" style=\"display:none\">`; - } - bid.ad = `${nurl}<script>${bidObject.adm}${syncString}</script>`; - bid.bidderCode = IMPROVE_DIGITAL_BIDDER_CODE; - bid.cpm = parseFloat(bidObject.price); - bid.width = bidObject.w; - bid.height = bidObject.h; - - bidmanager.addBidResponse(bidRequest.placementCode, bid); - } + // Register user sync URLs + if (utils.isArray(bidObject.sync)) { + utils._each(bidObject.sync, function (syncElement) { + userSync.registerSync('image', spec.code, syncElement); }); - }); - } - }; - - baseAdapter.callBids = function (params) { - // params will contain an array - let bidRequests = params.bids || []; - let loc = utils.getTopWindowLocation(); - let requestParameters = { - singleRequestMode: false, - httpRequestType: this.idClient.CONSTANTS.HTTP_REQUEST_TYPE.GET, - callback: CALLBACK_FUNCTION, - secure: (loc.protocol === 'https:') ? 1 : 0, - libVersion: this.LIB_VERSION - }; - - let normalizedBids = bidRequests.map((bidRequest) => { - let normalizedBidRequest = this.getNormalizedBidRequest(bidRequest); - if (bidRequest.params && bidRequest.params.singleRequest) { - requestParameters.singleRequestMode = true; } - return normalizedBidRequest; }); + return bids; + } +}; - let request = this.idClient.createRequest( - normalizedBids, // requestObject - requestParameters - ); +function getNormalizedBidRequest(bid) { + let adUnitId = utils.getBidIdParameter('adUnitCode', bid) || null; + let placementId = utils.getBidIdParameter('placementId', bid.params) || null; + let publisherId = null; + let placementKey = null; - if (request.errors && request.errors.length > 0) { - utils.logError('ID WARNING 0x01'); - } + if (placementId === null) { + publisherId = utils.getBidIdParameter('publisherId', bid.params) || null; + placementKey = utils.getBidIdParameter('placementKey', bid.params) || null; + } + let keyValues = utils.getBidIdParameter('keyValues', bid.params) || null; + let localSize = utils.getBidIdParameter('size', bid.params) || null; + let bidId = utils.getBidIdParameter('bidId', bid); + let transactionId = utils.getBidIdParameter('transactionId', bid); - if (request && request.requests && request.requests[0]) { - utils._each(request.requests, function (requestElement) { - if (requestElement.url) { - adloader.loadScript(requestElement.url, null); - } - }); + let normalizedBidRequest = {}; + if (placementId) { + normalizedBidRequest.placementId = placementId; + } else { + if (publisherId) { + normalizedBidRequest.publisherId = publisherId; + } + if (placementKey) { + normalizedBidRequest.placementKey = placementKey; } } - // Export the callBids function, so that prebid.js can execute this function - // when the page asks to send out bid requests. - return Object.assign(this, { - LIB_VERSION: LIB_VERSION, - idClient: baseAdapter.idClient, - getNormalizedBidRequest: baseAdapter.getNormalizedBidRequest, - callBids: baseAdapter.callBids - }); -}; - -ImproveDigitalAdapter.createNew = function () { - return new ImproveDigitalAdapter(); -}; - -adaptermanager.registerBidAdapter(new ImproveDigitalAdapter(), IMPROVE_DIGITAL_BIDDER_CODE); - -module.exports = ImproveDigitalAdapter; + if (keyValues) { + normalizedBidRequest.keyValues = keyValues; + } + if (localSize && localSize.w && localSize.h) { + normalizedBidRequest.size = {}; + normalizedBidRequest.size.h = localSize.h; + normalizedBidRequest.size.w = localSize.w; + } + if (bidId) { + normalizedBidRequest.id = bidId; + } + if (adUnitId) { + normalizedBidRequest.adUnitId = adUnitId; + } + if (transactionId) { + normalizedBidRequest.transactionId = transactionId; + } + return normalizedBidRequest; +} +registerBidder(spec); function ImproveDigitalAdServerJSClient(endPoint) { this.CONSTANTS = { @@ -184,13 +158,17 @@ function ImproveDigitalAdServerJSClient(endPoint) { }, AD_SERVER_BASE_URL: 'ad.360yield.com', END_POINT: endPoint || 'hb', - AD_SERVER_URL_PARAM: '?jsonp=', - CLIENT_VERSION: 'JS-4.0.2', + AD_SERVER_URL_PARAM: 'jsonp=', + CLIENT_VERSION: 'JS-4.2.0', MAX_URL_LENGTH: 2083, ERROR_CODES: { BAD_HTTP_REQUEST_TYPE_PARAM: 1, MISSING_PLACEMENT_PARAMS: 2, LIB_VERSION_MISSING: 3 + }, + RETURN_OBJ_TYPE: { + DEFAULT: 0, + PREBID: 1 } }; @@ -210,6 +188,8 @@ function ImproveDigitalAdServerJSClient(endPoint) { return this.getErrorReturn(this.CONSTANTS.ERROR_CODES.LIB_VERSION_MISSING); } + requestParameters.returnObjType = requestParameters.returnObjType || this.CONSTANTS.RETURN_OBJ_TYPE.DEFAULT; + let impressionObjects = []; let impressionObject; let counter; @@ -223,12 +203,19 @@ function ImproveDigitalAdServerJSClient(endPoint) { impressionObjects.push(impressionObject); } + let returnIdMappings = true; + if (requestParameters.returnObjType === this.CONSTANTS.RETURN_OBJ_TYPE.PREBID) { + returnIdMappings = false; + } + let returnObject = {}; - returnObject.idMappings = []; returnObject.requests = []; + if (returnIdMappings) { + returnObject.idMappings = []; + } let errors = null; - let baseUrl = `${(requestParameters.secure === 1 ? 'https' : 'http')}://${this.CONSTANTS.AD_SERVER_BASE_URL}/${this.CONSTANTS.END_POINT}${this.CONSTANTS.AD_SERVER_URL_PARAM}`; + let baseUrl = `${(requestParameters.secure === 1 ? 'https' : 'http')}://${this.CONSTANTS.AD_SERVER_BASE_URL}/${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; let bidRequestObject = { bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) @@ -243,49 +230,39 @@ function ImproveDigitalAdServerJSClient(endPoint) { adUnitId: impressionObject.adUnitId }); } else { - returnObject.idMappings.push({ - adUnitId: impressionObject.adUnitId, - id: impressionObject.impressionObject.id - }); - bidRequestObject.bid_request.imp = bidRequestObject.bid_request.imp || []; - - bidRequestObject.bid_request.imp.push(impressionObject.impressionObject); - let outputUri = encodeURIComponent(baseUrl + JSON.stringify(bidRequestObject)); - - if (!requestParameters.singleRequestMode) { - returnObject.requests.push({ - url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) + if (returnIdMappings) { + returnObject.idMappings.push({ + adUnitId: impressionObject.adUnitId, + id: impressionObject.impressionObject.id }); - bidRequestObject = { - bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) - }; } + bidRequestObject.bid_request.imp = bidRequestObject.bid_request.imp || []; + bidRequestObject.bid_request.imp.push(impressionObject.impressionObject); + let writeLongRequest = false; + const outputUri = baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)); if (outputUri.length > this.CONSTANTS.MAX_URL_LENGTH) { + writeLongRequest = true; if (bidRequestObject.bid_request.imp.length > 1) { + // Pop the current request and process it again in the next iteration bidRequestObject.bid_request.imp.pop(); - returnObject.requests.push({ - url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) - }); - bidRequestObject = { - bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) - }; - bidRequestObject.bid_request.imp = []; - bidRequestObject.bid_request.imp.push(impressionObject.impressionObject); - } else { - // We have a problem. Single request is too long for a URI + if (returnIdMappings) { + returnObject.idMappings.pop(); + } + counter--; } } + + if (writeLongRequest || + !requestParameters.singleRequestMode || + counter === impressionObjects.length - 1) { + returnObject.requests.push(this.formatRequest(requestParameters, bidRequestObject)); + bidRequestObject = { + bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) + }; + } } } - if (bidRequestObject.bid_request && - bidRequestObject.bid_request.imp && - bidRequestObject.bid_request.imp.length > 0) { - returnObject.requests = returnObject.requests || []; - returnObject.requests.push({ - url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) - }); - } if (errors) { returnObject.errors = errors; @@ -294,6 +271,24 @@ function ImproveDigitalAdServerJSClient(endPoint) { return returnObject; }; + this.formatRequest = function(requestParameters, bidRequestObject) { + switch (requestParameters.returnObjType) { + case this.CONSTANTS.RETURN_OBJ_TYPE.PREBID: + return { + method: 'GET', + url: `//${this.CONSTANTS.AD_SERVER_BASE_URL}/${this.CONSTANTS.END_POINT}`, + data: `${this.CONSTANTS.AD_SERVER_URL_PARAM}${JSON.stringify(bidRequestObject)}` + }; + default: + const baseUrl = `${(requestParameters.secure === 1 ? 'https' : 'http')}://` + + `${this.CONSTANTS.AD_SERVER_BASE_URL}/` + + `${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; + return { + url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) + } + } + }; + this.createBasicBidRequestObject = function(requestParameters, extraRequestParameters) { let impressionBidRequestObject = {}; if (requestParameters.requestId) { diff --git a/modules/improvedigitalBidAdapter.md b/modules/improvedigitalBidAdapter.md new file mode 100644 index 00000000000..3d91d4f82f2 --- /dev/null +++ b/modules/improvedigitalBidAdapter.md @@ -0,0 +1,47 @@ +# Overview + +**Module Name**: Improve Digital Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: hb@improvedigital.com + +# Description + +Module that connects to Improve Digital's demand sources + +# Test Parameters +``` + var adUnits = [{ + code: 'div-gpt-ad-1499748733608-0', + bids: [ + { + bidder: 'improvedigital', + params: { + placementId:1053688 + } + } + ] + }, { + code: 'div-gpt-ad-1499748833901-0', + bids: [{ + bidder: 'improvedigital', + params: { + placementId:1053689, + keyValues: { + testKey: ["testValue"] + } + } + }] + }, { + code: 'div-gpt-ad-1499748913322-0', + bids: [{ + bidder: 'improvedigital', + params: { + placementId:1053687, + size: { + w:300, + h:300 + } + } + }] + }]; +``` \ No newline at end of file diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 5b0a9d37d57..750eecc2a7d 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -1,599 +1,313 @@ -describe('improvedigital adapter tests', function () { - const expect = require('chai').expect; - const Adapter = require('modules/improvedigitalBidAdapter'); - const bidmanager = require('src/bidmanager'); - const adloader = require('src/adloader'); - const constants = require('src/constants.json'); - var bidfactory = require('src/bidfactory'); - var utils = require('src/utils.js'); - - var improveDigitalAdapter, - sandbox, - bidsRequestedOriginal; +import { expect } from 'chai'; +import { ImproveDigitalAdServerJSClient, spec } from 'modules/improvedigitalBidAdapter'; +import { userSync } from 'src/userSync'; + +describe('Improve Digital Adapter Tests', function () { + let idClient = new ImproveDigitalAdServerJSClient('hb'); + + const METHOD = 'GET'; + const URL = '//ad.360yield.com/hb'; + const PARAM_PREFIX = 'jsonp='; const simpleBidRequest = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - placementId: 1012544 - } - } - ] + bidder: 'improvedigital', + params: { + placementId: 1053688 + }, + adUnitCode: 'div-gpt-ad-1499748733608-0', + transactionId: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', + bidId: '33e9500b21129f', + bidderRequestId: '2772c1e566670b', + auctionId: '192721e36a0239' }; const simpleSmartTagBidRequest = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - publisherId: 1032, - placementKey: 'data_team_test_hb_smoke_test' - } - } - ] + bidder: 'improvedigital', + bidId: '1a2b3c', + placementCode: 'placement1', + params: { + publisherId: 1032, + placementKey: 'data_team_test_hb_smoke_test' + } }; - const keyValueBidRequest = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - placementId: 1012546, - keyValues: { - hbkv: ['01'] - } - } - } - ] - }; + describe('isBidRequestValid', () => { + it('should return false when no bid', () => { + expect(spec.isBidRequestValid()).to.equal(false); + }); - const sizeBidRequest = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - placementId: 1012545, - size: { - w: 800, - h: 600 - } - } - } - ] - }; + it('should return false when no bid.params', () => { + let bid = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); - const twoAdSlots = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - placementId: 1012544, - } - }, - { - bidId: '4d5e6f', - placementCode: 'placement2', - params: { - placementId: 1012545, - size: { - w: 800, - h: 600 - } - } - } - ] - }; + it('should return false when both placementId and placementKey + publisherId are missing', () => { + let bid = { 'params': {} }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); - const threeAdSlots = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', + it('should return false when only one of placementKey and publisherId is present', () => { + let bid = { params: { - placementId: 1012544, + publisherId: 1234 } - }, - { - bidId: '4d5e6f', - placementCode: 'placement2', + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid = { params: { - placementId: 1012545, - size: { - w: 800, - h: 600 - } + placementKey: 'xyz' } - }, - { - bidId: '7g8h9i', - placementCode: 'placement3', - params: { - placementId: 1012546, - keyValues: { - hbkv: ['01'] - } - } - } - ] - }; + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); - const badRequest1 = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - unknownId: 123456 - } - } - ] - }; + it('should return true when placementId is passed', () => { + let bid = { 'params': {} }; + expect(spec.isBidRequestValid(simpleBidRequest)).to.equal(true); + }); - const twoAdSlotsSingleRequest = { - bidderCode: 'improvedigital', - bids: [ - { - bidId: '1a2b3c', - placementCode: 'placement1', - params: { - singleRequest: true, - placementId: 1012544, - } - }, - { - bidId: '4d5e6f', - placementCode: 'placement2', - params: { - placementId: 1012545, - size: { - w: 800, - h: 600 - } + it('should return true when both placementKey and publisherId are passed', () => { + let bid = { 'params': {} }; + expect(spec.isBidRequestValid(simpleSmartTagBidRequest)).to.equal(true); + }); + }); + + describe('buildRequests', () => { + it('should make a well-formed request objects', () => { + const requests = spec.buildRequests([simpleBidRequest]); + expect(requests).to.be.an('array'); + expect(requests.length).to.equal(1); + + const request = requests[0]; + expect(request.method).to.equal(METHOD); + expect(request.url).to.equal(URL); + expect(request.data.substring(0, PARAM_PREFIX.length)).to.equal(PARAM_PREFIX); + + const params = JSON.parse(request.data.substring(PARAM_PREFIX.length)); + expect(params.bid_request).to.be.an('object'); + expect(params.bid_request.id).to.be.a('string'); + expect(params.bid_request.version).to.equal(`${spec.version}-${idClient.CONSTANTS.CLIENT_VERSION}`); + expect(params.bid_request.imp).to.deep.equal([ + { + id: '33e9500b21129f', + pid: 1053688, + tid: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', + banner: {} } - } - ] - }; + ]); + }); - const simpleResponse = { - id: '701903620', - site_id: 191642, - bid: [ - { - price: 1.85185185185185, - lid: 268514, - advid: '5279', - id: '1a2b3c', - sync: [ - 'http://link', - 'http://link2', - 'http://link3' - ], - nurl: 'http://nurl', - h: 300, - pid: 1053687, - crid: '422030', - w: 300, - cid: '99005', - adm: 'document.writeln(\"<a href=\\\"http:\\/\\/creativelink\\/\");' - } - ] - }; + it('should set placementKey and publisherId for smart tags', () => { + const requests = spec.buildRequests([simpleSmartTagBidRequest]); + const params = JSON.parse(requests[0].data.substring(PARAM_PREFIX.length)); + expect(params.bid_request.imp[0].pubid).to.equal(1032); + expect(params.bid_request.imp[0].pkey).to.equal('data_team_test_hb_smoke_test'); + }); - const zeroPriceResponse = { - id: '701903620', - site_id: 191642, - bid: [ - { - price: 0, - lid: 268514, - advid: '5279', - id: '1a2b3c', - sync: [ - 'http://link', - 'http://link2', - 'http://link3' - ], - nurl: 'http://nurl', - h: 300, - pid: 1053687, - crid: '422030', - w: 300, - cid: '99005', - adm: 'document.writeln(\"<a href=\\\"http:\\/\\/creativelink\\/\");' - } - ], - debug: '' - }; + it('should add keyValues', () => { + let bidRequest = Object.assign({}, simpleBidRequest); + const keyValues = { + testKey: [ + 'testValue' + ] + }; + bidRequest.params.keyValues = keyValues; + const request = spec.buildRequests([bidRequest])[0]; + const params = JSON.parse(request.data.substring(PARAM_PREFIX.length)); + expect(params.bid_request.imp[0].kvw).to.deep.equal(keyValues); + }); - const multipleResponse = { - id: '701903620', - site_id: 191642, - bid: [ - { - price: 1.85185185185185, - lid: 268514, - advid: '5279', - id: '1a2b3c', - sync: [ - 'http://link', - 'http://link2', - 'http://link3' - ], - nurl: 'http://nurl', - h: 300, - pid: 1053687, - crid: '422030', - w: 300, - cid: '99005', - adm: 'document.writeln(\"<a href=\\\"http:\\/\\/creativelink\\/\");' - }, - { - price: 1.44563918757467, - lid: 268514, - advid: '5279', - id: '4d5e6f', - sync: [ - 'http://link4', - 'http://link5' - ], - nurl: 'http://nurl2', - h: 600, - pid: 1053687, - crid: '422030', + it('should add size', () => { + let bidRequest = Object.assign({}, simpleBidRequest); + const size = { w: 800, - cid: '99005', - adm: 'document.writeln(\"<a href=\\\"http:\\/\\/creativelink2\\/\");' - } - ], - debug: '' - }; + h: 600 + }; + bidRequest.params.size = size; + const request = spec.buildRequests([bidRequest])[0]; + const params = JSON.parse(request.data.substring(PARAM_PREFIX.length)); + expect(params.bid_request.imp[0].banner).to.deep.equal(size); + }); - const multipleResponseWithOneNoBid = { - id: '701903620', - site_id: 191642, - bid: [ - { - price: 1.85185185185185, - lid: 268514, - advid: '5279', - id: '1a2b3c', - sync: [ - 'http://link', - 'http://link2', - 'http://link3' - ], - nurl: 'http://nurl', - h: 300, - pid: 1053687, - crid: '422030', - w: 300, - cid: '99005', - adm: 'document.writeln(\"<a href=\\\"http:\\/\\/creativelink\\/\");' - }, - { - price: 0, - lid: 268514, - advid: '5279', - id: '4d5e6f', - sync: [ - 'http://link4', - 'http://link5' + it('should return 2 requests', () => { + const requests = spec.buildRequests([ + simpleBidRequest, + simpleSmartTagBidRequest + ]); + expect(requests).to.be.an('array'); + expect(requests.length).to.equal(2); + }); + }); + + describe('interpretResponse', () => { + const serverResponse = { + 'body': { + 'id': '687a06c541d8d1', + 'site_id': 191642, + 'bid': [ + { + 'isNet': false, + 'id': '33e9500b21129f', + 'advid': '5279', + 'price': 1.45888594164456, + 'nurl': 'http://ad.360yield.com/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=', + 'h': 290, + 'pid': 1053688, + 'sync': [ + 'http://link1', + 'http://link2' + ], + 'crid': '422031', + 'w': 600, + 'cid': '99006', + 'adm': 'document.writeln(\"<a href=\\\"http:\\/\\/ad.360yield.com\\/click\\/wVmhKEKFeJufyP3hFfp7fv95ynoKe7vnG9V-j8EyAzklSoKRkownAclw4Zzcw-OcbJMg2KfjNiO8GoO9WP1jbNM8Q5GtmClbG9hZPBS4v6oBBiDi50AjRqHQsDAoBOJrIJtVyCfrnAIxvbysozCpLt20ov6jz2JPi6fe.D55HNeDLDyiLNgxVPa3y9jJZf65JBirCjOoZ-1Mj1BLB.57VdMaEhpGjjl5HnPgw0Pv7Hm1BO7PB9nCXJ9IwOH3IrKo.Wyy1iKDk6zeGwGOkQHSOMuQnCHyD35x6bhDQrpl5H6fTRTR8D2m5.-Zjh3fs8SKlo0i25EjKPw65iF.tvgcnq01U08OIh86EeSciamJgV0hNsk20TcTubfsoPN4are4nQ0y2gB-lz9tf3AjqHpSz5NoJWrpWtnrBHbjm.dS1XUQB1tzcLpIkA34nDe2eNxRZbZkZNSSs.Y8jQemfbjuLpttcemHqidFZo3xp37eSfUImw.HbyFdnK-wxFDYudgsIDxGJWI=\\/\\/http%3A%2F%2Fwww.improvedigital.com\\\" target=\\\"_blank\\\"><img style=\\\"border: 0;\\\" border=\\\"0\\\" width=\\\"600\\\" height=\\\"290\\\" src=\\\"http:\\/\\/creative.360yield.com\\/file\\/221728\\/ImproveDigital600x290.jpg\\\" alt=\\\"\\\"\\/><\\/a>\");document.writeln(\"<improvedigital_ad_output_information tp_id=\\\"\\\" buyer_id=\\\"0\\\" rtb_advertiser=\\\"\\\" campaign_id=\\\"99006\\\" line_item_id=\\\"268515\\\" creative_id=\\\"422031\\\" crid=\\\"0\\\" placement_id=\\\"1053688\\\"><\\/improvedigital_ad_output_information>\");' + } ], - nurl: 'http://nurl2', - h: 600, - pid: 1053687, - crid: '422030', - w: 800, - cid: '99005', - adm: 'document.writeln(\"<a href=\\\"http:\\/\\/creativelink2\\/\");' + 'debug': '' } - ], - debug: '' - }; + }; - const multipleInvalidResponses = { - id: '701903620', - site_id: 191642, - bid: [ - { - id: '1a2b3c', - adm: {}, - errorCode: 1, - price: 1.00 - }, - { - price: 1.74747474747447, - lid: 268514, - advid: '5279', - id: '4d5e6f', - sync: [ - 'http://link4', - 'http://link5' + const serverResponseTwoBids = { + 'body': { + 'id': '687a06c541d8d1', + 'site_id': 191642, + 'bid': [ + serverResponse.body.bid[0], + { + 'isNet': true, + 'id': '1234', + 'advid': '5280', + 'price': 1.23, + 'nurl': 'http://link/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=', + 'h': 400, + 'pid': 1053688, + 'sync': [ + 'http://link3' + ], + 'crid': '422033', + 'w': 700, + 'cid': '99006', + 'adm': 'document.writeln(\"<a href=\\\"http:\\/\\/ad.360yield.com\\/click\\/wVmhKEKFeJufyP3hFfp7fv95ynoKe7vnG9V-j8EyAzklSoKRkownAclw4Zzcw-OcbJMg2KfjNiO8GoO9WP1jbNM8Q5GtmClbG9hZPBS4v6oBBiDi50AjRqHQsDAoBOJrIJtVyCfrnAIxvbysozCpLt20ov6jz2JPi6fe.D55HNeDLDyiLNgxVPa3y9jJZf65JBirCjOoZ-1Mj1BLB.57VdMaEhpGjjl5HnPgw0Pv7Hm1BO7PB9nCXJ9IwOH3IrKo.Wyy1iKDk6zeGwGOkQHSOMuQnCHyD35x6bhDQrpl5H6fTRTR8D2m5.-Zjh3fs8SKlo0i25EjKPw65iF.tvgcnq01U08OIh86EeSciamJgV0hNsk20TcTubfsoPN4are4nQ0y2gB-lz9tf3AjqHpSz5NoJWrpWtnrBHbjm.dS1XUQB1tzcLpIkA34nDe2eNxRZbZkZNSSs.Y8jQemfbjuLpttcemHqidFZo3xp37eSfUImw.HbyFdnK-wxFDYudgsIDxGJWI=\\/\\/http%3A%2F%2Fwww.improvedigital.com\\\" target=\\\"_blank\\\"><img style=\\\"border: 0;\\\" border=\\\"0\\\" width=\\\"600\\\" height=\\\"290\\\" src=\\\"http:\\/\\/creative.360yield.com\\/file\\/221728\\/ImproveDigital600x290.jpg\\\" alt=\\\"\\\"\\/><\\/a>\");document.writeln(\"<improvedigital_ad_output_information tp_id=\\\"\\\" buyer_id=\\\"0\\\" rtb_advertiser=\\\"\\\" campaign_id=\\\"99006\\\" line_item_id=\\\"268515\\\" creative_id=\\\"422031\\\" crid=\\\"0\\\" placement_id=\\\"1053688\\\"><\\/improvedigital_ad_output_information>\");' + } ], - nurl: 'http://nurl2', - h: 600, - pid: 1053687, - crid: '422030', - w: 800, - cid: '99005' + 'debug': '' } - ], - debug: '' - }; + }; - const simpleResponseNoSync = { - id: '701903620', - site_id: 191642, - bid: [ + let expectedBid = [ { - price: 1.85185185185185, - lid: 268514, - advid: '5279', - id: '1a2b3c', - sync: [], - nurl: 'http://nurl', - h: 300, - pid: 1053687, - crid: '422030', - w: 300, - cid: '99005', - adm: 'document.writeln(\"<a href=\\\"http:\\/\\/creativelink\\/\");' + 'ad': '<img src=\"http://ad.360yield.com/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=\" width=\"0\" height=\"0\" style=\"display:none\"><script>document.writeln(\"<a href=\\\"http:\\/\\/ad.360yield.com\\/click\\/wVmhKEKFeJufyP3hFfp7fv95ynoKe7vnG9V-j8EyAzklSoKRkownAclw4Zzcw-OcbJMg2KfjNiO8GoO9WP1jbNM8Q5GtmClbG9hZPBS4v6oBBiDi50AjRqHQsDAoBOJrIJtVyCfrnAIxvbysozCpLt20ov6jz2JPi6fe.D55HNeDLDyiLNgxVPa3y9jJZf65JBirCjOoZ-1Mj1BLB.57VdMaEhpGjjl5HnPgw0Pv7Hm1BO7PB9nCXJ9IwOH3IrKo.Wyy1iKDk6zeGwGOkQHSOMuQnCHyD35x6bhDQrpl5H6fTRTR8D2m5.-Zjh3fs8SKlo0i25EjKPw65iF.tvgcnq01U08OIh86EeSciamJgV0hNsk20TcTubfsoPN4are4nQ0y2gB-lz9tf3AjqHpSz5NoJWrpWtnrBHbjm.dS1XUQB1tzcLpIkA34nDe2eNxRZbZkZNSSs.Y8jQemfbjuLpttcemHqidFZo3xp37eSfUImw.HbyFdnK-wxFDYudgsIDxGJWI=\\/\\/http%3A%2F%2Fwww.improvedigital.com\\\" target=\\\"_blank\\\"><img style=\\\"border: 0;\\\" border=\\\"0\\\" width=\\\"600\\\" height=\\\"290\\\" src=\\\"http:\\/\\/creative.360yield.com\\/file\\/221728\\/ImproveDigital600x290.jpg\\\" alt=\\\"\\\"\\/><\\/a>\");document.writeln(\"<improvedigital_ad_output_information tp_id=\\\"\\\" buyer_id=\\\"0\\\" rtb_advertiser=\\\"\\\" campaign_id=\\\"99006\\\" line_item_id=\\\"268515\\\" creative_id=\\\"422031\\\" crid=\\\"0\\\" placement_id=\\\"1053688\\\"><\\/improvedigital_ad_output_information>\");</script>', + 'adId': '33e9500b21129f', + 'creativeId': '422031', + 'cpm': 1.45888594164456, + 'currency': 'USD', + 'height': 290, + 'netRevenue': false, + 'requestId': '33e9500b21129f', + 'width': 600 } - ] - }; + ]; - var randomNumber = 9876543210; - beforeEach(() => { - improveDigitalAdapter = new Adapter(); - sandbox = sinon.sandbox.create(); - sandbox.stub( - utils, - 'getUniqueIdentifierStr', - function() { - var retValue = randomNumber.toString(); - randomNumber++; - return retValue; + let expectedTwoBids = [ + expectedBid[0], + { + 'ad': '<img src=\"http://link/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=\" width=\"0\" height=\"0\" style=\"display:none\"><script>document.writeln(\"<a href=\\\"http:\\/\\/ad.360yield.com\\/click\\/wVmhKEKFeJufyP3hFfp7fv95ynoKe7vnG9V-j8EyAzklSoKRkownAclw4Zzcw-OcbJMg2KfjNiO8GoO9WP1jbNM8Q5GtmClbG9hZPBS4v6oBBiDi50AjRqHQsDAoBOJrIJtVyCfrnAIxvbysozCpLt20ov6jz2JPi6fe.D55HNeDLDyiLNgxVPa3y9jJZf65JBirCjOoZ-1Mj1BLB.57VdMaEhpGjjl5HnPgw0Pv7Hm1BO7PB9nCXJ9IwOH3IrKo.Wyy1iKDk6zeGwGOkQHSOMuQnCHyD35x6bhDQrpl5H6fTRTR8D2m5.-Zjh3fs8SKlo0i25EjKPw65iF.tvgcnq01U08OIh86EeSciamJgV0hNsk20TcTubfsoPN4are4nQ0y2gB-lz9tf3AjqHpSz5NoJWrpWtnrBHbjm.dS1XUQB1tzcLpIkA34nDe2eNxRZbZkZNSSs.Y8jQemfbjuLpttcemHqidFZo3xp37eSfUImw.HbyFdnK-wxFDYudgsIDxGJWI=\\/\\/http%3A%2F%2Fwww.improvedigital.com\\\" target=\\\"_blank\\\"><img style=\\\"border: 0;\\\" border=\\\"0\\\" width=\\\"600\\\" height=\\\"290\\\" src=\\\"http:\\/\\/creative.360yield.com\\/file\\/221728\\/ImproveDigital600x290.jpg\\\" alt=\\\"\\\"\\/><\\/a>\");document.writeln(\"<improvedigital_ad_output_information tp_id=\\\"\\\" buyer_id=\\\"0\\\" rtb_advertiser=\\\"\\\" campaign_id=\\\"99006\\\" line_item_id=\\\"268515\\\" creative_id=\\\"422031\\\" crid=\\\"0\\\" placement_id=\\\"1053688\\\"><\\/improvedigital_ad_output_information>\");</script>', + 'adId': '1234', + 'creativeId': '422033', + 'cpm': 1.23, + 'currency': 'USD', + 'height': 400, + 'netRevenue': true, + 'requestId': '1234', + 'width': 700 } - ); - bidsRequestedOriginal = $$PREBID_GLOBAL$$._bidsRequested; - $$PREBID_GLOBAL$$._bidsRequested = []; - }); + ]; - afterEach(() => { - sandbox.restore(); - $$PREBID_GLOBAL$$._bidsRequested = bidsRequestedOriginal; - }); - - describe('callBids simpleBidRequest', () => { - beforeEach(() => { - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(simpleBidRequest); + it('should return a well-formed bid', () => { + const bids = spec.interpretResponse(serverResponse); + expect(bids).to.deep.equal(expectedBid); }); - it('should call loadScript with correct parameters', () => { - sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - }); - }); - describe('callBids simpleSmartTagBidRequest', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(simpleSmartTagBidRequest); + it('should return two bids', () => { + const bids = spec.interpretResponse(serverResponseTwoBids); + expect(bids).to.deep.equal(expectedTwoBids); }); - it('should call loadScript with correct parameters', () => { - sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pubid%22%3A1032%2C%22pkey%22%3A%22data_team_test_hb_smoke_test%22%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - }); - }); - describe('callBids keyValueBidRequest', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(keyValueBidRequest); - }); - it('should call loadScript with correct parameters', () => { - sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + it('should register user syncs', () => { + const registerSyncSpy = sinon.spy(userSync, 'registerSync'); + const bids = spec.interpretResponse(serverResponse); + expect(registerSyncSpy.withArgs('image', 'improvedigital', 'http://link1').calledOnce).to.equal(true); + expect(registerSyncSpy.withArgs('image', 'improvedigital', 'http://link2').calledOnce).to.equal(true); }); - }); - describe('callBids sizeBidRequest', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(sizeBidRequest); - }); - it('should call loadScript with correct parameters', () => { - sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); - }); - }); + it('should set dealId correctly', () => { + let response = JSON.parse(JSON.stringify(serverResponse)); + let bids; - describe('callBids twoAdSlots', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(twoAdSlots); - }); - it('should call loadScript twice with correct parameters', () => { - sinon.assert.calledTwice(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); - }); - }); + response.body.bid[0].lid = 'xyz'; + bids = spec.interpretResponse(response); + expect(bids[0].dealId).to.not.exist; - describe('callBids threeAdSlots', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(threeAdSlots); - }); - it('should call loadScript thrice with correct parameters', () => { - sinon.assert.calledThrice(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543212%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%227g8h9i%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - }); - }); + response.body.bid[0].lid = 268515; + bids = spec.interpretResponse(response); + expect(bids[0].dealId).to.equal(268515); - describe('callBids bad request 1', () => { - beforeEach(() => { - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(badRequest1); + response.body.bid[0].lid = { + 1: 268515 + }; + bids = spec.interpretResponse(response); + expect(bids[0].dealId).to.equal(268515); }); - it('should not call loadScript', () => { - sinon.assert.notCalled(adloader.loadScript); - }); - }); - describe('callBids twoAdSlotsSingleRequest', () => { - beforeEach(() => { - randomNumber = 9876543210; - sandbox.stub( - adloader, - 'loadScript' - ); - improveDigitalAdapter.callBids(twoAdSlotsSingleRequest); + it('should set currency', () => { + let response = JSON.parse(JSON.stringify(serverResponse)); + response.body.bid[0].currency = 'eur'; + const bids = spec.interpretResponse(response); + expect(bids[0].currency).to.equal('EUR'); }); - it('should call loadScript twice with correct parameters', () => { - sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%2C%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); - }); - }); - describe('improveDigitalResponse no response', () => { - beforeEach(() => { - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(simpleBidRequest); - improveDigitalAdapter.callBids(simpleBidRequest); - $$PREBID_GLOBAL$$.improveDigitalResponse([]); - }); - it('should not call bidmanager.addBidResponse', () => { - sinon.assert.notCalled(bidmanager.addBidResponse); - }); - }); - - describe('improveDigitalResponse simpleResponse', () => { - beforeEach(() => { - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(simpleBidRequest); - improveDigitalAdapter.callBids(simpleBidRequest); - $$PREBID_GLOBAL$$.improveDigitalResponse(simpleResponse); - }); - it('should call bidmanager.addBidResponse once with correct parameters', () => { - sinon.assert.calledOnce(bidmanager.addBidResponse); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement1', sinon.match({bidderCode: 'improvedigital', width: 300, height: 300, statusMessage: 'Bid available', ad: '<img src=\"http://nurl\" width=\"0\" height=\"0\" style=\"display:none\"><script>document.writeln(\"<a href=\\\"http:\\/\\/creativelink\\/\");document.writeln(\"<img src=\\\"http:\\/\\/link\\\" style=\\\"display:none\\\"/><img src=\\\"http:\\/\\/link2\\\" style=\\\"display:none\\\"/><img src=\\\"http:\\/\\/link3\\\" style=\\\"display:none\\\"/>\")</script>', cpm: 1.85185185185185, adId: '1a2b3c'})); - }); - }); + it('should return empty array for bad response or no price', () => { + let response = JSON.parse(JSON.stringify(serverResponse)); + let bids; - describe('improveDigitalResponse zero bid', () => { - beforeEach(() => { - randomNumber = 1111111111; - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(simpleBidRequest); - improveDigitalAdapter.callBids(simpleBidRequest); - $$PREBID_GLOBAL$$.improveDigitalResponse(zeroPriceResponse); - }); - it('should call bidmanager.addBidResponse once with correct parameters', () => { - sinon.assert.calledOnce(bidmanager.addBidResponse); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement1', sinon.match({bidderCode: 'improvedigital', width: 0, height: 0, statusMessage: 'Bid returned empty or error response', adId: '1a2b3c'})); - }); - }); + // Price missing or 0 + response.body.bid[0].price = 0; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); + delete response.body.bid[0].price; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); + response.body.bid[0].price = null; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); - describe('improveDigitalResponse multipleResponseWithOneNoBid', () => { - beforeEach(() => { - randomNumber = 1111111111; - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(twoAdSlots); - improveDigitalAdapter.callBids(twoAdSlots); - $$PREBID_GLOBAL$$.improveDigitalResponse(multipleResponseWithOneNoBid); - }); - it('should call bidmanager.addBidResponse once with correct parameters', () => { - sinon.assert.calledTwice(bidmanager.addBidResponse); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement1', sinon.match({bidderCode: 'improvedigital', width: 300, height: 300, adId: '1a2b3c', statusMessage: 'Bid available', ad: '<img src=\"http://nurl\" width=\"0\" height=\"0\" style=\"display:none\"><script>document.writeln(\"<a href=\\\"http:\\/\\/creativelink\\/\");document.writeln(\"<img src=\\\"http:\\/\\/link\\\" style=\\\"display:none\\\"/><img src=\\\"http:\\/\\/link2\\\" style=\\\"display:none\\\"/><img src=\\\"http:\\/\\/link3\\\" style=\\\"display:none\\\"/>\")</script>', cpm: 1.85185185185185})); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement2', sinon.match({bidderCode: 'improvedigital', width: 0, height: 0, adId: '4d5e6f', statusMessage: 'Bid returned empty or error response'})); - }); - }); + // errorCode present + response = JSON.parse(JSON.stringify(serverResponse)); + response.body.bid[0].errorCode = undefined; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); - describe('improveDigitalResponse multipleInvalidResponses', () => { - beforeEach(() => { - randomNumber = 1111111111; - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(twoAdSlots); - improveDigitalAdapter.callBids(twoAdSlots); - $$PREBID_GLOBAL$$.improveDigitalResponse(multipleInvalidResponses); - }); - it('should call bidmanager.addBidResponse twice both with invalid', () => { - sinon.assert.calledTwice(bidmanager.addBidResponse); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement1', sinon.match({bidderCode: 'improvedigital', width: 0, height: 0, adId: '1a2b3c', statusMessage: 'Bid returned empty or error response'})); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement2', sinon.match({bidderCode: 'improvedigital', width: 0, height: 0, adId: '4d5e6f', statusMessage: 'Bid returned empty or error response'})); + // Adm missing or bad + response = JSON.parse(JSON.stringify(serverResponse)); + delete response.body.bid[0].adm; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); + response.body.bid[0].adm = null; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); + response.body.bid[0].adm = 1234; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); + response.body.bid[0].adm = {}; + bids = spec.interpretResponse(response); + expect(bids).to.deep.equal([]); }); - }); - describe('improveDigitalResponse simpleResponseNoSync', () => { - beforeEach(() => { - sandbox.stub( - bidmanager, - 'addBidResponse' - ); - $$PREBID_GLOBAL$$._bidsRequested.push(simpleBidRequest); - improveDigitalAdapter.callBids(simpleBidRequest); - $$PREBID_GLOBAL$$.improveDigitalResponse(simpleResponseNoSync); - }); - it('should call bidmanager.addBidResponse once with correct parameters', () => { - sinon.assert.calledOnce(bidmanager.addBidResponse); - sinon.assert.calledWith(bidmanager.addBidResponse, 'placement1', sinon.match({bidderCode: 'improvedigital', width: 300, height: 300, statusMessage: 'Bid available', ad: '<img src=\"http://nurl\" width=\"0\" height=\"0\" style=\"display:none\"><script>document.writeln(\"<a href=\\\"http:\\/\\/creativelink\\/\");</script>', cpm: 1.85185185185185, adId: '1a2b3c'})); + it('should set netRevenue', () => { + let response = JSON.parse(JSON.stringify(serverResponse)); + response.body.bid[0].isNet = true; + const bids = spec.interpretResponse(response); + expect(bids[0].netRevenue).to.equal(true); }); }); }); From 6a0c5cabdbbbe1a45234c84fad33fcb794b13a92 Mon Sep 17 00:00:00 2001 From: PWyrembak <paul@trustx.org> Date: Thu, 26 Oct 2017 19:02:59 +0300 Subject: [PATCH 22/44] Migrating TrustX adapter to 1.0 (#1709) * Add trustx adapter and tests for it * update integration example * Update trustx adapter * Post-review fixes of Trustx adapter * Code improvement for trustx adapter: changed default price type from gross to net * Update TrustX adapter to support the 1.0 version * Make requested changes for TrustX adapter * Updated markdown file for TrustX adapter * Fix TrustX adapter and spec file --- modules/trustxBidAdapter.js | 287 ++++++------- modules/trustxBidAdapter.md | 40 ++ test/spec/modules/trustxBidAdapter_spec.js | 469 ++++++++++++--------- 3 files changed, 437 insertions(+), 359 deletions(-) create mode 100755 modules/trustxBidAdapter.md diff --git a/modules/trustxBidAdapter.js b/modules/trustxBidAdapter.js index 13f893a841d..f16b8b96ec8 100644 --- a/modules/trustxBidAdapter.js +++ b/modules/trustxBidAdapter.js @@ -1,165 +1,148 @@ -const utils = require('src/utils.js'); -const bidfactory = require('src/bidfactory.js'); -const bidmanager = require('src/bidmanager.js'); -const adloader = require('src/adloader'); -const adaptermanager = require('src/adaptermanager'); -const CONSTANTS = require('src/constants.json'); - -var TrustxAdapter = function TrustxAdapter() { - const bidderCode = 'trustx'; - const reqHost = '//sofia.trustx.org'; - const reqPath = '/hb?'; - const LOG_ERROR_MESS = { - noAuid: 'Bid from response has no auid parameter - ', - noAdm: 'Bid from response has no adm parameter - ', - noBid: 'Array of bid objects is empty', - noPlacementCode: 'Can\'t find placementCode for bid with auid - ', - havePCodeFor: ', placementCode is available only for the following uids - ', - emptyUids: 'Uids should be not empty', - emptySeatbid: 'Seatbid array from response has empty item', - emptyResponse: 'Response is empty', - hasEmptySeatbidArray: 'Response has empty seatbid array', - hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' - }; +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; +const BIDDER_CODE = 'trustx'; +const ENDPOINT_URL = '//sofia.trustx.org/hb'; +const TIME_TO_LIVE = 360; +const ADAPTER_SYNC_URL = '//sofia.trustx.org/push_sync'; +const LOG_ERROR_MESS = { + noAuid: 'Bid from response has no auid parameter - ', + noAdm: 'Bid from response has no adm parameter - ', + noBid: 'Array of bid objects is empty', + noPlacementCode: 'Can\'t find in requested bids the bid with auid - ', + emptyUids: 'Uids should be not empty', + emptySeatbid: 'Seatbid array from response has empty item', + emptyResponse: 'Response is empty', + hasEmptySeatbidArray: 'Response has empty seatbid array', + hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' +}; +export const spec = { + code: BIDDER_CODE, + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!bid.params.uid; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests) { + const auids = []; + const bidsMap = {}; + const bids = validBidRequests || []; + let priceType = 'net'; + + bids.forEach(bid => { + if (bid.params.priceType === 'gross') { + priceType = 'gross'; + } + if (!bidsMap[bid.params.uid]) { + bidsMap[bid.params.uid] = [bid]; + auids.push(bid.params.uid); + } else { + bidsMap[bid.params.uid].push(bid); + } + }); - function _makeHandler(auids, placementMap) { - var cbName = bidderCode + '_callback_wrapper_' + auids.join('_'); - $$PREBID_GLOBAL$$[cbName] = function(resp) { - delete $$PREBID_GLOBAL$$[cbName]; - _responseProcessing(resp, auids, placementMap); + const payload = { + u: utils.getTopWindowUrl(), + pt: priceType, + auids: auids.join(','), }; - return '$$PREBID_GLOBAL$$.' + cbName; - } - function _sendRequest(auids, placementMap) { - var query = []; - var path = reqPath; - query.push('u=' + encodeURIComponent(location.href)); - query.push('auids=' + encodeURIComponent(auids.join(','))); - query.push('cb=' + _makeHandler(auids, placementMap)); - query.push('pt=' + (window.globalPrebidTrustxPriceType === 'gross' ? 'gross' : 'net')); - - adloader.loadScript(reqHost + path + query.join('&')); - } - - function _callBids(params) { - var auids = []; - var placementMap = {}; - var hasBid; - var bid; - var bids = params.bids || []; - for (var i = 0; i < bids.length; i++) { - bid = bids[i]; - if (bid && bid.bidder === bidderCode && bid.placementCode) { - hasBid = true; - if (bid.params && bid.params.uid) { - if (!placementMap[bid.params.uid]) { - placementMap[bid.params.uid] = [bid.placementCode]; - auids.push(bid.params.uid); - } else { - placementMap[bid.params.uid].push(bid.placementCode); - } - } - } + return { + method: 'GET', + url: ENDPOINT_URL, + data: payload, + bidsMap: bidsMap, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @param {*} bidRequest + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + serverResponse = serverResponse && serverResponse.body + const bidResponses = []; + const bidsMap = bidRequest.bidsMap; + const priceType = bidRequest.data.pt; + + let errorMessage; + + if (!serverResponse) errorMessage = LOG_ERROR_MESS.emptyResponse; + else if (serverResponse.seatbid && !serverResponse.seatbid.length) { + errorMessage = LOG_ERROR_MESS.hasEmptySeatbidArray; } - if (auids.length) { - _sendRequest(auids, placementMap); - } else if (hasBid) { - utils.logError(LOG_ERROR_MESS.emptyUids); + if (!errorMessage && serverResponse.seatbid) { + serverResponse.seatbid.forEach(respItem => { + _addBidResponse(_getBidFromResponse(respItem), bidsMap, priceType, bidResponses); + }); } - } - - function _getBidFromResponse(resp) { - if (!resp) { - utils.logError(LOG_ERROR_MESS.emptySeatbid); - } else if (!resp.bid) { - utils.logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(resp)); - } else if (!resp.bid[0]) { - utils.logError(LOG_ERROR_MESS.noBid); + if (errorMessage) utils.logError(errorMessage); + return bidResponses; + }, + getUserSyncs: function(syncOptions) { + if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: ADAPTER_SYNC_URL + }]; } - return resp && resp.bid && resp.bid[0]; } - - function _forEachPlacement(error, bid, placementCode) { - var bidObject; - if (error) { - bidObject = bidfactory.createBid(CONSTANTS.STATUS.NO_BID, bid); - } else { - bidObject = bidfactory.createBid(CONSTANTS.STATUS.GOOD, bid); - bidObject.cpm = bid.price; - bidObject.ad = bid.adm; - bidObject.width = bid.w; - bidObject.height = bid.h; - if (bid.dealid) { - bidObject.dealId = bid.dealid; - } - } - bidObject.bidderCode = bidderCode; - bidmanager.addBidResponse(placementCode, bidObject); +} + +function _getBidFromResponse(respItem) { + if (!respItem) { + utils.logError(LOG_ERROR_MESS.emptySeatbid); + } else if (!respItem.bid) { + utils.logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); + } else if (!respItem.bid[0]) { + utils.logError(LOG_ERROR_MESS.noBid); } - - function _addBidResponse(bid, auids, placementMap) { - if (!bid) return; - var errorMessage, placementCodes; - if (!bid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(bid); - else { - placementCodes = placementMap.hasOwnProperty(bid.auid) && placementMap[bid.auid]; - if (!placementCodes) { - errorMessage = LOG_ERROR_MESS.noPlacementCode + bid.auid + LOG_ERROR_MESS.havePCodeFor + auids.join(','); - } - } - - if (!errorMessage) { - if (!bid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(bid); - - var l = placementCodes.length; - while (l--) { - _forEachPlacement(errorMessage, bid, placementCodes[l]); - } - - delete placementMap[bid.auid]; - } - - if (errorMessage) { - utils.logError(errorMessage); + return respItem && respItem.bid && respItem.bid[0]; +} + +function _addBidResponse(serverBid, bidsMap, priceType, bidResponses) { + if (!serverBid) return; + let errorMessage; + if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid); + if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); + else { + const awaitingBids = bidsMap[serverBid.auid]; + if (awaitingBids) { + awaitingBids.forEach(bid => { + const bidResponse = { + requestId: bid.bidId, // bid.bidderRequestId, + bidderCode: spec.code, + cpm: serverBid.price, + width: serverBid.w, + height: serverBid.h, + creativeId: serverBid.auid, // bid.bidId, + currency: 'USD', + netRevenue: priceType !== 'gross', + ttl: TIME_TO_LIVE, + ad: serverBid.adm, + dealId: serverBid.dealid + }; + bidResponses.push(bidResponse); + }); + } else { + errorMessage = LOG_ERROR_MESS.noPlacementCode + serverBid.auid; } } - - function _responseProcessing(resp, auids, placementMap) { - var errorMessage; - - if (!resp) errorMessage = LOG_ERROR_MESS.emptyResponse; - else if (resp.seatbid && !resp.seatbid.length) errorMessage = LOG_ERROR_MESS.hasEmptySeatbidArray; - - if (!errorMessage) { - resp = resp.seatbid || []; - var l = resp.length; - while (l--) { - _addBidResponse(_getBidFromResponse(resp[l]), auids, placementMap); - } - } - - var n, bidObj; - for (var auid in placementMap) { - if (placementMap.hasOwnProperty(auid) && placementMap[auid]) { - n = placementMap[auid].length; - while (n--) { - bidObj = bidfactory.createBid(CONSTANTS.STATUS.NO_BID); - bidObj.bidderCode = bidderCode; - bidmanager.addBidResponse(placementMap[auid][n], bidObj); - } - } - } - - if (errorMessage) utils.logError(errorMessage); + if (errorMessage) { + utils.logError(errorMessage); } +} - return { - callBids: _callBids - }; -}; - -adaptermanager.registerBidAdapter(new TrustxAdapter(), 'trustx'); - -module.exports = TrustxAdapter; +registerBidder(spec); diff --git a/modules/trustxBidAdapter.md b/modules/trustxBidAdapter.md new file mode 100755 index 00000000000..ca407b0c5e8 --- /dev/null +++ b/modules/trustxBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +Module Name: TrustX Bidder Adapter +Module Type: Bidder Adapter +Maintainer: paul@trustx.org + +# Description + +Module that connects to TrustX demand source to fetch bids. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "trustx", + params: { + uid: '44', + priceType: 'gross' // by default is 'net' + } + } + ] + },{ + code: 'test-div', + sizes: [[728, 90]], + bids: [ + { + bidder: "trustx", + params: { + uid: 45, + priceType: 'gross' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/test/spec/modules/trustxBidAdapter_spec.js b/test/spec/modules/trustxBidAdapter_spec.js index 7208ebef343..918e03674a9 100644 --- a/test/spec/modules/trustxBidAdapter_spec.js +++ b/test/spec/modules/trustxBidAdapter_spec.js @@ -1,234 +1,289 @@ -describe('trustx adapter tests', function () { - var expect = require('chai').expect; - var assert = require('chai').assert; - var urlParse = require('url-parse'); - var querystringify = require('querystringify'); - - var adapter = require('modules/trustxBidAdapter'); - var bidmanager = require('src/bidmanager'); - var adLoader = require('src/adloader'); - var utils = require('src/utils'); - window.$$PREBID_GLOBAL$$ = window.$$PREBID_GLOBAL$$ || {}; - - if (typeof (pbjs) === 'undefined') { - var pbjs = window.$$PREBID_GLOBAL$$; - } - let stubLoadScript; - beforeEach(function () { - stubLoadScript = sinon.stub(adLoader, 'loadScript'); - }); - afterEach(function () { - stubLoadScript.restore(); - }); - var logErrorSpy; - beforeEach(function () { - logErrorSpy = sinon.spy(utils, 'logError'); - }); - afterEach(function () { - logErrorSpy.restore(); +import { expect } from 'chai'; +import { spec } from 'modules/trustxBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; + +describe('TrustXAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); }); - describe('creation of request url', function () { - if (typeof (pbjs._bidsRequested) === 'undefined') { - pbjs._bidsRequested = []; - } - it('should fix parameter name', function () { - var params = { - bidderCode: 'trustx', - bids: [ - { - bidder: 'trustx', - params: { - uid: 5 - }, - placementCode: 'div-1' - }, - { - bidder: 'trustx', - params: { - uid: 6 - }, - placementCode: 'div-1' - }, - { - bidder: 'trustx', - params: {}, - placementCode: 'div-2' - }, - { - bidder: 'trustx', - params: { - uid: 6, - test: true - }, - placementCode: 'div-3' - }, - { - bidder: 'trustx', - placementCode: 'div-4' - } - ] + + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'trustx', + 'params': { + 'uid': '44' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'uid': 0 }; - adapter().callBids(params); - var bidUrl = stubLoadScript.getCall(0).args[0]; - sinon.assert.calledWith(stubLoadScript, bidUrl); - var parsedBidUrl = urlParse(bidUrl); - var parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); - var generatedCallback = '$$PREBID_GLOBAL$$.trustx_callback_wrapper_5_6'; - expect(parsedBidUrl.hostname).to.equal('sofia.trustx.org'); - expect(parsedBidUrl.pathname).to.equal('/hb'); - expect(parsedBidUrlQueryString).to.have.property('auids').and.to.equal('5,6'); - expect(parsedBidUrlQueryString).to.have.property('u').and.to.equal(location.href); - expect(parsedBidUrlQueryString).to.have.property('cb').and.to.equal(generatedCallback); + expect(spec.isBidRequestValid(bid)).to.equal(false); }); }); - describe('validate incoming params', function () { - if (typeof (pbjs._bidsRequested) === 'undefined') { - pbjs._bidsRequested = []; - } - it('has no correct item in config', function () { - var params = { - bidderCode: 'trustx', - bids: [ - { - bidder: 'trustx', - params: {}, - placementCode: 'div-1' - }, - { - bidder: 'trustx', - placementCode: 'div-1' - } - ] - }; - adapter().callBids(params); - sinon.assert.notCalled(stubLoadScript); - expect(logErrorSpy.getCall(0).args[0]).to.equal('Uids should be not empty'); + + describe('buildRequests', () => { + let bidRequests = [ + { + 'bidder': 'trustx', + 'params': { + 'uid': '43' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'trustx', + 'params': { + 'uid': '43' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '3150ccb55da321', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'trustx', + 'params': { + 'uid': '45' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '42dbe3a7168a6a', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('should attach valid params to the tag', () => { + const request = spec.buildRequests([bidRequests[0]]); + const payload = request.data; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '43'); + }); + + it('auids must not be duplicated', () => { + const request = spec.buildRequests(bidRequests); + const payload = request.data; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '43,45'); + }); + + it('pt parameter must be "gross" if params.priceType === "gross"', () => { + bidRequests[1].params.priceType = 'gross'; + const request = spec.buildRequests(bidRequests); + const payload = request.data; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'gross'); + expect(payload).to.have.property('auids', '43,45'); + delete bidRequests[1].params.priceType; + }); + + it('pt parameter must be "net" or "gross"', () => { + bidRequests[1].params.priceType = 'some'; + const request = spec.buildRequests(bidRequests); + const payload = request.data; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '43,45'); + delete bidRequests[1].params.priceType; }); }); - describe('handling of the callback response', function () { - if (typeof (pbjs._bidsReceived) === 'undefined') { - pbjs._bidsReceived = []; - } - if (typeof (pbjs._bidsRequested) === 'undefined') { - pbjs._bidsRequested = []; - } - if (typeof (pbjs._adsReceived) === 'undefined') { - pbjs._adsReceived = []; - } - var params = { - bidderCode: 'trustx', - bids: [ + + describe('interpretResponse', () => { + const responses = [ + {'bid': [{'price': 1.15, 'adm': '<div>test content 1</div>', 'auid': 43, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.5, 'adm': '<div>test content 2</div>', 'auid': 44, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'price': 0, 'auid': 45, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0, 'adm': '<div>test content 4</div>', 'h': 250, 'w': 300}], 'seat': '1'}, + undefined, + {'bid': [], 'seat': '1'}, + {'seat': '1'}, + ]; + + it('should get correct bid response', () => { + const bidRequests = [ + { + 'bidder': 'trustx', + 'params': { + 'uid': '43' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '659423fff799cb', + 'bidderRequestId': '5f2009617a7c0a', + 'auctionId': '1cbd2feafe5e8b', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '659423fff799cb', + 'cpm': 1.15, + 'creativeId': 43, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '<div>test content 1</div>', + 'bidderCode': 'trustx', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': [responses[0]]}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('should get correct multi bid response', () => { + const bidRequests = [ { - bidder: 'trustx', - params: { - uid: 5 + 'bidder': 'trustx', + 'params': { + 'uid': '43' }, - placementCode: '/19968336/header-bid-tag-0' + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d71a5b', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', }, { - bidder: 'trustx', - params: { - uid: 6 + 'bidder': 'trustx', + 'params': { + 'uid': '44' }, - placementCode: '/19968336/header-bid-tag-1' + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '4dff80cc4ee346', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', }, { - bidder: 'trustx', - params: { - uid: 42 + 'bidder': 'trustx', + 'params': { + 'uid': '43' }, - placementCode: '/19968336/header-bid-tag-2' + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '5703af74d0472a', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '300bfeb0d71a5b', + 'cpm': 1.15, + 'creativeId': 43, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '<div>test content 1</div>', + 'bidderCode': 'trustx', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '5703af74d0472a', + 'cpm': 1.15, + 'creativeId': 43, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '<div>test content 1</div>', + 'bidderCode': 'trustx', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360, }, { - bidder: 'trustx', - params: { - uid: 43 + 'requestId': '4dff80cc4ee346', + 'cpm': 0.5, + 'creativeId': 44, + 'dealId': undefined, + 'width': 728, + 'height': 90, + 'ad': '<div>test content 2</div>', + 'bidderCode': 'trustx', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': [responses[0], responses[1]]}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('handles wrong and nobid responses', () => { + const bidRequests = [ + { + 'bidder': 'trustx', + 'params': { + 'uid': '45' }, - placementCode: '/19968336/header-bid-tag-3' + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d7190gf', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', }, { - bidder: 'trustx', - params: { - uid: 44 + 'bidder': 'trustx', + 'params': { + 'uid': '46' }, - placementCode: '/19968336/header-bid-tag-4' + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d71321', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', }, { - bidder: 'trustx', - params: { - uid: 45 + 'bidder': 'trustx', + 'params': { + 'uid': '50' }, - placementCode: '/19968336/header-bid-tag-5' + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '300bfeb0d7183bb', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', } - ] - }; - it('callback function should exist', function () { - adapter().callBids(params); - expect(pbjs['trustx_callback_wrapper_5_6_42_43_44_45']) - .to.exist.and.to.be.a('function'); - }); - it('bidmanager.addBidResponse should be called with correct arguments', function () { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - adapter().callBids(params); - var adUnits = []; - var unit = {}; - unit.bids = params.bids; - unit.code = '/19968336/header-bid-tag'; - adUnits.push(unit); - if (typeof (pbjs._bidsRequested) === 'undefined') { - pbjs._bidsRequested = [params]; - } else { - pbjs._bidsRequested.push(params); - } - pbjs.adUnits = adUnits; - var response = { - seatbid: [ - {bid: [{price: 1.15, adm: '<div>test content 1</div>', auid: 5, h: 90, w: 728}], seat: '1'}, - {bid: [{price: 0, auid: 6, h: 250, w: 300}], seat: '1'}, - {bid: [{price: 0, adm: '<div>test content 3</div>', h: 250, w: 300}], seat: '1'}, - undefined, - {bid: [], seat: '1'}, - {seat: '1'}, - {bid: [{price: 0, adm: '<div>test content 7</div>', auid: 46, h: 250, w: 300}], seat: '1'} - ] - }; - pbjs['trustx_callback_wrapper_5_6_42_43_44_45'](response); - var bidPlacementCode1 = stubAddBidResponse.getCall(1).args[0]; - var bidObject1 = stubAddBidResponse.getCall(1).args[1]; - var bidPlacementCode2 = stubAddBidResponse.getCall(0).args[0]; - var bidObject2 = stubAddBidResponse.getCall(0).args[1]; - var bidPlacementCode3 = stubAddBidResponse.getCall(2).args[0]; - var bidObject3 = stubAddBidResponse.getCall(2).args[1]; - var bidPlacementCode4 = stubAddBidResponse.getCall(3).args[0]; - var bidObject4 = stubAddBidResponse.getCall(3).args[1]; - var bidPlacementCode5 = stubAddBidResponse.getCall(4).args[0]; - var bidObject5 = stubAddBidResponse.getCall(4).args[1]; - var bidPlacementCode6 = stubAddBidResponse.getCall(5).args[0]; - var bidObject6 = stubAddBidResponse.getCall(5).args[1]; - expect(logErrorSpy.getCall(5).args[0]).to.equal('Bid from response has no adm parameter - {"price":0,"auid":6,"h":250,"w":300}'); - expect(logErrorSpy.getCall(4).args[0]).to.equal('Bid from response has no auid parameter - {"price":0,"adm":"<' + 'div>test content 3</' + 'div>","h":250,"w":300}'); - expect(logErrorSpy.getCall(3).args[0]).to.equal('Seatbid array from response has empty item'); - expect(logErrorSpy.getCall(2).args[0]).to.equal('Array of bid objects is empty'); - expect(logErrorSpy.getCall(1).args[0]).to.equal('Seatbid from response has no array of bid objects - {"seat":"1"}'); - expect(logErrorSpy.getCall(0).args[0]).to.equal('Can\'t find placementCode for bid with auid - 46, placementCode is available only for the following uids - 5,6,42,43,44,45'); - expect(bidPlacementCode1).to.equal('/19968336/header-bid-tag-0'); - expect(bidObject1.cpm).to.equal(1.15); - expect(bidObject1.ad).to.equal('<div>test content 1</div>'); - expect(bidObject1.width).to.equal(728); - expect(bidObject1.height).to.equal(90); - expect(bidObject1.getStatusCode()).to.equal(1); - expect(bidObject1.bidderCode).to.equal('trustx'); - expect(bidPlacementCode2).to.equal('/19968336/header-bid-tag-1'); - expect(bidObject2.getStatusCode()).to.equal(2); - expect(bidPlacementCode3).to.equal('/19968336/header-bid-tag-2'); - expect(bidObject3.getStatusCode()).to.equal(2); - expect(bidPlacementCode4).to.equal('/19968336/header-bid-tag-3'); - expect(bidObject4.getStatusCode()).to.equal(2); - expect(bidPlacementCode5).to.equal('/19968336/header-bid-tag-4'); - expect(bidObject5.getStatusCode()).to.equal(2); - expect(bidPlacementCode6).to.equal('/19968336/header-bid-tag-5'); - expect(bidObject6.getStatusCode()).to.equal(2); - stubAddBidResponse.restore(); + ]; + const request = spec.buildRequests(bidRequests); + const result = spec.interpretResponse({'body': {'seatbid': responses.slice(2)}}, request); + expect(result.length).to.equal(0); }); }); }); From caec5c0022405fada40562e6483abc7f92856d6a Mon Sep 17 00:00:00 2001 From: dbemiller <dbemiller@appnexus.com> Date: Thu, 26 Oct 2017 12:03:44 -0400 Subject: [PATCH 23/44] Fix test-coverage bug (#1765) * Fixed a bug in test-coverage single-file tests. * Reverted unintended changes. --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index d7a151ce357..7b05b22ad06 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -189,7 +189,7 @@ gulp.task('test', ['clean'], function (done) { // If --file "<path-to-test-file>" is given, the task will only run tests in the specified file. gulp.task('test-coverage', ['clean'], function(done) { - new KarmaServer(karmaConfMaker(true, false, argv.file), newKarmaCallback(done)).start(); + new KarmaServer(karmaConfMaker(true, false, false, argv.file), newKarmaCallback(done)).start(); }); // View the code coverage report in the browser. From 9a6ddc28313d9a0a09e1153fd9465ee52bd0e6a3 Mon Sep 17 00:00:00 2001 From: Paul Yang <pyang@conversantmedia.com> Date: Thu, 26 Oct 2017 09:31:51 -0700 Subject: [PATCH 24/44] Update Conversant adapter to Prebid 1.0 (#1711) * Conversant adapter initial support for prebid 1.0 * Add video support for conversant adapter * Add tests and md * Update conversant contact address * Return data object in buildRequests without converting it to a string * Conversant adapter initial support for prebid 1.0 * Add video support for conversant adapter * Add tests and md * Update conversant contact address * Return data object in buildRequests without converting it to a string * Better validation for site id * Switch to use utils._each and utils._map * Add tests for displaymanagerver * Review changes for conversant --- modules/conversantBidAdapter.js | 458 +++++++------ modules/conversantBidAdapter.md | 41 ++ .../spec/modules/conversantBidAdapter_spec.js | 613 ++++++++---------- 3 files changed, 517 insertions(+), 595 deletions(-) create mode 100644 modules/conversantBidAdapter.md diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index d51008559f2..7e71d3be8aa 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -1,99 +1,70 @@ -'use strict'; -var VERSION = '2.1.0'; -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 ajax = require('src/ajax').ajax; -var adaptermanager = require('src/adaptermanager'); +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; +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'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['cnvr'], // short code + supportedMediaTypes: [VIDEO], + + /** + * 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 || !bid.params) { + utils.logWarn(BIDDER_CODE + ': Missing bid parameters'); + return false; + } -/** - * Adapter for requesting bids from Conversant - */ -var ConversantAdapter = function () { - var w = window; - var n = navigator; - - // production endpoint - var conversantUrl = '//media.msg.dotomi.com/s2s/header/24?callback=$$PREBID_GLOBAL$$.conversantResponse'; - - // SSAPI returns JSONP with window.pbjs.conversantResponse as the cb - var appendScript = function (code) { - var script = document.createElement('script'); - script.type = 'text/javascript'; - script.className = 'cnvr-response'; - - try { - script.appendChild(document.createTextNode(code)); - document.getElementsByTagName('head')[0].appendChild(script); - } catch (e) { - script.text = code; - document.getElementsByTagName('head')[0].appendChild(script); + if (!utils.isStr(bid.params.site_id)) { + utils.logWarn(BIDDER_CODE + ': site_id must be specified as a string') + return false; } - }; - var getDNT = function () { - return n.doNotTrack === '1' || w.doNotTrack === '1' || n.msDoNotTrack === '1' || n.doNotTrack === 'yes'; - }; + if (isVideoRequest(bid)) { + if (!bid.params.mimes) { + // Give a warning but let it pass + utils.logWarn(BIDDER_CODE + ': mimes should be specified for videos'); + } else if (!utils.isArray(bid.params.mimes) || !bid.params.mimes.every(s => utils.isStr(s))) { + utils.logWarn(BIDDER_CODE + ': mimes must be an array of strings'); + return false; + } + } - var getDevice = function () { - const language = n.language ? 'language' : 'userLanguage'; - return { - h: screen.height, - w: screen.width, - dnt: getDNT() ? 1 : 0, - language: n[language].split('-')[0], - make: n.vendor ? n.vendor : '', - ua: n.userAgent - }; - }; + return true; + }, - var callBids = function (params) { - var conversantBids = params.bids || []; - requestBids(conversantBids); - }; + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - an array of bids + * @return {ServerRequest} Info describing the request to the server. + */ + buildRequests: function(validBidRequests) { + const loc = utils.getTopWindowLocation(); + const page = loc.pathname + loc.search + loc.hash; + const isPageSecure = (loc.protocol === 'https:') ? 1 : 0; + let siteId = ''; + let requestId = ''; - var requestBids = function (bidReqs) { - // build bid request object - var page = location.pathname + location.search + location.hash; - var siteId = ''; - var conversantImps = []; - var conversantBidReqs; - var secure = 0; - - // build impression array for conversant - utils._each(bidReqs, function (bid) { - var bidfloor = utils.getBidIdParameter('bidfloor', bid.params); - var adW = 0; - var adH = 0; - var format; - var tagId; - var pos; - var imp; - - secure = utils.getBidIdParameter('secure', bid.params) ? 1 : secure; - siteId = utils.getBidIdParameter('site_id', bid.params) + ''; - tagId = utils.getBidIdParameter('tag_id', bid.params); - pos = utils.getBidIdParameter('position', bid.params); - - // Allow sizes to be overridden per placement - var bidSizes = Array.isArray(bid.params.sizes) ? bid.params.sizes : bid.sizes; - - if (bidSizes.length === 2 && typeof bidSizes[0] === 'number' && typeof bidSizes[1] === 'number') { - adW = bidSizes[0]; - adH = bidSizes[1]; - } else { - format = []; - utils._each(bidSizes, function (bidSize) { - format.push({ - w: bidSize[0], - h: bidSize[1] - }); - }); - } + const conversantImps = validBidRequests.map(function(bid) { + const bidfloor = utils.getBidIdParameter('bidfloor', bid.params); + const secure = isPageSecure || (utils.getBidIdParameter('secure', bid.params) ? 1 : 0); + + siteId = utils.getBidIdParameter('site_id', bid.params); + requestId = bid.requestId; + + const format = convertSizes(bid.sizes); - imp = { + const imp = { id: bid.bidId, secure: secure, bidfloor: bidfloor || 0, @@ -101,178 +72,187 @@ var ConversantAdapter = function () { displaymanagerver: VERSION }; - if (tagId !== '') { - imp.tagid = tagId; - } - - if (bid.mediaType === 'video') { - var mimes = []; - var maxduration = 0; - var protocols = []; - var api = []; - - var video = Array.isArray(format) ? {format: format} : {w: adW, h: adH}; - - mimes = utils.getBidIdParameter('mimes', bid.params); - if (mimes !== '') { - video.mimes = mimes; - } + copyOptProperty(bid.params, 'tag_id', imp, 'tagid'); - maxduration = utils.getBidIdParameter('maxduration', bid.params); - if (maxduration !== '') { - video.maxduration = maxduration; - } + if (isVideoRequest(bid)) { + const video = {format: format}; - protocols = utils.getBidIdParameter('protocols', bid.params); - if (protocols !== '') { - video.protocols = protocols; - } - - api = utils.getBidIdParameter('api', bid.params); - if (api !== '') { - video.api = api; - } - - if (pos !== '') { - video.pos = pos; - } + copyOptProperty(bid.params, 'position', video, 'pos'); + copyOptProperty(bid.params, 'mimes', video); + copyOptProperty(bid.params, 'maxduration', video); + copyOptProperty(bid.params, 'protocols', video); + copyOptProperty(bid.params, 'api', video); imp.video = video; } else { - var banner = Array.isArray(format) ? {format: format} : {w: adW, h: adH}; + const banner = {format: format}; + + copyOptProperty(bid.params, 'position', banner, 'pos'); - if (pos !== '') { - banner.pos = pos; - } imp.banner = banner; } - conversantImps.push(imp); + return imp; }); - conversantBidReqs = { - 'id': utils.getUniqueIdentifierStr(), - 'imp': conversantImps, - - 'site': { - 'id': siteId, - 'mobile': document.querySelector('meta[name="viewport"][content*="width=device-width"]') !== null ? 1 : 0, - 'page': page + const payload = { + id: requestId, + imp: conversantImps, + site: { + id: siteId, + mobile: document.querySelector('meta[name="viewport"][content*="width=device-width"]') !== null ? 1 : 0, + page: page }, - - 'device': getDevice(), - 'at': 1 + device: getDevice(), + at: 1 }; - var url = secure ? 'https:' + conversantUrl : location.protocol + conversantUrl; - ajax(url, appendScript, JSON.stringify(conversantBidReqs), { - withCredentials: true - }); - }; + return { + method: 'POST', + url: URL, + data: payload, + }; + }, + /** + * 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) { + const bidResponses = []; + const requestMap = {}; + serverResponse = serverResponse.body; + + if (bidRequest && bidRequest.data && bidRequest.data.imp) { + utils._each(bidRequest.data.imp, imp => requestMap[imp.id] = imp); + } - var addEmptyBidResponses = function (placementsWithBidsBack) { - var allConversantBidRequests = $$PREBID_GLOBAL$$._bidsRequested.find(bidSet => bidSet.bidderCode === 'conversant'); - - if (allConversantBidRequests && allConversantBidRequests.bids) { - utils._each(allConversantBidRequests.bids, function (conversantBid) { - if (!utils.contains(placementsWithBidsBack, conversantBid.placementCode)) { - // Add a no-bid response for this placement. - var bid = bidfactory.createBid(2, conversantBid); - bid.bidderCode = 'conversant'; - bidmanager.addBidResponse(conversantBid.placementCode, bid); - } + if (serverResponse && utils.isArray(serverResponse.seatbid)) { + utils._each(serverResponse.seatbid, function(bidList) { + utils._each(bidList.bid, function(conversantBid) { + const responseCPM = parseFloat(conversantBid.price); + if (responseCPM > 0.0 && conversantBid.impid) { + const responseAd = conversantBid.adm || ''; + const responseNurl = conversantBid.nurl || ''; + const request = requestMap[conversantBid.impid]; + + const bid = { + requestId: conversantBid.impid, + currency: serverResponse.cur || 'USD', + cpm: responseCPM, + creativeId: conversantBid.crid || '' + }; + + 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; + } + } else { + bid.ad = responseAd + '<img src="' + responseNurl + '" />'; + bid.width = conversantBid.w; + bid.height = conversantBid.h; + } + + bidResponses.push(bid); + } + }) }); } - }; - var parseSeatbid = function (bidResponse) { - var placementsWithBidsBack = []; - utils._each(bidResponse.bid, function (conversantBid) { - var responseCPM; - var placementCode = ''; - var id = conversantBid.impid; - var bid = {}; - var responseAd; - var responseNurl; - var sizeArrayLength; - - // Bid request we sent Conversant - var bidRequested = $$PREBID_GLOBAL$$._bidsRequested.find(bidSet => bidSet.bidderCode === 'conversant').bids.find(bid => bid.bidId === id); - - if (bidRequested) { - placementCode = bidRequested.placementCode; - bidRequested.status = CONSTANTS.STATUS.GOOD; - responseCPM = parseFloat(conversantBid.price); - - if (responseCPM !== 0.0) { - conversantBid.placementCode = placementCode; - placementsWithBidsBack.push(placementCode); - conversantBid.size = bidRequested.sizes; - responseAd = conversantBid.adm || ''; - responseNurl = conversantBid.nurl || ''; - - // Our bid! - bid = bidfactory.createBid(1, bidRequested); - bid.creative_id = conversantBid.id || ''; - bid.bidderCode = 'conversant'; - bid.cpm = responseCPM; - - if (bidRequested.mediaType === 'video') { - bid.vastUrl = responseAd; - } else { - // Track impression image onto returned html - bid.ad = responseAd + '<img src="' + responseNurl + '" />'; - } + return bidResponses; + }, + + /** + * Return use sync info + * + * @param {SyncOptions} syncOptions - Info about usersyncs that the adapter should obey + * @return {UserSync} Adapter sync type and url + */ + getUserSyncs: function(syncOptions) { + if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: SYNC_URL + }]; + } + } +}; - sizeArrayLength = bidRequested.sizes.length; - if (sizeArrayLength === 2 && typeof bidRequested.sizes[0] === 'number' && typeof bidRequested.sizes[1] === 'number') { - bid.width = bidRequested.sizes[0]; - bid.height = bidRequested.sizes[1]; - } else { - bid.width = bidRequested.sizes[0][0]; - bid.height = bidRequested.sizes[0][1]; - } +/** + * Determine do-not-track state + * + * @returns {boolean} + */ +function getDNT() { + return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; +} - bidmanager.addBidResponse(placementCode, bid); - } - } - }); - addEmptyBidResponses(placementsWithBidsBack); +/** + * Return openrtb device object that includes ua, width, and height. + * + * @returns {Device} Openrtb device object + */ +function getDevice() { + const language = navigator.language ? 'language' : 'userLanguage'; + return { + h: screen.height, + w: screen.width, + dnt: getDNT() ? 1 : 0, + language: navigator[language].split('-')[0], + make: navigator.vendor ? navigator.vendor : '', + ua: navigator.userAgent }; +} - // Register our callback to the global object: - $$PREBID_GLOBAL$$.conversantResponse = function (conversantResponseObj, path) { - // valid object? - if (conversantResponseObj && conversantResponseObj.id) { - if (conversantResponseObj.seatbid && conversantResponseObj.seatbid.length > 0 && conversantResponseObj.seatbid[0].bid && conversantResponseObj.seatbid[0].bid.length > 0) { - utils._each(conversantResponseObj.seatbid, parseSeatbid); - } else { - // no response data for any placements - addEmptyBidResponses([]); - } - } else { - // no response data for any placements - addEmptyBidResponses([]); - } - // for debugging purposes - if (path) { - adloader.loadScript(path, function () { - var allConversantBidRequests = $$PREBID_GLOBAL$$._bidsRequested.find(bidSet => bidSet.bidderCode === 'conversant'); - - if ($$PREBID_GLOBAL$$.conversantDebugResponse) { - $$PREBID_GLOBAL$$.conversantDebugResponse(allConversantBidRequests); - } - }); - } - }; // conversantResponse +/** + * Convert arrays of widths and heights to an array of objects with w and h properties. + * + * [[300, 250], [300, 600]] => [{w: 300, h: 250}, {w: 300, h: 600}] + * + * @param {number[2][]|number[2]} bidSizes - arrays of widths and heights + * @returns {object[]} Array of objects with w and h + */ +function convertSizes(bidSizes) { + let format; - return { - callBids: callBids - }; -}; + if (bidSizes.length === 2 && typeof bidSizes[0] === 'number' && typeof bidSizes[1] === 'number') { + format = [{w: bidSizes[0], h: bidSizes[1]}]; + } else { + format = utils._map(bidSizes, d => { return {w: d[0], h: d[1]}; }); + } -adaptermanager.registerBidAdapter(new ConversantAdapter(), 'conversant', { - supportedMediaTypes: ['video'] -}); + return format; +} -module.exports = ConversantAdapter; +/** + * Check if it's a video bid request + * + * @param {BidRequest} bid - Bid request generated from ad slots + * @returns {boolean} True if it's a video bid + */ +function isVideoRequest(bid) { + return bid.mediaType === 'video' || !!utils.deepAccess(bid, 'mediaTypes.video'); +} + +/** + * Copy property if exists from src to dst + * + * @param {object} src + * @param {string} srcName + * @param {object} dst + * @param {string} [dstName] - Optional. If not specified then srcName is used. + */ +function copyOptProperty(src, srcName, dst, dstName) { + dstName = dstName || srcName; + const obj = utils.getBidIdParameter(srcName, src); + if (obj !== '') { + dst[dstName] = obj; + } +} + +registerBidder(spec); diff --git a/modules/conversantBidAdapter.md b/modules/conversantBidAdapter.md new file mode 100644 index 00000000000..1afdad6d544 --- /dev/null +++ b/modules/conversantBidAdapter.md @@ -0,0 +1,41 @@ +# Overview + +- Module Name: Conversant Bidder Adapter +- Module Type: Bidder Adapter +- Maintainer: mediapsr@conversantmedia.com + +# Description + +Module that connects to Conversant's demand sources. Supports banners and videos. + +# Test Parameters +``` +var adUnits = [ + { + code: 'banner-test-div', + sizes: [[300, 250]], + bids: [{ + bidder: "conversant", + params: { + site_id: '108060' + } + }] + },{ + code: 'video-test-div', + sizes: [640, 480], + mediaTypes: { + video: { + context: 'instream' + } + }, + bids: [{ + bidder: "conversant", + params: { + site_id: '88563', + api: [2], + protocols: [1, 2], + mimes: ['video/mp4'] + } + }] + }]; +``` \ No newline at end of file diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index 57cd9411e66..81da6867132 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -1,376 +1,277 @@ -var expect = require('chai').expect; +import {expect} from 'chai'; +import {spec} from 'modules/conversantBidAdapter'; +import * as utils from 'src/utils'; + var Adapter = require('modules/conversantBidAdapter'); var bidManager = require('src/bidmanager'); -describe('Conversant adapter tests', function () { - var addBidResponseSpy; - var adapter; - - var bidderRequest = { - bidderCode: 'conversant', - bids: [ - { - bidId: 'bidId1', - bidder: 'conversant', - placementCode: 'div1', - sizes: [[300, 600]], - params: { - site_id: '87293', - position: 1, - tag_id: 'tagid-1', - secure: false - } - }, { - bidId: 'bidId2', - bidder: 'conversant', - placementCode: 'div2', - sizes: [[300, 600]], - params: { - site_id: '87293', - secure: false - } - }, { - bidId: 'bidId3', - bidder: 'conversant', - placementCode: 'div3', - sizes: [[300, 600], [160, 600]], - params: { - site_id: '87293', - position: 1, - tag_id: '', - secure: false +describe('Conversant adapter tests', function() { + const siteId = '108060'; + + const bidRequests = [ + { + bidder: 'conversant', + params: { + site_id: siteId, + position: 1, + tag_id: 'tagid-1', + secure: false, + bidfloor: 0.5 + }, + placementCode: 'pcode000', + transactionId: 'tx000', + sizes: [[300, 250]], + bidId: 'bid000', + bidderRequestId: '117d765b87bed38', + requestId: 'req000' + }, { + bidder: 'conversant', + params: { + site_id: siteId, + secure: false + }, + placementCode: 'pcode001', + transactionId: 'tx001', + sizes: [[468, 60]], + bidId: 'bid001', + bidderRequestId: '117d765b87bed38', + requestId: 'req000' + }, { + bidder: 'conversant', + params: { + site_id: siteId, + position: 2, + tag_id: '', + secure: false + }, + placementCode: 'pcode002', + transactionId: 'tx002', + sizes: [[300, 600], [160, 600]], + bidId: 'bid002', + bidderRequestId: '117d765b87bed38', + requestId: 'req000' + }, { + bidder: 'conversant', + params: { + site_id: siteId, + api: [2], + protocols: [1, 2], + mimes: ['video/mp4', 'video/x-flv'], + maxduration: 30 + }, + mediaTypes: { + video: { + context: 'instream' } - }, { - bidId: 'bidId4', - bidder: 'conversant', - placementCode: 'div4', - mediaType: 'video', - sizes: [[480, 480]], - params: { - site_id: '89192', - pos: 1, - tagid: 'tagid-4', - secure: false - } - } - ] - }; - - it('The Conversant response should exist and be a function', function () { - expect($$PREBID_GLOBAL$$.conversantResponse).to.exist.and.to.be.a('function'); - }); - - describe('Should submit bid responses correctly', function () { - beforeEach(function () { - addBidResponseSpy = sinon.stub(bidManager, 'addBidResponse'); - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - adapter = new Adapter(); - }); - - afterEach(function () { - addBidResponseSpy.restore(); - }); - - it('Should correctly submit valid and empty bids to the bid manager', function () { - var bidResponse = { - id: 123, - seatbid: [{ - bid: [{ - id: 1111111, - impid: 'bidId1', - price: 0 - }, { - id: 2345, - impid: 'bidId2', - price: 0.22, - nurl: '', - adm: 'adm2', - h: 300, - w: 600 - }] - }] - }; - - $$PREBID_GLOBAL$$.conversantResponse(bidResponse); - - // in this case, the valid bid (div2) is submitted before the empty bids (div1, div3) - var firstBid = addBidResponseSpy.getCall(0).args[1]; - var secondBid = addBidResponseSpy.getCall(1).args[1]; - var thirdBid = addBidResponseSpy.getCall(2).args[1]; - var placementCode1 = addBidResponseSpy.getCall(0).args[0]; - var placementCode2 = addBidResponseSpy.getCall(1).args[0]; - var placementCode3 = addBidResponseSpy.getCall(2).args[0]; - - expect(firstBid.getStatusCode()).to.equal(1); - expect(firstBid.bidderCode).to.equal('conversant'); - expect(firstBid.cpm).to.equal(0.22); - expect(firstBid.ad).to.equal('adm2' + '<img src="" />'); - expect(placementCode1).to.equal('div2'); - - expect(secondBid.getStatusCode()).to.equal(2); - expect(secondBid.bidderCode).to.equal('conversant'); - expect(placementCode2).to.equal('div1'); - - expect(thirdBid.getStatusCode()).to.equal(2); - expect(thirdBid.bidderCode).to.equal('conversant'); - expect(placementCode3).to.equal('div3'); - - expect(addBidResponseSpy.getCalls().length).to.equal(4); - }); - - it('Should submit bids with statuses of 2 to the bid manager for empty bid responses', function () { - $$PREBID_GLOBAL$$.conversantResponse({id: 1, seatbid: []}); - - var placementCode1 = addBidResponseSpy.getCall(0).args[0]; - var firstBid = addBidResponseSpy.getCall(0).args[1]; - var placementCode2 = addBidResponseSpy.getCall(1).args[0]; - var secondBid = addBidResponseSpy.getCall(1).args[1]; - var placementCode3 = addBidResponseSpy.getCall(2).args[0]; - var thirdBid = addBidResponseSpy.getCall(2).args[1]; - - expect(placementCode1).to.equal('div1'); - expect(firstBid.getStatusCode()).to.equal(2); - expect(firstBid.bidderCode).to.equal('conversant'); - - expect(placementCode2).to.equal('div2'); - expect(secondBid.getStatusCode()).to.equal(2); - expect(secondBid.bidderCode).to.equal('conversant'); - - expect(placementCode3).to.equal('div3'); - expect(thirdBid.getStatusCode()).to.equal(2); - expect(thirdBid.bidderCode).to.equal('conversant'); - - expect(addBidResponseSpy.getCalls().length).to.equal(4); - }); - - it('Should submit valid bids to the bid manager', function () { - var bidResponse = { - id: 123, - seatbid: [{ - bid: [{ - id: 1111111, - impid: 'bidId1', - price: 0.11, - nurl: '', - adm: 'adm', - h: 250, - w: 300, - ext: {} - }, { - id: 2345, - impid: 'bidId2', - price: 0.22, - nurl: '', - adm: 'adm2', - h: 300, - w: 600 - }, { - id: 33333, - impid: 'bidId3', - price: 0.33, - nurl: '', - adm: 'adm3', - h: 160, - w: 600 - }] - }] - }; - - $$PREBID_GLOBAL$$.conversantResponse(bidResponse); - - var firstBid = addBidResponseSpy.getCall(0).args[1]; - var secondBid = addBidResponseSpy.getCall(1).args[1]; - var thirdBid = addBidResponseSpy.getCall(2).args[1]; - var placementCode1 = addBidResponseSpy.getCall(0).args[0]; - var placementCode2 = addBidResponseSpy.getCall(1).args[0]; - var placementCode3 = addBidResponseSpy.getCall(2).args[0]; - - expect(firstBid.getStatusCode()).to.equal(1); - expect(firstBid.bidderCode).to.equal('conversant'); - expect(firstBid.cpm).to.equal(0.11); - expect(firstBid.ad).to.equal('adm' + '<img src="" />'); - expect(placementCode1).to.equal('div1'); - - expect(secondBid.getStatusCode()).to.equal(1); - expect(secondBid.bidderCode).to.equal('conversant'); - expect(secondBid.cpm).to.equal(0.22); - expect(secondBid.ad).to.equal('adm2' + '<img src="" />'); - expect(placementCode2).to.equal('div2'); - - expect(thirdBid.getStatusCode()).to.equal(1); - expect(thirdBid.bidderCode).to.equal('conversant'); - expect(thirdBid.cpm).to.equal(0.33); - expect(thirdBid.ad).to.equal('adm3' + '<img src="" />'); - expect(placementCode3).to.equal('div3'); - - expect(addBidResponseSpy.getCalls().length).to.equal(4); - }); - - it('Should submit video bid responses correctly.', function () { - var bidResponse = { - id: 123, - seatbid: [{ - bid: [{ - id: 1111111, - impid: 'bidId4', - price: 0.11, - nurl: 'imp_tracker', - adm: 'vasturl' - }] - }] - }; - - $$PREBID_GLOBAL$$.conversantResponse(bidResponse); - - var videoBid = addBidResponseSpy.getCall(0).args[1]; - var placementCode = addBidResponseSpy.getCall(0).args[0]; - - expect(videoBid.getStatusCode()).to.equal(1); - expect(videoBid.bidderCode).to.equal('conversant'); - expect(videoBid.cpm).to.equal(0.11); - expect(videoBid.vastUrl).to.equal('vasturl'); - expect(placementCode).to.equal('div4'); - }) - }); - - describe('Should submit the correct headers in the xhr', function () { - var server, - adapter; - - var bidResponse = { - id: 123, + }, + placementCode: 'pcode003', + transactionId: 'tx003', + sizes: [640, 480], + bidId: 'bid003', + bidderRequestId: '117d765b87bed38', + requestId: 'req000' + }]; + + const bidResponses = { + body: { + id: 'req000', seatbid: [{ bid: [{ - id: 1111, - impid: 'bidId1', - price: 0.11, - nurl: '', - adm: 'adm', - h: 250, + nurl: 'notify000', + adm: 'markup000', + crid: '1000', + impid: 'bid000', + price: 0.99, w: 300, - ext: {} + h: 250, + adomain: ['https://example.com'], + id: 'bid000' }, { - id: 2222, - impid: 'bidId2', - price: 0.22, - nurl: '', - adm: 'adm2', - h: 300, - w: 600 + impid: 'bid001', + price: 0.00000, + id: 'bid001' }, { - id: 3333, - impid: 'bidId3', - price: 0.33, - nurl: '', - adm: 'adm3', - h: 160, - w: 600 - }] - }] - }; - - beforeEach(function () { - server = sinon.fakeServer.create(); - adapter = new Adapter(); - }); - - afterEach(function () { - server.restore(); - }); - - beforeEach(function () { - var resp = [200, {'Content-type': 'text/javascript'}, '$$PREBID_GLOBAL$$.conversantResponse(\'' + JSON.stringify(bidResponse) + '\')']; - server.respondWith('POST', new RegExp('media.msg.dotomi.com/s2s/header'), resp); - }); - - it('Should contain valid request header properties', function () { - adapter.callBids(bidderRequest); - server.respond(); - - var request = server.requests[0]; - expect(request.requestBody).to.not.be.empty; - }); - }); - describe('Should create valid bid requests.', function () { - var server, - adapter; - - var bidResponse = { - id: 123, - seatbid: [{ - bid: [{ - id: 1111, - impid: 'bidId1', - price: 0.11, - nurl: '', - adm: 'adm', - h: 250, + nurl: 'notify002', + adm: 'markup002', + crid: '1002', + impid: 'bid002', + price: 2.99, w: 300, - ext: {} - }, { - id: 2222, - impid: 'bidId2', - price: 0.22, - nurl: '', - adm: 'adm2', - h: 300, - w: 600 + h: 600, + adomain: ['https://example.com'], + id: 'bid002' }, { - id: 3333, - impid: 'bidId3', - price: 0.33, - nurl: '', - adm: 'adm3', - h: 160, - w: 600 + nurl: 'notify003', + adm: 'markup003', + crid: '1003', + impid: 'bid003', + price: 3.99, + adomain: ['https://example.com'], + id: 'bid003' }] }] - }; - - beforeEach(function () { - server = sinon.fakeServer.create(); - adapter = new Adapter(); - }); - - afterEach(function () { - server.restore(); - }); + }, + headers: {}}; + + it('Verify basic properties', function() { + expect(spec.code).to.equal('conversant'); + expect(spec.aliases).to.be.an('array').with.lengthOf(1); + expect(spec.aliases[0]).to.equal('cnvr'); + expect(spec.supportedMediaTypes).to.be.an('array').with.lengthOf(1); + expect(spec.supportedMediaTypes[0]).to.equal('video'); + }); - beforeEach(function () { - var resp = [200, {'Content-type': 'text/javascript'}, '$$PREBID_GLOBAL$$.conversantResponse(\'' + JSON.stringify(bidResponse) + '\')']; - server.respondWith('POST', new RegExp('media.msg.dotomi.com/s2s/header'), resp); - }); + it('Verify user syncs', function() { + expect(spec.getUserSyncs({})).to.be.undefined; + expect(spec.getUserSyncs({iframeEnabled: true})).to.be.undefined; + expect(spec.getUserSyncs({pixelEnabled: false})).to.be.undefined; - it('Should create valid bid requests.', function () { - adapter.callBids(bidderRequest); - server.respond(); - var request = JSON.parse(server.requests[0].requestBody); - expect(request.imp[0].banner.format[0].w).to.equal(300); - expect(request.imp[0].banner.format[0].h).to.equal(600); - expect(request.imp[0].tagid).to.equal('tagid-1'); - expect(request.imp[0].banner.pos).to.equal(1); - expect(request.imp[0].secure).to.equal(0); - expect(request.site.id).to.equal('89192'); - }); + const syncs = spec.getUserSyncs({pixelEnabled: true}); + expect(syncs).to.be.an('array').with.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('//media.msg.dotomi.com/w/user.sync'); + }); - it('Should not pass empty or missing optional parameters on requests.', function () { - adapter.callBids(bidderRequest); - server.respond(); + it('Verify isBidRequestValid', function() { + expect(spec.isBidRequestValid({})).to.be.false; + expect(spec.isBidRequestValid({params: {}})).to.be.false; + expect(spec.isBidRequestValid({params: {site_id: '123'}})).to.be.true; + expect(spec.isBidRequestValid(bidRequests[0])).to.be.true; + expect(spec.isBidRequestValid(bidRequests[1])).to.be.true; + expect(spec.isBidRequestValid(bidRequests[2])).to.be.true; + expect(spec.isBidRequestValid(bidRequests[3])).to.be.true; + + const simpleVideo = JSON.parse(JSON.stringify(bidRequests[3])); + simpleVideo.params.site_id = 123; + expect(spec.isBidRequestValid(simpleVideo)).to.be.false; + simpleVideo.params.site_id = siteId; + simpleVideo.params.mimes = [1, 2, 3]; + expect(spec.isBidRequestValid(simpleVideo)).to.be.false; + simpleVideo.params.mimes = 'bad type'; + expect(spec.isBidRequestValid(simpleVideo)).to.be.false; + delete simpleVideo.params.mimes; + expect(spec.isBidRequestValid(simpleVideo)).to.be.true; + }); - var request = JSON.parse(server.requests[0].requestBody); - expect(request.imp[1].tagid).to.equal(undefined); - expect(request.imp[2].tagid).to.equal(undefined); - expect(request.imp[1].pos).to.equal(undefined); - }); + it('Verify buildRequest', function() { + const request = spec.buildRequests(bidRequests); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('//media.msg.dotomi.com/s2s/header/24'); + const payload = request.data; + + expect(payload).to.have.property('id', 'req000'); + expect(payload).to.have.property('at', 1); + expect(payload).to.have.property('imp'); + expect(payload.imp).to.be.an('array').with.lengthOf(4); + + expect(payload.imp[0]).to.have.property('id', 'bid000'); + expect(payload.imp[0]).to.have.property('secure', 0); + expect(payload.imp[0]).to.have.property('bidfloor', 0.5); + expect(payload.imp[0]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(/^\d+\.\d+\.\d+$/); + expect(payload.imp[0]).to.have.property('tagid', 'tagid-1'); + expect(payload.imp[0]).to.have.property('banner'); + expect(payload.imp[0].banner).to.have.property('pos', 1); + expect(payload.imp[0].banner).to.have.property('format'); + expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]); + expect(payload.imp[0]).to.not.have.property('video'); + + expect(payload.imp[1]).to.have.property('id', 'bid001'); + expect(payload.imp[1]).to.have.property('secure', 0); + expect(payload.imp[1]).to.have.property('bidfloor', 0); + expect(payload.imp[1]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(/^\d+\.\d+\.\d+$/); + expect(payload.imp[1]).to.not.have.property('tagid'); + expect(payload.imp[1]).to.have.property('banner'); + expect(payload.imp[1].banner).to.not.have.property('pos'); + expect(payload.imp[1].banner).to.have.property('format'); + expect(payload.imp[1].banner.format).to.deep.equal([{w: 468, h: 60}]); + + expect(payload.imp[2]).to.have.property('id', 'bid002'); + expect(payload.imp[2]).to.have.property('secure', 0); + expect(payload.imp[2]).to.have.property('bidfloor', 0); + expect(payload.imp[2]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(/^\d+\.\d+\.\d+$/); + expect(payload.imp[2]).to.have.property('banner'); + expect(payload.imp[2].banner).to.have.property('pos', 2); + expect(payload.imp[2].banner).to.have.property('format'); + expect(payload.imp[2].banner.format).to.deep.equal([{w: 300, h: 600}, {w: 160, h: 600}]); + + expect(payload.imp[3]).to.have.property('id', 'bid003'); + expect(payload.imp[3]).to.have.property('secure', 0); + expect(payload.imp[3]).to.have.property('bidfloor', 0); + expect(payload.imp[3]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(/^\d+\.\d+\.\d+$/); + 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('mimes'); + expect(payload.imp[3].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(payload.imp[3].video).to.have.property('protocols'); + expect(payload.imp[3].video.protocols).to.deep.equal([1, 2]); + expect(payload.imp[3].video).to.have.property('api'); + expect(payload.imp[3].video.api).to.deep.equal([2]); + expect(payload.imp[3].video).to.have.property('maxduration', 30); + expect(payload.imp[3]).to.not.have.property('banner'); + + expect(payload).to.have.property('site'); + expect(payload.site).to.have.property('id', siteId); + expect(payload.site).to.have.property('mobile').that.is.oneOf([0, 1]); + const loc = utils.getTopWindowLocation(); + const page = loc.pathname + loc.search + loc.hash; + expect(payload.site).to.have.property('page', page); + + expect(payload).to.have.property('device'); + expect(payload.device).to.have.property('w', screen.width); + expect(payload.device).to.have.property('h', screen.height); + expect(payload.device).to.have.property('dnt').that.is.oneOf([0, 1]); + expect(payload.device).to.have.property('ua', navigator.userAgent); + }); - it('Should create the format objects correctly.', function () { - adapter.callBids(bidderRequest); - server.respond(); + it('Verify interpretResponse', function() { + const request = spec.buildRequests(bidRequests); + const response = spec.interpretResponse(bidResponses, request); + expect(response).to.be.an('array').with.lengthOf(3); + + let bid = response[0]; + expect(bid).to.have.property('requestId', 'bid000'); + expect(bid).to.have.property('currency', 'USD'); + expect(bid).to.have.property('cpm', 0.99); + expect(bid).to.have.property('creativeId', '1000'); + expect(bid).to.have.property('width', 300); + expect(bid).to.have.property('height', 250); + expect(bid).to.have.property('ad', 'markup000<img src="notify000" />'); + + // There is no bid001 because cpm is $0 + + bid = response[1]; + expect(bid).to.have.property('requestId', 'bid002'); + expect(bid).to.have.property('currency', 'USD'); + expect(bid).to.have.property('cpm', 2.99); + expect(bid).to.have.property('creativeId', '1002'); + expect(bid).to.have.property('width', 300); + expect(bid).to.have.property('height', 600); + expect(bid).to.have.property('ad', 'markup002<img src="notify002" />'); + + bid = response[2]; + expect(bid).to.have.property('requestId', 'bid003'); + expect(bid).to.have.property('currency', 'USD'); + expect(bid).to.have.property('cpm', 3.99); + expect(bid).to.have.property('creativeId', '1003'); + expect(bid).to.have.property('width', 640); + expect(bid).to.have.property('height', 480); + expect(bid).to.have.property('vastUrl', 'markup003'); + expect(bid).to.have.property('mediaType', 'video'); + }); - var request = JSON.parse(server.requests[0].requestBody); - expect(request.imp[2].banner.format.length).to.equal(2); - expect(request.imp[2].banner.format[0].w).to.equal(300); - expect(request.imp[2].banner.format[1].w).to.equal(160); - }); + it('Verify handling of bad responses', function() { + let response = spec.interpretResponse({}, {}); + expect(response).to.be.an('array').with.lengthOf(0); + response = spec.interpretResponse({id: '123'}, {}); + expect(response).to.be.an('array').with.lengthOf(0); + response = spec.interpretResponse({id: '123', seatbid: []}, {}); + expect(response).to.be.an('array').with.lengthOf(0); }); -}); +}) From edc22740c37f0e0f5eaf0f28498ada6a8aa8d45d Mon Sep 17 00:00:00 2001 From: Jacek Drobiecki <jacek.drobiecki@gmail.com> Date: Thu, 26 Oct 2017 18:38:40 +0200 Subject: [PATCH 25/44] Add AdOcean adapter (#1735) * Initial revision of adocean bid adapter (ADOCEAN-13634, ADOCEAN-13635) * Minor fixes * new demo placement * formating after lint * move request parameters to params * adocean adpater tests * minor fixes * added ttl, netRevenue nad creativeId. merged with upstream --- modules/adoceanBidAdapter.js | 101 +++++++++++++ modules/adoceanBidAdapter.md | 30 ++++ test/spec/modules/adoceanBidAdapter_spec.js | 158 ++++++++++++++++++++ 3 files changed, 289 insertions(+) create mode 100644 modules/adoceanBidAdapter.js create mode 100644 modules/adoceanBidAdapter.md create mode 100644 test/spec/modules/adoceanBidAdapter_spec.js diff --git a/modules/adoceanBidAdapter.js b/modules/adoceanBidAdapter.js new file mode 100644 index 00000000000..1949e0414a7 --- /dev/null +++ b/modules/adoceanBidAdapter.js @@ -0,0 +1,101 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'adocean'; + +function buildEndpointUrl(emiter, payload) { + let payloadString = ''; + utils._each(payload, function(v, k) { + if (payloadString.length) { + payloadString += '&'; + } + payloadString += k + '=' + encodeURIComponent(v); + }); + + return 'https://' + emiter + '/ad.json?' + payloadString; +} + +function buildRequest(masterBidRequests, masterId) { + const firstBid = masterBidRequests[0]; + const payload = { + id: masterId, + }; + + const bidIdMap = {}; + + utils._each(masterBidRequests, function(v) { + bidIdMap[v.params.slaveId] = v.bidId; + }); + + return { + method: 'GET', + url: buildEndpointUrl(firstBid.params.emiter, payload), + data: {}, + bidIdMap: bidIdMap + }; +} + +function assignToMaster(bidRequest, bidRequestsByMaster) { + const masterId = bidRequest.params.masterId; + bidRequestsByMaster[masterId] = bidRequestsByMaster[masterId] || []; + bidRequestsByMaster[masterId].push(bidRequest); +} + +function interpretResponse(placementResponse, bidRequest, bids) { + if (!placementResponse.error) { + let adCode = '<script type="application/javascript">(function(){var wu="' + (placementResponse.winUrl || '') + '",su="' + (placementResponse.statsUrl || '') + '".replace(/\\[TIMESTAMP\\]/,(new Date()).getTime());'; + adCode += 'if(navigator.sendBeacon){if(wu){navigator.sendBeacon(wu)||((new Image(1,1)).src=wu)};if(su){navigator.sendBeacon(su)||((new Image(1,1)).src=su)}}'; + adCode += 'else{if(wu){(new Image(1,1)).src=wu;}if(su){(new Image(1,1)).src=su;}}'; + adCode += '})();<\/script>'; + adCode += decodeURIComponent(placementResponse.code); + + const bid = { + ad: adCode, + cpm: parseFloat(placementResponse.price), + currency: placementResponse.currency, + height: parseInt(placementResponse.height, 10), + requestId: bidRequest.bidIdMap[placementResponse.id], + width: parseInt(placementResponse.width, 10), + netRevenue: false, + ttl: parseInt(placementResponse.ttl), + creativeId: placementResponse.crid + }; + + bids.push(bid); + } +} + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + return !!(bid.params.slaveId && bid.params.masterId && bid.params.emiter); + }, + + buildRequests: function(validBidRequests) { + const bidRequestsByMaster = {}; + let requests = []; + + utils._each(validBidRequests, function(v) { + assignToMaster(v, bidRequestsByMaster); + }); + requests = utils._map(bidRequestsByMaster, function(v, k) { + return buildRequest(v, k); + }); + + return requests; + }, + + interpretResponse: function(serverResponse, bidRequest) { + let bids = []; + + if (utils.isArray(serverResponse.body)) { + utils._each(serverResponse.body, function(placementResponse) { + interpretResponse(placementResponse, bidRequest, bids); + }); + } + + return bids; + } +}; +registerBidder(spec); diff --git a/modules/adoceanBidAdapter.md b/modules/adoceanBidAdapter.md new file mode 100644 index 00000000000..748c4567fe2 --- /dev/null +++ b/modules/adoceanBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +Module Name: AdOcean Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@gemius.com + +# Description + +Module that connects to AdOcean demand sources. +Banner formats are supported. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "adocean", + params: { + slaveId: 'adoceanmyaozpniqismex', + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + emiter: 'myao.adocean.pl' + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/adoceanBidAdapter_spec.js b/test/spec/modules/adoceanBidAdapter_spec.js new file mode 100644 index 00000000000..149b9eb4d53 --- /dev/null +++ b/test/spec/modules/adoceanBidAdapter_spec.js @@ -0,0 +1,158 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adoceanBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; + +describe('AdoceanAdapter', () => { + 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', () => { + const bid = { + 'bidder': 'adocean', + 'params': { + 'masterId': 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + 'slaveId': 'adoceanmyaozpniqismex', + 'emiter': 'myao.adocean.pl' + }, + '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); + }); + + it('should return false when required params are not passed', () => { + const bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'masterId': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + const bidRequests = [ + { + 'bidder': 'adocean', + 'params': { + 'masterId': 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + 'slaveId': 'adoceanmyaozpniqismex', + 'emiter': 'myao.adocean.pl' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('should add bidIdMap with slaveId => bidId mapping', () => { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.bidIdMap).to.exists; + const bidIdMap = request.bidIdMap; + expect(bidIdMap[bidRequests[0].params.slaveId]).to.equal(bidRequests[0].bidId); + }); + + it('sends bid request to url via GET', () => { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.method).to.equal('GET'); + expect(request.url).to.match(new RegExp(`^https://${bidRequests[0].params.emiter}/ad.json`)); + }); + + it('should attach id to url', () => { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.url).to.include('id=' + bidRequests[0].params.masterId); + }); + }) + + describe('interpretResponse', () => { + const response = { + 'body': [ + { + 'id': 'adoceanmyaozpniqismex', + 'price': '0.019000', + 'winurl': '', + 'statsUrl': '', + 'code': '%3C!--%20Creative%20--%3E', + 'currency': 'EUR', + 'minFloorPrice': '0.01', + 'width': '300', + 'height': '250', + 'crid': '0af345b42983cc4bc0', + 'ttl': '300' + } + ], + 'headers': { + 'get': function() {} + } + }; + + const bidRequest = { + 'bidder': 'adocean', + 'params': { + 'masterId': 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + 'slaveId': 'adoceanmyaozpniqismex', + 'emiter': 'myao.adocean.pl' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'bidIdMap': { + 'adoceanmyaozpniqismex': '30b31c1838de1e' + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should get correct bid response', () => { + const expectedResponse = [ + { + 'requestId': '30b31c1838de1e', + 'cpm': 0.019000, + 'currency': 'EUR', + 'width': 300, + 'height': 250, + 'ad': '<!-- Creative -->', + 'creativeId': '0af345b42983cc4bc0', + 'ttl': 300, + 'netRevenue': false + } + ]; + + const result = spec.interpretResponse(response, bidRequest); + expect(result).to.have.lengthOf(1); + let resultKeys = Object.keys(result[0]); + expect(resultKeys.sort()).to.deep.equal(Object.keys(expectedResponse[0]).sort()); + resultKeys.forEach(function(k) { + if (k === 'ad') { + expect(result[0][k]).to.match(/<!-- Creative -->$/); + } else { + expect(result[0][k]).to.equal(expectedResponse[0][k]); + } + }); + }); + + it('handles nobid responses', () => { + response.body = [ + { + 'id': 'adoceanmyaolafpjwftbz', + 'error': 'true' + } + ]; + + const result = spec.interpretResponse(response, bidRequest); + expect(result).to.have.lengthOf(0); + }); + }); +}); From d4a14234ed52c700101dea46f96da1e639448a2a Mon Sep 17 00:00:00 2001 From: Denis Logachev <ckbo3hrk@gmail.com> Date: Thu, 26 Oct 2017 19:41:10 +0300 Subject: [PATCH 26/44] updated for prebid 1.0 api (#1722) --- modules/adkernelBidAdapter.js | 445 +++++++------------ modules/adkernelBidAdapter.md | 46 ++ test/spec/modules/adkernelBidAdapter_spec.js | 250 ++++------- 3 files changed, 289 insertions(+), 452 deletions(-) create mode 100644 modules/adkernelBidAdapter.md diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 855014c2411..095a6cec4b9 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -1,310 +1,185 @@ -import bidmanager from 'src/bidmanager'; -import bidfactory from 'src/bidfactory'; import * as utils from 'src/utils'; -import {ajax} from 'src/ajax'; -import Adapter from 'src/adapter'; -import adaptermanager from 'src/adaptermanager'; +import { BANNER, VIDEO } from 'src/mediaTypes'; +import {registerBidder} from 'src/adapters/bidderFactory'; + +const VIDEO_TARGETING = ['mimes', 'minduration', 'maxduration', 'protocols', + 'startdelay', 'linearity', 'boxingallowed', 'playbackmethod', 'delivery', + 'pos', 'api', 'ext']; +const VERSION = '1.0'; /** - * Adapter for requesting bids from AdKernel white-label platform - * @class + * Adapter for requesting bids from AdKernel white-label display platform */ -const AdKernelAdapter = function AdKernelAdapter() { - const AJAX_REQ_PARAMS = { - contentType: 'text/plain', - withCredentials: true, - method: 'GET' - }; - const EMPTY_BID_RESPONSE = {'seatbid': [{'bid': []}]}; - - const VIDEO_TARGETING = ['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'linearity', 'sequence', - 'boxingallowed', 'playbackmethod', 'delivery', 'pos', 'api', 'ext']; - - let baseAdapter = new Adapter('adkernel'); - - /** - * Helper object to build multiple bid requests in case of multiple zones/ad-networks - * @constructor - */ - function RtbRequestDispatcher() { - const _dispatch = {}; - const originalBids = {}; - const syncedHostZones = {}; - const site = createSite(); - - // translate adunit info into rtb impression dispatched by host/zone - this.addImp = function (bid) { - let host = bid.params.host; - let zone = bid.params.zoneId; - - if (!(host in _dispatch)) { - _dispatch[host] = {}; - } - /* istanbul ignore else */ - if (!(zone in _dispatch[host])) { - _dispatch[host][zone] = []; - } - let imp = buildImp(bid); - // save rtb impression for specified ad-network host and zone - _dispatch[host][zone].push(imp); - originalBids[bid.bidId] = bid; - // perform user-sync - if (!(host in syncedHostZones)) { - syncedHostZones[host] = []; - } - if (syncedHostZones[host].indexOf(zone) === -1) { - syncedHostZones[host].push(zone); - } - }; - - function buildImp(bid) { - const size = getBidSize(bid); - const imp = { - 'id': bid.bidId, - 'tagid': bid.placementCode - }; - - if (bid.mediaType === 'video') { - imp.video = {w: size[0], h: size[1]}; - if (bid.params.video) { - Object.keys(bid.params.video) - .filter(param => VIDEO_TARGETING.includes(param)) - .forEach(param => imp.video[param] = bid.params.video[param]); - } - } else { - imp.banner = {w: size[0], h: size[1]}; - } - if (utils.getTopWindowLocation().protocol === 'https:') { - imp.secure = 1; - } - return imp; - } - - function getBidSize(bid) { - if (bid.mediaType === 'video') { - return bid.sizes; - } - return bid.sizes[0]; - } - - /** - * Main function to get bid requests - */ - this.dispatch = function (callback) { - utils._each(_dispatch, (zones, host) => { - utils.logMessage(`processing network ${host}`); - utils._each(zones, (impressions, zone) => { - utils.logMessage(`processing zone ${zone}`); - dispatchRtbRequest(host, zone, impressions, callback); - }); - }); - }; - /** - * Build flat user-sync queue from host->zones mapping - */ - this.buildUserSyncQueue = function() { - return Object.keys(syncedHostZones) - .reduce((m, k) => { - syncedHostZones[k].forEach((v) => m.push([k, v])); - return m; - }, []); - }; - - function dispatchRtbRequest(host, zone, impressions, callback) { - let url = buildEndpointUrl(host); - let rtbRequest = buildRtbRequest(impressions); - let params = buildRequestParams(zone, rtbRequest); - ajax(url, (bidResp) => { - bidResp = bidResp === '' ? EMPTY_BID_RESPONSE : JSON.parse(bidResp); - utils._each(rtbRequest.imp, (imp) => { - let bidFound = false; - utils._each(bidResp.seatbid[0].bid, (bid) => { - /* istanbul ignore else */ - if (!bidFound && bid.impid === imp.id) { - bidFound = true; - callback(originalBids[imp.id], imp, bid); - } - }); - if (!bidFound) { - callback(originalBids[imp.id], imp); +export const spec = { + + code: 'adkernel', + aliases: ['headbidding'], + supportedMediaTypes: [VIDEO], + isBidRequestValid: function(bidRequest) { + return 'params' in bidRequest && typeof bidRequest.params.host !== 'undefined' && + 'zoneId' in bidRequest.params && !isNaN(Number(bidRequest.params.zoneId)); + }, + buildRequests: function(bidRequests) { + let auctionId; + let dispatch = bidRequests.map(buildImp) + .reduce((acc, curr, index) => { + let bidRequest = bidRequests[index]; + let zoneId = bidRequest.params.zoneId; + let host = bidRequest.params.host; + acc[host] = acc[host] || {}; + acc[host][zoneId] = acc[host][zoneId] || []; + acc[host][zoneId].push(curr); + auctionId = bidRequest.bidderRequestId; + return acc; + }, {}); + let requests = []; + Object.keys(dispatch).forEach(host => { + Object.keys(dispatch[host]).forEach(zoneId => { + const request = buildRtbRequest(dispatch[host][zoneId], auctionId); + requests.push({ + method: 'GET', + url: `${window.location.protocol}//${host}/rtbg`, + data: { + zone: Number(zoneId), + ad_type: 'rtb', + v: VERSION, + r: JSON.stringify(request) } }); - }, params, AJAX_REQ_PARAMS); - } - - /** - * Builds complete rtb bid request - * @param imps collection of impressions - */ - function buildRtbRequest(imps) { - return { - 'id': utils.getUniqueIdentifierStr(), - 'imp': imps, - 'site': site, - 'at': 1, - 'device': { - 'ip': 'caller', - 'ua': 'caller' - } - }; - } - - /** - * Build ad-network specific endpoint url - */ - function buildEndpointUrl(host) { - return `${window.location.protocol}//${host}/rtbg`; + }); + }); + return requests; + }, + interpretResponse: function(serverResponse, request) { + let response = serverResponse.body; + if (!response.seatbid) { + return []; } - function buildRequestParams(zone, rtbReq) { - return { - 'zone': encodeURIComponent(zone), - 'ad_type': 'rtb', - 'r': encodeURIComponent(JSON.stringify(rtbReq)) + let rtbRequest = JSON.parse(request.data.r); + let rtbImps = rtbRequest.imp; + let rtbBids = response.seatbid + .map(seatbid => seatbid.bid) + .reduce((a, b) => a.concat(b), []); + + return rtbBids.map(rtbBid => { + let imp = rtbImps.find(imp => imp.id === rtbBid.impid); + let prBid = { + requestId: rtbBid.impid, + cpm: rtbBid.price, + creativeId: rtbBid.crid, + currency: 'USD', + ttl: 360, + netRevenue: true }; - } - } - - /** - * Main module export function implementation - */ - baseAdapter.callBids = function (params) { - var bids = params.bids || []; - processBids(bids); - }; - - /** - * Process all bids grouped by network/zone - */ - function processBids(bids) { - const dispatcher = new RtbRequestDispatcher(); - // process individual bids - utils._each(bids, (bid) => { - if (!validateBidParams(bid.params)) { - utils.logError(`Incorrect configuration for adkernel bidder: ${bid.params}`); - bidmanager.addBidResponse(bid.placementCode, createEmptyBidObject(bid)); - } else { - dispatcher.addImp(bid); + if ('banner' in imp) { + prBid.mediaType = BANNER; + prBid.width = imp.banner.w; + prBid.height = imp.banner.h; + prBid.ad = formatAdMarkup(rtbBid); } - }); - // start async usersync - processUserSyncQueue(dispatcher.buildUserSyncQueue()); - - // process bids grouped into bid requests - dispatcher.dispatch((bid, imp, bidResp) => { - let adUnitId = bid.placementCode; - if (bidResp) { - utils.logMessage(`got response for ${adUnitId}`); - let dimensions = getCreativeSize(imp, bidResp); - bidmanager.addBidResponse(adUnitId, createBidObject(bidResp, bid, dimensions.w, dimensions.h)); - } else { - utils.logMessage(`got empty response for ${adUnitId}`); - bidmanager.addBidResponse(adUnitId, createEmptyBidObject(bid)); + if ('video' in imp) { + prBid.mediaType = VIDEO; + prBid.vastUrl = rtbBid.nurl; + prBid.width = imp.video.w; + prBid.height = imp.video.h; } + return prBid; }); - } - - /** - * Evaluate creative size from response or from request - */ - function getCreativeSize(imp, bid) { - let dimensions = (bid.h && bid.w) ? bid : (imp.banner || imp.video); - return { - w: dimensions.w, - h: dimensions.h - }; - } - - /** - * Create bid object for the bid manager - */ - function createBidObject(resp, bid, width, height) { - let bidObj = Object.assign(bidfactory.createBid(1, bid), { - bidderCode: bid.bidder, - width: width, - height: height, - cpm: parseFloat(resp.price) - }); - if (bid.mediaType === 'video') { - bidObj.vastUrl = resp.nurl; - bidObj.mediaType = 'video'; - } else { - bidObj.ad = formatAdMarkup(resp); + }, + getUserSyncs: function(syncOptions, serverResponses) { + if (!syncOptions.iframeEnabled || !serverResponses || serverResponses.length === 0) { + return []; } - return bidObj; + return serverResponses.filter(rsp => rsp.body && rsp.body.ext && rsp.body.ext.adk_usersync) + .map(rsp => rsp.body.ext.adk_usersync) + .reduce((a, b) => a.concat(b), []) + .map(sync_url => { + return { + type: 'iframe', + url: sync_url + } + }); } +}; - /** - * Create empty bid object for the bid manager - */ - function createEmptyBidObject(bid) { - return Object.assign(bidfactory.createBid(2, bid), { - bidderCode: bid.bidder - }); - } +registerBidder(spec); - /** - * Format creative with optional nurl call - */ - function formatAdMarkup(bid) { - var adm = bid.adm; - if ('nurl' in bid) { - adm += utils.createTrackPixelHtml(`${bid.nurl}&px=1`); - } - return adm; - } +/** + * Builds parameters object for single impression + */ +function buildImp(bid) { + const size = getAdUnitSize(bid); + const imp = { + 'id': bid.bidId, + 'tagid': bid.placementCode + }; - function validateBidParams(params) { - return typeof params.host !== 'undefined' && typeof params.zoneId !== 'undefined'; + if (bid.mediaType === 'video') { + imp.video = {w: size[0], h: size[1]}; + if (bid.params.video) { + Object.keys(bid.params.video) + .filter(param => VIDEO_TARGETING.includes(param)) + .forEach(param => imp.video[param] = bid.params.video[param]); + } + } else { + imp.banner = {w: size[0], h: size[1]}; } - - /** - * Creates site description object - */ - function createSite() { - var location = utils.getTopWindowLocation(); - return { - 'domain': location.hostname, - 'page': location.href.split('?')[0] - }; + if (utils.getTopWindowLocation().protocol === 'https:') { + imp.secure = 1; } + return imp; +} - /** - * Recursively process user-sync queue - */ - function processUserSyncQueue(queue) { - if (queue.length === 0) { - return; - } - let entry = queue.pop(); - insertUserSync(entry[0], entry[1], () => processUserSyncQueue(queue)); +/** + * Return ad unit single size + * @param bid adunit size definition + * @return {*} + */ +function getAdUnitSize(bid) { + if (bid.mediaType === 'video') { + return bid.sizes; } + return bid.sizes[0]; +} - /** - * Insert single iframe user-sync - */ - function insertUserSync(host, zone, callback) { - var iframe = utils.createInvisibleIframe(); - iframe.src = `//sync.adkernel.com/user-sync?zone=${zone}&r=%2F%2F${host}%2Fuser-synced%3Fuid%3D%7BUID%7D`; - utils.addEventHandler(iframe, 'load', callback); - try { - document.body.appendChild(iframe); - } catch (error) { - /* istanbul ignore next */ - utils.logError(error); +/** + * Builds complete rtb request + * @param imps collection of impressions + * @param auctionId + */ +function buildRtbRequest(imps, auctionId) { + return { + 'id': auctionId, + 'imp': imps, + 'site': createSite(), + 'at': 1, + 'device': { + 'ip': 'caller', + 'ua': 'caller' } - } - - return Object.assign(this, { - callBids: baseAdapter.callBids, - setBidderCode: baseAdapter.setBidderCode, - getBidderCode: baseAdapter.getBidderCode - }); -}; + }; +} -adaptermanager.registerBidAdapter(new AdKernelAdapter(), 'adkernel', { - supportedMediaTypes: ['video'] -}); -adaptermanager.aliasBidAdapter('adkernel', 'headbidding'); +/** + * Creates site description object + */ +function createSite() { + var location = utils.getTopWindowLocation(); + return { + 'domain': location.hostname, + 'page': location.href.split('?')[0] + }; +} -module.exports = AdKernelAdapter; +/** + * Format creative with optional nurl call + * @param bid rtb Bid object + */ +function formatAdMarkup(bid) { + var adm = bid.adm; + if ('nurl' in bid) { + adm += utils.createTrackPixelHtml(`${bid.nurl}&px=1`); + } + return `<!DOCTYPE html><html><head><title></title><body style='margin:0px;padding:0px;'>${adm}</body></head>`; +} diff --git a/modules/adkernelBidAdapter.md b/modules/adkernelBidAdapter.md new file mode 100644 index 00000000000..bc30e6cc8e5 --- /dev/null +++ b/modules/adkernelBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: AdKernel Bidder Adapter +Module Type: Bidder Adapter +Maintainer: denis@adkernel.com +``` + +# Description + +Connects to AdKernel whitelabel platform. +Banner and video formats are supported. +The AdKernel adapter doesn't support multiple sizes per ad-unit and will use the first one if multiple sizes are defined. + + +# Test Parameters +``` + var adUnits = [ + { + code: 'banner-ad-div', + sizes: [[300, 250]], // banner size + bids: [ + { + bidder: 'adkernel', + params: { + zoneId: '30164', //required parameter + host: 'cpm.metaadserving.com' //required parameter + } + } + ] + }, { + code: 'video-ad-player', + sizes: [640, 480], // video player size + bids: [ + { + bidder: 'adkernel', + mediaType : 'video', + params: { + zoneId: '30164', //required parameter + host: 'cpm.metaadserving.com' //required parameter + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 5fabbad7fbf..b958e96f656 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -1,9 +1,6 @@ import {expect} from 'chai'; -import Adapter from 'modules/adkernelBidAdapter'; -import * as ajax from 'src/ajax'; +import {spec} from 'modules/adkernelBidAdapter'; import * as utils from 'src/utils'; -import bidmanager from 'src/bidmanager'; -import CONSTANTS from 'src/constants.json'; describe('Adkernel adapter', () => { const bid1_zone1 = { @@ -36,6 +33,12 @@ describe('Adkernel adapter', () => { params: {zoneId: 1}, placementCode: 'ad-unit-1', sizes: [[728, 90]] + }, bid_with_wrong_zoneId = { + bidder: 'adkernel', + bidId: 'Bid_02', + params: {zoneId: 'wrong id', host: 'rtb.adkernel.com'}, + placementCode: 'ad-unit-2', + sizes: [[728, 90]] }, bid_video = { bidder: 'adkernel', bidId: 'Bid_Video', @@ -57,18 +60,23 @@ describe('Adkernel adapter', () => { bid: [{ id: '1', impid: 'Bid_01', + crid: '100_001', price: 3.01, nurl: 'https://rtb.com/win?i=ZjKoPYSFI3Y_0', adm: '<!-- admarkup here -->' }] }], - cur: 'USD' + cur: 'USD', + ext: { + adk_usersync: ['http://adk.sync.com/sync'] + } }, bidResponse2 = { id: 'bid2', seatbid: [{ bid: [{ id: '2', impid: 'Bid_02', + crid: '100_002', price: 1.31, adm: '<!-- admarkup here -->' }] @@ -80,85 +88,58 @@ describe('Adkernel adapter', () => { bid: [{ id: 'sZSYq5zYMxo_0', impid: 'Bid_Video', + crid: '100_003', price: 0.00145, adid: '158801', nurl: 'https://rtb.com/win?i=sZSYq5zYMxo_0&f=nurl', - cid: '16855', - crid: '158801', - w: 600, - h: 400 + cid: '16855' }] }], cur: 'USD' + }, usersyncOnlyResponse = { + id: 'nobid1', + ext: { + adk_usersync: ['http://adk.sync.com/sync'] + } }; - let adapter, - sandbox, - ajaxStub; - - beforeEach(() => { - sandbox = sinon.sandbox.create(); - adapter = new Adapter(); - ajaxStub = sandbox.stub(ajax, 'ajax'); - }); - - afterEach(() => { - sandbox.restore(); - }); - - function doRequest(bids) { - adapter.callBids({ - bidderCode: 'adkernel', - bids: bids - }); - } - describe('input parameters validation', () => { - let spy; - - beforeEach(() => { - spy = sandbox.spy(); - sandbox.stub(bidmanager, 'addBidResponse'); - }); - it('empty request shouldn\'t generate exception', () => { - expect(adapter.callBids({ + expect(spec.isBidRequestValid({ bidderCode: 'adkernel' - })).to.be.an('undefined'); + })).to.be.equal(false); }); it('request without zone shouldn\'t issue a request', () => { - doRequest([bid_without_zone]); - sinon.assert.notCalled(ajaxStub); - expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); - expect(bidmanager.addBidResponse.firstCall.args[1].bidderCode).to.equal('adkernel'); + expect(spec.isBidRequestValid(bid_without_zone)).to.be.equal(false); }); it('request without host shouldn\'t issue a request', () => { - doRequest([bid_without_host]); - sinon.assert.notCalled(ajaxStub); - expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); - expect(bidmanager.addBidResponse.firstCall.args[1].bidderCode).to.equal('adkernel'); + expect(spec.isBidRequestValid(bid_without_host)).to.be.equal(false); + }); + + it('empty request shouldn\'t generate exception', () => { + expect(spec.isBidRequestValid(bid_with_wrong_zoneId)).to.be.equal(false); }); }); describe('banner request building', () => { let bidRequest; + let mock; - beforeEach(() => { - sandbox.stub(utils, 'getTopWindowLocation', () => { + before(() => { + mock = sinon.stub(utils, 'getTopWindowLocation', () => { return { protocol: 'https:', hostname: 'example.com', host: 'example.com', pathname: '/index.html', - href: 'http://example.com/index.html' + href: 'https://example.com/index.html' }; }); - - ajaxStub.onCall(0).callsArgWith(1, JSON.stringify(bidResponse1)); - doRequest([bid1_zone1]); - bidRequest = JSON.parse(decodeURIComponent(ajaxStub.getCall(0).args[2].r)); + let request = spec.buildRequests([bid1_zone1])[0]; + bidRequest = JSON.parse(request.data.r); + mock.restore(); }); it('should be a first-price auction', () => { @@ -184,7 +165,7 @@ describe('Adkernel adapter', () => { it('should create proper site block', () => { expect(bidRequest.site).to.have.property('domain', 'example.com'); - expect(bidRequest.site).to.have.property('page', 'http://example.com/index.html'); + expect(bidRequest.site).to.have.property('page', 'https://example.com/index.html'); }); it('should fill device with caller macro', () => { @@ -197,19 +178,9 @@ describe('Adkernel adapter', () => { describe('video request building', () => { let bidRequest; - beforeEach(() => { - sandbox.stub(utils, 'getTopWindowLocation', () => { - return { - protocol: 'https:', - hostname: 'example.com', - host: 'example.com', - pathname: '/index.html', - href: 'http://example.com/index.html' - }; - }); - ajaxStub.onCall(0).callsArgWith(1, JSON.stringify(videoBidResponse)); - doRequest([bid_video]); - bidRequest = JSON.parse(decodeURIComponent(ajaxStub.getCall(0).args[2].r)); + before(() => { + let request = spec.buildRequests([bid_video])[0]; + bidRequest = JSON.parse(request.data.r); }); it('should have video object', () => { @@ -227,123 +198,68 @@ describe('Adkernel adapter', () => { }); describe('requests routing', () => { - it('should issue a request for each network', () => { - ajaxStub.onFirstCall().callsArgWith(1, '') - .onSecondCall().callsArgWith(1, ''); - doRequest([bid1_zone1, bid3_host2]); - expect(ajaxStub.calledTwice); - expect(ajaxStub.firstCall.args[0]).to.include(bid1_zone1.params.host); - expect(ajaxStub.secondCall.args[0]).to.include(bid3_host2.params.host); + it('should issue a request for each host', () => { + let pbRequests = spec.buildRequests([bid1_zone1, bid3_host2]); + expect(pbRequests).to.have.length(2); + expect(pbRequests[0].url).to.have.string(`//${bid1_zone1.params.host}/`); + expect(pbRequests[1].url).to.have.string(`//${bid3_host2.params.host}/`); }); it('should issue a request for each zone', () => { - ajaxStub.onCall(0).callsArgWith(1, JSON.stringify(bidResponse1)); - ajaxStub.onCall(1).callsArgWith(1, JSON.stringify(bidResponse2)); - doRequest([bid1_zone1, bid2_zone2]); - expect(ajaxStub.calledTwice); - }); - - it('should route calls to proper zones', () => { - ajaxStub.onCall(0).callsArgWith(1, JSON.stringify(bidResponse1)); - ajaxStub.onCall(1).callsArgWith(1, JSON.stringify(bidResponse2)); - doRequest([bid1_zone1, bid2_zone2]); - expect(ajaxStub.firstCall.args[2].zone).to.equal('1'); - expect(ajaxStub.secondCall.args[2].zone).to.equal('2'); + let pbRequests = spec.buildRequests([bid1_zone1, bid2_zone2]); + expect(pbRequests).to.have.length(2); + expect(pbRequests[0].data.zone).to.be.equal(bid1_zone1.params.zoneId); + expect(pbRequests[1].data.zone).to.be.equal(bid2_zone2.params.zoneId); }); }); describe('responses processing', () => { - beforeEach(() => { - sandbox.stub(bidmanager, 'addBidResponse'); - }); - - it('should return fully-initialized bid-response', () => { - ajaxStub.onCall(0).callsArgWith(1, JSON.stringify(bidResponse1)); - doRequest([bid1_zone1]); - let bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal('ad-unit-1'); - expect(bidResponse.getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bidResponse.bidderCode).to.equal('adkernel'); - expect(bidResponse.cpm).to.equal(3.01); - expect(bidResponse.ad).to.include('<!-- admarkup here -->'); - expect(bidResponse.width).to.equal(300); - expect(bidResponse.height).to.equal(250); + it('should return fully-initialized banner bid-response', () => { + let request = spec.buildRequests([bid1_zone1])[0]; + let resp = spec.interpretResponse({body: bidResponse1}, request)[0]; + expect(resp).to.have.property('requestId', 'Bid_01'); + expect(resp).to.have.property('cpm', 3.01); + expect(resp).to.have.property('width', 300); + expect(resp).to.have.property('height', 250); + expect(resp).to.have.property('creativeId', '100_001'); + 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('<!-- admarkup here -->'); }); it('should return fully-initialized video bid-response', () => { - ajaxStub.onCall(0).callsArgWith(1, JSON.stringify(videoBidResponse)); - doRequest([bid_video]); - let bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal('ad-unit-1'); - expect(bidResponse.getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bidResponse.mediaType).to.equal('video'); - expect(bidResponse.bidderCode).to.equal('adkernel'); - expect(bidResponse.cpm).to.equal(0.00145); - expect(bidResponse.vastUrl).to.equal('https://rtb.com/win?i=sZSYq5zYMxo_0&f=nurl'); - expect(bidResponse.width).to.equal(600); - expect(bidResponse.height).to.equal(400); - }); - - it('should map responses to proper ad units', () => { - ajaxStub.onCall(0).callsArgWith(1, JSON.stringify(bidResponse1)); - ajaxStub.onCall(1).callsArgWith(1, JSON.stringify(bidResponse2)); - doRequest([bid1_zone1, bid2_zone2]); - expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bidmanager.addBidResponse.firstCall.args[1].bidderCode).to.equal('adkernel'); - expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal('ad-unit-1'); - expect(bidmanager.addBidResponse.secondCall.args[1].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bidmanager.addBidResponse.secondCall.args[1].bidderCode).to.equal('adkernel'); - expect(bidmanager.addBidResponse.secondCall.args[0]).to.equal('ad-unit-2'); - }); - - it('should process empty responses', () => { - ajaxStub.onCall(0).callsArgWith(1, JSON.stringify(bidResponse1)); - ajaxStub.onCall(1).callsArgWith(1, ''); - doRequest([bid1_zone1, bid2_zone2]); - expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bidmanager.addBidResponse.firstCall.args[1].bidderCode).to.equal('adkernel'); - expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal('ad-unit-1'); - expect(bidmanager.addBidResponse.secondCall.args[1].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); - expect(bidmanager.addBidResponse.secondCall.args[1].bidderCode).to.equal('adkernel'); - expect(bidmanager.addBidResponse.secondCall.args[0]).to.equal('ad-unit-2'); + let request = spec.buildRequests([bid_video])[0]; + let resp = spec.interpretResponse({body: videoBidResponse}, request)[0]; + expect(resp).to.have.property('requestId', 'Bid_Video'); + expect(resp.mediaType).to.equal('video'); + expect(resp.cpm).to.equal(0.00145); + expect(resp.vastUrl).to.equal('https://rtb.com/win?i=sZSYq5zYMxo_0&f=nurl'); + expect(resp.width).to.equal(640); + expect(resp.height).to.equal(480); }); it('should add nurl as pixel for banner response', () => { - sandbox.spy(utils, 'createTrackPixelHtml'); - ajaxStub.onCall(0).callsArgWith(1, JSON.stringify(bidResponse1)); - doRequest([bid1_zone1]); - expect(utils.createTrackPixelHtml.calledOnce); - expect(bidmanager.addBidResponse.firstCall.args[1].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); + let request = spec.buildRequests([bid1_zone1])[0]; + let resp = spec.interpretResponse({body: bidResponse1}, request)[0]; let expectedNurl = bidResponse1.seatbid[0].bid[0].nurl + '&px=1'; - expect(bidmanager.addBidResponse.firstCall.args[1].ad).to.include(expectedNurl); + expect(resp.ad).to.have.string(expectedNurl); }); - it('should perform usersync for each unique host/zone combination', () => { - ajaxStub.callsArgWith(1, ''); - const expectedSyncUrls = ['//sync.adkernel.com/user-sync?zone=1&r=%2F%2Frtb-private.adkernel.com%2Fuser-synced%3Fuid%3D%7BUID%7D', - '//sync.adkernel.com/user-sync?zone=2&r=%2F%2Frtb.adkernel.com%2Fuser-synced%3Fuid%3D%7BUID%7D', - '//sync.adkernel.com/user-sync?zone=1&r=%2F%2Frtb.adkernel.com%2Fuser-synced%3Fuid%3D%7BUID%7D']; - let userSyncUrls = []; - sandbox.stub(utils, 'createInvisibleIframe', () => { - return {}; - }); - sandbox.stub(utils, 'addEventHandler', (el, ev, cb) => { - userSyncUrls.push(el.src); - cb(); // instant callback - }); - doRequest([bid1_zone1, bid2_zone2, bid2_zone2, bid3_host2]); - expect(utils.createInvisibleIframe.calledThrice); - expect(userSyncUrls).to.be.eql(expectedSyncUrls); + it('should handle bidresponse with user-sync only', () => { + let request = spec.buildRequests([bid1_zone1])[0]; + let resp = spec.interpretResponse({body: usersyncOnlyResponse}, request); + expect(resp).to.have.length(0); }); - }); - - describe('adapter aliasing', () => { - const ALIAS_NAME = 'adkernelAlias'; - it('should allow bidder code changing', () => { - expect(adapter.getBidderCode()).to.equal('adkernel'); - adapter.setBidderCode(ALIAS_NAME); - expect(adapter.getBidderCode()).to.equal(ALIAS_NAME); + it('should perform usersync', () => { + let syncs = spec.getUserSyncs({iframeEnabled: false}, [{body: bidResponse1}]); + expect(syncs).to.have.length(0); + syncs = spec.getUserSyncs({iframeEnabled: true}, [{body: bidResponse1}]); + expect(syncs).to.have.length(1); + expect(syncs[0]).to.have.property('type', 'iframe'); + expect(syncs[0]).to.have.property('url', 'http://adk.sync.com/sync'); }); }); }); From ebaec7a060b3ec095ab059e411547989764027d5 Mon Sep 17 00:00:00 2001 From: Samuel Horwitz <samuelhorwitz@users.noreply.github.com> Date: Thu, 26 Oct 2017 12:48:29 -0400 Subject: [PATCH 27/44] Kargo Adapter for Prebid 1.0 (#1729) --- modules/kargoBidAdapter.js | 172 +++---- modules/kargoBidAdapter.md | 23 + test/spec/modules/kargoBidAdapter_spec.js | 594 +++++++++++----------- 3 files changed, 404 insertions(+), 385 deletions(-) create mode 100644 modules/kargoBidAdapter.md diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 433cbb15433..154e4a7b3b7 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -1,60 +1,60 @@ -const bidfactory = require('src/bidfactory.js'); -const bidmanager = require('src/bidmanager.js'); -const adloader = require('src/adloader.js'); -const utils = require('src/utils.js'); -const adaptermanager = require('src/adaptermanager'); -const CONSTANTS = require('src/constants.json'); -const HOST = $$PREBID_GLOBAL$$.kargo_kraken_host || 'https://krk.kargo.com'; - -const KargoAdapter = function KargoAdapter() { - function _handleBid(bids) { - return function wrappedHandleBid(adUnits) { - utils._map(bids, bid => { - let adUnit = adUnits[bid.params.placementId]; - - if (adUnit) { - bidmanager.addBidResponse(bid.placementCode, _createBid(adUnit)); - - if (adUnit.receivedTracker) { - var el = document.createElement('img'); - el.src = adUnit.receivedTracker; - document.body.appendChild(el); - } - } - }); - }; - } - - function _createBid(adUnit) { - let bidObject = bidfactory.createBid(CONSTANTS.STATUS.GOOD); - bidObject.bidderCode = 'kargo'; - bidObject.cpm = Number(adUnit.cpm); - bidObject.ad = adUnit.adm; - bidObject.width = adUnit.width; - bidObject.height = adUnit.height; - return bidObject; - } - - function _callBids(params) { +import * as utils from 'src/utils'; +import {config} from 'src/config'; +import {registerBidder} from 'src/adapters/bidderFactory'; +const BIDDER_CODE = 'kargo'; +const HOST = 'https://krk.kargo.com'; +export const spec = { + code: BIDDER_CODE, + isBidRequestValid: function(bid) { + if (!bid || !bid.params) { + return false; + } + return !!bid.params.placementId; + }, + buildRequests: function(validBidRequests, bidderRequest) { + const currency = config.getConfig('currency'); const transformedParams = Object.assign({}, { - timeout: params.timeout, - currency: 'USD', + timeout: bidderRequest.timeout, + currency: currency, cpmGranularity: 1, cpmRange: { floor: 0, ceil: 20 }, - adSlotIds: utils._map(params.bids, bid => bid.params.placementId) - }, _getAllMetadata()); + adSlotIds: utils._map(validBidRequests, bid => bid.params.placementId) + }, spec._getAllMetadata()); const encodedParams = encodeURIComponent(JSON.stringify(transformedParams)); - const callbackName = `kargo_prebid_${params.requestId.replace(/-/g, '_')}`; - - window.$$PREBID_GLOBAL$$[callbackName] = _handleBid(params.bids); - - adloader.loadScript(`${HOST}/api/v1/bid?json=${encodedParams}&cb=window.$$PREBID_GLOBAL$$.${callbackName}`); - } + return Object.assign({}, bidderRequest, { + method: 'GET', + url: `${HOST}/api/v1/bid`, + data: `json=${encodedParams}`, + currency: currency + }); + }, + interpretResponse: function(response, bidRequest) { + let adUnits = response.body; + let bids = {}; + utils._each(bidRequest.bids, bid => bids[bid.params.placementId] = bid); + const bidResponses = []; + for (let adUnitId in adUnits) { + let adUnit = adUnits[adUnitId]; + bidResponses.push({ + requestId: bids[adUnitId].bidId, + cpm: Number(adUnit.cpm), + width: adUnit.width, + height: adUnit.height, + ad: adUnit.adm, + ttl: 300, + creativeId: adUnitId, + netRevenue: true, + currency: bidRequest.currency + }); + } + return bidResponses; + }, - function _readCookie(name) { + // PRIVATE + _readCookie(name) { let nameEquals = `${name}=`; let cookies = document.cookie.split(';'); @@ -70,15 +70,15 @@ const KargoAdapter = function KargoAdapter() { } return null; - } + }, - function _getCrbIds() { + _getCrbIds() { try { - const crb = JSON.parse(decodeURIComponent(_readCookie('krg_crb'))); - var syncIds = {}; + const crb = JSON.parse(decodeURIComponent(spec._readCookie('krg_crb'))); + let syncIds = {}; if (crb && crb.v) { - var vParsed = JSON.parse(atob(crb.v)); + let vParsed = JSON.parse(atob(crb.v)); if (vParsed && vParsed.syncIds) { syncIds = vParsed.syncIds; @@ -89,12 +89,12 @@ const KargoAdapter = function KargoAdapter() { } catch (e) { return {}; } - } + }, - function _getUid() { + _getUid() { try { - const uid = JSON.parse(decodeURIComponent(_readCookie('krg_uid'))); - var vData = {}; + const uid = JSON.parse(decodeURIComponent(spec._readCookie('krg_uid'))); + let vData = {}; if (uid && uid.v) { vData = uid.v; @@ -104,41 +104,41 @@ const KargoAdapter = function KargoAdapter() { } catch (e) { return {}; } - } + }, - function _getKruxUserId() { - return _getLocalStorageSafely('kxkar_user'); - } + _getKruxUserId() { + return spec._getLocalStorageSafely('kxkar_user'); + }, - function _getKruxSegments() { - return _getLocalStorageSafely('kxkar_segs'); - } + _getKruxSegments() { + return spec._getLocalStorageSafely('kxkar_segs'); + }, - function _getKrux() { - const segmentsStr = _getKruxSegments(); - var segments = []; + _getKrux() { + const segmentsStr = spec._getKruxSegments(); + let segments = []; if (segmentsStr) { segments = segmentsStr.split(','); } return { - userID: _getKruxUserId(), + userID: spec._getKruxUserId(), segments: segments }; - } + }, - function _getLocalStorageSafely(key) { + _getLocalStorageSafely(key) { try { return localStorage.getItem(key); } catch (e) { return null; } - } + }, - function _getUserIds() { - const uid = _getUid(); - const crbIds = _getCrbIds(); + _getUserIds() { + const uid = spec._getUid(); + const crbIds = spec._getCrbIds(); return { kargoID: uid.userId, @@ -146,23 +146,15 @@ const KargoAdapter = function KargoAdapter() { crbIDs: crbIds, optOut: uid.optOut }; - } + }, - function _getAllMetadata() { + _getAllMetadata() { return { - userIDs: _getUserIds(), - krux: _getKrux(), - pageURL: window.location.href + userIDs: spec._getUserIds(), + krux: spec._getKrux(), + pageURL: window.location.href, + rawCRB: spec._readCookie('krg_crb') }; } - - // Export the callBids function, so that prebid.js can execute - // this function when the page asks to send out bid requests. - return { - callBids: _callBids - }; }; - -adaptermanager.registerBidAdapter(new KargoAdapter(), 'kargo'); - -module.exports = KargoAdapter; +registerBidder(spec); diff --git a/modules/kargoBidAdapter.md b/modules/kargoBidAdapter.md new file mode 100644 index 00000000000..99fe82e7f54 --- /dev/null +++ b/modules/kargoBidAdapter.md @@ -0,0 +1,23 @@ +# Overview + +**Module Name**: Kargo Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: kraken@kargo.com + +# Description + +Please use `kargo` as the bidder code. Also, you *must* test on a mobile device, or emulate a mobile device by manipulating the user agent string sent to the server. + +# Test Parameters +``` + var adUnits = [{ + code: 'div-gpt-ad-1460505748561-1', + sizes: [[300,250],[1,1]], + bids: [{ + bidder: 'kargo', + params: { + placementId: '_m1Xt2E5dez' + } + }] + }]; +``` diff --git a/test/spec/modules/kargoBidAdapter_spec.js b/test/spec/modules/kargoBidAdapter_spec.js index f74457d9f9b..87c16dabfae 100644 --- a/test/spec/modules/kargoBidAdapter_spec.js +++ b/test/spec/modules/kargoBidAdapter_spec.js @@ -1,25 +1,49 @@ -describe('kargo adapter tests', function () { - const expect = require('chai').expect; - const assert = require('chai').assert; - const adapter = require('modules/kargoBidAdapter'); - const bidmanager = require('src/bidmanager'); - const bidfactory = require('src/bidfactory'); - const adloader = require('src/adloader'); - const CONSTANTS = require('src/constants.json'); +import {expect, assert} from 'chai'; +import {spec} from 'modules/kargoBidAdapter'; +import {registerBidder} from 'src/adapters/bidderFactory'; +import {config} from 'src/config'; - var sandbox, params, krakenParams, adUnits, bidFactorySpy, addBidResponseSpy, bodyAppendSpy, cookies = [], localStorageItems = []; +describe('kargo adapter tests', function () { + var sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); - addBidResponseSpy = sandbox.stub(bidmanager, 'addBidResponse'); - bodyAppendSpy = sandbox.stub(document.body, 'appendChild'); - simulateBidFactory(); - simulateAdLoader(); - - params = { - timeout: 200, - requestId: 'f4cf851b-665a-43d7-b22c-33c8fdebe577', - bids: [ + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('bid request validity', function() { + it('passes when the bid includes a placement ID', function() { + assert(spec.isBidRequestValid({params: {placementId: 'foo'}}) === true); + }); + + it('fails when the bid does not include a placement ID', function() { + assert(spec.isBidRequestValid({params: {}}) === false); + }); + + it('fails when bid is falsey', function() { + assert(spec.isBidRequestValid() === false); + }); + + it('fails when the bid has no params at all', function() { + assert(spec.isBidRequestValid({}) === false); + }); + }); + + describe('build request', function() { + var bids, cookies = [], localStorageItems = []; + + beforeEach(() => { + sandbox.stub(config, 'getConfig', function(key) { + if (key === 'currency') { + return 'USD'; + } + throw new Error(`Config stub incomplete! Missing key "${key}"`) + }); + + bids = [ { params: { placementId: 'foo' @@ -32,321 +56,301 @@ describe('kargo adapter tests', function () { }, placementCode: 2 } - ] - }; + ]; + }); - adUnits = { - foo: { - receivedTracker: 'fake-tracker-1', - cpm: 3, - adm: '<div id="1"></div>', - width: 320, - height: 50 - }, - bar: { - cpm: 2.5, - adm: '<div id="2"></div>', - width: 300, - height: 250 + afterEach(() => { + for (let key in cookies) { + let cookie = cookies[key]; + removeCookie(cookie); } + + for (let key in localStorageItems) { + let localStorageItem = localStorageItems[key]; + localStorage.removeItem(localStorageItem); + } + + cookies.length = 0; + localStorageItems.length = 0; + }); + + function setCookie(cname, cvalue, exdays = 1) { + _setCookie(cname, cvalue, exdays); + cookies.push(cname); } - }); - afterEach(() => { - sandbox.restore(); + function removeCookie(cname) { + _setCookie(cname, '', -1); + } - for (let key in cookies) { - let cookie = cookies[key]; - removeCookie(cookie); + function _setCookie(cname, cvalue, exdays = 1) { + var d = new Date(), + expires; + + d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); + expires = `expires=${d.toUTCString()}`; + document.cookie = `${cname}=${cvalue};${expires};path=/`; } - for (let key in localStorageItems) { - let localStorageItem = localStorageItems[key]; - localStorage.removeItem(localStorageItem); + function setLocalStorageItem(name, val) { + localStorage.setItem(name, val); + localStorageItems.push(name); } - cookies.length = 0; - localStorageItems.length = 0; - }); + function simulateNoLocalStorage() { + return sandbox.stub(localStorage, 'getItem').throws(); + } - function setCookie(cname, cvalue, exdays = 1) { - _setCookie(cname, cvalue, exdays); - cookies.push(cname); - } - - function removeCookie(cname) { - _setCookie(cname, '', -1); - } - - function _setCookie(cname, cvalue, exdays = 1) { - var d = new Date(), - expires; - - d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); - expires = `expires=${d.toUTCString()}`; - document.cookie = `${cname}=${cvalue};${expires};path=/`; - } - - function setLocalStorageItem(name, val) { - localStorage.setItem(name, val); - localStorageItems.push(name); - } - - function simulateAdLoader() { - sandbox.stub(adloader, 'loadScript', (url) => { - window.$$PREBID_GLOBAL$$.kargo_prebid_f4cf851b_665a_43d7_b22c_33c8fdebe577(adUnits); - krakenParams = JSON.parse(decodeURIComponent(url.match(/\?json=(.*)&cb=/)[1])); - }); - } + function initializeKruxUser() { + setLocalStorageItem('kxkar_user', 'rsgr9pnij'); + } - function simulateNoLocalStorage() { - return sandbox.stub(localStorage, 'getItem').throws(); - } + function initializeKruxSegments() { + setLocalStorageItem('kxkar_segs', 'qv9v984dy,rpx2gy365,qrd5u4axv,rnub9nmtd,reha00jnu'); + } - function simulateBidFactory() { - bidFactorySpy = sandbox.stub(bidfactory, 'createBid').withArgs(CONSTANTS.STATUS.GOOD); + function initializeKrgUid() { + setCookie('krg_uid', '%7B%22v%22%3A%7B%22userId%22%3A%225f108831-302d-11e7-bf6b-4595acd3bf6c%22%2C%22clientId%22%3A%222410d8f2-c111-4811-88a5-7b5e190e475f%22%2C%22optOut%22%3Afalse%7D%7D'); + } - bidFactorySpy.onCall(0).returns({ - statusMessage: 'Bid available', - adId: '12dd646671a959' - }); + function getKrgCrb() { + return '%7B%22v%22%3A%22eyJzeW5jSWRzIjp7IjIiOiI4MmZhMjU1NS01OTY5LTQ2MTQtYjRjZS00ZGNmMTA4MGU5ZjkiLCIxNiI6IlZveElrOEFvSnowQUFFZENleUFBQUFDMiY1MDIiLCIyMyI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjI0IjoiVm94SWs4QW9KejBBQUVkQ2V5QUFBQUMyJjUwMiIsIjI1IjoiNWVlMjQxMzgtNWUwMy00YjlkLWE5NTMtMzhlODMzZjI4NDlmIiwiMl84MCI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjJfOTMiOiI1ZWUyNDEzOC01ZTAzLTRiOWQtYTk1My0zOGU4MzNmMjg0OWYifSwiZXhwaXJlVGltZSI6MTQ5NzQ0OTM4MjY2OCwibGFzdFN5bmNlZEF0IjoxNDk3MzYyOTc5MDEyfQ%3D%3D%22%7D'; + } - bidFactorySpy.onCall(1).returns({ - statusMessage: 'Bid available', - adId: '33f07659bdaf94' - }); - } - - function initializeKruxUser() { - setLocalStorageItem('kxkar_user', 'rsgr9pnij'); - } - - function initializeKruxSegments() { - setLocalStorageItem('kxkar_segs', 'qv9v984dy,rpx2gy365,qrd5u4axv,rnub9nmtd,reha00jnu'); - } - - function initializeKrgUid() { - setCookie('krg_uid', '%7B%22v%22%3A%7B%22userId%22%3A%225f108831-302d-11e7-bf6b-4595acd3bf6c%22%2C%22clientId%22%3A%222410d8f2-c111-4811-88a5-7b5e190e475f%22%2C%22optOut%22%3Afalse%7D%7D'); - } - - function initializeKrgCrb() { - setCookie('krg_crb', '%7B%22v%22%3A%22eyJzeW5jSWRzIjp7IjIiOiI4MmZhMjU1NS01OTY5LTQ2MTQtYjRjZS00ZGNmMTA4MGU5ZjkiLCIxNiI6IlZveElrOEFvSnowQUFFZENleUFBQUFDMiY1MDIiLCIyMyI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjI0IjoiVm94SWs4QW9KejBBQUVkQ2V5QUFBQUMyJjUwMiIsIjI1IjoiNWVlMjQxMzgtNWUwMy00YjlkLWE5NTMtMzhlODMzZjI4NDlmIiwiMl84MCI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjJfOTMiOiI1ZWUyNDEzOC01ZTAzLTRiOWQtYTk1My0zOGU4MzNmMjg0OWYifSwiZXhwaXJlVGltZSI6MTQ5NzQ0OTM4MjY2OCwibGFzdFN5bmNlZEF0IjoxNDk3MzYyOTc5MDEyfQ%3D%3D%22%7D'); - } - - function initializeInvalidKrgUid() { - setCookie('krg_uid', 'invalid-krg-uid'); - } - - function initializeInvalidKrgCrbType1() { - setCookie('krg_crb', 'invalid-krg-crb'); - } - - function initializeInvalidKrgCrbType2() { - setCookie('krg_crb', '%7B%22v%22%3A%22%26%26%26%26%26%26%22%7D'); - } - - function initializeInvalidKrgCrbType3() { - setCookie('krg_crb', '%7B%22v%22%3A%22Ly8v%22%7D'); - } - - function initializeEmptyKrgUid() { - setCookie('krg_uid', '%7B%7D'); - } - - function initializeEmptyKrgCrb() { - setCookie('krg_crb', '%7B%22v%22%3A%22eyJleHBpcmVUaW1lIjoxNDk3NDQ5MzgyNjY4LCJsYXN0U3luY2VkQXQiOjE0OTczNjI5NzkwMTJ9%22%7D'); - } - - function getExpectedKrakenParams(excludeUserIds, excludeKrux) { - var base = { - timeout: 200, - currency: 'USD', - cpmGranularity: 1, - cpmRange: { - floor: 0, - ceil: 20 - }, - adSlotIds: [ - 'foo', - 'bar' - ], - userIDs: { - kargoID: '5f108831-302d-11e7-bf6b-4595acd3bf6c', - clientID: '2410d8f2-c111-4811-88a5-7b5e190e475f', - crbIDs: { - 2: '82fa2555-5969-4614-b4ce-4dcf1080e9f9', - 16: 'VoxIk8AoJz0AAEdCeyAAAAC2&502', - 23: 'd2a855a5-1b1c-4300-940e-a708fa1f1bde', - 24: 'VoxIk8AoJz0AAEdCeyAAAAC2&502', - 25: '5ee24138-5e03-4b9d-a953-38e833f2849f', - '2_80': 'd2a855a5-1b1c-4300-940e-a708fa1f1bde', - '2_93': '5ee24138-5e03-4b9d-a953-38e833f2849f' - }, - optOut: false - }, - krux: { - userID: 'rsgr9pnij', - segments: [ - 'qv9v984dy', - 'rpx2gy365', - 'qrd5u4axv', - 'rnub9nmtd', - 'reha00jnu' - ] - }, - pageURL: window.location.href - }; - - if (excludeUserIds === true) { - base.userIDs = { - crbIDs: {} - }; - } else if (excludeUserIds) { - if (excludeUserIds.uid) { - delete base.userIDs.kargoID; - delete base.userIDs.clientID; - delete base.userIDs.optOut; - } + function initializeKrgCrb() { + setCookie('krg_crb', getKrgCrb()); + } - if (excludeUserIds.crb) { - base.userIDs.crbIDs = {}; - } + function initializeInvalidKrgUid() { + setCookie('krg_uid', 'invalid-krg-uid'); } - if (excludeKrux) { - base.krux = { - userID: null, - segments: [] - }; + function getInvalidKrgCrbType1() { + return 'invalid-krg-crb'; } - return base; - } - - function getExpectedFirstBid() { - return { - 'bidderCode': 'kargo', - 'width': 320, - 'height': 50, - 'statusMessage': 'Bid available', - 'adId': '12dd646671a959', - 'cpm': 3, - 'ad': '<div id=\"1\"></div>' - }; - } - - function getExpectedSecondBid() { - return { - 'bidderCode': 'kargo', - 'width': 300, - 'height': 250, - 'statusMessage': 'Bid available', - 'adId': '33f07659bdaf94', - 'cpm': 2.5, - 'ad': '<div id=\"2\"></div>' - }; - } - - function generalAssertions() { - assert(bidFactorySpy.calledTwice); - - assert(addBidResponseSpy.getCall(0).calledWithExactly(1, sinon.match(getExpectedFirstBid()))); - assert(addBidResponseSpy.getCall(1).calledWithExactly(2, sinon.match(getExpectedSecondBid()))); - assert(addBidResponseSpy.calledTwice); - - var trackerEl = bodyAppendSpy.getCall(0).args[0]; - assert(trackerEl instanceof HTMLImageElement); - assert(trackerEl.src === `${window.location.origin}/fake-tracker-1`); - assert(bodyAppendSpy.calledOnce); - } - - it('works when all params and cookies are correctly set', function() { - initializeKruxUser(); - initializeKruxSegments(); - initializeKrgUid(); - initializeKrgCrb(); - - adapter().callBids(params); - - generalAssertions(); - expect(krakenParams).to.deep.equal(getExpectedKrakenParams()); - }); + function initializeInvalidKrgCrbType1() { + setCookie('krg_crb', getInvalidKrgCrbType1()); + } - it('gracefully handles nothing being set', function() { - adapter().callBids(params); + function getInvalidKrgCrbType2() { + return '%7B%22v%22%3A%22%26%26%26%26%26%26%22%7D'; + } - generalAssertions(); - expect(krakenParams).to.deep.equal(getExpectedKrakenParams(true, true)); - }); + function initializeInvalidKrgCrbType2() { + setCookie('krg_crb', getInvalidKrgCrbType2()); + } - it('gracefully handles browsers without localStorage', function() { - simulateNoLocalStorage(); - initializeKrgUid(); - initializeKrgCrb(); + function getInvalidKrgCrbType3() { + return '%7B%22v%22%3A%22Ly8v%22%7D'; + } - adapter().callBids(params); + function initializeInvalidKrgCrbType3() { + setCookie('krg_crb', getInvalidKrgCrbType3()); + } - generalAssertions(); - expect(krakenParams).to.deep.equal(getExpectedKrakenParams(false, true)); - }); + function initializeEmptyKrgUid() { + setCookie('krg_uid', '%7B%7D'); + } - it('handles empty yet valid Kargo CRBs and UIDs', function() { - initializeKruxUser(); - initializeKruxSegments(); - initializeEmptyKrgUid(); - initializeEmptyKrgCrb(); + function getEmptyKrgCrb() { + return '%7B%22v%22%3A%22eyJleHBpcmVUaW1lIjoxNDk3NDQ5MzgyNjY4LCJsYXN0U3luY2VkQXQiOjE0OTczNjI5NzkwMTJ9%22%7D'; + } - adapter().callBids(params); + function initializeEmptyKrgCrb() { + setCookie('krg_crb', getEmptyKrgCrb()); + } - generalAssertions(); - expect(krakenParams).to.deep.equal(getExpectedKrakenParams(true)); - }); + function getExpectedKrakenParams(excludeUserIds, excludeKrux, expectedRawCRB) { + var base = { + timeout: 200, + currency: 'USD', + cpmGranularity: 1, + cpmRange: { + floor: 0, + ceil: 20 + }, + adSlotIds: [ + 'foo', + 'bar' + ], + userIDs: { + kargoID: '5f108831-302d-11e7-bf6b-4595acd3bf6c', + clientID: '2410d8f2-c111-4811-88a5-7b5e190e475f', + crbIDs: { + 2: '82fa2555-5969-4614-b4ce-4dcf1080e9f9', + 16: 'VoxIk8AoJz0AAEdCeyAAAAC2&502', + 23: 'd2a855a5-1b1c-4300-940e-a708fa1f1bde', + 24: 'VoxIk8AoJz0AAEdCeyAAAAC2&502', + 25: '5ee24138-5e03-4b9d-a953-38e833f2849f', + '2_80': 'd2a855a5-1b1c-4300-940e-a708fa1f1bde', + '2_93': '5ee24138-5e03-4b9d-a953-38e833f2849f' + }, + optOut: false + }, + krux: { + userID: 'rsgr9pnij', + segments: [ + 'qv9v984dy', + 'rpx2gy365', + 'qrd5u4axv', + 'rnub9nmtd', + 'reha00jnu' + ] + }, + pageURL: window.location.href, + rawCRB: expectedRawCRB + }; - it('handles broken Kargo UIDs', function() { - initializeKruxUser(); - initializeKruxSegments(); - initializeInvalidKrgUid(); - initializeKrgCrb(); + if (excludeUserIds === true) { + base.userIDs = { + crbIDs: {} + }; + } else if (excludeUserIds) { + if (excludeUserIds.uid) { + delete base.userIDs.kargoID; + delete base.userIDs.clientID; + delete base.userIDs.optOut; + } - adapter().callBids(params); + if (excludeUserIds.crb) { + base.userIDs.crbIDs = {}; + } + } - generalAssertions(); - expect(krakenParams).to.deep.equal(getExpectedKrakenParams({uid: true})); - }); + if (excludeKrux) { + base.krux = { + userID: null, + segments: [] + }; + } - it('handles broken Kargo CRBs where top level JSON is invalid', function() { - initializeKruxUser(); - initializeKruxSegments(); - initializeKrgUid(); - initializeInvalidKrgCrbType1(); + return base; + } - adapter().callBids(params); + function testBuildRequests(expected) { + var request = spec.buildRequests(bids, {timeout: 200, foo: 'bar'}); + var krakenParams = JSON.parse(decodeURIComponent(request.data.slice(5))); + expect(request.data.slice(0, 5)).to.equal('json='); + expect(request.url).to.equal('https://krk.kargo.com/api/v1/bid'); + expect(request.method).to.equal('GET'); + expect(request.currency).to.equal('USD'); + expect(request.timeout).to.equal(200); + expect(request.foo).to.equal('bar'); + expect(krakenParams).to.deep.equal(expected); + } - generalAssertions(); - expect(krakenParams).to.deep.equal(getExpectedKrakenParams({crb: true})); - }); + it('works when all params and cookies are correctly set', function() { + initializeKruxUser(); + initializeKruxSegments(); + initializeKrgUid(); + initializeKrgCrb(); + testBuildRequests(getExpectedKrakenParams(undefined, undefined, getKrgCrb())); + }); - it('handles broken Kargo CRBs where inner base 64 is invalid', function() { - initializeKruxUser(); - initializeKruxSegments(); - initializeKrgUid(); - initializeInvalidKrgCrbType2(); + it('gracefully handles nothing being set', function() { + testBuildRequests(getExpectedKrakenParams(true, true, null)); + }); - adapter().callBids(params); + it('gracefully handles browsers without localStorage', function() { + simulateNoLocalStorage(); + initializeKrgUid(); + initializeKrgCrb(); + testBuildRequests(getExpectedKrakenParams(false, true, getKrgCrb())); + }); - generalAssertions(); - expect(krakenParams).to.deep.equal(getExpectedKrakenParams({crb: true})); - }); + it('handles empty yet valid Kargo CRBs and UIDs', function() { + initializeKruxUser(); + initializeKruxSegments(); + initializeEmptyKrgUid(); + initializeEmptyKrgCrb(); + testBuildRequests(getExpectedKrakenParams(true, undefined, getEmptyKrgCrb())); + }); - it('handles broken Kargo CRBs where inner JSON is invalid', function() { - initializeKruxUser(); - initializeKruxSegments(); - initializeKrgUid(); - initializeInvalidKrgCrbType3(); + it('handles broken Kargo UIDs', function() { + initializeKruxUser(); + initializeKruxSegments(); + initializeInvalidKrgUid(); + initializeKrgCrb(); + testBuildRequests(getExpectedKrakenParams({uid: true}, undefined, getKrgCrb())); + }); + + it('handles broken Kargo CRBs where top level JSON is invalid', function() { + initializeKruxUser(); + initializeKruxSegments(); + initializeKrgUid(); + initializeInvalidKrgCrbType1(); + testBuildRequests(getExpectedKrakenParams({crb: true}, undefined, getInvalidKrgCrbType1())); + }); - adapter().callBids(params); + it('handles broken Kargo CRBs where inner base 64 is invalid', function() { + initializeKruxUser(); + initializeKruxSegments(); + initializeKrgUid(); + initializeInvalidKrgCrbType2(); + testBuildRequests(getExpectedKrakenParams({crb: true}, undefined, getInvalidKrgCrbType2())); + }); + + it('handles broken Kargo CRBs where inner JSON is invalid', function() { + initializeKruxUser(); + initializeKruxSegments(); + initializeKrgUid(); + initializeInvalidKrgCrbType3(); + testBuildRequests(getExpectedKrakenParams({crb: true}, undefined, getInvalidKrgCrbType3())); + }); + }); - generalAssertions(); - expect(krakenParams).to.deep.equal(getExpectedKrakenParams({crb: true})); + describe('response handler', function() { + it('handles bid responses', function() { + var resp = spec.interpretResponse({body: { + foo: { + cpm: 3, + adm: '<div id="1"></div>', + width: 320, + height: 50 + }, + bar: { + cpm: 2.5, + adm: '<div id="2"></div>', + width: 300, + height: 250 + } + }}, { + currency: 'USD', + bids: [{ + bidId: 'fake bid id 1', + params: { + placementId: 'foo' + } + }, { + bidId: 'fake bid id 2', + params: { + placementId: 'bar' + } + }] + }); + var expectation = [{ + requestId: 'fake bid id 1', + cpm: 3, + width: 320, + height: 50, + ad: '<div id="1"></div>', + ttl: 300, + creativeId: 'foo', + netRevenue: true, + currency: 'USD' + }, { + requestId: 'fake bid id 2', + cpm: 2.5, + width: 300, + height: 250, + ad: '<div id="2"></div>', + ttl: 300, + creativeId: 'bar', + netRevenue: true, + currency: 'USD' + }]; + expect(resp).to.deep.equal(expectation); + }); }); }); From c5d1ecfa42772d969d61390d0bcd54b8d6192dfb Mon Sep 17 00:00:00 2001 From: onaydenov <onaydenov@users.noreply.github.com> Date: Thu, 26 Oct 2017 19:51:56 +0300 Subject: [PATCH 28/44] Fidelity Media Adapter update. Prebid v1.0 (#1719) * Fidelity Media Adapter update. Prebid v1.0. Fidelity Media Adapter update. Prebid v1.0 plus featutes update. Add Prebid v1.0 support Add parameter "floor" Removed: "Loc" "Click" "SubId" Subid=HB trasferred to SSP by default now. * Fidelity Media Adapter update. Prebid v1.0. Fidelity Media Adapter update. Prebid v1.0 plus featutes update. Add Prebid v1.0 support Add parameter "floor" Removed: "Loc" "Click" "SubId" Subid=HB trasferred to SSP by default now. * Fidelity Media fmxSSP Adapter update. Prebid v1.0 * Fidelity Media. Prebid v 1.0 Add tmax * Fidelity Media. Prebid v 1.0. ttl * Fidelity Media. Prebid v 1.0. spec * Less bidderCode, add creativeId * Adapter v.1, less bidderCode, add creativeId * Prebid v1. InterpretResponse. Spec. * InterpretResponse * InterpretResponse --- modules/fidelityBidAdapter.js | 173 +++++------ modules/fidelityBidAdapter.md | 26 ++ test/spec/modules/fidelityBidAdapter_spec.js | 311 ++++++++----------- 3 files changed, 243 insertions(+), 267 deletions(-) create mode 100644 modules/fidelityBidAdapter.md diff --git a/modules/fidelityBidAdapter.js b/modules/fidelityBidAdapter.js index 73ee9d44ddd..ca7dc9d6fd6 100644 --- a/modules/fidelityBidAdapter.js +++ b/modules/fidelityBidAdapter.js @@ -1,102 +1,87 @@ -var utils = require('src/utils.js'); -var bidfactory = require('src/bidfactory.js'); -var bidmanager = require('src/bidmanager.js'); -var adloader = require('src/adloader'); -var STATUS = require('src/constants').STATUS; -var adaptermanager = require('src/adaptermanager'); +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'fidelity'; +const BIDDER_SERVER = 'x.fidelity-media.com'; +export const spec = { + code: BIDDER_CODE, + isBidRequestValid: function(bid) { + return !!(bid && bid.params && bid.params.zoneid); + }, + buildRequests: function(validBidRequests, bidderRequest) { + return validBidRequests.map(bidRequest => { + var server = bidRequest.params.server || BIDDER_SERVER; -var FidelityAdapter = function FidelityAdapter() { - var FIDELITY_BIDDER_NAME = 'fidelity'; - var FIDELITY_SERVER_NAME = 'x.fidelity-media.com'; + const payload = { + from: 'hb', + v: '1.0', + requestid: bidRequest.bidderRequestId, + impid: bidRequest.bidId, + zoneid: bidRequest.params.zoneid, + floor: parseFloat(bidRequest.params.floor) > 0 ? bidRequest.params.floor : 0, + charset: document.charSet || document.characterSet, + defloc: utils.getTopWindowUrl(), + altloc: window.location.href, + subid: 'hb', + flashver: getFlashVersion(), + tmax: bidderRequest.timeout, + }; + if (document.referrer) { + payload.referrer = document.referrer; + } - function _callBids(params) { - var bids = params.bids || []; - bids.forEach(function (currentBid) { - var server = currentBid.params.server || FIDELITY_SERVER_NAME; - var m3_u = window.location.protocol + '//' + server + '/delivery/hb.php?'; - m3_u += 'callback=window.$$PREBID_GLOBAL$$.fidelityResponse'; - m3_u += '&requestid=' + utils.getUniqueIdentifierStr(); - m3_u += '&impid=' + currentBid.bidId; - m3_u += '&zoneid=' + currentBid.params.zoneid; - m3_u += '&cb=' + Math.floor(Math.random() * 99999999999); - m3_u += document.charset ? '&charset=' + document.charset : (document.characterSet ? '&charset=' + document.characterSet : ''); - - var loc; - try { - loc = window.top !== window ? document.referrer : window.location.href; - } catch (e) { - loc = document.referrer; - } - loc = currentBid.params.loc || loc; - m3_u += '&loc=' + encodeURIComponent(loc); - - var subid = currentBid.params.subid || 'hb'; - m3_u += '&subid=' + subid; - if (document.referrer) m3_u += '&referer=' + encodeURIComponent(document.referrer); - if (currentBid.params.click) m3_u += '&ct0=' + encodeURIComponent(currentBid.params.click); - m3_u += '&flashver=' + encodeURIComponent(getFlashVersion()); - - adloader.loadScript(m3_u); + return { + method: 'GET', + url: '//' + server + '/delivery/hb.php', + data: payload + }; }); - } + }, + interpretResponse: function(serverResponse) { + serverResponse = serverResponse.body; + const bidResponses = []; + if (serverResponse && serverResponse.seatbid) { + serverResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => { + const bidResponse = { + requestId: bid.impid, + creativeId: bid.impid, + cpm: bid.price, + width: bid.width, + height: bid.height, + ad: bid.adm, + netRevenue: bid.netRevenue, + currency: bid.cur, + ttl: bid.ttl, + }; - function getFlashVersion() { - var plugins, plugin, result; - - if (navigator.plugins && navigator.plugins.length > 0) { - plugins = navigator.plugins; - for (var i = 0; i < plugins.length && !result; i++) { - plugin = plugins[i]; - if (plugin.name.indexOf('Shockwave Flash') > -1) { - result = plugin.description.split('Shockwave Flash ')[1]; - } - } + bidResponses.push(bidResponse); + })); } - return result || ''; - } - - function addBlankBidResponses(placementsWithBidsBack) { - var allFidelityBidRequests = $$PREBID_GLOBAL$$._bidsRequested.find(bidSet => bidSet.bidderCode === FIDELITY_BIDDER_NAME); - - if (allFidelityBidRequests && allFidelityBidRequests.bids) { - utils._each(allFidelityBidRequests.bids, function (fidelityBid) { - if (!utils.contains(placementsWithBidsBack, fidelityBid.placementCode)) { - // Add a no-bid response for this placement. - var bid = bidfactory.createBid(STATUS.NO_BID, fidelityBid); - bid.bidderCode = FIDELITY_BIDDER_NAME; - bidmanager.addBidResponse(fidelityBid.placementCode, bid); - } - }); - } - } - - $$PREBID_GLOBAL$$.fidelityResponse = function(responseObj) { - if (!responseObj || !responseObj.seatbid || responseObj.seatbid.length === 0 || !responseObj.seatbid[0].bid || responseObj.seatbid[0].bid.length === 0) { - addBlankBidResponses([]); - return; + return bidResponses; + }, + getUserSyncs: function getUserSyncs(syncOptions) { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: '//' + BIDDER_SERVER + '/delivery/matches.php?type=iframe', + }]; } + } +} + +function getFlashVersion() { + var plugins, plugin, result; - var bid = responseObj.seatbid[0].bid[0]; - var status = bid.adm ? STATUS.GOOD : STATUS.NO_BID; - var requestObj = utils.getBidRequest(bid.impid); - - var bidResponse = bidfactory.createBid(status); - bidResponse.bidderCode = FIDELITY_BIDDER_NAME; - if (status === STATUS.GOOD) { - bidResponse.cpm = parseFloat(bid.price); - bidResponse.ad = bid.adm; - bidResponse.width = parseInt(bid.width); - bidResponse.height = parseInt(bid.height); + if (navigator.plugins && navigator.plugins.length > 0) { + plugins = navigator.plugins; + for (var i = 0; i < plugins.length && !result; i++) { + plugin = plugins[i]; + if (plugin.name.indexOf('Shockwave Flash') > -1) { + result = plugin.description.split('Shockwave Flash ')[1]; + } } - var placementCode = requestObj && requestObj.placementCode; - bidmanager.addBidResponse(placementCode, bidResponse); - }; - - return { - callBids: _callBids - }; -}; - -adaptermanager.registerBidAdapter(new FidelityAdapter(), 'fidelity'); - -module.exports = FidelityAdapter; + } + return result || ''; +} + +registerBidder(spec); diff --git a/modules/fidelityBidAdapter.md b/modules/fidelityBidAdapter.md new file mode 100644 index 00000000000..a4f1e91cd3d --- /dev/null +++ b/modules/fidelityBidAdapter.md @@ -0,0 +1,26 @@ +# Overview + +**Module Name**: Fidelity Media fmxSSP Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: on@fidelity-media.com + +# Description + +Connects to Fidelity Media fmxSSP demand source to fetch bids. + +# Test Parameters +``` + var adUnits = [{ + code: 'banner-ad-div', + sizes: [[300, 250]], + bids: [{ + bidder: 'fidelity', + params: { + zoneid: '27248', + floor: 0.005, + server: 'x.fidelity-media.com' + } + }] + }]; + +``` diff --git a/test/spec/modules/fidelityBidAdapter_spec.js b/test/spec/modules/fidelityBidAdapter_spec.js index 5777c6af8cd..036a34ee0b0 100644 --- a/test/spec/modules/fidelityBidAdapter_spec.js +++ b/test/spec/modules/fidelityBidAdapter_spec.js @@ -1,195 +1,160 @@ -describe('fidelity adapter tests', function() { - const expect = require('chai').expect; - const adapter = require('modules/fidelityBidAdapter'); - const adLoader = require('src/adloader'); - const bidmanager = require('src/bidmanager'); - const STATUS = require('src/constants').STATUS; - var urlParse = require('url-parse'); - var querystringify = require('querystringify'); - - describe('creation of bid url', function () { - it('should be called', function () { - var stubLoadScript; - stubLoadScript = sinon.stub(adLoader, 'loadScript'); - - var bidderRequest = { - bidderCode: 'fidelity', - bids: [ - { - bidId: 'bidId-123456-1', - bidder: 'fidelity', - params: { - zoneid: '37' - }, - placementCode: 'div-gpt-ad-123456-1' - }, - ] - }; +import { expect } from 'chai'; +import { spec } from 'modules/fidelityBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; - adapter().callBids(bidderRequest); - sinon.assert.called(stubLoadScript); +describe('FidelityAdapter', () => { + const adapter = newBidder(spec); - stubLoadScript.restore(); + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); }); + }); - it('should populate required parameters', function () { - var stubLoadScript; - stubLoadScript = sinon.stub(adLoader, 'loadScript'); - - var bidderRequest = { - bidderCode: 'fidelity', - bids: [ - { - bidId: 'bidId-123456-1', - bidder: 'fidelity', - params: { - zoneid: '37', - }, - placementCode: 'div-gpt-ad-123456-1' - }, - ] - }; - - adapter().callBids(bidderRequest); - - stubLoadScript.restore(); + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'fidelity', + 'params': { + 'zoneid': '37', + 'floor': '0.05', + 'server': 't.fidelity-media.com', + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should populate required and optional parameters', function () { - var stubLoadScript; - stubLoadScript = sinon.stub(adLoader, 'loadScript'); - - var bidderRequest = { - bidderCode: 'fidelity', - bids: [ - { - bidId: 'bidId-123456-1', - bidder: 'fidelity', - params: { - zoneid: '37', - server: 't.fidelity-media.com', - loc: 'http://locurl', - click: 'http://clickurl', - subid: '000' - }, - placementCode: 'div-gpt-ad-123456-1' - }, - ] + it('should return true when required params found', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'zoneid': '37', }; - - adapter().callBids(bidderRequest); - - var requestURI = stubLoadScript.getCall(0).args[0]; - var parsedBidUrl = urlParse(requestURI); - var parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); - - expect(parsedBidUrl.hostname).to.equal('t.fidelity-media.com'); - - expect(parsedBidUrlQueryString).to.have.property('zoneid').and.to.equal('37'); - expect(parsedBidUrlQueryString).to.have.property('impid').and.to.equal('bidId-123456-1'); - expect(parsedBidUrlQueryString).to.have.property('callback').and.to.equal('window.$$PREBID_GLOBAL$$.fidelityResponse'); - expect(parsedBidUrlQueryString).to.have.property('loc').and.to.equal('http://locurl'); - expect(parsedBidUrlQueryString).to.have.property('ct0').and.to.equal('http://clickurl'); - expect(parsedBidUrlQueryString).to.have.property('subid').and.to.equal('000'); - - stubLoadScript.restore(); + expect(spec.isBidRequestValid(bid)).to.equal(true); }); - }); - describe('fidelityResponse', function () { - it('should exist and be a function', function () { - expect($$PREBID_GLOBAL$$.fidelityResponse).to.exist.and.to.be.a('function'); + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'zoneid': 0, + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); }); + }); - it('should add empty bid response if no bids returned', function () { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - - var bidderRequest = { - bidderCode: 'fidelity', - bids: [ - { - bidId: 'bidId-123456-1', - bidder: 'fidelity', - params: { - zoneid: '37' - }, - placementCode: 'div-gpt-ad-123456-1' + describe('buildRequests', () => { + let bidderRequest = { + bidderCode: 'fidelity', + requestId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', + bidderRequestId: '178e34bad3658f', + bids: [ + { + bidder: 'fidelity', + params: { + zoneid: '37', + floor: '0.05', + server: 't.fidelity-media.com', }, - ] - }; - - // no bids returned in the response. - var response = { - 'id': '543210', - 'seatbid': [] - }; - - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - // adapter needs to be called, in order for the stub to register. - adapter() - - $$PREBID_GLOBAL$$.fidelityResponse(response); - - var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - - expect(bidPlacementCode1).to.equal('div-gpt-ad-123456-1'); - expect(bidObject1.getStatusCode()).to.equal(2); - expect(bidObject1.bidderCode).to.equal('fidelity'); - - stubAddBidResponse.restore(); + placementCode: '/19968336/header-bid-tag-0', + sizes: [[300, 250], [320, 50]], + bidId: '2ffb201a808da7', + bidderRequestId: '178e34bad3658f', + requestId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + ], + start: 1472239426002, + auctionStart: 1472239426000, + timeout: 5000 + }; + + it('should add source and verison to the tag', () => { + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + const payload = request.data; + expect(payload.from).to.exist; + expect(payload.v).to.exist; + expect(payload.requestid).to.exist; + expect(payload.impid).to.exist; + expect(payload.zoneid).to.exist; + expect(payload.floor).to.exist; + expect(payload.charset).to.exist; + expect(payload.defloc).to.exist; + expect(payload.altloc).to.exist; + expect(payload.subid).to.exist; + expect(payload.flashver).to.exist; + expect(payload.tmax).to.exist; }); - it('should add a bid response for bid returned', function () { - var stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse'); - - var bidderRequest = { - bidderCode: 'fidelity', - bids: [ - { - bidId: 'bidId-123456-1', - bidder: 'fidelity', - params: { - zoneid: '37' - }, - placementCode: 'div-gpt-ad-123456-1' - }, - ] - }; + it('sends bid request to ENDPOINT via GET', () => { + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.url).to.equal('//t.fidelity-media.com/delivery/hb.php'); + expect(request.method).to.equal('GET'); + }); + }) + + describe('interpretResponse', () => { + let response = { + 'id': '543210', + 'seatbid': [ { + 'bid': [ { + 'id': '1111111', + 'impid': 'bidId-123456-1', + 'price': 0.09, + 'adm': '<!-- Creative -->', + 'width': 728, + 'height': 90, + } ] + } ] + }; + + it('should get correct bid response', () => { + let expectedResponse = [ + { + requestId: 'bidId-123456-1', + creativeId: 'bidId-123456-1', + cpm: 0.09, + width: 728, + height: 90, + ad: '<!-- Creative -->', + netRevenue: true, + currency: 'USD', + ttl: 360, + } + ]; + + let result = spec.interpretResponse({ body: response }); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + }); - // Returning a single bid in the response. - var response = { + it('handles nobid responses', () => { + let response = { 'id': '543210', - 'seatbid': [ { - 'bid': [ { - 'id': '1111111', - 'impid': 'bidId-123456-1', - 'price': 0.09, - 'adm': '<<creative>>', - 'height': 90, - 'width': 728 - } ] - } ] + 'seatbid': [ ] }; - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - // adapter needs to be called, in order for the stub to register. - adapter() - - $$PREBID_GLOBAL$$.fidelityResponse(response); - - var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; - var bidObject1 = stubAddBidResponse.getCall(0).args[1]; - - expect(bidPlacementCode1).to.equal('div-gpt-ad-123456-1'); - expect(bidObject1.getStatusCode()).to.equal(1); - expect(bidObject1.bidderCode).to.equal('fidelity'); - expect(bidObject1.cpm).to.equal(0.09); - expect(bidObject1.height).to.equal(90); - expect(bidObject1.width).to.equal(728); - expect(bidObject1.ad).to.equal('<<creative>>'); + let result = spec.interpretResponse({ body: response }); + expect(result.length).to.equal(0); + }); + }); - stubAddBidResponse.restore(); + describe('user sync', () => { + const syncUrl = '//x.fidelity-media.com/delivery/matches.php?type=iframe'; + + it('should register the sync iframe', () => { + expect(spec.getUserSyncs({})).to.be.undefined; + expect(spec.getUserSyncs({iframeEnabled: false})).to.be.undefined; + const options = spec.getUserSyncs({iframeEnabled: true}); + expect(options).to.not.be.undefined; + expect(options).to.have.lengthOf(1); + expect(options[0].type).to.equal('iframe'); + expect(options[0].url).to.equal(syncUrl); }); }); }); From 51ffb4eaa96a2c82641a9c8f47c3356e3d70dd1e Mon Sep 17 00:00:00 2001 From: Xinyang Li <lixinyang1026@Gmail.com> Date: Fri, 27 Oct 2017 01:08:17 +0800 Subject: [PATCH 29/44] Upgrade Quantcast adapter for Prebid 1.0 (#1753) * Add v1.0 adapter skeleton * Fix a typo in variable name declaration * Change spec dependencies * Add new describes for test * Move URL detechtion inside buildRequests method * Add necessary cases * Clean test cases * Export Quantcast adapter constants * Implemenent test cases * Fix wrong ports number * Correct test cases * Make string with single quote * Remove unused statements * Revert "Remove unused statements" This reverts commit 459ca31ef8ac501311b738e61634cac216aaf7cf. * Remove unused statements * Change string to single quote * Fix sizes type * Fix the sizes propty name typo in the spec * Remove unused method, key value and comments * Update the spec * Change code using single quote * Update the first argument for interpretResponse method and its spec * Handle undefined Server Response * Add required params from the server response --- modules/quantcastBidAdapter.js | 255 +++++++------ modules/quantcastBidAdapter.md | 31 ++ test/spec/modules/quantcastBidAdapter_spec.js | 355 +++++++++--------- 3 files changed, 347 insertions(+), 294 deletions(-) create mode 100644 modules/quantcastBidAdapter.md diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index f199e9fad30..f10fd48502f 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -1,128 +1,165 @@ -const utils = require('src/utils.js'); -const bidfactory = require('src/bidfactory.js'); -const bidmanager = require('src/bidmanager.js'); -const ajax = require('src/ajax.js'); -const CONSTANTS = require('src/constants.json'); -const adaptermanager = require('src/adaptermanager'); -const QUANTCAST_CALLBACK_URL = 'http://global.qc.rtb.quantserve.com:8080/qchb'; - -var QuantcastAdapter = function QuantcastAdapter() { - const BIDDER_CODE = 'quantcast'; - - const DEFAULT_BID_FLOOR = 0.0000000001; - let bidRequests = {}; - - let returnEmptyBid = function(bidId) { - var bidRequested = utils.getBidRequest(bidId); - if (!utils.isEmpty(bidRequested)) { - let bid = bidfactory.createBid(CONSTANTS.STATUS.NO_BID, bidRequested); - bid.bidderCode = BIDDER_CODE; - bidmanager.addBidResponse(bidRequested.placementCode, bid); +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'quantcast'; +const DEFAULT_BID_FLOOR = 0.0000000001; + +export const QUANTCAST_CALLBACK_URL = 'global.qc.rtb.quantserve.com'; +export const QUANTCAST_CALLBACK_URL_TEST = 's2s-canary.quantserve.com'; +export const QUANTCAST_NET_REVENUE = true; +export const QUANTCAST_TEST_PUBLISHER = 'test-publisher'; +export const QUANTCAST_TTL = 4; + +/** + * The documentation for Prebid.js Adapter 1.0 can be found at link below, + * http://prebid.org/dev-docs/bidder-adapter-1.html + */ +export const spec = { + code: BIDDER_CODE, + + /** + * Verify the `AdUnits.bids` response with `true` for valid request and `false` + * for invalid request. + * + * @param {object} bid + * @return boolean `true` is this is a valid bid, and `false` otherwise + */ + isBidRequestValid(bid) { + if (!bid) { + return false; } - }; - // expose the callback to the global object: - $$PREBID_GLOBAL$$.handleQuantcastCB = function (responseText) { - if (utils.isEmpty(responseText)) { - return; - } - let response = null; - try { - response = JSON.parse(responseText); - } catch (e) { - // Malformed JSON - utils.logError("Malformed JSON received from server - can't do anything here"); - return; - } - - if (response === null || !response.hasOwnProperty('bids') || utils.isEmpty(response.bids)) { - utils.logError("Sub-optimal JSON received from server - can't do anything here"); - return; + if (bid.mediaType === 'video') { + return false; } - for (let i = 0; i < response.bids.length; i++) { - let seatbid = response.bids[i]; - let key = seatbid.placementCode; - var request = bidRequests[key]; - if (request === null || request === undefined) { - return returnEmptyBid(seatbid.placementCode); - } - // This line is required since this is the field - // that bidfactory.createBid looks for - request.bidId = request.imp[0].placementCode; - let responseBid = bidfactory.createBid(CONSTANTS.STATUS.GOOD, request); - - responseBid.cpm = seatbid.cpm; - responseBid.ad = seatbid.ad; - responseBid.height = seatbid.height; - responseBid.width = seatbid.width; - responseBid.bidderCode = response.bidderCode; - responseBid.requestId = request.requestId; - responseBid.bidderCode = BIDDER_CODE; - - bidmanager.addBidResponse(request.bidId, responseBid); + return true; + }, + + /** + * Make a server request when the page asks Prebid.js for bids from a list of + * `BidRequests`. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be send to Quantcast server + * @return ServerRequest information describing the request to the server. + */ + buildRequests(bidRequests) { + const bids = bidRequests || []; + + const referrer = utils.getTopWindowUrl(); + const loc = utils.getTopWindowLocation(); + const domain = loc.hostname; + + let publisherTagURL; + let publisherTagURLTest; + + // Switch the callback URL to Quantcast Canary Endpoint for testing purpose + // `//` is not used because we have different port setting at our end + switch (window.location.protocol) { + case 'https:': + publisherTagURL = `https://${QUANTCAST_CALLBACK_URL}:8443/qchb`; + publisherTagURLTest = `https://${QUANTCAST_CALLBACK_URL_TEST}:8443/qchb`; + break; + default: + publisherTagURL = `http://${QUANTCAST_CALLBACK_URL}:8080/qchb`; + publisherTagURLTest = `http://${QUANTCAST_CALLBACK_URL_TEST}:8080/qchb`; } - }; - function callBids(params) { - let bids = params.bids || []; - if (bids.length === 0) { - return; - } - - let referrer = utils.getTopWindowUrl(); - let loc = utils.getTopWindowLocation(); - let domain = loc.hostname; - let publisherId = 0; + const bidRequestsList = bids.map(bid => { + const bidSizes = []; - publisherId = '' + bids[0].params.publisherId; - utils._each(bids, function(bid) { - let key = bid.placementCode; - var bidSizes = []; - utils._each(bid.sizes, function (size) { + bid.sizes.forEach(size => { bidSizes.push({ - 'width': size[0], - 'height': size[1] + width: size[0], + height: size[1] }); }); - bidRequests[key] = bidRequests[key] || { - 'publisherId': publisherId, - 'requestId': bid.bidId, - 'bidId': bid.bidId, - 'site': { - 'page': loc.href, - 'referrer': referrer, - 'domain': domain, + // Request Data Format can be found at https://wiki.corp.qc/display/adinf/QCX + const requestData = { + publisherId: bid.params.publisherId, + requestId: bid.bidId, + imp: [ + { + banner: { + battr: bid.params.battr, + sizes: bidSizes + }, + placementCode: bid.placementCode, + bidFloor: bid.params.bidFloor || DEFAULT_BID_FLOOR + } + ], + site: { + page: loc.href, + referrer, + domain }, - 'imp': [{ - - 'banner': { - 'battr': bid.params.battr, - 'sizes': bidSizes, - }, - 'placementCode': bid.placementCode, - 'bidFloor': bid.params.bidFloor || DEFAULT_BID_FLOOR, - }] + bidId: bid.bidId }; - }); - utils._each(bidRequests, function (bidRequest) { - ajax.ajax(QUANTCAST_CALLBACK_URL, $$PREBID_GLOBAL$$.handleQuantcastCB, JSON.stringify(bidRequest), { + const data = JSON.stringify(requestData); + + const url = + bid.params.publisherId === QUANTCAST_TEST_PUBLISHER + ? publisherTagURLTest + : publisherTagURL; + + return { + data, method: 'POST', - withCredentials: true - }); + url + }; }); - } - // Export the `callBids` function, so that Prebid.js can execute - // this function when the page asks to send out bid requests. - return { - callBids: callBids, - QUANTCAST_CALLBACK_URL: QUANTCAST_CALLBACK_URL - }; -}; + return bidRequestsList; + }, + + /** + * Function get called when the browser has received the response from Quantcast server. + * The function parse the response and create a `bidResponse` object containing one/more bids. + * Returns an empty array if no valid bids + * + * Response Data Format can be found at https://wiki.corp.qc/display/adinf/QCX + * + * @param {*} serverResponse A successful response from Quantcast server. + * @return {Bid[]} An array of bids which were nested inside the server. + * + */ + interpretResponse(serverResponse) { + if (serverResponse === undefined) { + utils.logError('Server Response is undefined'); + return []; + } + + const response = serverResponse['body']; + + if ( + response === undefined || + !response.hasOwnProperty('bids') || + utils.isEmpty(response.bids) + ) { + utils.logError('Sub-optimal JSON received from Quantcast server'); + return []; + } -adaptermanager.registerBidAdapter(new QuantcastAdapter(), 'quantcast'); + const bidResponsesList = response.bids.map(bid => { + const { ad, cpm, width, height, creativeId, currency } = bid; + + return { + requestId: response.requestId, + cpm, + width, + height, + ad, + ttl: QUANTCAST_TTL, + creativeId, + netRevenue: QUANTCAST_NET_REVENUE, + currency + }; + }); + + return bidResponsesList; + } +}; -module.exports = QuantcastAdapter; +registerBidder(spec); diff --git a/modules/quantcastBidAdapter.md b/modules/quantcastBidAdapter.md new file mode 100644 index 00000000000..20cf25bffbf --- /dev/null +++ b/modules/quantcastBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +``` +Module Name: Quantcast Bidder Adapter +Module Type: Bidder Adapter +Maintainer: xli@quantcast.com +``` + +# Description + +Module that connects to Quantcast demand sources to fetch bids. + +# Test Parameters + +```js +const adUnits = [{ + code: 'banner', + sizes: [ + [300, 250] + ], + bids: [ + { + bidder: 'quantcast', + params: { + publisherId: 'test-publisher', // REQUIRED - Publisher ID provided by Quantcast + battr: [1, 2] // OPTIONAL - Array of blocked creative attributes as per OpenRTB Spec List 5.3 + } + } + ] +}]; +``` \ No newline at end of file diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index 05748d85845..14981f198b6 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -1,231 +1,216 @@ -import {expect} from 'chai'; -import Adapter from '../../../modules/quantcastBidAdapter'; -import * as ajax from 'src/ajax'; -import bidManager from '../../../src/bidmanager'; -import adLoader from '../../../src/adloader'; - -describe('quantcast adapter', () => { - let bidsRequestedOriginal; - let adapter; - let sandbox; - let ajaxStub; - - const bidderRequest = { - bidderCode: 'quantcast', - requestId: '595ffa73-d78a-46c9-b18e-f99548a5be6b', - bidderRequestId: '1cc026909c24c8', - bids: [ - { - bidId: '2f7b179d443f14', - bidder: 'quantcast', - placementCode: 'div-gpt-ad-1438287399331-0', - sizes: [[300, 250], [300, 600]], - params: { - publisherId: 'test-publisher', - battr: [1, 2], - } - } - ] - }; +import * as utils from 'src/utils'; +import { expect } from 'chai'; +import { + QUANTCAST_CALLBACK_URL_TEST, + QUANTCAST_CALLBACK_URL, + QUANTCAST_NET_REVENUE, + QUANTCAST_TTL, + spec as qcSpec +} from '../../../modules/quantcastBidAdapter'; +import { newBidder } from '../../../src/adapters/bidderFactory'; + +describe('Quantcast adapter', () => { + const quantcastAdapter = newBidder(qcSpec); + let bidRequest; beforeEach(() => { - bidsRequestedOriginal = $$PREBID_GLOBAL$$._bidsRequested; - $$PREBID_GLOBAL$$._bidsRequested = []; - - adapter = new Adapter(); - sandbox = sinon.sandbox.create(); - ajaxStub = sandbox.stub(ajax, 'ajax'); - }); - - afterEach(() => { - sandbox.restore(); - - $$PREBID_GLOBAL$$._bidsRequested = bidsRequestedOriginal; - }); - - describe('sizes', () => { - let bidderRequest = { - bidderCode: 'quantcast', + bidRequest = { + bidder: 'quantcast', + bidId: '2f7b179d443f14', requestId: '595ffa73-d78a-46c9-b18e-f99548a5be6b', bidderRequestId: '1cc026909c24c8', - bids: [ - { - bidId: '2f7b179d443f14', - bidder: 'quantcast', - placementCode: 'div-gpt-ad-1438287399331-0', - sizes: [[300, 250], [300, 600]], - params: { - publisherId: 'test-publisher', - battr: [1, 2], - } - } - ] + placementCode: 'div-gpt-ad-1438287399331-0', + params: { + publisherId: 'test-publisher', // REQUIRED - Publisher ID provided by Quantcast + battr: [1, 2] // OPTIONAL - Array of blocked creative attributes as per OpenRTB Spec List 5.3 + }, + sizes: [[300, 250]] }; + }); - it('should not call server when empty input is provided', () => { - adapter.callBids({}); - sinon.assert.notCalled(ajaxStub); + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(quantcastAdapter.callBids).to.exist.and.to.be.a('function'); }); + }); - it('should call server once even when multiple sizes are passed', () => { - adapter.callBids(bidderRequest); - sinon.assert.calledOnce(ajaxStub); - - expect(ajaxStub.firstCall.args[0]).to.eql(adapter.QUANTCAST_CALLBACK_URL); - expect(ajaxStub.firstCall.args[1]).to.exist.and.to.be.a('function'); - expect(ajaxStub.firstCall.args[2]).to.include('div-gpt-ad-1438287399331-0'); - expect(ajaxStub.firstCall.args[2]).to.include('test-publisher'); - expect(ajaxStub.firstCall.args[2]).to.include('2f7b179d443f14'); - expect(ajaxStub.firstCall.args[3]).to.eql({method: 'POST', withCredentials: true}); + describe('`isBidRequestValid`', () => { + it('should return `false` when bid is not passed', () => { + expect(qcSpec.isBidRequestValid()).to.equal(false); }); - it('should call server once when one size is passed', () => { - bidderRequest.bids[0].sizes = [728, 90]; - adapter.callBids(bidderRequest); - sinon.assert.calledOnce(ajaxStub); + it('should return `false` when bid `mediaType` is `video`', () => { + const bidRequest = { mediaType: 'video' }; - expect(ajaxStub.firstCall.args[0]).to.eql(adapter.QUANTCAST_CALLBACK_URL); - expect(ajaxStub.firstCall.args[1]).to.exist.and.to.be.a('function'); - expect(ajaxStub.firstCall.args[3]).to.eql({method: 'POST', withCredentials: true}); + expect(qcSpec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('should call server once when size is passed as string', () => { - bidderRequest.bids[0].sizes = '728x90'; - adapter.callBids(bidderRequest); - sinon.assert.calledOnce(ajaxStub); + it('should return `true` when bid contains required params', () => { + const bidRequest = { mediaType: 'banner' }; - expect(ajaxStub.firstCall.args[0]).to.eql(adapter.QUANTCAST_CALLBACK_URL); - expect(ajaxStub.firstCall.args[1]).to.exist.and.to.be.a('function'); - expect(ajaxStub.firstCall.args[3]).to.eql({method: 'POST', withCredentials: true}); + expect(qcSpec.isBidRequestValid(bidRequest)).to.equal(true); }); + }); - it('should call server once when sizes are passed as a comma-separated string', () => { - bidderRequest.bids[0].sizes = '728x90,360x240'; - adapter.callBids(bidderRequest); - sinon.assert.calledOnce(ajaxStub); + describe('`buildRequests`', () => { + it('sends bid requests to Quantcast Canary Endpoint if `publisherId` is `test-publisher`', () => { + const requests = qcSpec.buildRequests([bidRequest]); + + switch (window.location.protocol) { + case 'https:': + expect(requests[0]['url']).to.equal( + `https://${QUANTCAST_CALLBACK_URL_TEST}:8443/qchb` + ); + break; + default: + expect(requests[0]['url']).to.equal( + `http://${QUANTCAST_CALLBACK_URL_TEST}:8080/qchb` + ); + break; + } + }); - expect(ajaxStub.firstCall.args[0]).to.eql(adapter.QUANTCAST_CALLBACK_URL); - expect(ajaxStub.firstCall.args[1]).to.exist.and.to.be.a('function'); - expect(ajaxStub.firstCall.args[3]).to.eql({method: 'POST', withCredentials: true}); + it('sends bid requests to Quantcast Global Endpoint for regular `publisherId`', () => { + const bidRequest = { + bidder: 'quantcast', + bidId: '2f7b179d443f14', + requestId: '595ffa73-d78a-46c9-b18e-f99548a5be6b', + bidderRequestId: '1cc026909c24c8', + placementCode: 'div-gpt-ad-1438287399331-0', + params: { + publisherId: 'regular-publisher', // REQUIRED - Publisher ID provided by Quantcast + battr: [1, 2] // OPTIONAL - Array of blocked creative attributes as per OpenRTB Spec List 5.3 + }, + sizes: [[300, 250]] + }; + const requests = qcSpec.buildRequests([bidRequest]); + + switch (window.location.protocol) { + case 'https:': + expect(requests[0]['url']).to.equal( + `https://${QUANTCAST_CALLBACK_URL}:8443/qchb` + ); + break; + default: + expect(requests[0]['url']).to.equal( + `http://${QUANTCAST_CALLBACK_URL}:8080/qchb` + ); + break; + } }); - }); - describe('multiple requests', () => { - let bidderRequest = { - bidderCode: 'quantcast', - requestId: '595ffa73-d78a-46c9-b18e-f99548a5be6b', - bidderRequestId: '1cc026909c24c8', - bids: [ - { - bidId: '2f7b179d443f14', - bidder: 'quantcast', - placementCode: 'div-gpt-ad-1438287399331-0', - sizes: [[300, 250]], - params: { - publisherId: 'test-publisher', - battr: [1, 2], - } - }, { - bidId: '2f7b179d443f15', - bidder: 'quantcast', - placementCode: 'div-gpt-ad-1438287399331-1', - sizes: [[300, 600]], - params: { - publisherId: 'test-publisher', - battr: [1, 2], - } - } - ] - }; + it('sends bid requests to Quantcast Header Bidding Endpoints via POST', () => { + const requests = qcSpec.buildRequests([bidRequest]); - it('request is fired twice for two bids', () => { - adapter.callBids(bidderRequest); - sinon.assert.calledTwice(ajaxStub); + expect(requests[0].method).to.equal('POST'); + }); - let firstReq = JSON.parse(ajaxStub.firstCall.args[2]); - expect(firstReq.requestId).to.eql('2f7b179d443f14'); - expect(firstReq.imp[0].placementCode).to.eql('div-gpt-ad-1438287399331-0'); + it('sends bid requests contains all the required parameters', () => { + const referrer = utils.getTopWindowUrl(); + const loc = utils.getTopWindowLocation(); + const domain = loc.hostname; - let secondReq = JSON.parse(ajaxStub.secondCall.args[2]); - expect(secondReq.requestId).to.eql('2f7b179d443f15'); - expect(secondReq.imp[0].placementCode).to.eql('div-gpt-ad-1438287399331-1'); + const requests = qcSpec.buildRequests([bidRequest]); + const expectedBidRequest = { + publisherId: 'test-publisher', + requestId: '2f7b179d443f14', + imp: [ + { + banner: { + battr: [1, 2], + sizes: [{ width: 300, height: 250 }] + }, + placementCode: 'div-gpt-ad-1438287399331-0', + bidFloor: 1e-10 + } + ], + site: { + page: loc.href, + referrer, + domain + }, + bidId: '2f7b179d443f14' + }; + + expect(requests[0].data).to.equal(JSON.stringify(expectedBidRequest)); }); }); - describe('handleQuantcastCB add bids to the manager', () => { - let firstBid; - let addBidReponseStub; - let bidsRequestedOriginal; - // respond - let bidderReponse = { - 'bidderCode': 'quantcast', - 'requestId': bidderRequest.requestId, - 'bids': [ + describe('`interpretResponse`', () => { + // The sample response is from https://wiki.corp.qc/display/adinf/QCX + const body = { + bidderCode: 'qcx', // Renaming it to use CamelCase since that is what is used in the Prebid.js variable name + requestId: 'erlangcluster@qa-rtb002.us-ec.adtech.com-11417780270886458', // Added this field. This is not used now but could be useful in troubleshooting later on. Specially for sites using iFrames + bids: [ { - 'statusCode': 1, - 'placementCode': bidderRequest.bids[0].bidId, - 'cpm': 4.5, - 'ad': '<!DOCTYPE html>\n\n\n<div style="height: 250; width: 300; display: table-cell; vertical-align: middle;">\n<div style="width: 300px; margin-left: auto; margin-right: auto;"> \n\n <script src="https://adserver.adtechus.com/addyn/3.0/5399.1/2394397/0/-1/QUANTCAST;size=300x250;target=_blank;alias=;kvp36=;sub1=;kvl=;kvc=;kvs=300x250;kvi=;kva=;sub2=;rdclick=http://exch.quantserve.com/r?a=;labels=_qc.clk,_click.adserver.rtb,_click.rand.;rtbip=;rtbdata2=;redirecturl2=" type="text/javascript"></script>\n\n<img src="https://exch.quantserve.com/pixel/p_12345.gif?media=ad&p=&r=&rand=&labels=_qc.imp,_imp.adserver.rtb&rtbip=&rtbdata2=" style="display: none;" border="0" height="1" width="1" alt="Quantcast"/>\n\n</div>\n</div>', - 'width': 300, - 'height': 250 + statusCode: 1, + placementCode: 'imp1', // Changing this to placementCode to be reflective + cpm: 4.5, + currency: 'USD', + ad: + '<!DOCTYPE html><div style="height: 250; width: 300; display: table-cell; vertical-align: middle;"><div style="width: 300px; margin-left: auto; margin-right: auto;"><script src="https://adserver.adtechus.com/addyn/3.0/5399.1/2394397/0/-1/QUANTCAST;size=300x250;target=_blank;alias=;kvp36=;sub1=;kvl=;kvc=;kvs=300x250;kvi=;kva=;sub2=;rdclick=http://exch.quantserve.com/r?a=;labels=_qc.clk,_click.adserver.rtb,_click.rand.;rtbip=;rtbdata2=;redirecturl2=" type="text/javascript"></script><img src="https://exch.quantserve.com/pixel/p_12345.gif?media=ad&p=&r=&rand=&labels=_qc.imp,_imp.adserver.rtb&rtbip=&rtbdata2=" style="display: none;" border="0" height="1" width="1" alt="Quantcast"/></div></div>', + creativeId: 1001, + width: 300, + height: 250 } ] }; - beforeEach(() => { - bidsRequestedOriginal = $$PREBID_GLOBAL$$._bidsRequested; - addBidReponseStub = sandbox.stub(bidManager, 'addBidResponse'); - $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - }); + const response = { + body, + headers: {} + }; - afterEach(() => { - sandbox.restore(); - $$PREBID_GLOBAL$$._bidsRequested = bidsRequestedOriginal; - }); + it('should return an empty array if `serverResponse` is `undefined`', () => { + const interpretedResponse = qcSpec.interpretResponse(); - it('should exist and be a function', () => { - expect($$PREBID_GLOBAL$$.handleQuantcastCB).to.exist.and.to.be.a('function'); + expect(interpretedResponse.length).to.equal(0); }); - it('should not add bid when empty text response comes', () => { - $$PREBID_GLOBAL$$.handleQuantcastCB(); - sinon.assert.notCalled(addBidReponseStub); - }); + it('should return an empty array if the parsed response does NOT include `bids`', () => { + const interpretedResponse = qcSpec.interpretResponse({}); - it('should not add bid when empty json response comes', () => { - $$PREBID_GLOBAL$$.handleQuantcastCB(JSON.stringify({})); - sinon.assert.notCalled(addBidReponseStub); + expect(interpretedResponse.length).to.equal(0); }); - it('should not add bid when malformed json response comes', () => { - $$PREBID_GLOBAL$$.handleQuantcastCB('non json text'); - sinon.assert.notCalled(addBidReponseStub); + it('should return an empty array if the parsed response has an empty `bids`', () => { + const interpretedResponse = qcSpec.interpretResponse({ bids: [] }); + + expect(interpretedResponse.length).to.equal(0); }); - it('should add a bid object for each bid', () => { - // You need the following call so that the in-memory storage of the bidRequest is carried out. Without this the callback won't work correctly. - adapter.callBids(bidderRequest); - $$PREBID_GLOBAL$$.handleQuantcastCB(JSON.stringify(bidderReponse)); - sinon.assert.calledOnce(addBidReponseStub); - expect(addBidReponseStub.firstCall.args[0]).to.eql('div-gpt-ad-1438287399331-0'); + it('should get correct bid response', () => { + const expectedResponse = { + requestId: 'erlangcluster@qa-rtb002.us-ec.adtech.com-11417780270886458', + cpm: 4.5, + width: 300, + height: 250, + ad: + '<!DOCTYPE html><div style="height: 250; width: 300; display: table-cell; vertical-align: middle;"><div style="width: 300px; margin-left: auto; margin-right: auto;"><script src="https://adserver.adtechus.com/addyn/3.0/5399.1/2394397/0/-1/QUANTCAST;size=300x250;target=_blank;alias=;kvp36=;sub1=;kvl=;kvc=;kvs=300x250;kvi=;kva=;sub2=;rdclick=http://exch.quantserve.com/r?a=;labels=_qc.clk,_click.adserver.rtb,_click.rand.;rtbip=;rtbdata2=;redirecturl2=" type="text/javascript"></script><img src="https://exch.quantserve.com/pixel/p_12345.gif?media=ad&p=&r=&rand=&labels=_qc.imp,_imp.adserver.rtb&rtbip=&rtbdata2=" style="display: none;" border="0" height="1" width="1" alt="Quantcast"/></div></div>', + ttl: QUANTCAST_TTL, + creativeId: 1001, + netRevenue: QUANTCAST_NET_REVENUE, + currency: 'USD' + }; + const interpretedResponse = qcSpec.interpretResponse(response); + + expect(interpretedResponse[0]).to.deep.equal(expectedResponse); }); - it('should return no bid even when requestId and sizes are missing', () => { - let bidderReponse = { - 'bidderCode': 'quantcast', - 'bids': [ - { - 'statusCode': 0, - 'placementCode': bidderRequest.bids[0].bidId, - } - ] + it('handles no bid response', () => { + const body = { + bidderCode: 'qcx', // Renaming it to use CamelCase since that is what is used in the Prebid.js variable name + requestId: 'erlangcluster@qa-rtb002.us-ec.adtech.com-11417780270886458', // Added this field. This is not used now but could be useful in troubleshooting later on. Specially for sites using iFrames + bids: [] + }; + const response = { + body, + headers: {} }; + const expectedResponse = []; + const interpretedResponse = qcSpec.interpretResponse(response); - // You need the following call so that the in-memory storage of the bidRequest is carried out. Without this the callback won't work correctly. - adapter.callBids(bidderRequest); - $$PREBID_GLOBAL$$.handleQuantcastCB(JSON.stringify(bidderReponse)); - // sinon.assert.calledOnce(addBidReponseStub); - // expect(addBidReponseStub.firstCall.args[0]).to.eql("div-gpt-ad-1438287399331-0"); + expect(interpretedResponse.length).to.equal(0); }); }); }); From 90c66f2ad535e580d78c9ae12c225f9bb1933b35 Mon Sep 17 00:00:00 2001 From: Connor Doherty <cdoher01@gmail.com> Date: Thu, 26 Oct 2017 23:36:01 +0300 Subject: [PATCH 30/44] Update yieldmoBid adapter request url (#1771) --- modules/yieldmoBidAdapter.js | 2 +- test/spec/modules/yieldmoBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index d311bb5722c..73f18794ea7 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -19,7 +19,7 @@ var YieldmoAdapter = function YieldmoAdapter() { function buildYieldmoCall(bids) { // build our base tag, based on if we are http or https - var ymURI = '//bid.yieldmo.com/exchange/prebid?'; + var ymURI = '//ads.yieldmo.com/exchange/prebid?'; var ymCall = document.location.protocol + ymURI; // Placement specific information diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 370aeb15457..d2a533be17e 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -52,7 +52,7 @@ describe('Yieldmo adapter', () => { }); it('should load a script with passed bid params', () => { - let route = 'http://bid.yieldmo.com/exchange/prebid?'; + let route = 'http://ads.yieldmo.com/exchange/prebid?'; let requestParams = parseURL(bidRequestURL).search; let parsedPlacementParams = JSON.parse(decodeURIComponent(requestParams.p)); From d5f1cd108a722966aafb6932d94df9f77c41cd7d Mon Sep 17 00:00:00 2001 From: adxcgcom <31470944+adxcgcom@users.noreply.github.com> Date: Thu, 26 Oct 2017 23:08:45 +0200 Subject: [PATCH 31/44] Update adxcg adapter for prebid 1.0 (#1741) * updated adxcg adapter for prebid 1.0 * update for prebid 1.0 with dead code and bidder removed * updates to spec file for prebid 1.0 - removal of biddercode check * updated adxcg bidadapter for prebid-1.0 with response headers --- modules/adxcgBidAdapter.js | 268 ++++++++------- modules/adxcgBidAdapter.md | 50 +++ test/spec/modules/adxcgBidAdapter_spec.js | 390 ++++++++++++---------- 3 files changed, 414 insertions(+), 294 deletions(-) create mode 100644 modules/adxcgBidAdapter.md diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index 476cb5989e0..9073a17bda3 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -1,139 +1,155 @@ -import bidfactory from 'src/bidfactory'; -import bidmanager from 'src/bidmanager'; import * as utils from 'src/utils'; -import { STATUS } from 'src/constants'; -import adaptermanager from 'src/adaptermanager'; -import { ajax } from 'src/ajax'; import * as url from 'src/url'; +import {registerBidder} from 'src/adapters/bidderFactory'; +import {NATIVE, VIDEO} from 'src/mediaTypes'; /** - * Adapter for requesting bids from Adxcg - * updated from latest prebid repo on 2017.08.30 + * Adapter for requesting bids from adxcg.net + * updated to latest prebid repo on 2017.10.20 */ -function AdxcgAdapter() { - let bidRequests = {}; - - function _callBids(params) { - if (params.bids && params.bids.length > 0) { - let adZoneIds = []; - let prebidBidIds = []; - let sizes = []; - - params.bids.forEach(bid => { - bidRequests[bid.bidId] = bid; - adZoneIds.push(utils.getBidIdParameter('adzoneid', bid.params)); - prebidBidIds.push(bid.bidId); - sizes.push(utils.parseSizesInput(bid.sizes).join('|')); - }); - - let location = utils.getTopWindowLocation(); - let secure = location.protocol == 'https:'; - - let requestUrl = url.parse(location.href); - requestUrl.search = null; - requestUrl.hash = null; - - let adxcgRequestUrl = url.format({ - protocol: secure ? 'https' : 'http', - hostname: secure ? 'ad-emea-secure.adxcg.net' : 'ad-emea.adxcg.net', - pathname: '/get/adi', - search: { - renderformat: 'javascript', - ver: 'r20141124', - adzoneid: adZoneIds.join(','), - format: sizes.join(','), - prebidBidIds: prebidBidIds.join(','), - url: escape(url.format(requestUrl)), - secure: secure ? '1' : '0' - } - }); - utils.logMessage(`submitting request: ${adxcgRequestUrl}`); - ajax(adxcgRequestUrl, handleResponse, null, { - withCredentials: true - }); - } - } - - function handleResponse(response) { - let adxcgBidReponseList; - - try { - adxcgBidReponseList = JSON.parse(response); - utils.logMessage(`adxcgBidReponseList: ${JSON.stringify(adxcgBidReponseList)}`); - } catch (error) { - adxcgBidReponseList = []; - utils.logError(error); - } +const BIDDER_CODE = 'adxcg'; +const SUPPORTED_AD_TYPES = [VIDEO, NATIVE]; +const SOURCE = 'pbjs10'; +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_AD_TYPES, + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid.params.adzoneid); + }, + + /** + * Make a server request from the list of BidRequests. + * + * an array of validBidRequests + * Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + utils.logMessage(`buildRequests: ${JSON.stringify(validBidRequests)}`); + + let adZoneIds = []; + let prebidBidIds = []; + let sizes = []; + + validBidRequests.forEach(bid => { + adZoneIds.push(utils.getBidIdParameter('adzoneid', bid.params)); + prebidBidIds.push(bid.bidId); + sizes.push(utils.parseSizesInput(bid.sizes).join('|')); + }); - adxcgBidReponseList.forEach(adxcgBidReponse => { - let bidRequest = bidRequests[adxcgBidReponse.bidId]; - delete bidRequests[adxcgBidReponse.bidId]; - - let bid = bidfactory.createBid(STATUS.GOOD, bidRequest); - - bid.creative_id = adxcgBidReponse.creativeId; - bid.code = 'adxcg'; - bid.bidderCode = 'adxcg'; - bid.cpm = adxcgBidReponse.cpm; - - if (adxcgBidReponse.ad) { - bid.ad = adxcgBidReponse.ad; - } else if (adxcgBidReponse.vastUrl) { - bid.vastUrl = adxcgBidReponse.vastUrl; - bid.descriptionUrl = adxcgBidReponse.vastUrl; - bid.mediaType = 'video'; - } else if (adxcgBidReponse.nativeResponse) { - bid.mediaType = 'native'; - - let nativeResponse = adxcgBidReponse.nativeResponse; - - bid['native'] = { - clickUrl: escape(nativeResponse.link.url), - impressionTrackers: nativeResponse.imptrackers - }; - - nativeResponse.assets.forEach(asset => { - if (asset.title && asset.title.text) { - bid['native'].title = asset.title.text; - } - - if (asset.img && asset.img.url) { - bid['native'].image = asset.img.url; - } - - if (asset.data && asset.data.label == 'DESC' && asset.data.value) { - bid['native'].body = asset.data.value; - } - - if (asset.data && asset.data.label == 'SPONSORED' && asset.data.value) { - bid['native'].sponsoredBy = asset.data.value; - } - }); + let location = utils.getTopWindowLocation(); + let secure = location.protocol === 'https:'; + + let requestUrl = url.parse(location.href); + requestUrl.search = null; + requestUrl.hash = null; + + let adxcgRequestUrl = url.format({ + protocol: secure ? 'https' : 'http', + hostname: secure ? 'hbps.adxcg.net' : 'hbp.adxcg.net', + pathname: '/get/adi', + search: { + renderformat: 'javascript', + ver: 'r20171019PB10', + adzoneid: adZoneIds.join(','), + format: sizes.join(','), + prebidBidIds: prebidBidIds.join(','), + url: encodeURIComponent(url.format(requestUrl)), + secure: secure ? '1' : '0', + source: SOURCE, + pbjs: '$prebid.version$' } + }); - bid.width = adxcgBidReponse.width; - bid.height = adxcgBidReponse.height; + return { + method: 'GET', + url: adxcgRequestUrl, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {bidRequests[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequests) { + let bids = []; + + serverResponse = serverResponse.body; + if (serverResponse) { + serverResponse.forEach(serverResponseOneItem => { + let bid = {}; + + bid.requestId = serverResponseOneItem.bidId; + bid.cpm = serverResponseOneItem.cpm; + bid.creativeId = parseInt(serverResponseOneItem.creativeId); + bid.currency = 'USD'; + bid.netRevenue = serverResponseOneItem.netRevenue ? serverResponseOneItem.netRevenue : true; + bid.ttl = 300; + + if (serverResponseOneItem.deal_id != null && serverResponseOneItem.deal_id.trim().length > 0) { + bid.dealId = serverResponseOneItem.deal_id; + } - utils.logMessage(`submitting bid[${bidRequest.placementCode}]: ${JSON.stringify(bid)}`); - bidmanager.addBidResponse(bidRequest.placementCode, bid); - }); + if (serverResponseOneItem.ad) { + bid.ad = serverResponseOneItem.ad; + } else if (serverResponseOneItem.vastUrl) { + bid.vastUrl = serverResponseOneItem.vastUrl; + bid.descriptionUrl = serverResponseOneItem.vastUrl; + bid.mediaType = 'video'; + } else if (serverResponseOneItem.nativeResponse) { + bid.mediaType = 'native'; + + let nativeResponse = serverResponseOneItem.nativeResponse; + + bid['native'] = { + clickUrl: encodeURIComponent(nativeResponse.link.url), + impressionTrackers: nativeResponse.imptrackers + }; + + nativeResponse.assets.forEach(asset => { + if (asset.title && asset.title.text) { + bid['native'].title = asset.title.text; + } + + if (asset.img && asset.img.url) { + bid['native'].image = asset.img.url; + } + + if (asset.data && asset.data.label === 'DESC' && asset.data.value) { + bid['native'].body = asset.data.value; + } + + if (asset.data && asset.data.label === 'SPONSORED' && asset.data.value) { + bid['native'].sponsoredBy = asset.data.value; + } + }); + } - Object.keys(bidRequests) - .map(bidId => bidRequests[bidId].placementCode) - .forEach(placementCode => { - utils.logMessage(`creating no_bid bid for: ${placementCode}`); - bidmanager.addBidResponse(placementCode, bidfactory.createBid(STATUS.NO_BID)); + bid.width = serverResponseOneItem.width; + bid.height = serverResponseOneItem.height; + utils.logMessage(`submitting bid[${serverResponseOneItem.bidId}]: ${JSON.stringify(bid)}`); + bids.push(bid); }); - }; - - return { - callBids: _callBids - }; + } else { + utils.logMessage(`empty bid response`); + } + return bids; + }, + getUserSyncs: function (syncOptions) { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: '//cdn.adxcg.net/pb-sync.html' + }]; + } + } }; - -adaptermanager.registerBidAdapter(new AdxcgAdapter(), 'adxcg', { - supportedMediaTypes: ['video', 'native'] -}); - -module.exports = AdxcgAdapter; +registerBidder(spec); diff --git a/modules/adxcgBidAdapter.md b/modules/adxcgBidAdapter.md new file mode 100644 index 00000000000..f3cd8c6d308 --- /dev/null +++ b/modules/adxcgBidAdapter.md @@ -0,0 +1,50 @@ +# Overview + +**Module Name**: Adxcg Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: info@adxcg.com + +# Description + +Module that connects to an Adxcg.com zone to fetch bids. + +# Test Parameters +``` + `` + var adUnits = [{ + code: 'banner-ad-div', + sizes: [[300, 250]], + bids: [{ + bidder: 'adxcg', + params: { + adzoneid: '1' + } + }] + },{ + code: 'native-ad-div', + sizes: [[300, 250], [1, 1]], + nativeParams: { + title: { required: true, len: 75 }, + image: { required: true }, + body: { len: 200 }, + sponsoredBy: { len: 20 } + }, + bids: [{ + bidder: 'adxcg', + params: { + adzoneid: '2379' + } + } + }] + },{ + code: 'video', + sizes: [[640, 480]], + bids: [{ + bidder: 'adxcg', + params: { + adzoneid: '20' + } + } + }] + }]; +``` diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index fa55bf92e2e..dbf7359e98d 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -1,116 +1,62 @@ -import { expect } from 'chai'; -import Adapter from 'modules/adxcgBidAdapter'; -import bidmanager from 'src/bidmanager'; +import {expect} from 'chai'; import * as url from 'src/url'; +import {spec} from 'modules/adxcgBidAdapter'; -const REQUEST = { - 'bidderCode': 'adxcg', - 'bids': [ - { +describe('AdxcgAdapter', () => { + describe('isBidRequestValid', () => { + let bid = { 'bidder': 'adxcg', 'params': { - 'adzoneid': '1', + 'adzoneid': '1' }, - 'sizes': [ - [300, 250], - [640, 360], - [1, 1] - ], + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [640, 360], [1, 1]], 'bidId': '84ab500420319d', - 'bidderRequestId': '7101db09af0db2' - } - ] -}; - -const RESPONSE = [{ - 'bidId': '84ab500420319d', - 'width': 300, - 'height': 250, - 'creativeId': '42', - 'cpm': 0.45, - 'ad': '<!-- adContent -->' -}] - -const VIDEO_RESPONSE = [{ - 'bidId': '84ab500420319d', - 'width': 640, - 'height': 360, - 'creativeId': '42', - 'cpm': 0.45, - 'vastUrl': 'vastContentUrl' -}] - -const NATIVE_RESPONSE = [{ - 'bidId': '84ab500420319d', - 'width': 0, - 'height': 0, - 'creativeId': '42', - 'cpm': 0.45, - 'nativeResponse': { - 'assets': [{ - 'id': 1, - 'required': 0, - 'title': { - 'text': 'titleContent' - } - }, { - 'id': 2, - 'required': 0, - 'img': { - 'url': 'imageContent', - 'w': 600, - 'h': 600 - } - }, { - 'id': 3, - 'required': 0, - 'data': { - 'label': 'DESC', - 'value': 'descriptionContent' - } - }, { - 'id': 0, - 'required': 0, - 'data': { - 'label': 'SPONSORED', - 'value': 'sponsoredByContent' - } - }], - 'link': { - 'url': 'linkContent' - }, - 'imptrackers': ['impressionTracker1', 'impressionTracker2'] - } -}] + 'bidderRequestId': '7101db09af0db2', + 'auctionId': '1d1a030790a475', + }; -describe('AdxcgAdapter', () => { - let adapter; - - beforeEach(() => adapter = new Adapter()); - - describe('request function', () => { - let xhr; - let requests; + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); }); + }); - afterEach(() => xhr.restore()); + describe('request function http', () => { + let bid = { + 'bidder': 'adxcg', + 'params': { + 'adzoneid': '1' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [640, 360], [1, 1]], + 'bidId': '84ab500420319d', + 'bidderRequestId': '7101db09af0db2', + 'auctionId': '1d1a030790a475', + }; it('creates a valid adxcg request url', () => { - adapter.callBids(REQUEST); + let request = spec.buildRequests([bid]); + expect(request).to.exist; + // console.log('IS:' + JSON.stringify(request)); - let parsedRequestUrl = url.parse(requests[0].url); + expect(request.method).to.equal('GET'); + let parsedRequestUrl = url.parse(request.url); - expect(parsedRequestUrl.hostname).to.equal('ad-emea.adxcg.net'); + expect(parsedRequestUrl.hostname).to.equal('hbp.adxcg.net'); expect(parsedRequestUrl.pathname).to.equal('/get/adi'); let query = parsedRequestUrl.search; expect(query.renderformat).to.equal('javascript'); - expect(query.ver).to.equal('r20141124'); + expect(query.ver).to.equal('r20171019PB10'); + expect(query.source).to.equal('pbjs10'); + expect(query.pbjs).to.equal('$prebid.version$'); expect(query.adzoneid).to.equal('1'); expect(query.format).to.equal('300x250|640x360|1x1'); expect(query.jsonp).to.be.empty; @@ -119,94 +65,202 @@ describe('AdxcgAdapter', () => { }); describe('response handler', () => { - let server; + let BIDDER_REQUEST = { + 'bidder': 'adxcg', + 'params': { + 'adzoneid': '1' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [640, 360], [1, 1]], + 'bidId': '84ab500420319d', + 'bidderRequestId': '7101db09af0db2', + 'auctionId': '1d1a030790a475', + }; - beforeEach(() => { - server = sinon.fakeServer.create(); - sinon.stub(bidmanager, 'addBidResponse'); - }); + let BANNER_RESPONSE = + { + body: [{ + 'bidId': '84ab500420319d', + 'bidderCode': 'adxcg', + 'width': 300, + 'height': 250, + 'creativeId': '42', + 'cpm': 0.45, + 'currency': 'USD', + 'netRevenue': true, + 'ad': '<!-- adContent -->' + }], + header: {'someheader': 'fakedata'} + } - afterEach(() => { - server.restore() - bidmanager.addBidResponse.restore(); - }); + let BANNER_RESPONSE_WITHDEALID = + { + body: [{ + 'bidId': '84ab500420319d', + 'bidderCode': 'adxcg', + 'width': 300, + 'height': 250, + 'deal_id': '7722', + 'creativeId': '42', + 'cpm': 0.45, + 'currency': 'USD', + 'netRevenue': true, + 'ad': '<!-- adContent -->' + }], + header: {'someheader': 'fakedata'} + } + + let VIDEO_RESPONSE = + { + body: [{ + 'bidId': '84ab500420319d', + 'bidderCode': 'adxcg', + 'width': 640, + 'height': 360, + 'creativeId': '42', + 'cpm': 0.45, + 'currency': 'USD', + 'netRevenue': true, + 'vastUrl': 'vastContentUrl' + }], + header: {'someheader': 'fakedata'} + } + + let NATIVE_RESPONSE = + { + body: [{ + 'bidId': '84ab500420319d', + 'bidderCode': 'adxcg', + 'width': 0, + 'height': 0, + 'creativeId': '42', + 'cpm': 0.45, + 'currency': 'USD', + 'netRevenue': true, + 'nativeResponse': { + 'assets': [{ + 'id': 1, + 'required': 0, + 'title': { + 'text': 'titleContent' + } + }, { + 'id': 2, + 'required': 0, + 'img': { + 'url': 'imageContent', + 'w': 600, + 'h': 600 + } + }, { + 'id': 3, + 'required': 0, + 'data': { + 'label': 'DESC', + 'value': 'descriptionContent' + } + }, { + 'id': 0, + 'required': 0, + 'data': { + 'label': 'SPONSORED', + 'value': 'sponsoredByContent' + } + }], + 'link': { + 'url': 'linkContent' + }, + 'imptrackers': ['impressionTracker1', 'impressionTracker2'] + } + }], + header: {'someheader': 'fakedata'} + } it('handles regular responses', () => { - server.respondWith(JSON.stringify(RESPONSE)); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.bidderCode).to.equal('adxcg'); - expect(bidResponse.width).to.equal(300); - expect(bidResponse.height).to.equal(250); - expect(bidResponse.statusMessage).to.equal('Bid available'); - expect(bidResponse.adId).to.equal('84ab500420319d'); - expect(bidResponse.mediaType).to.equal('banner'); - expect(bidResponse.creative_id).to.equal('42'); - expect(bidResponse.code).to.equal('adxcg'); - expect(bidResponse.cpm).to.equal(0.45); - expect(bidResponse.ad).to.equal('<!-- adContent -->'); + let result = spec.interpretResponse(BANNER_RESPONSE, BIDDER_REQUEST); + + expect(result).to.have.lengthOf(1); + + expect(result[0]).to.exist; + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].creativeId).to.equal(42); + expect(result[0].cpm).to.equal(0.45); + expect(result[0].ad).to.equal('<!-- adContent -->'); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(300); + expect(result[0].dealId).to.not.exist; + }); + + it('handles regular responses with dealid', () => { + let result = spec.interpretResponse(BANNER_RESPONSE_WITHDEALID, BIDDER_REQUEST); + + expect(result).to.have.lengthOf(1); + + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].creativeId).to.equal(42); + expect(result[0].cpm).to.equal(0.45); + expect(result[0].ad).to.equal('<!-- adContent -->'); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(300); }); it('handles video responses', () => { - server.respondWith(JSON.stringify(VIDEO_RESPONSE)); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.bidderCode).to.equal('adxcg'); - expect(bidResponse.width).to.equal(640); - expect(bidResponse.height).to.equal(360); - expect(bidResponse.statusMessage).to.equal('Bid available'); - expect(bidResponse.adId).to.equal('84ab500420319d'); - expect(bidResponse.mediaType).to.equal('video'); - expect(bidResponse.creative_id).to.equal('42'); - expect(bidResponse.code).to.equal('adxcg'); - expect(bidResponse.cpm).to.equal(0.45); - expect(bidResponse.vastUrl).to.equal('vastContentUrl'); - expect(bidResponse.descriptionUrl).to.equal('vastContentUrl'); + let result = spec.interpretResponse(VIDEO_RESPONSE, BIDDER_REQUEST); + expect(result).to.have.lengthOf(1); + + expect(result[0].width).to.equal(640); + expect(result[0].height).to.equal(360); + expect(result[0].mediaType).to.equal('video'); + expect(result[0].creativeId).to.equal(42); + expect(result[0].cpm).to.equal(0.45); + expect(result[0].vastUrl).to.equal('vastContentUrl'); + expect(result[0].descriptionUrl).to.equal('vastContentUrl'); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(300); }); it('handles native responses', () => { - server.respondWith(JSON.stringify(NATIVE_RESPONSE)); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.bidderCode).to.equal('adxcg'); - expect(bidResponse.width).to.equal(0); - expect(bidResponse.height).to.equal(0); - expect(bidResponse.statusMessage).to.equal('Bid available'); - expect(bidResponse.adId).to.equal('84ab500420319d'); - expect(bidResponse.mediaType).to.equal('native'); - expect(bidResponse.creative_id).to.equal('42'); - expect(bidResponse.code).to.equal('adxcg'); - expect(bidResponse.cpm).to.equal(0.45); - - expect(bidResponse.native.clickUrl).to.equal('linkContent'); - expect(bidResponse.native.impressionTrackers).to.deep.equal(['impressionTracker1', 'impressionTracker2']); - expect(bidResponse.native.title).to.equal('titleContent'); - expect(bidResponse.native.image).to.equal('imageContent'); - expect(bidResponse.native.body).to.equal('descriptionContent'); - expect(bidResponse.native.sponsoredBy).to.equal('sponsoredByContent'); + let result = spec.interpretResponse(NATIVE_RESPONSE, BIDDER_REQUEST); + + expect(result[0].width).to.equal(0); + expect(result[0].height).to.equal(0); + expect(result[0].mediaType).to.equal('native'); + expect(result[0].creativeId).to.equal(42); + expect(result[0].cpm).to.equal(0.45); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(300); + + expect(result[0].native.clickUrl).to.equal('linkContent'); + expect(result[0].native.impressionTrackers).to.deep.equal(['impressionTracker1', 'impressionTracker2']); + expect(result[0].native.title).to.equal('titleContent'); + expect(result[0].native.image).to.equal('imageContent'); + expect(result[0].native.body).to.equal('descriptionContent'); + expect(result[0].native.sponsoredBy).to.equal('sponsoredByContent'); }); it('handles nobid responses', () => { - server.respondWith('[]'); + let response = []; + let bidderRequest = BIDDER_REQUEST; + + let result = spec.interpretResponse(response, bidderRequest); + expect(result.length).to.equal(0); + }); + }); - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + describe('getUserSyncs', () => { + let syncoptionsIframe = { + 'iframeEnabled': 'true' + }; - const bidResponse = bidmanager.addBidResponse.firstCall.args[1]; - expect(bidResponse.statusMessage).to.equal('Bid returned empty or error response'); + it('should return iframe sync option', () => { + expect(spec.getUserSyncs(syncoptionsIframe)[0].type).to.equal('iframe'); + expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.equal('//cdn.adxcg.net/pb-sync.html'); }); }); }); From c2dd4c771dcb7bdec12e46302dad752f48c1ddd1 Mon Sep 17 00:00:00 2001 From: Matt Kendall <mkendall@appnexus.com> Date: Fri, 27 Oct 2017 09:54:44 -0400 Subject: [PATCH 32/44] add vastUrl + media type for video bids Prebid Server (#1739) * add vastUrl + media type for video bids * updates per review * updates per review * updates per review --- modules/prebidServerBidAdapter.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 0906a1a0b3d..89a59fa7c67 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -7,6 +7,7 @@ import { STATUS, S2S } from 'src/constants'; import { cookieSet } from 'src/cookie.js'; import adaptermanager from 'src/adaptermanager'; import { config } from 'src/config'; +import { VIDEO } from 'src/mediaTypes'; const getConfig = config.getConfig; @@ -111,7 +112,9 @@ function PrebidServer() { if (videoMediaType) { // pbs expects a ad_unit.video attribute if the imp is video adUnit.video = Object.assign({}, videoMediaType); - delete adUnit.mediaTypes.video; + delete adUnit.mediaTypes; + // default is assumed to be 'banner' so if there is a video type we assume video only until PBS can support multi format auction. + adUnit.media_types = [VIDEO]; } }) convertTypes(adUnits); @@ -196,10 +199,26 @@ function PrebidServer() { bidObject.creative_id = bidObj.creative_id; bidObject.bidderCode = bidObj.bidder; bidObject.cpm = cpm; - bidObject.ad = bidObj.adm; - if (bidObj.nurl) { - bidObject.ad += utils.createTrackPixelHtml(decodeURIComponent(bidObj.nurl)); + // From ORTB see section 4.2.3: adm Optional means of conveying ad markup in case the bid wins; supersedes the win notice if markup is included in both. + if (bidObj.media_type === VIDEO) { + bidObject.mediaType = VIDEO; + if (bidObj.adm) { + bidObject.vastXml = bidObj.adm; + } + if (bidObj.nurl) { + bidObject.vastUrl = bidObj.nurl; + } + } else { + if (bidObj.adm && bidObj.nurl) { + bidObject.ad = bidObj.adm; + bidObject.ad += utils.createTrackPixelHtml(decodeURIComponent(bidObj.nurl)); + } else if (bidObj.adm) { + bidObject.ad = bidObj.adm; + } else if (bidObj.nurl) { + bidObject.adUrl = bidObj.nurl + } } + bidObject.width = bidObj.width; bidObject.height = bidObj.height; bidObject.adserverTargeting = bidObj.ad_server_targeting; @@ -223,7 +242,6 @@ function PrebidServer() { bidObject.source = TYPE; bidObject.adUnitCode = bidRequest.placementCode; bidObject.bidderCode = bidRequest.bidder; - bidmanager.addBidResponse(bidObject.adUnitCode, bidObject); }); }); From a592a7a003ebee8e75342dfb27cf7dc5a0cfee29 Mon Sep 17 00:00:00 2001 From: Niksok <belnamtar@mail.ru> Date: Fri, 27 Oct 2017 20:48:42 +0300 Subject: [PATCH 33/44] Fix Centro adapter to allow requests of the same units (#1746) * Add centro adapter and tests for it. * fix bug with different types of bid.sectionID and bid.unit from config * add query parameter adapter=prebid * update tests for centro adapter * fixed bug with call of JSONP callback with name, that contain invalid characters * Centro adapter fix: do not call logError if 'No Bid' was received * Centro adapter: pass the bid request object to bidfactory.createBid * Centro adapter: fix ESLintError * Fix Centro adapter to allow requests of the same units * Fix spec file for Centro adapter --- modules/centroBidAdapter.js | 2 +- test/spec/modules/centroBidAdapter_spec.js | 23 ++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/modules/centroBidAdapter.js b/modules/centroBidAdapter.js index 9cd3ec004fa..34162f8d1ec 100644 --- a/modules/centroBidAdapter.js +++ b/modules/centroBidAdapter.js @@ -54,7 +54,7 @@ var CentroAdapter = function CentroAdapter() { query.push('sz=' + size.join('x')); } // make handler name for JSONP request - var handlerName = handlerPrefix + bid.unit + size.join('x') + encodeURIComponent(requestedBid.placementCode); + var handlerName = handlerPrefix + bid.unit + size.join('x') + encodeURIComponent(requestedBid.bidId); query.push('callback=' + encodeURIComponent('window["' + handlerName + '"]')); // maybe is needed add some random parameter to disable cache diff --git a/test/spec/modules/centroBidAdapter_spec.js b/test/spec/modules/centroBidAdapter_spec.js index 9f354e1ba56..a4bceb5de39 100644 --- a/test/spec/modules/centroBidAdapter_spec.js +++ b/test/spec/modules/centroBidAdapter_spec.js @@ -43,6 +43,7 @@ describe('centro adapter tests', function () { unit: 28136, page_url: 'http://test_url.ru' }, + bidId: '1234', placementCode: 'div-gpt-ad-12345-1' }, { @@ -51,12 +52,14 @@ describe('centro adapter tests', function () { params: { unit: 28137 }, + bidId: '5678', placementCode: 'div-gpt-ad-12345-2' }, { bidder: 'centro', sizes: [[728, 90]], params: {}, + bidId: '9101112', placementCode: 'div-gpt-ad-12345-3' } ] @@ -71,7 +74,7 @@ describe('centro adapter tests', function () { var parsedBidUrl = urlParse(bidUrl1); var parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); - var generatedCallback = 'window["adCentroHandler_28136300x250div-gpt-ad-12345-1"]'; + var generatedCallback = 'window["adCentroHandler_28136300x2501234"]'; expect(parsedBidUrl.hostname).to.equal('staging.brand-server.com'); expect(parsedBidUrl.pathname).to.equal('/hb'); @@ -85,7 +88,7 @@ describe('centro adapter tests', function () { parsedBidUrl = urlParse(bidUrl2); parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); - generatedCallback = 'window["adCentroHandler_28137728x90div-gpt-ad-12345-2"]'; + generatedCallback = 'window["adCentroHandler_28137728x905678"]'; expect(parsedBidUrl.hostname).to.equal('t.brand-server.com'); expect(parsedBidUrl.pathname).to.equal('/hb'); @@ -117,6 +120,7 @@ describe('centro adapter tests', function () { params: { unit: 28136 }, + bidId: '12345', placementCode: '/19968336/header-bid-tag-0' }, { @@ -125,6 +129,7 @@ describe('centro adapter tests', function () { params: { unit: 111111 }, + bidId: '12346', placementCode: '/19968336/header-bid-tag-1' }, { @@ -133,6 +138,7 @@ describe('centro adapter tests', function () { params: { unit: 222222 }, + bidId: '12347', placementCode: '/19968336/header-bid-tag-2' }, { @@ -141,6 +147,7 @@ describe('centro adapter tests', function () { params: { unit: 333333 }, + bidId: '12348', placementCode: '/19968336/header-bid-tag-3' } ] @@ -149,9 +156,9 @@ describe('centro adapter tests', function () { it('callback function should exist', function () { adapter().callBids(params); - expect(window['adCentroHandler_28136300x250%2F19968336%2Fheader-bid-tag-0']) + expect(window['adCentroHandler_28136300x25012345']) .to.exist.and.to.be.a('function'); - expect(window['adCentroHandler_111111728x90%2F19968336%2Fheader-bid-tag-1']) + expect(window['adCentroHandler_111111728x9012346']) .to.exist.and.to.be.a('function'); }); @@ -180,10 +187,10 @@ describe('centro adapter tests', function () { var response3 = {'adTag': '', 'height': 0, 'value': 0, 'width': 0, 'sectionID': 222222}; var response4 = ''; - window['adCentroHandler_28136300x250%2F19968336%2Fheader-bid-tag-0'](response); - window['adCentroHandler_111111728x90%2F19968336%2Fheader-bid-tag-1'](response2); - window['adCentroHandler_222222728x90%2F19968336%2Fheader-bid-tag-2'](response3); - window['adCentroHandler_333333728x90%2F19968336%2Fheader-bid-tag-3'](response4); + window['adCentroHandler_28136300x25012345'](response); + window['adCentroHandler_111111728x9012346'](response2); + window['adCentroHandler_222222728x9012347'](response3); + window['adCentroHandler_333333728x9012348'](response4); var bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; var bidObject1 = stubAddBidResponse.getCall(0).args[1]; From 265a8a9b6fbcff24fbc7caabbb2df6113efc100d Mon Sep 17 00:00:00 2001 From: Rich Snapp <rsnapp@rubiconproject.com> Date: Mon, 30 Oct 2017 06:38:20 -0600 Subject: [PATCH 34/44] Change prebidServer to call client user syncs if they exist (#1734) --- modules/prebidServerBidAdapter.js | 8 +++++ modules/rubiconBidAdapter.js | 4 +-- src/adaptermanager.js | 4 +++ src/adapters/bidderFactory.js | 33 +++++++++++-------- .../modules/prebidServerBidAdapter_spec.js | 18 ++++++++++ test/spec/modules/rubiconBidAdapter_spec.js | 9 +++-- 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 89a59fa7c67..7120d67eb56 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -183,6 +183,14 @@ function PrebidServer() { }); } + // do client-side syncs if available + requestedBidders.forEach(bidder => { + let clientAdapter = adaptermanager.getBidAdapter(bidder); + if (clientAdapter && clientAdapter.registerSyncs) { + clientAdapter.registerSyncs(); + } + }); + if (result.bids) { result.bids.forEach(bidObj => { let bidRequest = utils.getBidRequest(bidObj.bid_id); diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 866e02bc258..2830711a4c9 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -287,8 +287,8 @@ export const spec = { return bids; }, []); }, - getUserSyncs: function() { - if (!hasSynced) { + getUserSyncs: function(syncOptions) { + if (!hasSynced && syncOptions.iframeEnabled) { hasSynced = true; return { type: 'iframe', diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 37ab5dffafb..34866365445 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -325,6 +325,10 @@ exports.setBidderSequence = function (order) { } }; +exports.getBidAdapter = function(bidder) { + return _bidderRegistry[bidder]; +}; + exports.setS2SConfig = function (config) { _s2sConfig = config; }; diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 241891640c5..9641c4c484b 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -154,6 +154,7 @@ export function newBidder(spec) { getSpec: function() { return Object.freeze(spec); }, + registerSyncs, callBids: function(bidderRequest) { if (!Array.isArray(bidderRequest.bids)) { return; @@ -195,20 +196,7 @@ export function newBidder(spec) { const responses = []; function afterAllResponses() { fillNoBids(); - if (spec.getUserSyncs) { - let syncs = spec.getUserSyncs({ - iframeEnabled: config.getConfig('userSync.iframeEnabled'), - pixelEnabled: config.getConfig('userSync.pixelEnabled'), - }, responses); - if (syncs) { - if (!Array.isArray(syncs)) { - syncs = [syncs]; - } - syncs.forEach((sync) => { - userSync.registerSync(sync.type, spec.code, sync.url) - }); - } - } + registerSyncs(responses); } const validBidRequests = bidderRequest.bids.filter(filterAndWarn); @@ -337,6 +325,23 @@ export function newBidder(spec) { } }); + function registerSyncs(responses) { + if (spec.getUserSyncs) { + let syncs = spec.getUserSyncs({ + iframeEnabled: config.getConfig('userSync.iframeEnabled'), + pixelEnabled: config.getConfig('userSync.pixelEnabled'), + }, responses); + if (syncs) { + if (!Array.isArray(syncs)) { + syncs = [syncs]; + } + syncs.forEach((sync) => { + userSync.registerSync(sync.type, spec.code, sync.url) + }); + } + } + } + function filterAndWarn(bid) { if (!spec.isBidRequestValid(bid)) { logWarn(`Invalid bid sent to bidder ${spec.code}: ${JSON.stringify(bid)}`); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 21098a2859f..78eef4b016b 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; import Adapter from 'modules/prebidServerBidAdapter'; +import adapterManager from 'src/adaptermanager'; import bidmanager from 'src/bidmanager'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils'; @@ -353,6 +354,23 @@ describe('S2S Adapter', () => { expect(response).to.have.property('adserverTargeting').that.deep.equals({'foo': 'bar'}); }); + it('registers client user syncs when client bid adapter is present', () => { + let rubiconAdapter = { + registerSyncs: sinon.spy() + }; + sinon.stub(adapterManager, 'getBidAdapter', () => rubiconAdapter); + + server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); + + adapter.setConfig(CONFIG); + adapter.callBids(REQUEST); + server.respond(); + + sinon.assert.calledOnce(rubiconAdapter.registerSyncs); + + adapterManager.getBidAdapter.restore(); + }); + it('registers bid responses when server requests cookie sync', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 6c08c66f485..f77391dffe2 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -4,6 +4,7 @@ import { spec, masSizeOrdering, resetUserSync } from 'modules/rubiconBidAdapter' import { parse as parseQuery } from 'querystring'; import { newBidder } from 'src/adapters/bidderFactory'; import { userSync } from 'src/userSync'; +import { config } from 'src/config'; var CONSTANTS = require('src/constants.json'); @@ -779,13 +780,17 @@ describe('the rubicon adapter', () => { }); it('should register the Emily iframe', () => { - let syncs = spec.getUserSyncs(); + let syncs = spec.getUserSyncs({ + iframeEnabled: true + }); expect(syncs).to.deep.equal({type: 'iframe', url: emilyUrl}); }); it('should not register the Emily iframe more than once', () => { - let syncs = spec.getUserSyncs(); + let syncs = spec.getUserSyncs({ + iframeEnabled: true + }); expect(syncs).to.deep.equal({type: 'iframe', url: emilyUrl}); // when called again, should still have only been called once From 8420558d2e565b1951713b9a90a940bfb9d0b248 Mon Sep 17 00:00:00 2001 From: Rich Snapp <rsnapp@rubiconproject.com> Date: Mon, 30 Oct 2017 08:53:18 -0600 Subject: [PATCH 35/44] New hooks API (replaces monkey-patching for currency) (#1683) * added hook module to prebid core that allows extension of arbitrary functions * remove unused dependency tiny-queue * change PluginFunction to HookedFunction * more hook documentation fixes * allow context for hooked functions * added tests for context * remove withContext, just use bind * fix in hooks so asyncSeries keeps proper bound context --- modules/currency.js | 67 ++++++------- src/bidmanager.js | 5 +- src/hook.js | 78 +++++++++++++++ test/spec/hook_spec.js | 151 +++++++++++++++++++++++++++++ test/spec/modules/currency_spec.js | 60 +++++------- 5 files changed, 283 insertions(+), 78 deletions(-) create mode 100644 src/hook.js create mode 100644 test/spec/hook_spec.js diff --git a/modules/currency.js b/modules/currency.js index 1d0286ed569..19e7d2903f4 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -13,9 +13,6 @@ var conversionCache = {}; var currencyRatesLoaded = false; var adServerCurrency = 'USD'; -// Used as reference to the original bidmanager.addBidResponse -var originalBidResponse; - export var currencySupportEnabled = false; export var currencyRates = {}; var bidderCurrencyDefault = {}; @@ -81,12 +78,9 @@ function initCurrency(url) { conversionCache = {}; currencySupportEnabled = true; - if (!originalBidResponse) { - utils.logInfo('Installing addBidResponse decorator for currency module', arguments); + utils.logInfo('Installing addBidResponse decorator for currency module', arguments); - originalBidResponse = bidmanager.addBidResponse; - bidmanager.addBidResponse = addBidResponseDecorator(bidmanager.addBidResponse); - } + bidmanager.addBidResponse.addHook(addBidResponseHook, 100); if (!currencyRates.conversions) { ajax(url, function (response) { @@ -103,12 +97,9 @@ function initCurrency(url) { } function resetCurrency() { - if (originalBidResponse) { - utils.logInfo('Uninstalling addBidResponse decorator for currency module', arguments); + utils.logInfo('Uninstalling addBidResponse decorator for currency module', arguments); - bidmanager.addBidResponse = originalBidResponse; - originalBidResponse = undefined; - } + bidmanager.addBidResponse.removeHook(addBidResponseHook); adServerCurrency = 'USD'; conversionCache = {}; @@ -118,37 +109,35 @@ function resetCurrency() { bidderCurrencyDefault = {}; } -export function addBidResponseDecorator(fn) { - return function(adUnitCode, bid) { - if (!bid) { - return fn.apply(this, arguments); // if no bid, call original and let it display warnings - } +export function addBidResponseHook(adUnitCode, bid, fn) { + if (!bid) { + return fn.apply(this, arguments); // if no bid, call original and let it display warnings + } - let bidder = bid.bidderCode || bid.bidder; - if (bidderCurrencyDefault[bidder]) { - let currencyDefault = bidderCurrencyDefault[bidder]; - if (bid.currency && currencyDefault !== bid.currency) { - utils.logWarn(`Currency default '${bidder}: ${currencyDefault}' ignored. adapter specified '${bid.currency}'`); - } else { - bid.currency = currencyDefault; - } + let bidder = bid.bidderCode || bid.bidder; + if (bidderCurrencyDefault[bidder]) { + let currencyDefault = bidderCurrencyDefault[bidder]; + if (bid.currency && currencyDefault !== bid.currency) { + utils.logWarn(`Currency default '${bidder}: ${currencyDefault}' ignored. adapter specified '${bid.currency}'`); + } else { + bid.currency = currencyDefault; } + } - // default to USD if currency not set - if (!bid.currency) { - utils.logWarn('Currency not specified on bid. Defaulted to "USD"'); - bid.currency = 'USD'; - } + // default to USD if currency not set + if (!bid.currency) { + utils.logWarn('Currency not specified on bid. Defaulted to "USD"'); + bid.currency = 'USD'; + } - // execute immediately if the bid is already in the desired currency - if (bid.currency === adServerCurrency) { - return fn.apply(this, arguments); - } + // execute immediately if the bid is already in the desired currency + if (bid.currency === adServerCurrency) { + return fn.apply(this, arguments); + } - bidResponseQueue.push(wrapFunction(fn, this, arguments)); - if (!currencySupportEnabled || currencyRatesLoaded) { - processBidResponseQueue(); - } + bidResponseQueue.push(wrapFunction(fn, this, arguments)); + if (!currencySupportEnabled || currencyRatesLoaded) { + processBidResponseQueue(); } } diff --git a/src/bidmanager.js b/src/bidmanager.js index c12cc4828e6..abd8fea135d 100644 --- a/src/bidmanager.js +++ b/src/bidmanager.js @@ -5,6 +5,7 @@ import { isValidVideoBid } from './video'; import { getCacheUrl, store } from './videoCache'; import { Renderer } from 'src/Renderer'; import { config } from 'src/config'; +import { createHook } from 'src/hook'; var CONSTANTS = require('./constants.json'); var AUCTION_END = CONSTANTS.EVENTS.AUCTION_END; @@ -82,7 +83,7 @@ exports.bidsBackAll = function () { /* * This function should be called to by the bidder adapter to register a bid response */ -exports.addBidResponse = function (adUnitCode, bid) { +exports.addBidResponse = createHook('asyncSeries', function (adUnitCode, bid) { if (isValid()) { prepareBidForAuction(); @@ -250,7 +251,7 @@ exports.addBidResponse = function (adUnitCode, bid) { doCallbacksIfNeeded(); } } -}; +}); function getKeyValueTargetingPairs(bidderCode, custBidObj) { var keyValues = {}; diff --git a/src/hook.js b/src/hook.js new file mode 100644 index 00000000000..5ba1d4b9bbf --- /dev/null +++ b/src/hook.js @@ -0,0 +1,78 @@ + +/** + * @typedef {function} HookedFunction + * @property {function(function(), [number])} addHook A method that takes a new function to attach as a hook + * to the HookedFunction + * @property {function(function())} removeHook A method to remove attached hooks + */ + +/** + * A map of global hook methods to allow easy extension of hooked functions that are intended to be extended globally + * @type {{}} + */ +export const hooks = {}; + +/** + * A utility function for allowing a regular function to be extensible with additional hook functions + * @param {string} type The method for applying all attached hooks when this hooked function is called + * @param {function()} fn The function to make hookable + * @param {string} hookName If provided this allows you to register a name for a global hook to have easy access to + * the addHook and removeHook methods for that hook (which are usually accessed as methods on the function itself) + * @returns {HookedFunction} A new function that implements the HookedFunction interface + */ +export function createHook(type, fn, hookName) { + let _hooks = [{fn, priority: 0}]; + + let types = { + sync: function(...args) { + _hooks.forEach(hook => { + hook.fn.apply(this, args); + }); + }, + asyncSeries: function(...args) { + let curr = 0; + + const asyncSeriesNext = (...args) => { + let hook = _hooks[++curr]; + if (typeof hook === 'object' && typeof hook.fn === 'function') { + return hook.fn.apply(this, args.concat(asyncSeriesNext)) + } + }; + + return _hooks[curr].fn.apply(this, args.concat(asyncSeriesNext)); + } + }; + + if (!types[type]) { + throw 'invalid hook type'; + } + + let methods = { + addHook: function(fn, priority = 10) { + if (typeof fn === 'function') { + _hooks.push({ + fn, + priority: priority + }); + + _hooks.sort((a, b) => b.priority - a.priority); + } + }, + removeHook: function(removeFn) { + _hooks = _hooks.filter(hook => hook.fn === fn || hook.fn !== removeFn); + } + }; + + if (typeof hookName === 'string') { + hooks[hookName] = methods; + } + + function hookedFn(...args) { + if (_hooks.length === 0) { + return fn.apply(this, args); + } + return types[type].apply(this, args); + } + + return Object.assign(hookedFn, methods); +} diff --git a/test/spec/hook_spec.js b/test/spec/hook_spec.js new file mode 100644 index 00000000000..1fab4ecd1b7 --- /dev/null +++ b/test/spec/hook_spec.js @@ -0,0 +1,151 @@ + +import { expect } from 'chai'; +import { createHook, hooks } from 'src/hook'; + +describe('the hook module', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should call all sync hooks attached to a function', () => { + let called = []; + let calledWith; + + let testFn = () => { + called.push(testFn); + }; + let testHook = (...args) => { + called.push(testHook); + calledWith = args; + }; + let testHook2 = () => { + called.push(testHook2); + }; + let testHook3 = () => { + called.push(testHook3); + }; + + let hookedTestFn = createHook('sync', testFn, 'testHook'); + + hookedTestFn.addHook(testHook, 50); + hookedTestFn.addHook(testHook2, 100); + + // make sure global test hooks work as well (with default priority) + hooks['testHook'].addHook(testHook3); + + hookedTestFn(1, 2, 3); + + expect(called).to.deep.equal([ + testHook2, + testHook, + testHook3, + testFn + ]); + + expect(calledWith).to.deep.equal([1, 2, 3]); + + called = []; + + hookedTestFn.removeHook(testHook); + hooks['testHook'].removeHook(testHook3); + + hookedTestFn(1, 2, 3); + + expect(called).to.deep.equal([ + testHook2, + testFn + ]); + }); + + it('should allow context to be passed to hooks, but keep bound contexts', () => { + let context; + let fn = function() { + context = this; + }; + + let boundContext = {}; + let calledBoundContext; + let hook = function() { + calledBoundContext = this; + }.bind(boundContext); + + let hookFn = createHook('sync', fn); + hookFn.addHook(hook); + + let newContext = {}; + hookFn.bind(newContext)(); + + expect(context).to.equal(newContext); + expect(calledBoundContext).to.equal(boundContext); + }); + + describe('asyncSeries', () => { + it('should call function as normal if no hooks attached', () => { + let fn = sandbox.spy(); + let hookFn = createHook('asyncSeries', fn); + + hookFn(1); + + expect(fn.calledOnce).to.equal(true); + expect(fn.firstCall.args[0]).to.equal(1); + }); + + it('should call hooks correctly applied in asyncSeries', () => { + let called = []; + + let testFn = (called) => { + called.push(testFn); + }; + let testHook = (called, next) => { + called.push(testHook); + next(called); + }; + let testHook2 = (called, next) => { + called.push(testHook2); + next(called); + }; + + let hookedTestFn = createHook('asyncSeries', testFn); + hookedTestFn.addHook(testHook); + hookedTestFn.addHook(testHook2); + + hookedTestFn(called); + + expect(called).to.deep.equal([ + testHook, + testHook2, + testFn + ]); + }); + + it('should allow context to be passed to hooks, but keep bound contexts', () => { + let context; + let fn = function() { + context = this; + }; + + let boundContext1 = {}; + let calledBoundContext1; + let hook1 = function(next) { + calledBoundContext1 = this; + next() + }.bind(boundContext1); + + let hookFn = createHook('asyncSeries', fn); + hookFn.addHook(hook1); + + let newContext = {}; + hookFn = hookFn.bind(newContext); + hookFn(); + + expect(context).to.equal(newContext); + expect(calledBoundContext1).to.equal(boundContext1); + }); + }); +}); diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index 937e6a084e4..06faa5665c9 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -5,7 +5,7 @@ import { import { setConfig, - addBidResponseDecorator, + addBidResponseHook, currencySupportEnabled, currencyRates @@ -46,10 +46,6 @@ describe('currency', function () { var bid = { cpm: 1, bidder: 'rubicon' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { - innerBid = bid; - }); - setConfig({ adServerCurrency: 'GBP', bidderCurrencyDefault: { @@ -57,7 +53,9 @@ describe('currency', function () { } }); - wrappedAddBidResponseFn('elementId', bid); + addBidResponseHook('elementId', bid, function(adCodeId, bid) { + innerBid = bid; + }); expect(innerBid.currency).to.equal('GBP') }); @@ -68,10 +66,6 @@ describe('currency', function () { var bid = { cpm: 1, currency: 'JPY', bidder: 'rubicon' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { - innerBid = bid; - }); - setConfig({ adServerCurrency: 'JPY', bidderCurrencyDefault: { @@ -79,7 +73,9 @@ describe('currency', function () { } }); - wrappedAddBidResponseFn('elementId', bid); + addBidResponseHook('elementId', bid, function(adCodeId, bid) { + innerBid = bid; + }); expect(innerBid.currency).to.equal('JPY') }); @@ -97,12 +93,10 @@ describe('currency', function () { var bid = { cpm: 100, currency: 'JPY', bidder: 'rubicon' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); - expect(innerBid.cpm).to.equal('1.0000'); }); }); @@ -113,14 +107,15 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); - var marker = false; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { - marker = true; - }); var bid = { 'cpm': 1, 'currency': 'USD' }; setConfig({ 'adServerCurrency': 'JPY' }); - wrappedAddBidResponseFn('elementId', bid); + + var marker = false; + addBidResponseHook('elementId', bid, function() { + marker = true; + }); + expect(marker).to.equal(false); fakeCurrencyFileServer.respond(); @@ -133,10 +128,9 @@ describe('currency', function () { setConfig({}); var bid = { 'cpm': 1, 'currency': 'USD' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal(1); }); @@ -144,10 +138,9 @@ describe('currency', function () { setConfig({}); var bid = { 'cpm': 1, 'currency': 'GBP' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); }); @@ -157,10 +150,9 @@ describe('currency', function () { }); var bid = { 'cpm': 1, 'currency': 'USD' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(bid).to.equal(innerBid); }); @@ -170,10 +162,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'ABC' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); }); @@ -183,10 +174,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'GBP' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); }); @@ -196,10 +186,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'JPY' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal(1); expect(innerBid.currency).to.equal('JPY'); }); @@ -210,10 +199,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'USD' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal('0.7798'); expect(innerBid.currency).to.equal('GBP'); }); @@ -224,10 +212,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'CNY' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal('0.1133'); expect(innerBid.currency).to.equal('GBP'); }); @@ -238,10 +225,9 @@ describe('currency', function () { fakeCurrencyFileServer.respond(); var bid = { 'cpm': 1, 'currency': 'JPY' }; var innerBid; - var wrappedAddBidResponseFn = addBidResponseDecorator(function(adCodeId, bid) { + addBidResponseHook('elementId', bid, function(adCodeId, bid) { innerBid = bid; }); - wrappedAddBidResponseFn('elementId', bid); expect(innerBid.cpm).to.equal('0.0623'); expect(innerBid.currency).to.equal('CNY'); }); From 47019481bb34f4071f85d899a16727d8aae606e0 Mon Sep 17 00:00:00 2001 From: Matt Kendall <mkendall@appnexus.com> Date: Mon, 30 Oct 2017 10:55:19 -0400 Subject: [PATCH 36/44] Add `usePaymentRule` param to AN bidders (#1778) --- modules/appnexusAstBidAdapter.js | 1 + modules/appnexusBidAdapter.js | 4 ++++ test/spec/modules/appnexusAstBidAdapter_spec.js | 17 +++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index 3c900e15312..f3173dc15bc 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -248,6 +248,7 @@ function bidToTag(bid) { tag.code = bid.params.invCode; } tag.allow_smaller_sizes = bid.params.allowSmallerSizes || false; + tag.use_pmt_rule = bid.params.usePaymentRule || false tag.prebid = true; tag.disable_psa = true; if (bid.params.reserve) { diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 3d6549542dc..1fb48b68fc4 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -44,12 +44,15 @@ AppNexusAdapter = function AppNexusAdapter() { var query = utils.getBidIdParameter('query', bid.params); var referrer = utils.getBidIdParameter('referrer', bid.params); var altReferrer = utils.getBidIdParameter('alt_referrer', bid.params); + let usePaymentRule = utils.getBidIdParameter('usePaymentRule', bid.params); var jptCall = '//ib.adnxs.com/jpt?'; jptCall = utils.tryAppendQueryString(jptCall, 'callback', '$$PREBID_GLOBAL$$.handleAnCB'); jptCall = utils.tryAppendQueryString(jptCall, 'callback_uid', callbackId); jptCall = utils.tryAppendQueryString(jptCall, 'psa', '0'); jptCall = utils.tryAppendQueryString(jptCall, 'id', placementId); + jptCall = utils.tryAppendQueryString(jptCall, 'use_pmt_rule', usePaymentRule); + if (member) { jptCall = utils.tryAppendQueryString(jptCall, 'member', member); } else if (memberId) { @@ -106,6 +109,7 @@ AppNexusAdapter = function AppNexusAdapter() { delete paramsCopy.referrer; delete paramsCopy.alt_referrer; delete paramsCopy.member; + delete paramsCopy.usePaymentRule; // get the reminder var queryParams = utils.parseQueryStringParameters(paramsCopy); diff --git a/test/spec/modules/appnexusAstBidAdapter_spec.js b/test/spec/modules/appnexusAstBidAdapter_spec.js index 83cbcc38a2b..3884b1c5863 100644 --- a/test/spec/modules/appnexusAstBidAdapter_spec.js +++ b/test/spec/modules/appnexusAstBidAdapter_spec.js @@ -244,6 +244,23 @@ describe('AppNexusAdapter', () => { 'value': ['123'] }]); }); + + it('should should add payment rules to the request', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + usePaymentRule: true + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].use_pmt_rule).to.equal(true); + }); }) describe('interpretResponse', () => { From ac6775b72463004e34292bd82108541cfb28ac6a Mon Sep 17 00:00:00 2001 From: Konstantin <kprokopchik@gmail.com> Date: Mon, 30 Oct 2017 21:56:38 +0300 Subject: [PATCH 37/44] Update GetIntent adapter to 1.0 version (#1721) * AD-2311: Make GetIntent adapter compatible with Prebid.js 1.0 version * AD-2311: remove blank line * Trigger * GetIntent adapter - added bid response fields: currency, ttl and netRevenue; fixed creative size selector (#1721) * GetIntent adapter - added bid response fields: bidId, creativeId (#1721) --- modules/getintentBidAdapter.js | 191 +++++++++----- modules/getintentBidAdapter.md | 48 ++++ test/spec/modules/getintentBidAdapter_spec.js | 249 ++++++++---------- 3 files changed, 293 insertions(+), 195 deletions(-) create mode 100644 modules/getintentBidAdapter.md diff --git a/modules/getintentBidAdapter.js b/modules/getintentBidAdapter.js index 72f1c1a0073..f677b107529 100644 --- a/modules/getintentBidAdapter.js +++ b/modules/getintentBidAdapter.js @@ -1,78 +1,143 @@ -import { STATUS } from 'src/constants'; -import adaptermanager from 'src/adaptermanager'; +import { registerBidder } from 'src/adapters/bidderFactory'; -var bidfactory = require('src/bidfactory.js'); -var bidmanager = require('src/bidmanager.js'); -var adloader = require('src/adloader.js'); +const BIDDER_CODE = 'getintent'; +const IS_NET_REVENUE = true; +const BID_HOST = 'px.adhigh.net'; +const BID_BANNER_PATH = '/rtb/direct_banner'; +const BID_VIDEO_PATH = '/rtb/direct_vast'; +const BID_RESPONSE_TTL_SEC = 360; +const VIDEO_PROPERTIES = [ + 'protocols', 'mimes', 'min_dur', 'max_dur', 'min_btr', 'max_btr', 'vi_format', 'api', 'skippable' +]; +const OPTIONAL_PROPERTIES = [ + 'cur', 'floor' +]; -var GetIntentAdapter = function GetIntentAdapter() { - var headerBiddingStaticJS = window.location.protocol + '//cdn.adhigh.net/adserver/hb.js'; +export const spec = { + code: BIDDER_CODE, + aliases: ['getintentAdapter'], + supportedMediaTypes: ['video', 'banner'], - function _callBids(params) { - if (typeof window.gi_hb === 'undefined') { - adloader.loadScript(headerBiddingStaticJS, function() { - bid(params); - }, true); - } else { - bid(params); + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid to validate. + * @return {boolean} True if this is a valid bid, and false otherwise. + * */ + isBidRequestValid: function(bid) { + return !!(bid && bid.params && bid.params.pid && bid.params.tid); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests - an array of bids. + * @return ServerRequest[] + */ + buildRequests: function(bidRequests) { + return bidRequests.map(bidRequest => { + let giBidRequest = buildGiBidRequest(bidRequest); + return { + method: 'GET', + url: buildUrl(giBidRequest), + data: giBidRequest, + }; + }); + }, + + /** + * Callback for bids, after the call to DSP completes. + * Parse the response from the server into a list of bids. + * + * @param {object} serverResponse A response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse) { + let responseBody = serverResponse.body; + const bids = []; + if (responseBody && responseBody.no_bid !== 1) { + let size = parseSize(responseBody.size); + let bid = { + requestId: responseBody.bid_id, + ttl: BID_RESPONSE_TTL_SEC, + netRevenue: IS_NET_REVENUE, + currency: responseBody.currency, + creativeId: responseBody.creative_id, + cpm: responseBody.cpm, + width: size[0], + height: size[1] + }; + if (responseBody.vast_url) { + bid.mediaType = 'video'; + bid.vastUrl = responseBody.vast_url; + } else { + bid.mediaType = 'banner'; + bid.ad = responseBody.ad; + } + bids.push(bid); } + return bids; } - function addOptional(params, request, props) { - for (var i = 0; i < props.length; i++) { - if (params.hasOwnProperty(props[i])) { - request[props[i]] = params[props[i]]; +} + +function buildUrl(bid) { + return '//' + BID_HOST + (bid.is_video ? BID_VIDEO_PATH : BID_BANNER_PATH); +} + +/** + * Builds GI bid request from BidRequest. + * + * @param {BidRequest} bidRequest. + * @return {object} GI bid request. + * */ +function buildGiBidRequest(bidRequest) { + let giBidRequest = { + bid_id: bidRequest.bidId, + pid: bidRequest.params.pid, // required + tid: bidRequest.params.tid, // required + known: bidRequest.params.known || 1, + is_video: bidRequest.mediaType === 'video', + resp_type: 'JSON' + }; + if (bidRequest.sizes) { + giBidRequest.size = produceSize(bidRequest.sizes); + } + addVideo(bidRequest.params.video, giBidRequest); + addOptional(bidRequest.params, giBidRequest, OPTIONAL_PROPERTIES); + return giBidRequest; +} + +function addVideo(video, giBidRequest) { + if (giBidRequest.is_video && video) { + for (let i = 0, l = VIDEO_PROPERTIES.length; i < l; i++) { + let key = VIDEO_PROPERTIES[i]; + if (video.hasOwnProperty(key)) { + giBidRequest[key] = Array.isArray(video[key]) ? video[key].join(',') : video[key]; } } } +} - function bid(params) { - var bids = params.bids || []; - for (var i = 0; i < bids.length; i++) { - var bidRequest = bids[i]; - var request = { - pid: bidRequest.params.pid, // required - tid: bidRequest.params.tid, // required - known: bidRequest.params.known || 1, - is_video: bidRequest.mediaType === 'video', - video: bidRequest.params.video || {}, - size: bidRequest.sizes[0].join('x'), - }; - addOptional(bidRequest.params, request, ['cur', 'floor']); - (function (r, br) { - window.gi_hb.makeBid(r, function(bidResponse) { - if (bidResponse.no_bid === 1) { - var nobid = bidfactory.createBid(STATUS.NO_BID); - nobid.bidderCode = br.bidder; - bidmanager.addBidResponse(br.placementCode, nobid); - } else { - var bid = bidfactory.createBid(STATUS.GOOD); - var size = bidResponse.size.split('x'); - bid.bidderCode = br.bidder; - bid.cpm = bidResponse.cpm; - bid.width = size[0]; - bid.height = size[1]; - if (br.mediaType === 'video') { - bid.vastUrl = bidResponse.vast_url; - bid.descriptionUrl = bidResponse.vast_url; - bid.mediaType = 'video'; - } else { - bid.ad = bidResponse.ad; - } - bidmanager.addBidResponse(br.placementCode, bid); - } - }); - })(request, bidRequest); +function addOptional(params, request, props) { + for (let i = 0; i < props.length; i++) { + if (params.hasOwnProperty(props[i])) { + request[props[i]] = params[props[i]]; } } +} - return { - callBids: _callBids - }; -}; +function parseSize(s) { + return s.split('x').map(Number); +} -adaptermanager.registerBidAdapter(new GetIntentAdapter(), 'getintent', { - supportedMediaTypes: ['video'] -}); +function produceSize(sizes) { + // TODO: add support for multiple sizes + if (Array.isArray(sizes[0])) { + return sizes[0].join('x'); + } else { + return sizes.join('x'); + } +} -module.exports = GetIntentAdapter; +registerBidder(spec); diff --git a/modules/getintentBidAdapter.md b/modules/getintentBidAdapter.md new file mode 100644 index 00000000000..7f9b38f6b22 --- /dev/null +++ b/modules/getintentBidAdapter.md @@ -0,0 +1,48 @@ +# Overview + +``` +Module Name: GetIntent Bidder Adapter +Module Type: Bidder Adapter +Maintainer: server-dev@getintent.com +``` + +# Description + +Module that connects to GetIntent's demand sources. +Banner and Video formats are supported. + +# Required parameters +* ```pid``` for Publisher ID +* ```tid``` for Tag ID. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: "getintent", + params: { + pid: "7", + tid: "test01" + } + } + ] + },{ + code: 'test-video-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: "getintent", + params: { + pid: "7", + tid: "test01" + }, + mediaType: "video" + } + ] + } + ]; +``` diff --git a/test/spec/modules/getintentBidAdapter_spec.js b/test/spec/modules/getintentBidAdapter_spec.js index e66d2138eaf..1b76c4852b4 100644 --- a/test/spec/modules/getintentBidAdapter_spec.js +++ b/test/spec/modules/getintentBidAdapter_spec.js @@ -1,146 +1,131 @@ -import Adapter from '../../../modules/getintentBidAdapter'; -import bidManager from '../../../src/bidmanager'; -import {expect} from 'chai'; - -var assert = require('chai').assert; - -describe('getintent media adapter test', () => { - let adapter; - - window.gi_hb = { - makeBid: function(bidRequest, callback) { - var pid = bidRequest.pid; - var tid = bidRequest.tid; - - if (pid == 'p1' || pid == 'p2') { - callback({ - ad: `Ad Markup ${pid} ${tid}`, - cpm: 2.71, - size: `${bidRequest.size}` - }, bidRequest); - } else if (pid == 'p3') { - callback({ - no_bid: 1 - }, bidRequest); - } else if (pid == 'p4') { - callback({ - vast_url: `http://test.com?pid=${pid}&tid=${tid}`, - cpm: 2.88, - size: `${bidRequest.size}` - }, bidRequest); +import { expect } from 'chai' +import { spec } from 'modules/getintentBidAdapter' + +describe('GetIntent Adapter Tests:', () => { + const bidRequests = [{ + bidId: 'bid12345', + params: { + pid: 'p1000', + tid: 't1000' + }, + sizes: [[300, 250]] + }]; + const videoBidRequest = { + bidId: 'bid789', + params: { + pid: 'p1001', + tid: 't1001', + video: { + mimes: ['video/mp4', 'application/javascript'], + max_dur: 20, + api: [1, 2], + skippable: true } - } + }, + sizes: [300, 250], + mediaType: 'video' }; - function callOut() { - adapter.callBids({ - bidderCode: 'getintent', - bids: [ - { - bidder: 'getintent', - adUnitCode: 'test1', - sizes: [[320, 240]], - params: { - pid: 'p1', - tid: 't1', - cur: 'USD' - } - }, - { - bidder: 'getintent', - adUnitCode: 'test2', - sizes: [[720, 90]], - params: { - pid: 'p2', - tid: 't1', - cur: 'USD' - } - }, - { - bidder: 'getintent', - adUnitCode: 'test3', - sizes: [[400, 500]], - params: { - pid: 'p3', - tid: 't2', - cur: 'USD' - } - }, - { - bidder: 'getintent', - adUnitCode: 'test4', - mediaType: 'video', - sizes: [[480, 352]], - params: { - pid: 'p4', - tid: 't3', - cur: 'USD' - } - } - ] - }); - } - - beforeEach(() => { - adapter = new Adapter(); + it('Verify build request', () => { + const serverRequests = spec.buildRequests(bidRequests); + let serverRequest = serverRequests[0]; + expect(serverRequest.url).to.equal('//px.adhigh.net/rtb/direct_banner'); + expect(serverRequest.method).to.equal('GET'); + expect(serverRequest.data.bid_id).to.equal('bid12345'); + expect(serverRequest.data.pid).to.equal('p1000'); + expect(serverRequest.data.tid).to.equal('t1000'); + expect(serverRequest.data.size).to.equal('300x250'); + expect(serverRequest.data.is_video).to.equal(false); }); - afterEach(() => { + it('Verify build video request', () => { + const serverRequests = spec.buildRequests([videoBidRequest]); + let serverRequest = serverRequests[0]; + expect(serverRequest.url).to.equal('//px.adhigh.net/rtb/direct_vast'); + expect(serverRequest.method).to.equal('GET'); + expect(serverRequest.data.bid_id).to.equal('bid789'); + expect(serverRequest.data.pid).to.equal('p1001'); + expect(serverRequest.data.tid).to.equal('t1001'); + expect(serverRequest.data.size).to.equal('300x250'); + expect(serverRequest.data.is_video).to.equal(true); + expect(serverRequest.data.mimes).to.equal('video/mp4,application/javascript'); + expect(serverRequest.data.max_dur).to.equal(20); + expect(serverRequest.data.api).to.equal('1,2'); + expect(serverRequest.data.skippable).to.equal(true); }); - describe('adding bids to the manager', () => { - let firstBid; - let secondBid; - let thirdBid; - let videoBid; - - beforeEach(() => { - sinon.stub(bidManager, 'addBidResponse'); - callOut(); - firstBid = bidManager.addBidResponse.firstCall.args[1]; - secondBid = bidManager.addBidResponse.secondCall.args[1]; - thirdBid = bidManager.addBidResponse.thirdCall.args[1]; - videoBid = bidManager.addBidResponse.lastCall.args[1]; - }); - - afterEach(() => { - bidManager.addBidResponse.restore(); - }); - - it('was called four times', () => { - assert.strictEqual(bidManager.addBidResponse.callCount, 4); - }); + it('Verify parse response', () => { + const serverResponse = { + body: { + bid_id: 'bid12345', + cpm: 2.25, + currency: 'USD', + size: '300x250', + creative_id: '1000', + ad: 'Ad markup' + }, + headers: { + } + }; + const bids = spec.interpretResponse(serverResponse); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(2.25); + expect(bid.currency).to.equal('USD'); + expect(bid.creativeId).to.equal('1000'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.requestId).to.equal('bid12345'); + expect(bid.mediaType).to.equal('banner'); + expect(bid.ad).to.equal('Ad markup'); + }); - it('will respond to the first bid', () => { - expect(firstBid).to.have.property('ad', 'Ad Markup p1 t1'); - expect(firstBid).to.have.property('cpm', 2.71); - expect(firstBid).to.have.property('width', '320'); - expect(firstBid).to.have.property('height', '240'); - }); + it('Verify parse video response', () => { + const serverResponse = { + body: { + bid_id: 'bid789', + cpm: 3.25, + currency: 'USD', + size: '300x250', + creative_id: '2000', + vast_url: '//vast.xml/url' + }, + headers: { + } + }; + const bids = spec.interpretResponse(serverResponse); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(3.25); + expect(bid.currency).to.equal('USD'); + expect(bid.creativeId).to.equal('2000'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.requestId).to.equal('bid789'); + expect(bid.mediaType).to.equal('video'); + expect(bid.vastUrl).to.equal('//vast.xml/url'); + }); - it('will respond to the second bid', () => { - expect(secondBid).to.have.property('ad', 'Ad Markup p2 t1'); - expect(secondBid).to.have.property('cpm', 2.71); - expect(secondBid).to.have.property('width', '720'); - expect(secondBid).to.have.property('height', '90'); - }); + it('Verify bidder code', () => { + expect(spec.code).to.equal('getintent'); + }); - it('wont respond to the third bid', () => { - expect(thirdBid).to.not.have.property('ad'); - expect(thirdBid).to.not.have.property('cpm'); - }); + it('Verify bidder aliases', () => { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('getintentAdapter'); + }); - it('will add the bidder code to the bid object', () => { - expect(firstBid).to.have.property('bidderCode', 'getintent'); - expect(secondBid).to.have.property('bidderCode', 'getintent'); - expect(thirdBid).to.have.property('bidderCode', 'getintent'); - }); + it('Verify supported media types', () => { + expect(spec.supportedMediaTypes).to.have.lengthOf(2); + expect(spec.supportedMediaTypes[0]).to.equal('video'); + expect(spec.supportedMediaTypes[1]).to.equal('banner'); + }); - it('will respond to the video bid', () => { - expect(videoBid).to.have.property('vastUrl', 'http://test.com?pid=p4&tid=t3'); - expect(videoBid).to.have.property('cpm', 2.88); - expect(videoBid).to.have.property('width', '480'); - expect(videoBid).to.have.property('height', '352'); - }); + it('Verify if bid request valid', () => { + expect(spec.isBidRequestValid(bidRequests[0])).to.equal(true); + expect(spec.isBidRequestValid({})).to.equal(false); + expect(spec.isBidRequestValid({ params: {} })).to.equal(false); + expect(spec.isBidRequestValid({ params: { test: 123 } })).to.equal(false); + expect(spec.isBidRequestValid({ params: { pid: 111, tid: 222 } })).to.equal(true); }); }); From 377f87e27c425abd1688da8cc0e11c14aee70a44 Mon Sep 17 00:00:00 2001 From: jbartek-improve <31618107+jbartek-improve@users.noreply.github.com> Date: Tue, 31 Oct 2017 18:04:04 +0100 Subject: [PATCH 38/44] Add TTL parameter to bid (#1784) * Update Improve Digital adapter for Prebid 1.0 * Removed bidderCode from bids * Added creativeId to bid response; updated format of the first argument of interpretResponse * Added bid ttl --- modules/improvedigitalBidAdapter.js | 1 + test/spec/modules/improvedigitalBidAdapter_spec.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index bc00127b269..82c045b9db6 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -83,6 +83,7 @@ export const spec = { bid.height = bidObject.h; bid.netRevenue = bidObject.isNet ? bidObject.isNet : false; bid.requestId = bidObject.id; + bid.ttl = 300; bid.width = bidObject.w; bids.push(bid); diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 750eecc2a7d..3f93a62e850 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -204,6 +204,7 @@ describe('Improve Digital Adapter Tests', function () { 'height': 290, 'netRevenue': false, 'requestId': '33e9500b21129f', + 'ttl': 300, 'width': 600 } ]; @@ -219,6 +220,7 @@ describe('Improve Digital Adapter Tests', function () { 'height': 400, 'netRevenue': true, 'requestId': '1234', + 'ttl': 300, 'width': 700 } ]; From ec9454afd5fbe6f3b8d88e692293d937c90a16a1 Mon Sep 17 00:00:00 2001 From: Rich Loveland <loveland.richard@gmail.com> Date: Tue, 31 Oct 2017 13:07:04 -0400 Subject: [PATCH 39/44] Remove 'supported' from analytics adapter info (#1780) (Prebid.org doesn't endorse or support any particular adapter.) --- src/prebid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prebid.js b/src/prebid.js index 54dc9c55118..3d25eff6761 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -635,7 +635,7 @@ $$PREBID_GLOBAL$$.loadScript = function (tagSrc, callback, useCache) { * For usage, see [Integrate with the Prebid Analytics * API](http://prebid.org/dev-docs/integrate-with-the-prebid-analytics-api.html). * - * For a list of supported analytics adapters, see [Analytics for + * For a list of analytics adapters, see [Analytics for * Prebid](http://prebid.org/overview/analytics.html). * @param {Object} config * @param {string} config.provider The name of the provider, e.g., `"ga"` for Google Analytics. From a62f917252ac0a873e2335cd165995fa7390c06a Mon Sep 17 00:00:00 2001 From: Jaimin Panchal <jaiminpanchal27@gmail.com> Date: Tue, 31 Oct 2017 14:18:27 -0400 Subject: [PATCH 40/44] Added adUnitCode for compatibility (#1781) --- src/adapters/bidderFactory.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 9641c4c484b..0b4b0d7cd0c 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -207,6 +207,10 @@ export function newBidder(spec) { const bidRequestMap = {}; validBidRequests.forEach(bid => { bidRequestMap[bid.bidId] = bid; + // Delete this once we are 1.0 + if (!bid.adUnitCode) { + bid.adUnitCode = bid.placementCode + } }); let requests = spec.buildRequests(validBidRequests, bidderRequest); From 99f7ca77c99801387ab322d34c5ab84d4fb2a108 Mon Sep 17 00:00:00 2001 From: Rich Snapp <rsnapp@rubiconproject.com> Date: Tue, 31 Oct 2017 12:31:06 -0600 Subject: [PATCH 41/44] Update rubicon adapter with new properties and 1.0 changes (#1776) * don't set bidderCode in adapter anymore for rubicon * update rubiconBidAdapter with new properties --- modules/rubiconBidAdapter.js | 27 +++++++++--------- test/spec/modules/rubiconBidAdapter_spec.js | 31 +++++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 2830711a4c9..2b7b0061430 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -1,10 +1,8 @@ import * as utils from 'src/utils'; import { registerBidder } from 'src/adapters/bidderFactory'; +import { config } from 'src/config'; -// use deferred function call since version isn't defined yet at this point -function getIntegration() { - return 'pbjs_lite_' + $$PREBID_GLOBAL$$.version; -} +const INTEGRATION = 'pbjs_lite_v$prebid.version$'; function isSecure() { return location.protocol === 'https:'; @@ -113,7 +111,7 @@ export const spec = { page_url: !params.referrer ? utils.getTopWindowUrl() : params.referrer, resolution: _getScreenResolution(), account_id: params.accountId, - integration: getIntegration(), + integration: INTEGRATION, timeout: bidderRequest.timeout - (Date.now() - bidderRequest.auctionStart + TIMEOUT_BUFFER), stash_creatives: true, ae_pass_through_parameters: params.video.aeParams, @@ -126,8 +124,8 @@ export const spec = { zone_id: params.zoneId, position: params.position || 'btf', floor: parseFloat(params.floor) > 0.01 ? params.floor : 0.01, - element_id: bidRequest.placementCode, - name: bidRequest.placementCode, + element_id: bidRequest.adUnitCode, + name: bidRequest.adUnitCode, language: params.video.language, width: size[0], height: size[1], @@ -187,7 +185,7 @@ export const spec = { 'p_pos', position, 'rp_floor', floor, 'rp_secure', isSecure() ? '1' : '0', - 'tk_flint', getIntegration(), + 'tk_flint', INTEGRATION, 'tid', bidRequest.transactionId, 'p_screen_res', _getScreenResolution(), 'kw', keywords, @@ -240,7 +238,7 @@ export const spec = { // video ads array is wrapped in an object if (typeof bidRequest === 'object' && bidRequest.mediaType === 'video' && typeof ads === 'object') { - ads = ads[bidRequest.placementCode]; + ads = ads[bidRequest.adUnitCode]; } // check the ad response @@ -259,10 +257,11 @@ export const spec = { let bid = { requestId: bidRequest.bidId, currency: 'USD', - creative_id: ad.creative_id, - bidderCode: spec.code, + creativeId: ad.creative_id, cpm: ad.cpm || 0, - dealId: ad.deal + dealId: ad.deal, + ttl: 300, // 5 minutes + netRevenue: config.getConfig('rubicon.netRevenue') || false }; if (bidRequest.mediaType === 'video') { bid.width = bidRequest.params.video.playerWidth; @@ -280,7 +279,7 @@ export const spec = { .reduce((memo, item) => { memo[item.key] = item.values[0]; return memo; - }, {'rpfl_elemid': bidRequest.placementCode}); + }, {'rpfl_elemid': bidRequest.adUnitCode}); bids.push(bid); @@ -308,7 +307,7 @@ function _getScreenResolution() { function _getDigiTrustQueryParams() { function getDigiTrustId() { - let digiTrustUser = window.DigiTrust && ($$PREBID_GLOBAL$$.getConfig('digiTrustId') || window.DigiTrust.getUser({member: 'T9QSFKPDN9'})); + let digiTrustUser = window.DigiTrust && (config.getConfig('digiTrustId') || window.DigiTrust.getUser({member: 'T9QSFKPDN9'})); return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; } let digiTrustId = getDigiTrustId(); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index f77391dffe2..552a86b5ac4 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -79,7 +79,7 @@ describe('the rubicon adapter', () => { position: 'atf', referrer: 'localhost' }, - placementCode: '/19968336/header-bid-tag-0', + adUnitCode: '/19968336/header-bid-tag-0', sizes: [[300, 250], [320, 50]], bidId: '2ffb201a808da7', bidderRequestId: '178e34bad3658f', @@ -313,16 +313,14 @@ describe('the rubicon adapter', () => { window.DigiTrust = { getUser: sandbox.spy() }; - origGetConfig = window.$$PREBID_GLOBAL$$.getConfig; }); afterEach(() => { delete window.DigiTrust; - window.$$PREBID_GLOBAL$$.getConfig = origGetConfig; }); it('should send digiTrustId config params', () => { - sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(config, 'getConfig', (key) => { var config = { digiTrustId: { success: true, @@ -355,7 +353,7 @@ describe('the rubicon adapter', () => { }); it('should not send digiTrustId config params due to optout', () => { - sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(config, 'getConfig', (key) => { var config = { digiTrustId: { success: true, @@ -384,7 +382,7 @@ describe('the rubicon adapter', () => { }); it('should not send digiTrustId config params due to failure', () => { - sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(config, 'getConfig', (key) => { var config = { digiTrustId: { success: false, @@ -413,7 +411,7 @@ describe('the rubicon adapter', () => { }); it('should not send digiTrustId config params if they do not exist', () => { - sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(config, 'getConfig', (key) => { var config = {}; return config[key]; }); @@ -474,8 +472,8 @@ describe('the rubicon adapter', () => { expect(slot.zone_id).to.equal('335918'); expect(slot.position).to.equal('atf'); expect(slot.floor).to.equal(0.01); - expect(slot.element_id).to.equal(bidderRequest.bids[0].placementCode); - expect(slot.name).to.equal(bidderRequest.bids[0].placementCode); + expect(slot.element_id).to.equal(bidderRequest.bids[0].adUnitCode); + expect(slot.name).to.equal(bidderRequest.bids[0].adUnitCode); expect(slot.language).to.equal('en'); expect(slot.width).to.equal(640); expect(slot.height).to.equal(320); @@ -611,11 +609,12 @@ describe('the rubicon adapter', () => { expect(bids).to.be.lengthOf(2); - expect(bids[0].bidderCode).to.equal('rubicon'); expect(bids[0].width).to.equal(320); expect(bids[0].height).to.equal(50); expect(bids[0].cpm).to.equal(0.911); - expect(bids[0].creative_id).to.equal('crid-9'); + expect(bids[0].ttl).to.equal(300); + expect(bids[0].netRevenue).to.equal(false); + expect(bids[0].creativeId).to.equal('crid-9'); expect(bids[0].currency).to.equal('USD'); expect(bids[0].ad).to.contain(`alert('foo')`) .and.to.contain(`<html>`) @@ -623,11 +622,12 @@ describe('the rubicon adapter', () => { expect(bids[0].rubiconTargeting.rpfl_elemid).to.equal('/19968336/header-bid-tag-0'); expect(bids[0].rubiconTargeting.rpfl_14062).to.equal('43_tier_all_test'); - expect(bids[1].bidderCode).to.equal('rubicon'); expect(bids[1].width).to.equal(300); expect(bids[1].height).to.equal(250); expect(bids[1].cpm).to.equal(0.811); - expect(bids[1].creative_id).to.equal('crid-9'); + expect(bids[1].ttl).to.equal(300); + expect(bids[1].netRevenue).to.equal(false); + expect(bids[1].creativeId).to.equal('crid-9'); expect(bids[1].currency).to.equal('USD'); expect(bids[1].ad).to.contain(`alert('foo')`) .and.to.contain(`<html>`) @@ -759,9 +759,10 @@ describe('the rubicon adapter', () => { expect(bids).to.be.lengthOf(1); - expect(bids[0].bidderCode).to.equal('rubicon'); - expect(bids[0].creative_id).to.equal('crid-999999'); + expect(bids[0].creativeId).to.equal('crid-999999'); expect(bids[0].cpm).to.equal(1); + expect(bids[0].ttl).to.equal(300); + expect(bids[0].netRevenue).to.equal(false); expect(bids[0].descriptionUrl).to.equal('a40fe16e-d08d-46a9-869d-2e1573599e0c'); expect(bids[0].vastUrl).to.equal( 'https://fastlane-adv.rubiconproject.com/v1/creative/a40fe16e-d08d-46a9-869d-2e1573599e0c.xml' From 5802bb917c054d2b3fad325f1435625852adbd3a Mon Sep 17 00:00:00 2001 From: Matt Lane <mlane@appnexus.com> Date: Tue, 31 Oct 2017 11:48:37 -0700 Subject: [PATCH 42/44] Update dfp.buildVideoUrl to accept adserver url (#1663) * Update dfp.buildVideoUrl to accept adserver url * Reject invalid param usage * Don't overwrite description_url if cache is disabled and input contains description_url * Reject xml-only bids when cache is disabled * Accept both url and params object in function call * Fix conflict and nobid condition * Update docs and refactor based on code review --- modules/dfpAdServerVideo.js | 71 ++++++++++++++++++++-- src/utils.js | 2 +- src/video.js | 12 +++- test/spec/modules/dfpAdServerVideo_spec.js | 58 ++++++++++++++++++ test/spec/video_spec.js | 17 +++++- 5 files changed, 153 insertions(+), 7 deletions(-) diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 4f56355a70c..591b8f7baf3 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -4,8 +4,9 @@ import { registerVideoSupport } from '../src/adServerManager'; import { getWinningBids } from '../src/targeting'; -import { formatQS, format as buildUrl } from '../src/url'; -import { parseSizesInput } from '../src/utils'; +import { formatQS, format as buildUrl, parse } from '../src/url'; +import { deepAccess, isEmpty, logError, parseSizesInput } from '../src/utils'; +import { config } from '../src/config'; /** * @typedef {Object} DfpVideoParams @@ -31,8 +32,9 @@ import { parseSizesInput } from '../src/utils'; * @param [Object] bid The bid which should be considered alongside the rest of the adserver's demand. * If this isn't defined, then we'll use the winning bid for the adUnit. * - * @param {DfpVideoParams} params Query params which should be set on the DFP request. + * @param {DfpVideoParams} [params] Query params which should be set on the DFP request. * These will override this module's defaults whenever they conflict. + * @param {string} [url] video adserver url */ /** Safe defaults which work on pretty much all video calls. */ @@ -55,9 +57,26 @@ const defaultParamConstants = { * demand in DFP. */ export default function buildDfpVideoUrl(options) { + if (!options.params && !options.url) { + logError(`A params object or a url is required to use pbjs.adServers.dfp.buildVideoUrl`); + return; + } + const adUnit = options.adUnit; const bid = options.bid || getWinningBids(adUnit.code)[0]; + let urlComponents = {}; + + if (options.url) { + // when both `url` and `params` are given, parsed url will be overwriten + // with any matching param components + urlComponents = parse(options.url); + + if (isEmpty(options.params)) { + return buildUrlFromAdserverUrlComponents(urlComponents, bid); + } + } + const derivedParams = { correlator: Date.now(), sz: parseSizesInput(adUnit.sizes).join('|'), @@ -73,9 +92,14 @@ export default function buildDfpVideoUrl(options) { const queryParams = Object.assign({}, defaultParamConstants, + urlComponents.search, derivedParams, options.params, - { cust_params: encodeURIComponent(formatQS(customParams)) }); + { cust_params: encodeURIComponent(formatQS(customParams)) } + ); + + const descriptionUrl = getDescriptionUrl(bid, options, 'params'); + if (descriptionUrl) { queryParams.description_url = descriptionUrl; } return buildUrl({ protocol: 'https', @@ -85,6 +109,45 @@ export default function buildDfpVideoUrl(options) { }); } +/** + * Builds a video url from a base dfp video url and a winning bid, appending + * Prebid-specific key-values. + * @param {Object} components base video adserver url parsed into components object + * @param {AdapterBidResponse} bid winning bid object to append parameters from + * @return {string} video url + */ +function buildUrlFromAdserverUrlComponents(components, bid) { + const descriptionUrl = getDescriptionUrl(bid, components, 'search'); + if (descriptionUrl) { components.search.description_url = descriptionUrl; } + + const adserverTargeting = (bid && bid.adserverTargeting) || {}; + const customParams = Object.assign({}, + adserverTargeting, + ); + components.search.cust_params = encodeURIComponent(formatQS(customParams)); + + return buildUrl(components); +} + +/** + * Returns the encoded vast url if it exists on a bid object, only if prebid-cache + * is disabled, and description_url is not already set on a given input + * @param {AdapterBidResponse} bid object to check for vast url + * @param {Object} components the object to check that description_url is NOT set on + * @param {string} prop the property of components that would contain description_url + * @return {string | undefined} The encoded vast url if it exists, or undefined + */ +function getDescriptionUrl(bid, components, prop) { + if (config.getConfig('usePrebidCache')) { return; } + + if (!deepAccess(components, `${prop}.description_url`)) { + const vastUrl = bid && bid.vastUrl; + if (vastUrl) { return encodeURIComponent(vastUrl); } + } else { + logError(`input cannnot contain description_url`); + } +} + registerVideoSupport('dfp', { buildVideoUrl: buildDfpVideoUrl }); diff --git a/src/utils.js b/src/utils.js index 00a06fcb091..9efa4f53c57 100644 --- a/src/utils.js +++ b/src/utils.js @@ -339,7 +339,7 @@ exports.isNumber = function(object) { */ exports.isEmpty = function (object) { if (!object) return true; - if (this.isArray(object) || this.isStr(object)) { + if (exports.isArray(object) || exports.isStr(object)) { return !(object.length > 0); } diff --git a/src/video.js b/src/video.js index 386b6b692e9..f5203e4b198 100644 --- a/src/video.js +++ b/src/video.js @@ -1,5 +1,6 @@ import { videoAdapters } from './adaptermanager'; -import { getBidRequest, deepAccess } from './utils'; +import { getBidRequest, deepAccess, logError } from './utils'; +import { config } from '../src/config'; const VIDEO_MEDIA_TYPE = 'video'; const OUTSTREAM = 'outstream'; @@ -32,6 +33,15 @@ export function isValidVideoBid(bid) { // if context not defined assume default 'instream' for video bids // instream bids require a vast url or vast xml content if (!bidRequest || (videoMediaType && context !== OUTSTREAM)) { + // xml-only video bids require prebid-cache to be enabled + if (!config.getConfig('usePrebidCache') && bid.vastXml && !bid.vastUrl) { + logError(` + This bid contains only vastXml and will not work when prebid-cache is disabled. + Try enabling prebid-cache with pbjs.setConfig({ usePrebidCache: true }); + `); + return false; + } + return !!(bid.vastUrl || bid.vastXml); } diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 3156c628abd..07439be126c 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -4,6 +4,7 @@ import parse from 'url-parse'; import buildDfpVideoUrl from 'modules/dfpAdServerVideo'; import { parseQS } from 'src/url'; import adUnit from 'test/fixtures/video/adUnit'; +import { newConfig } from 'src/config'; const bid = { videoCacheKey: 'abc', @@ -36,6 +37,43 @@ describe('The DFP video support module', () => { expect(queryParams).to.have.property('url'); }); + it('can take an adserver url as a parameter', () => { + const bidCopy = Object.assign({ }, bid); + bidCopy.vastUrl = 'vastUrl.example'; + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + url: 'https://video.adserver.example/', + })); + + expect(url.host).to.equal('video.adserver.example'); + + const queryObject = parseQS(url.query); + expect(queryObject.description_url).to.equal('vastUrl.example'); + }); + + it('requires a params object or url', () => { + const url = buildDfpVideoUrl({ + adUnit: adUnit, + bid: bid, + }); + + expect(url).to.be.undefined; + }); + + it('overwrites url params when both url and params object are given', () => { + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bid, + url: 'https://video.adserver.example/ads?sz=640x480&iu=/123/aduniturl&impl=s', + params: { iu: 'my/adUnit' } + })); + + const queryObject = parseQS(url.query); + expect(queryObject.iu).to.equal('my/adUnit'); + }); + it('should override param defaults with user-provided ones', () => { const url = parse(buildDfpVideoUrl({ adUnit: adUnit, @@ -92,6 +130,26 @@ describe('The DFP video support module', () => { expect(customParams).to.have.property('my_targeting', 'foo'); }); + it('should not overwrite an existing description_url for object input and cache disabled', () => { + const config = newConfig(); + config.setConfig({ usePrebidCache: true }); + + const bidCopy = Object.assign({}, bid); + bidCopy.vastUrl = 'vastUrl.example'; + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + iu: 'my/adUnit', + description_url: 'descriptionurl.example' + } + })); + + const queryObject = parseQS(url.query); + expect(queryObject.description_url).to.equal('descriptionurl.example'); + }); + it('should work with nobid responses', () => { const url = buildDfpVideoUrl({ adUnit: adUnit, diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 57a7f7a127e..512b56c334f 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -1,5 +1,6 @@ import { isValidVideoBid } from 'src/video'; -const utils = require('src/utils'); +import { newConfig } from 'src/config'; +import * as utils from 'src/utils'; describe('video.js', () => { afterEach(() => { @@ -34,6 +35,20 @@ describe('video.js', () => { expect(valid).to.be(false); }); + it('catches invalid bids when prebid-cache is disabled', () => { + sinon.stub(utils, 'getBidRequest', () => ({ + bidder: 'vastOnlyVideoBidder', + mediaTypes: { video: {} }, + })); + + const config = newConfig(); + config.setConfig({ usePrebidCache: false }); + + const valid = isValidVideoBid({ vastXml: '<xml>vast</xml>' }); + + expect(valid).to.be(false); + }); + it('validates valid outstream bids', () => { sinon.stub(utils, 'getBidRequest', () => ({ bidder: 'appnexusAst', From 7e541813ddf604fdd2315a9af64f14e2f9d22559 Mon Sep 17 00:00:00 2001 From: harpere <ericsbiz@yahoo.com> Date: Tue, 31 Oct 2017 16:30:29 -0400 Subject: [PATCH 43/44] Commenting out tests that are failing in IE10 (#1710) * temporarily commenting out tests that are failing in IE10 * Update utils_spec.js --- test/spec/utils_spec.js | 171 +++++++++++++++++++++------------------- 1 file changed, 88 insertions(+), 83 deletions(-) diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index 4ffc6a9f15f..ad2645b2351 100755 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -525,89 +525,94 @@ describe('Utils', function () { }); }); - describe('cookie support', function () { - // store original cookie getter and setter so we can reset later - var origCookieSetter = document.__lookupSetter__('cookie'); - var origCookieGetter = document.__lookupGetter__('cookie'); - - // store original cookieEnabled getter and setter so we can reset later - var origCookieEnabledSetter = window.navigator.__lookupSetter__('cookieEnabled'); - var origCookieEnabledGetter = window.navigator.__lookupGetter__('cookieEnabled'); - - // Replace the document cookie set function with the output of a custom function for testing - let setCookie = (v) => v; - - beforeEach(() => { - // Redefine window.navigator.cookieEnabled such that you can set otherwise "read-only" values - Object.defineProperty(window.navigator, 'cookieEnabled', (function (_value) { - return { - get: function _get() { - return _value; - }, - set: function _set(v) { - _value = v; - }, - configurable: true - }; - })(window.navigator.cookieEnabled)); - - // Reset the setCookie cookie function before each test - setCookie = (v) => v; - // Redefine the document.cookie object such that you can purposefully have it output nothing as if it is disabled - Object.defineProperty(window.document, 'cookie', (function (_value) { - return { - get: function _get() { - return _value; - }, - set: function _set(v) { - _value = setCookie(v); - }, - configurable: true - }; - })(window.navigator.cookieEnabled)); - }); - - afterEach(() => { - // redefine window.navigator.cookieEnabled to original getter and setter - Object.defineProperty(window.navigator, 'cookieEnabled', { - get: origCookieEnabledGetter, - set: origCookieEnabledSetter, - configurable: true - }); - // redefine document.cookie to original getter and setter - Object.defineProperty(document, 'cookie', { - get: origCookieGetter, - set: origCookieSetter, - configurable: true - }); - }); - - it('should be detected', function() { - assert.equal(utils.cookiesAreEnabled(), true, 'Cookies should be enabled by default'); - }); - - it('should be not available', function() { - setCookie = () => ''; - window.navigator.cookieEnabled = false; - window.document.cookie = ''; - assert.equal(utils.cookiesAreEnabled(), false, 'Cookies should be disabled'); - }); - - it('should be available', function() { - window.navigator.cookieEnabled = false; - window.document.cookie = 'key=value'; - assert.equal(utils.cookiesAreEnabled(), true, 'Cookies should already be set'); - window.navigator.cookieEnabled = false; - window.document.cookie = ''; - assert.equal(utils.cookiesAreEnabled(), true, 'Cookies should settable'); - setCookie = () => ''; - window.navigator.cookieEnabled = true; - window.document.cookie = ''; - assert.equal(utils.cookiesAreEnabled(), true, 'Cookies should be on via on window.navigator'); - // Reset the setCookie - setCookie = (v) => v; - }); - }); + /** + * tests fail in IE10 because __lookupSetter__ and __lookupGetter__ are + * not supported. See #1656. commenting out until they can be fixed. + * + * describe('cookie support', function () { + * // store original cookie getter and setter so we can reset later + * var origCookieSetter = document.__lookupSetter__('cookie'); + * var origCookieGetter = document.__lookupGetter__('cookie'); + * + * // store original cookieEnabled getter and setter so we can reset later + * var origCookieEnabledSetter = window.navigator.__lookupSetter__('cookieEnabled'); + * var origCookieEnabledGetter = window.navigator.__lookupGetter__('cookieEnabled'); + * + * // Replace the document cookie set function with the output of a custom function for testing + * let setCookie = (v) => v; + * + * beforeEach(() => { + * // Redefine window.navigator.cookieEnabled such that you can set otherwise "read-only" values + * Object.defineProperty(window.navigator, 'cookieEnabled', (function (_value) { + * return { + * get: function _get() { + * return _value; + * }, + * set: function _set(v) { + * _value = v; + * }, + * configurable: true + * }; + * })(window.navigator.cookieEnabled)); + * + * // Reset the setCookie cookie function before each test + * setCookie = (v) => v; + * // Redefine the document.cookie object such that you can purposefully have it output nothing as if it is disabled + * Object.defineProperty(window.document, 'cookie', (function (_value) { + * return { + * get: function _get() { + * return _value; + * }, + * set: function _set(v) { + * _value = setCookie(v); + * }, + * configurable: true + * }; + * })(window.navigator.cookieEnabled)); + * }); + * + * afterEach(() => { + * // redefine window.navigator.cookieEnabled to original getter and setter + * Object.defineProperty(window.navigator, 'cookieEnabled', { + * get: origCookieEnabledGetter, + * set: origCookieEnabledSetter, + * configurable: true + * }); + * // redefine document.cookie to original getter and setter + * Object.defineProperty(document, 'cookie', { + * get: origCookieGetter, + * set: origCookieSetter, + * configurable: true + * }); + * }); + * + * it('should be detected', function() { + * assert.equal(utils.cookiesAreEnabled(), true, 'Cookies should be enabled by default'); + * }); + * + * it('should be not available', function() { + * setCookie = () => ''; + * window.navigator.cookieEnabled = false; + * window.document.cookie = ''; + * assert.equal(utils.cookiesAreEnabled(), false, 'Cookies should be disabled'); + * }); + * + * it('should be available', function() { + * window.navigator.cookieEnabled = false; + * window.document.cookie = 'key=value'; + * assert.equal(utils.cookiesAreEnabled(), true, 'Cookies should already be set'); + * window.navigator.cookieEnabled = false; + * window.document.cookie = ''; + * assert.equal(utils.cookiesAreEnabled(), true, 'Cookies should settable'); + * setCookie = () => ''; + * window.navigator.cookieEnabled = true; + * window.document.cookie = ''; + * assert.equal(utils.cookiesAreEnabled(), true, 'Cookies should be on via on window.navigator'); + * // Reset the setCookie + * setCookie = (v) => v; + * }); + * }); + **/ describe('delayExecution', function () { it('should execute the core function after the correct number of calls', function () { From 9f4ffdae001ef5b1d55007061d94c3f730047216 Mon Sep 17 00:00:00 2001 From: Matt Lane <mlane@appnexus.com> Date: Tue, 31 Oct 2017 14:33:56 -0700 Subject: [PATCH 44/44] Prebid 0.32.0 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 062e0943815..b6f05afd29d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "0.32.0-pre", + "version": "0.32.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": {