Skip to content

Commit

Permalink
Added MobFox Adapter (#1312)
Browse files Browse the repository at this point in the history
* added matomy as an alias for appnexus

* added mobfox adapter

* added mobfox adapter

* lint fixes in mobfox test

* fixed mobfox adapter error handling on invalid json

* fixed mobfox adapter error handling on no ad response
  • Loading branch information
francoroy authored and dbemiller committed Jul 26, 2017
1 parent 0d1bd55 commit 03d86a0
Show file tree
Hide file tree
Showing 2 changed files with 347 additions and 0 deletions.
185 changes: 185 additions & 0 deletions modules/mobfoxBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
const bidfactory = require('src/bidfactory.js');
const bidmanager = require('src/bidmanager.js');
const adloader = require('src/adloader');
const ajax = require('src/ajax.js');
const CONSTANTS = require('src/constants.json');
const utils = require('src/utils.js');
const adaptermanager = require('src/adaptermanager');

const mobfoxAdapter = function () {
const BIDDER_CODE = 'mobfox';
const BID_REQUEST_BASE_URL = 'https://my.mobfox.com/request.php';

// request
function buildQueryStringFromParams(params) {
for (let key in params) {
if (params.hasOwnProperty(key)) {
if (params[key] === undefined) {
delete params[key];
} else {
params[key] = encodeURIComponent(params[key]);
}
}
}

return utils._map(Object.keys(params), key => `${key}=${params[key]}`)
.join('&');
}

function buildBidRequest(bid) {
let bidParams = bid.params;

let requestParams = {
// -------------------- Mandatory Parameters ------------------
rt: bidParams.rt || 'api-fetchip',
r_type: bidParams.r_type || 'banner',
r_resp: bidParams.r_resp || 'json', // string | vast20
// i: bidParams.i || undefined , // string | 69.197.148.18
s: bidParams.s, // string | 80187188f458cfde788d961b6882fd53
u: bidParams.u || window.navigator.userAgent, // string

// ------------------- Global Parameters ----------------------
adspace_width: bidParams.adspace_width || bid.sizes[0][0], // integer | 320
adspace_height: bidParams.adspace_height || bid.sizes[0][1], // integer | 48
r_floor: bidParams.r_floor || undefined, // 0.8

o_andadvid: bidParams.o_andadvid || undefined, // 'c6292267-56ad-4326-965d-deef6fcd5er9'
longitude: bidParams.longitude || undefined, // 12.12
latitude: bidParams.latitude || undefined, // 280.12
demo_age: bidParams.demo_age || undefined, // 1978

// ------------------- banner / interstitial ----------------------
adspace_strict: bidParams.adspace_strict || undefined,

// ------------------- interstitial / video ----------------------
imp_instl: bidParams.imp_instl || undefined, // integer | 1

// ------------------- mraid ----------------------
c_mraid: bidParams.c_mraid || undefined, // integer | 1

// ------------------- video ----------------------
v_dur_min: bidParams.v_dur_min || undefined, // integer | 0
v_dur_max: bidParams.v_dur_max || undefined, // integer | 999
v_autoplay: bidParams.v_autoplay || undefined, // integer | 1
v_startmute: bidParams.v_startmute || undefined, // integer | 0
v_rewarded: bidParams.v_rewarded || undefined, // integer | 0
v_api: bidParams.v_api || undefined, // string | vpaid20
n_ver: bidParams.n_ver || undefined, //
n_adunit: bidParams.n_adunit || undefined, //
n_layout: bidParams.n_layout || undefined, //
n_context: bidParams.n_context || undefined, //
n_plcmttype: bidParams.n_plcmttype || undefined, //
n_img_icon_req: bidParams.n_img_icon_req || undefined, // boolean0
n_img_icon_size: bidParams.n_img_icon_size || undefined, // string80
n_img_large_req: bidParams.n_img_large_req || undefined, // boolean0
n_img_large_w: bidParams.n_img_large_w || undefined, // integer1200
n_img_large_h: bidParams.n_img_large_h || undefined, // integer627
n_title_req: bidParams.n_title_req || undefined, // boolean0
n_title_len: bidParams.n_title_len || undefined, // string25
n_desc_req: bidParams.n_desc_req || undefined, // boolean0
n_desc_len: bidParams.n_desc_len || undefined, // string140
n_rating_req: bidParams.n_rating_req || undefined
};

return requestParams;
}

function sendBidRequest(bid) {
let requestParams = buildBidRequest(bid);
let queryString = buildQueryStringFromParams(requestParams);

ajax.ajax(`${BID_REQUEST_BASE_URL}?${queryString}`, {
success(resp, xhr) {
if (xhr.getResponseHeader('Content-Type') == 'application/json') {
try {
resp = JSON.parse(resp)
}
catch (e) {
resp = {error: resp}
}
}
onBidResponse({
data: resp,
xhr: xhr
}, bid);
},
error(err) {
if (xhr.getResponseHeader('Content-Type') == 'application/json') {
try {
err = JSON.parse(err);
}
catch (e) {
}
;
}
onBidResponseError(bid, [err]);
}
});
}

// response
function onBidResponseError(bid, err) {
utils.logError('Bid Response Error', bid, ...err);
let bidResponse = bidfactory.createBid(CONSTANTS.STATUS.NO_BID, bid);
bidResponse.bidderCode = BIDDER_CODE;
bidmanager.addBidResponse(bid.placementCode, bidResponse);
}

function onBidResponse(bidderResponse, bid) {
// transform the response to a valid prebid response
try {
let bidResponse = transformResponse(bidderResponse, bid);
bidmanager.addBidResponse(bid.placementCode, bidResponse);
} catch (e) {
onBidResponseError(bid, [e]);
}
}

function transformResponse(bidderResponse, bid) {
let responseBody = bidderResponse.data;

// Validate Request
let err = responseBody.error;
if (err) {
throw err;
}

let htmlString = responseBody.request && responseBody.request.htmlString;
if (!htmlString) {
throw [`htmlString is missing`, responseBody];
}

let cpm, cpmHeader = bidderResponse.xhr.getResponseHeader('X-Pricing-CPM');
try {
cpm = Number(cpmHeader);
} catch (e) {
throw ['Invalid CPM value:', cpmHeader];
}

// Validations passed - Got bid
let bidResponse = bidfactory.createBid(CONSTANTS.STATUS.GOOD, bid);
bidResponse.bidderCode = BIDDER_CODE;

bidResponse.ad = htmlString;
bidResponse.cpm = cpm;

bidResponse.width = bid.sizes[0][0];
bidResponse.height = bid.sizes[0][1];

return bidResponse;
}

// prebid api
function callBids(params) {
let bids = params.bids || [];
bids.forEach(sendBidRequest);
}

return {
callBids: callBids
};
};


adaptermanager.registerBidAdapter(new mobfoxAdapter(), 'mobfox');
module.exports = mobfoxAdapter;
162 changes: 162 additions & 0 deletions test/spec/modules/mobfoxBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
describe('mobfox adapter tests', function () {
const expect = require('chai').expect;
const utils = require('src/utils');
const adapter = require('modules/mobfoxBidAdapter');
const bidmanager = require('src/bidmanager');
const adloader = require('src/adloader');
const CONSTANTS = require('src/constants.json');
const ajax = require('src/ajax.js');
let mockResponses = {
banner: {
'request': {
'type': 'textAd',
'htmlString': '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title><\/title><style>body{margin:0;padding:0}#mobfoxCover{background:0 0;margin:0;padding:0;border:none;position:absolute;left:0;top:0;z-index:100}<\/style><\/head><body><div id="mobfoxCover"><\/div><script type="text\/javascript">function checkRedirect(e){return function(){if(state===REDIRECT){state=REDUNDANT;var t=window.document.querySelector("iframe").contentDocument.querySelector("html").innerHTML.toLowerCase();if(!(t.indexOf("<script")<0&&t.indexOf("<iframe")<0)){var o=new XMLHttpRequest,d={creativeId:creativeId,advertiserId:advertiserId,hParam:hParam,dspId:dspId,networkId:networkId,autoPilotInventoryConfId:autoPilotInventoryConfId,stackItemId:stackItemId,adSpaceId:adSpaceId,cId:cId,adomain:adomain,geo:geo,event:e,ua:window.navigator.userAgent,adId:adId,site:window.location.href,md5Hash:md5Hash,snapshot:btoa(unescape(encodeURIComponent(t)))};o.open("POST","http:\/\/my.mobfox.com\/fraud-integration",!1),o.setRequestHeader("Content-type","application\/json"),o.send(JSON.stringify(d))}}}}function init(){window.onbeforeunload=checkRedirect("onbeforeunload"),window.addEventListener("beforeunload",checkRedirect("beforeunload")),window.addEventListener("unload",checkRedirect("unload")),document.addEventListener("visibilitychange",function(){"hidden"===document.visibilityState&&checkRedirect("visibilityState")});var e=document.createElement("iframe");document.body.appendChild(e),e.width="320",e.height="50";var t=document.querySelector("#mobfoxCover");t.style.width=e.width+"px",t.style.height=e.height+"px",e.style.margin="0px",e.style.padding="0px",e.style.border="none",e.scrolling="no",e.style.overflow="hidden",e.sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-top-navigation allow-same-origin";var o=atob(markupB64);setTimeout(function(){state=NORMAL},200),setTimeout(function(){var e=document.querySelector("#mobfoxCover");document.body.removeChild(e)},200);var d="srcdoc"in e,n=o;o.indexOf("<body>")<0&&(n="<html><body style="margin:0">"+o+"<\/body><\/html>"),d?e.srcdoc=n:(e.contentWindow.document.open(),e.contentWindow.document.write(n),e.contentWindow.document.close())}var markupB64="PGEgaHJlZj0iaHR0cDovL3Rva3lvLW15Lm1vYmZveC5jb20vZXhjaGFuZ2UuY2xpY2sucGhwP2g9ZGI1ZjZkOTJiMDk1OGI0ZDFlNjU4ZjZlNWRkNWY0MmUiIHRhcmdldD0iX2JsYW5rIj48aW1nIHNyYz0iaHR0cHM6Ly9jcmVhdGl2ZWNkbi5tb2Jmb3guY29tL2U4ZTkxNWYzMmJhOTVkM2JmMzY4YTM5N2EyMzQ4NzVmLmdpZiIgYm9yZGVyPSIwIi8+PC9hPjxicj48aW1nIHN0eWxlPSJwb3NpdGlvbjphYnNvbHV0ZTsgbGVmdDogLTEwMDAwcHg7IiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBzcmM9Imh0dHA6Ly90b2t5by1teS5tb2Jmb3guY29tL2V4Y2hhbmdlLnBpeGVsLnBocD9oPWRiNWY2ZDkyYjA5NThiNGQxZTY1OGY2ZTVkZDVmNDJlIi8+PHNjcmlwdCB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiPmRvY3VtZW50LndyaXRlKCc8aW1nIHN0eWxlPSJwb3NpdGlvbjphYnNvbHV0ZTsgbGVmdDogLTEwMDAwcHg7IiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBzcmM9Imh0dHA6Ly90b2t5by1teS5tb2Jmb3guY29tL2V4Y2hhbmdlLnBpeGVsLnBocD9oPWRiNWY2ZDkyYjA5NThiNGQxZTY1OGY2ZTVkZDVmNDJlJnRlc3Q9MSIvPicpOzwvc2NyaXB0Pg==",INITIAL=0,REDIRECT=1,REDUNDANT=2,NORMAL=3,state=INITIAL,creativeId="",advertiserId="",hParam="db5f6d92b0958b4d1e658f6e5dd5f42e",dspId="",networkId="",autoPilotInventoryConfId="",stackItemId="392746",serverHost="184.172.209.50",adSpaceId="",adId="",cId="",adomain="",geo="US",md5Hash="f3bd183c0b19faf12c76e75461cb8cac";document.addEventListener("DOMContentLoaded",function(e){state=REDIRECT}),setTimeout(init,1)<\/script><\/body><\/html>',
'clicktype': 'safari',
'clickurl': 'http://tokyo-my.mobfox.com/exchange.click.php?h=db5f6d92b0958b4d1e658f6e5dd5f42e',
'urltype': 'link',
'refresh': '30',
'scale': 'no',
'skippreflight': 'yes'
}
}
};

let mockRequestsParams = {
banner: {
rt: 'api',
r_type: 'banner',
i: '69.197.148.18',
s: 'fe96717d9875b9da4339ea5367eff1ec',
u: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4',
adspace_strict: 0,

// o_iosadvid: '1976F519-26D0-4428-9891-3133253A453F',
// r_floor: '0.2',
// longitude: '12.12',
// latitude: '280.12',
// demo_gender: 'male',
// demo_age: '1982',
// demo_keywords: 'sports',
// adspace_width: 320,
// adspace_height: 50
}
};

before(() => sinon.stub(document.body, 'appendChild'));
after(() => document.body.appendChild.restore());

let xhrMock = {
getResponseHeader: getResponseHeaderMock
};
function getResponseHeaderMock(header) {
switch (header) {
case 'Content-Type':
return 'application/json';
case 'X-Pricing-CPM':
return '1';
}
}
function createMobfoxErrorStub() {
return sinon.stub(ajax, 'ajax', (url, callbacks) => {
callbacks.success(
JSON.stringify({error: 'No Ad Available'}),
xhrMock
);
});
}

function createMobfoxSuccessStub() {
return sinon.stub(ajax, 'ajax', (url, callbacks) => {
callbacks.success(
JSON.stringify(mockResponses.banner)
, xhrMock
);
});
}

describe('test mobfox error response', function () {
let stubAddBidResponse, stubAjax;
before(function () {
stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse');
stubAjax = createMobfoxErrorStub()
});

after(function () {
stubAddBidResponse.restore();
stubAjax.restore();
});

it('should add empty bid responses if no bids returned', function () {
let bidderRequest = {
bidderCode: 'mobfox',
bids: [
{
bidId: 'bidId1',
bidder: 'mobfox',
params: {},
sizes: [[300, 250]],
placementCode: 'test-gpt-div-1234'
}
]
};

// empty ads in bidresponse
let requestParams = utils.cloneJson(mockRequestsParams.banner);
requestParams.adspace_width = 1231564; // should return an error
bidderRequest.bids[0].params = requestParams;
pbjs._bidsRequested.push(bidderRequest);
// adapter needs to be called, in order for the stub to register.
adapter().callBids(bidderRequest);

let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0];
let bidResponse1 = stubAddBidResponse.getCall(0).args[1];
expect(bidPlacementCode1).to.equal('test-gpt-div-1234');
expect(bidResponse1.getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID);
expect(bidResponse1.bidderCode).to.equal('mobfox');
});
});

describe('test mobfox success response', function () {
let stubAddBidResponse, stubAjax;
before(function () {
stubAddBidResponse = sinon.stub(bidmanager, 'addBidResponse');
stubAjax = createMobfoxSuccessStub()
});

after(function () {
stubAddBidResponse.restore();
stubAjax.restore();
});

it('should add a bid response', function () {
let bidderRequest = {
bidderCode: 'mobfox',
bids: [
{
bidId: 'bidId1',
bidder: 'mobfox',
params: {},
sizes: [[300, 250]],
placementCode: 'test-gpt-div-1234'
}
]
};

let requestParams = utils.cloneJson(mockRequestsParams.banner);
bidderRequest.bids[0].params = requestParams;
pbjs._bidsRequested.push(bidderRequest);
// adapter needs to be called, in order for the stub to register.
adapter().callBids(bidderRequest);

let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0];
let bidResponse1 = stubAddBidResponse.getCall(0).args[1];
expect(bidPlacementCode1).to.equal('test-gpt-div-1234');
expect(bidResponse1.getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD);
expect(bidResponse1.bidderCode).to.equal('mobfox');

expect(bidResponse1.cpm).to.equal(1);
expect(bidResponse1.width).to.equal(bidderRequest.bids[0].sizes[0][0]);
expect(bidResponse1.height).to.equal(bidderRequest.bids[0].sizes[0][1]);
});
});
});

2 comments on commit 03d86a0

@francoroy
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @dbemiller
I will add tests.

Are there any expectations for when will mobfox be added in prebid's site in as a supported adapter?

Thanks,
Roy

@dbemiller
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll go out with the next release.

We usually do a release every 2 weeks or so... but nothing's set in stone.

Please sign in to comment.