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

Commit

Permalink
Merge pull request #1 from ad4game/feature/INC-9
Browse files Browse the repository at this point in the history
Add A4G prebid adapter
  • Loading branch information
sjukeshov authored Aug 9, 2017
2 parents 472a8c7 + 080f936 commit 8e680c1
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 0 deletions.
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);
});
});
});

0 comments on commit 8e680c1

Please sign in to comment.