Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Cox adapter #1228

Merged
merged 5 commits into from
Jun 13, 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
Expand Up @@ -68,6 +68,7 @@
"trion",
"prebidServer",
"adsupply",
"cox",
{
"appnexus": {
"alias": "brealtime"
Expand Down
255 changes: 255 additions & 0 deletions src/adapters/cox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
var bidfactory = require('../bidfactory.js');
var bidmanager = require('../bidmanager.js');
var adLoader = require('../adloader.js');

var CoxAdapter = function CoxAdapter() {
var adZoneAttributeKeys = ['id', 'size', 'thirdPartyClickUrl'],
otherKeys = ['siteId', 'wrapper', 'referrerUrl'],
placementMap = {},
W = window;

var COX_BIDDER_CODE = 'cox';

function _callBids(params) {
var env = '';

// Create global cdsTag and CMT object (for the latter, only if needed )
W.cdsTag = {};
if (!W.CMT) W.CMT = _getCoxLite();

// Populate the tag with the info from prebid
var bids = params.bids || [],
tag = W.cdsTag,
i,
j;
for (i = 0; i < bids.length; i++) {
var bid = bids[i],
cfg = bid.params || {};

if (cfg.id) {
tag.zones = tag.zones || {};
var zone = {};

for (j = 0; j < adZoneAttributeKeys.length; j++) {
if (cfg[adZoneAttributeKeys[j]]) zone[adZoneAttributeKeys[j]] = cfg[adZoneAttributeKeys[j]];
}
for (j = 0; j < otherKeys.length; j++) {
if (cfg[otherKeys[j]]) tag[otherKeys[j]] = cfg[otherKeys[j]];
}
var adZoneKey = 'as' + cfg.id;
tag.zones[adZoneKey] = zone;

// Check for an environment setting
if (cfg.env) env = cfg.env;

// Update the placement map
var xy = (cfg.size || '0x0').split('x');
placementMap[adZoneKey] = {
p: bid.placementCode,
w: xy[0],
h: xy[1]
};
}
}
if (tag.zones && Object.keys(tag.zones).length > 0) {
tag.__callback__ = function (r) {
tag.response = r;
_notify();
};
adLoader.loadScript(W.CMT.Service.buildSrc(tag, env));
}
}

function _notify() {
// Will execute in the context of a bid
// function finalizeAd(price) {
// this.ad = W.CMT.Service.setAuctionPrice(this.ad, price);
// return this;
// }
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you don't need the code that is commented out here you may want to remove this


for (var adZoneKey in placementMap) {
var bid = W.CMT.Service.getBidTrue(adZoneKey),
bidObj,
data = placementMap[adZoneKey];

if (bid > 0) {
bidObj = bidfactory.createBid(1);
bidObj.cpm = bid;
bidObj.ad = W.CMT.Service.getAd(adZoneKey);
bidObj.width = data.w;
bidObj.height = data.h;
// bidObj.floor = W.CMT.Service.getSecondPrice(adZoneKey);
// bidObj.finalizeAd = finalizeAd;
Copy link
Collaborator

Choose a reason for hiding this comment

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

More commented code for potential removal

} else {
bidObj = bidfactory.createBid(2);
}
bidObj.bidderCode = COX_BIDDER_CODE;
bidmanager.addBidResponse(data.p, bidObj);
}
}

function _getCoxLite() {
var CMT = {};

CMT.Util = (function () {
return {

getRand: function getRand() {
return Math.round(Math.random() * 100000000);
},

encodeUriObject: function encodeUriObject(obj) {
return encodeURIComponent(JSON.stringify(obj));
},

extractUrlInfo: function extractUrlInfo() {
function f2(callback) {
try {
if (!W.location.ancestorOrigins) return;
for (var i = 0, len = W.location.ancestorOrigins.length; len > i; i++) {
callback.call(null, W.location.ancestorOrigins[i], i);
}
} catch (ignore) { }
return [];
}

function f1(callback) {
var oneWindow,
infoArray = [];
do {
try {
oneWindow = oneWindow ? oneWindow.parent : W;
callback.call(null, oneWindow, infoArray);
} catch (t) {
infoArray.push({
referrer: null,
location: null,
isTop: !1
});
return infoArray;
}
} while (oneWindow !== W.top);
return infoArray;
}
var allInfo = f1(function (oneWindow, infoArray) {
try {
infoArray.push({ referrer: oneWindow.document.referrer || null, location: oneWindow.location.href || null, isTop: oneWindow === W.top });
} catch (e) {
infoArray.push({ referrer: null, location: null, isTop: oneWindow === W.top });
}
});
f2(function (n, r) {
allInfo[r].ancestor = n;
});
for (var t = '', e = !1, i = allInfo.length - 1, l = allInfo.length - 1; l >= 0; l--) {
t = allInfo[l].location;
if (!t && l > 0) {
t = allInfo[l - 1].referrer;
if (!t) t = allInfo[l - 1].ancestor;
if (t) {
e = W.location.ancestorOrigins ? !0 : l === allInfo.length - 1 && allInfo[allInfo.length - 1].isTop;
break;
}
}
} return { url: t, isTop: e, depth: i };
},

srTestCapabilities: function srTestCapabilities() {
var plugins = navigator.plugins,
flashVer = -1,
sf = 'Shockwave Flash';

if (plugins && plugins.length > 0) {
if (plugins[sf + ' 2.0'] || plugins[sf]) {
var swVer2 = plugins[sf + ' 2.0'] ? ' 2.0' : '';
var flashDescription = plugins[sf + swVer2].description;
flashVer = flashDescription.split(' ')[2].split('.')[0];
}
}
if (flashVer > 4) return 15; else return 7;
}

};
}());

// Ad calling functionality
CMT.Service = (function () {
// Closure variables shared by the service functions
var U = CMT.Util;

return {

buildSrc: function buildSrc(tag, env) {
var src = (document.location.protocol === 'https:' ? 'https://' : 'http://') + (!env || env === 'PRD' ? '' : env === 'PPE' ? 'ppe-' : env === 'STG' ? 'staging-' : '') + 'ad.afy11.net/ad' + '?mode=11' + '&ct=' + U.srTestCapabilities() + '&nif=0' + '&sf=0' + '&sfd=0' + '&ynw=0' + '&rand=' + U.getRand() + '&hb=1' + '&rk1=' + U.getRand() + '&rk2=' + new Date().valueOf() / 1000;

// Make sure we don't have a response object...
delete tag.response;

// Extracted url info...
var urlInfo = U.extractUrlInfo();
tag.pageUrl = urlInfo.url;
tag.puTop = urlInfo.isTop;

// Attach the serialized tag to our string
src += '&ab=' + U.encodeUriObject(tag);

return src;
},

getAd: function (zoneKey) {
if (!zoneKey) return;

return this._getData(zoneKey, 'ad') + (this._getResponse().tpCookieSync || ''); // ...also append cookie sync if present
},

// getSecondPrice: function getSecondPrice(zoneKey) {
// if (zoneKey.substring(0, 2) !== 'as') zoneKey = 'as' + zoneKey;
// var bid = this.getBidTrue(zoneKey),
// floor = this._getData(zoneKey, 'floor');

// // If no floor, just set it to 80% of the bid
// if (!floor) floor = bid * 0.80;

// // Adjust the floor if it's too high...it needs to always be lower
// if (floor >= bid) {
// floor = floor * 0.80; // Take off 20% to account for possible non-adjusted 2nd highest bid

// // If it's still too high, just take 80% to 90% of the bid
// if (floor >= bid) floor = bid * ((Math.random() * 10) + 80) / 100;
// }
// return Math.round(floor * 100) / 100;
// },

// setAuctionPrice: function setAuctionPrice(ad, bid) {
// return ad ? ad.replace('${AUCTION_PRICE}', bid) : ad;
// },
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this all required at some point?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have a partner that utilizes the commented-out code. We'd rather keep it in there for completeness' sake. But if y'all would prefer it be removed, we'll be happy to do so. Please let me know. Thanks!


getBidTrue: function getBidTrue(zoneKey) {
return Math.round(this._getData(zoneKey, 'price') * 100) / 100;
},

_getData: function (zoneKey, field) {
var response = this._getResponse(),
zoneResponseData = response.zones ? response.zones[zoneKey] : {};

return (zoneResponseData || {})[field] || null;
},

_getResponse: function () {
var tag = W.cdsTag;
return (tag && tag.response) ? tag.response : {};
},
};
}());

return CMT;
}

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

module.exports = CoxAdapter;
121 changes: 121 additions & 0 deletions test/spec/adapters/cox_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import Adapter from 'src/adapters/cox';
import bidManager from 'src/bidmanager';
import adLoader from 'src/adloader';
import utils from 'src/utils';
import {expect} from 'chai';

describe('CoxAdapter', () => {
let adapter;
let loadScriptStub;
let addBidResponseSpy;

let emitScript = (script) => {
let node = document.createElement('script');
node.type = 'text/javascript';
node.appendChild(document.createTextNode(script));
document.getElementsByTagName('head')[0].appendChild(node);
};

beforeEach(() => {
adapter = new Adapter();
addBidResponseSpy = sinon.spy(bidManager, 'addBidResponse');
});

afterEach(() => {
loadScriptStub.restore();
addBidResponseSpy.restore();
});

describe('response handling', () => {
const normalResponse = 'cdsTag.__callback__({"zones":{"as2000005991707":{"ad" : "<h1>FOO<\/h1>","uid" : "","price" : 1.51,"floor" : 0,}},"tpCookieSync":"<h1>FOOKIE<\/h1>"})';
const zeroPriceResponse = 'cdsTag.__callback__({"zones":{"as2000005991707":{"ad" : "<h1>DEFAULT FOO<\/h1>","uid" : "","price" : 0,"floor" : 0,}},"tpCookieSync":"<h1>FOOKIE<\/h1>"})';
const incompleteResponse = 'cdsTag.__callback__({"zones":{},"tpCookieSync":"<h1>FOOKIE<\/h1>"})';

const oneBidConfig = {
bidderCode: 'cox',
bids: [{
bidder: 'cox',
placementCode: 'FOO456789',
sizes: [300, 250],
params: { size: '300x250', id: 2000005991707, siteId: 2000100948180, env: 'PROD' },
}]
};

// ===== 1
it('should provide a correctly populated Bid given a valid response', () => {
loadScriptStub = sinon.stub(adLoader, 'loadScript', () => { emitScript(normalResponse); })

adapter.callBids(oneBidConfig);

let bid = addBidResponseSpy.args[0][1];
expect(bid.cpm).to.equal(1.51);
expect(bid.ad).to.be.a('string');
expect(bid.bidderCode).to.equal('cox');
});

// ===== 2
it('should provide an empty Bid given a zero-price response', () => {
loadScriptStub = sinon.stub(adLoader, 'loadScript', () => { emitScript(zeroPriceResponse); })

adapter.callBids(oneBidConfig);

let bid = addBidResponseSpy.args[0][1];
expect(bid.cpm).to.not.be.ok
expect(bid.ad).to.not.be.ok;
});

// ===== 3
it('should provide an empty Bid given an incomplete response', () => {
loadScriptStub = sinon.stub(adLoader, 'loadScript', () => { emitScript(incompleteResponse); })

adapter.callBids(oneBidConfig);

let bid = addBidResponseSpy.args[0][1];
expect(bid.cpm).to.not.be.ok
expect(bid.ad).to.not.be.ok;
});

// ===== 4
it('should not provide a Bid given no response', () => {
loadScriptStub = sinon.stub(adLoader, 'loadScript', () => { emitScript(''); });

adapter.callBids(oneBidConfig);

expect(addBidResponseSpy.callCount).to.equal(0);
});
});

describe('request generation', () => {
const missingBidsConfig = {
bidderCode: 'cox',
bids: null,
};
const missingParamsConfig = {
bidderCode: 'cox',
bids: [{
bidder: 'cox',
placementCode: 'FOO456789',
sizes: [300, 250],
params: null,
}]
};

// ===== 5
it('should not make an ad call given missing bids in config', () => {
loadScriptStub = sinon.stub(adLoader, 'loadScript');

adapter.callBids(missingBidsConfig);

expect(loadScriptStub.callCount).to.equal(0);
});

// ===== 6
it('should not make an ad call given missing params in config', () => {
loadScriptStub = sinon.stub(adLoader, 'loadScript');

adapter.callBids(missingParamsConfig);

expect(loadScriptStub.callCount).to.equal(0);
});
});
});