Skip to content

Commit

Permalink
Add new adapter ServerBid (by Adzerk) (prebid#1024)
Browse files Browse the repository at this point in the history
* Wrote initial call logic to make request to serverbid.

* Add initial bid response handling logic.

* Handle requests and responses, successfully pass to add server, write initial tests.

* Clean up pricing and fix tests.

* Enable request credentials.

* JSHint and JSCS

* Incorporate code review comments.

* Add function invocation to getTopWindowUrl
  • Loading branch information
LarryTurtis authored and dluxemburg committed Jul 17, 2018
1 parent 5a6894f commit 630598a
Show file tree
Hide file tree
Showing 3 changed files with 342 additions and 0 deletions.
1 change: 1 addition & 0 deletions adapters.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"smartyads",
"smartadserver",
"sekindoUM",
"serverbid",
"sonobi",
"sovrn",
"springserve",
Expand Down
163 changes: 163 additions & 0 deletions src/adapters/serverbid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import Adapter from 'src/adapters/adapter';
import bidfactory from 'src/bidfactory';
import bidmanager from 'src/bidmanager';
import * as utils from 'src/utils';
import { ajax } from 'src/ajax';

const ServerBidAdapter = function ServerBidAdapter() {

const baseAdapter = Adapter.createNew('serverbid');

const BASE_URI = '//e.serverbid.com/api/v2';

const sizeMap = [null,
"120x90",
"120x90",
"468x60",
"728x90",
"300x250",
"160x600",
"120x600",
"300x100",
"180x150",
"336x280",
"240x400",
"234x60",
"88x31",
"120x60",
"120x240",
"125x125",
"220x250",
"250x250",
"250x90",
"0x0",
"200x90",
"300x50",
"320x50",
"320x480",
"185x185",
"620x45",
"300x125",
"800x250"
];

const bidIds = [];

baseAdapter.callBids = function(params) {

if (params && params.bids && utils.isArray(params.bids) && params.bids.length) {

const data = {
placements: [],
time: Date.now(),
user: {},
url: utils.getTopWindowUrl(),
referrer: document.referrer,
enableBotFiltering: true,
includePricingData: true
};

const bids = params.bids || [];
for (let i = 0; i < bids.length; i++) {
const bid = bids[i];

bidIds.push(bid.bidId);

const bid_data = {
networkId: bid.params.networkId,
siteId: bid.params.siteId,
zoneIds: bid.params.zoneIds,
campaignId: bid.params.campaignId,
flightId: bid.params.flightId,
adId: bid.params.adId,
divName: bid.bidId,
adTypes: bid.adTypes || getSize(bid.sizes)
};

if (bid_data.networkId && bid_data.siteId) {
data.placements.push(bid_data);
}

}

if (data.placements.length) {
ajax(BASE_URI, _responseCallback, JSON.stringify(data), { method: 'POST', withCredentials: true, contentType: 'application/json' });
}

}

};

function _responseCallback(result) {

let bid;
let bidId;
let bidObj;
let bidCode;
let placementCode;

try {
result = JSON.parse(result);
} catch (error) {
utils.logError(error);
}

for (let i = 0; i < bidIds.length; i++) {

bidId = bidIds[i];
bidObj = utils.getBidRequest(bidId);
bidCode = bidObj.bidder;
placementCode = bidObj.placementCode;

if (result) {
const decision = result.decisions && result.decisions[bidId];
const price = decision && decision.pricing && decision.pricing.clearPrice;

if (decision && price) {
bid = bidfactory.createBid(1, bidObj);
bid.bidderCode = bidCode;
bid.cpm = price;
bid.width = decision.width;
bid.height = decision.height;
bid.ad = retrieveAd(decision);
} else {
bid = bidfactory.createBid(2, bidObj);
bid.bidderCode = bidCode;
}

} else {
bid = bidfactory.createBid(2, bidObj);
bid.bidderCode = bidCode;
}
bidmanager.addBidResponse(placementCode, bid);
}
}

function retrieveAd(decision) {
return decision.contents && decision.contents[0] && decision.contents[0].body + utils.createTrackPixelHtml(decision.impressionUrl);
}

function getSize(sizes) {
const result = [];
sizes.forEach(function(size) {
const index = sizeMap.indexOf(size[0] + "x" + size[1]);
if (index >= 0) {
result.push(index);
}
});
return result;
}

// Export the `callBids` function, so that Prebid.js can execute
// this function when the page asks to send out bid requests.
return {
callBids: baseAdapter.callBids
};

};

ServerBidAdapter.createNew = function() {
return new ServerBidAdapter();
};

module.exports = ServerBidAdapter;
178 changes: 178 additions & 0 deletions test/spec/adapters/serverbid_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/* jshint -W024 */
/* jshint expr:true */

import { expect } from 'chai';
import Adapter from 'src/adapters/serverbid';
import bidmanager from 'src/bidmanager';
import * as utils from 'src/utils';

const ENDPOINT = '//e.serverbid.com/api/v2';

const REQUEST = {
"bidderCode": "serverbid",
"requestId": "a4713c32-3762-4798-b342-4ab810ca770d",
"bidderRequestId": "109f2a181342a9",
"bids": [{
"bidder": "serverbid",
"params": {
"networkId": 9969,
"siteId": 730181
},
"placementCode": "div-gpt-ad-1487778092495-0",
"sizes": [
[728, 90],
[970, 90]
],
"bidId": "2b0f82502298c9",
"bidderRequestId": "109f2a181342a9",
"requestId": "a4713c32-3762-4798-b342-4ab810ca770d"
}],
"start": 1487883186070,
"auctionStart": 1487883186069,
"timeout": 3000
};

const RESPONSE = {
"user": { "key": "ue1-2d33e91b71e74929b4aeecc23f4376f1" },
"decisions": {
"2b0f82502298c9": {
"adId": 2364764,
"creativeId": 1950991,
"flightId": 2788300,
"campaignId": 542982,
"clickUrl": "http://e.serverbid.com/r",
"impressionUrl": "http://e.serverbid.com/i.gif",
"contents": [{
"type": "html",
"body": "<html></html>",
"data": {
"height": 90,
"width": 728,
"imageUrl": "http://static.adzerk.net/Advertisers/b0ab77db8a7848c8b78931aed022a5ef.gif",
"fileName": "b0ab77db8a7848c8b78931aed022a5ef.gif"
},
"template": "image"
}],
"height": 90,
"width": 728,
"events": [],
"pricing":{"price":0.5,"clearPrice":0.5,"revenue":0.0005,"rateType":2,"eCPM":0.5}
},
}
};

describe('serverbidAdapter', () => {

let adapter;

beforeEach(() => adapter = Adapter.createNew());

describe('request function', () => {

let xhr;
let requests;
let pbConfig;


beforeEach(() => {
xhr = sinon.useFakeXMLHttpRequest();
requests = [];
xhr.onCreate = request => requests.push(request);
pbConfig = REQUEST;
//just a single slot
pbConfig.bids = [pbConfig.bids[0]];
});

afterEach(() => xhr.restore());

it('exists and is a function', () => {
expect(adapter.callBids).to.exist.and.to.be.a('function');
});

it('requires paramaters to make request', () => {
adapter.callBids({});
expect(requests).to.be.empty;
});

it('requires networkId and siteId', () => {
let backup = pbConfig.bids[0].params;
pbConfig.bids[0].params = { networkId: 1234 }; //no hbid
adapter.callBids(pbConfig);
expect(requests).to.be.empty;

pbConfig.bids[0].params = { siteId: 1234 }; //no placementid
adapter.callBids(pbConfig);
expect(requests).to.be.empty;

pbConfig.bids[0].params = backup;
});

it('sends bid request to ENDPOINT via POST', () => {
adapter.callBids(pbConfig);
expect(requests[0].url).to.equal(ENDPOINT);
expect(requests[0].method).to.equal('POST');
});
});

describe('response handler', () => {

let server;

beforeEach(() => {
server = sinon.fakeServer.create();
sinon.stub(bidmanager, 'addBidResponse');
sinon.stub(utils, "getBidRequest").returns(REQUEST);
});

afterEach(() => {
server.restore();
bidmanager.addBidResponse.restore();
utils.getBidRequest.restore();
});

it('registers bids', () => {
server.respondWith(JSON.stringify(RESPONSE));

adapter.callBids(REQUEST);
server.respond();
sinon.assert.calledOnce(bidmanager.addBidResponse);

const response = bidmanager.addBidResponse.firstCall.args[1];
expect(response).to.have.property('statusMessage', 'Bid available');
expect(response).to.have.property('cpm');
expect(response.cpm).to.be.above(0);
});

it('handles nobid responses', () => {
server.respondWith(JSON.stringify({
"decisions": []
}));

adapter.callBids(REQUEST);
server.respond();
sinon.assert.calledOnce(bidmanager.addBidResponse);

const response = bidmanager.addBidResponse.firstCall.args[1];
expect(response).to.have.property(
'statusMessage',
'Bid returned empty or error response'
);
});

it('handles JSON.parse errors', () => {
server.respondWith('');

adapter.callBids(REQUEST);
server.respond();
sinon.assert.calledOnce(bidmanager.addBidResponse);

const response = bidmanager.addBidResponse.firstCall.args[1];
expect(response).to.have.property(
'statusMessage',
'Bid returned empty or error response'
);
});

});

});

0 comments on commit 630598a

Please sign in to comment.