Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions adapters.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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'
);
});

});

});