Skip to content
This repository has been archived by the owner on Mar 30, 2018. It is now read-only.

Add A4G prebid adapter #1

Merged
merged 1 commit into from
Aug 9, 2017
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
@@ -1,4 +1,5 @@
[
"a4g",
"aardvark",
"adblade",
"adbund",
Expand Down
170 changes: 170 additions & 0 deletions src/adapters/a4g.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
const bidfactory = require('../bidfactory.js'),
bidmanager = require('../bidmanager.js'),
constants = require('../constants.json'),
adloader = require('../adloader'),
utils = require('../utils.js');

const A4G_BIDDER_CODE = 'a4g';
const A4G_DEFAULT_BID_URL = '//ads.ad4game.com/v1/bid';

const IFRAME_NESTING_PARAM_NAME = 'if';
const LOCATION_PARAM_NAME = 'siteurl';
const ID_PARAM_NAME = 'id';
const ZONE_ID_PARAM_NAME = 'zoneId';
const SIZE_PARAM_NAME = 'size';

const A4G_SUPPORTED_PARAMS = [
IFRAME_NESTING_PARAM_NAME,
LOCATION_PARAM_NAME,
ID_PARAM_NAME,
ZONE_ID_PARAM_NAME,
SIZE_PARAM_NAME
];

const ARRAY_PARAM_SEPARATOR = ';';
const ARRAY_SIZE_SEPARATOR = ',';
const SIZE_SEPARATOR = 'x';

const JSONP_PARAM_NAME = 'jsonp';

function appendUrlParam(url, paramName, paramValue) {
const isQueryParams = url.indexOf('?') !== -1,
separator = isQueryParams ? '&' : '?';
return url + separator + encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue);
}

function isInIframe(windowObj) {
return windowObj !== windowObj.parent;
}

function getIframeInfo(window) {
let currentWindow = window,
iframeNestingLevel = 0,
hasExternalMeet = false,
hostHref = window.location.href;

while (isInIframe(currentWindow)) {
currentWindow = currentWindow.parent;

try {
if (hasExternalMeet) {
iframeNestingLevel = 1;
} else {
iframeNestingLevel++;
}

hostHref = currentWindow.document.referrer || currentWindow.location.href;
} catch (e) {
hasExternalMeet = true;
}
}

return {
nestingLevel: iframeNestingLevel,
hostHref: hostHref
};
}

function isValidStatus(status) {
return status === 200;
}

function bidParamsToQuery(params) {
return A4G_SUPPORTED_PARAMS
.reduce((url, paramName) => paramName in params
? appendUrlParam(url, paramName, params[paramName])
: url,
'');
}

function buildBidRequestUrl(bidDeliveryUrl, params) {
return bidDeliveryUrl + bidParamsToQuery(params);
}

function createBidRequest(bidRequest) {
return (status) => bidfactory.createBid(status, bidRequest);
}

function mapBidToPrebidFormat(bidRequest, bid) {
const bidResponse = bidRequest(constants.STATUS.GOOD);

bidResponse.bidderCode = A4G_BIDDER_CODE;
bidResponse.cpm = bid.cpm;
bidResponse.ad = bid.ad;
bidResponse.width = bid.width;
bidResponse.height = bid.height;

return bidResponse;
}

function mapBidErrorToPrebid(bidRequest) {
return bidRequest(constants.STATUS.NO_BID);
}

function extractBidParams(bids) {
const idParams = [];
const sizeParams = [];
const zoneIds = [];

let deliveryUrl = '';

for (let i = 0; i < bids.length; i++) {
const bid = bids[i];
if (!deliveryUrl && typeof bid.params.deliveryUrl === 'string') {
deliveryUrl = bid.params.deliveryUrl;
}
idParams.push(bid.placementCode);
sizeParams.push(bid.sizes.map(size => size.join(SIZE_SEPARATOR)).join(ARRAY_SIZE_SEPARATOR));
zoneIds.push(bid.params.zoneId);
}

return [deliveryUrl, {
[ID_PARAM_NAME]: idParams.join(ARRAY_PARAM_SEPARATOR),
[ZONE_ID_PARAM_NAME]: zoneIds.join(ARRAY_PARAM_SEPARATOR),
[SIZE_PARAM_NAME]: sizeParams.join(ARRAY_PARAM_SEPARATOR)
}];
}

