From 89c6300cfb69c081b4c356e5e8576d3bfc759520 Mon Sep 17 00:00:00 2001 From: reynold-cox Date: Tue, 13 Jun 2017 14:56:22 -0700 Subject: [PATCH] New Cox adapter (#1228) * Cox adapter (new) * Unit test for cox adapter (new) * Added cox entry * Styling for ESLint * Styling for ESLint --- adapters.json | 1 + src/adapters/cox.js | 255 +++++++++++++++++++++++++++++++++ test/spec/adapters/cox_spec.js | 121 ++++++++++++++++ 3 files changed, 377 insertions(+) create mode 100644 src/adapters/cox.js create mode 100644 test/spec/adapters/cox_spec.js diff --git a/adapters.json b/adapters.json index 691b0a55bc6..704bb0bd388 100644 --- a/adapters.json +++ b/adapters.json @@ -70,6 +70,7 @@ "trion", "prebidServer", "adsupply", + "cox", { "appnexus": { "alias": "brealtime" diff --git a/src/adapters/cox.js b/src/adapters/cox.js new file mode 100644 index 00000000000..e0e2a053251 --- /dev/null +++ b/src/adapters/cox.js @@ -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; + // } + + 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; + } 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; + // }, + + 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; diff --git a/test/spec/adapters/cox_spec.js b/test/spec/adapters/cox_spec.js new file mode 100644 index 00000000000..3f1342230db --- /dev/null +++ b/test/spec/adapters/cox_spec.js @@ -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" : "

FOO<\/h1>","uid" : "","price" : 1.51,"floor" : 0,}},"tpCookieSync":"

FOOKIE<\/h1>"})'; + const zeroPriceResponse = 'cdsTag.__callback__({"zones":{"as2000005991707":{"ad" : "

DEFAULT FOO<\/h1>","uid" : "","price" : 0,"floor" : 0,}},"tpCookieSync":"

FOOKIE<\/h1>"})'; + const incompleteResponse = 'cdsTag.__callback__({"zones":{},"tpCookieSync":"

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); + }); + }); +});