function a4gBidFactory() {

function generateJsonpCallbackName() {
return '__A4G' + Date.now();
}

function jsonp(url, callback) {
const callbackName = generateJsonpCallbackName(),
jsnopUrl = appendUrlParam(url, JSONP_PARAM_NAME, callbackName);

window[callbackName] = ({ status, response }) => {
!isValidStatus(status)
? callback(new Error(`Failed fetching ad with status ${status}`), response)
: callback(null, response);
delete window[callbackName];
};

adloader.loadScript(jsnopUrl);
}

return {
callBids({ bids }) {
const bidRequests = bids.map(bid => createBidRequest(utils.getBidRequest(bid.bidId)));
const [ deliveryUrl, bidParams ] = extractBidParams(bids);
const { nestingLevel, hostHref } = getIframeInfo(window);
const envParams = { [IFRAME_NESTING_PARAM_NAME]: nestingLevel, [LOCATION_PARAM_NAME]: hostHref };
const bidsRequestUrl = buildBidRequestUrl(deliveryUrl || A4G_DEFAULT_BID_URL, Object.assign({}, bidParams, envParams));

jsonp(bidsRequestUrl, (error, bidsResponse) => {
for (let i = 0; i < bidRequests.length; i++) {
const bidRequest = bidRequests[i],
placementCode = bids[i].placementCode;

bidmanager.addBidResponse(placementCode,
error
? mapBidErrorToPrebid(bidRequest)
: mapBidToPrebidFormat(bidRequest, bidsResponse[i]));
}});
}
};
}

module.exports = a4gBidFactory;
112 changes: 112 additions & 0 deletions test/spec/adapters/a4g_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
describe('a4g adapter tests', function () {
const expect = require('chai').expect;
const a4gBidFactory = require('src/adapters/a4g');
const bidmanager = require('src/bidmanager');
const adloader = require('src/adloader');
const constants = require('src/constants.json');

function readJsonpCallbackName(url) {
return /&jsonp=([_a-zA-Z0-9]+)/.exec(url)[1];
}

let spyLoadScript,
spyAddBidResponse,
a4gAdapter;

before(() => {
spyLoadScript = sinon.spy(adloader, 'loadScript');
spyAddBidResponse = sinon.spy(bidmanager, 'addBidResponse');
});

after(() => {
adloader.loadScript.restore();
bidmanager.addBidResponse.restore();
});

beforeEach(() => {
a4gAdapter = a4gBidFactory();
});

it('should send proper jsonp request to default deliveryUrl', () => {
a4gAdapter.callBids({ bids: [{
placementCode: 'pc1',
sizes: [[1, 2], [3, 4]],
params: {
zoneId: 1
}
}, {
placementCode: 'pc2',
sizes: [[5, 6]],
params: {
zoneId: 2
}
}]});

let targetUrl = spyLoadScript.lastCall.args[0];
expect(targetUrl).to.contain('ads.ad4game.com/v1/bid');
expect(targetUrl).to.contain('jsonp=');
expect(targetUrl).to.contain('id=pc1%3Bpc2');
expect(targetUrl).to.contain('size=1x2%2C3x4%3B5x6');
expect(targetUrl).to.contain('zoneId=1%3B2');
});

it('should send proper jsonp request to deliveryUrl from 1st bid', () => {
a4gAdapter.callBids({ bids: [{
placementCode: 'pc1',
sizes: [[1, 2], [3, 4]],
params: {
zoneId: 1,
deliveryUrl: 'new.test.delivery.com:8080/v105/new_bid'
}
}, {
placementCode: 'pc2',
sizes: [[5, 6]],
params: {
zoneId: 2,
deliveryUrl: 'nonused.test.delivery.com:8080/v105/new_bid'
}
}]});

let targetUrl = spyLoadScript.lastCall.args[0];
expect(targetUrl).to.contain('new.test.delivery.com:8080/v105/new_bid');
});

describe('on jsonp callback', () => {
let jsonpCallbackName;

beforeEach(() => {
a4gAdapter.callBids({ bids: [{
placementCode: 'pc1',
sizes: [[1, 2], [3, 4]],
params: {
zoneId: 1
}
}]});
jsonpCallbackName = readJsonpCallbackName(spyLoadScript.lastCall.args[0]);
});

it('should unregister', () => {
window[jsonpCallbackName]({status: 200, response: [{id: 'pc1', width: 1, height: 2, cpm: 1.0, ad: '' }]});
expect(window[jsonpCallbackName]).to.not.be.a('function');
});

it('should set all responses as bad if error received', () => {
window[jsonpCallbackName]({status: 400, response: []});
let [placementCode, bid] = spyAddBidResponse.lastCall.args;
expect(placementCode).to.equal('pc1');
expect(bid.getStatusCode()).to.equal(constants.STATUS.NO_BID);
});

it('should set all responses as good with appropriate values if ok', () => {
window[jsonpCallbackName]({status: 200, response: [{id: 'pc1', width: 1, height: 2, cpm: 1.0, ad: 'test' }]});
let [placementCode, bid] = spyAddBidResponse.lastCall.args;
expect(placementCode).to.equal('pc1');

expect(bid.getStatusCode()).to.equal(constants.STATUS.GOOD);
expect(bid.cpm).to.equal(1);
expect(bid.ad).to.equal('test');
expect(bid.width).to.equal(1);
expect(bid.height).to.equal(2);
});
});
});