From 594ed4de2d64b839f1dd7aff0adf3123d34c0c84 Mon Sep 17 00:00:00 2001 From: reynold-cox Date: Mon, 23 Apr 2018 09:24:13 -0700 Subject: [PATCH 1/4] Bid adapter for 1.0+ --- modules/coxBidAdapter.js | 156 +++++++++++++++++++++++++++++++++++++++ modules/coxBidAdapter.md | 41 ++++++++++ 2 files changed, 197 insertions(+) create mode 100644 modules/coxBidAdapter.js create mode 100644 modules/coxBidAdapter.md diff --git a/modules/coxBidAdapter.js b/modules/coxBidAdapter.js new file mode 100644 index 00000000000..4a2abe384d2 --- /dev/null +++ b/modules/coxBidAdapter.js @@ -0,0 +1,156 @@ +'use strict'; + +import * as utils from 'src/utils'; +import { BANNER } from 'src/mediaTypes'; +import { config } from 'src/config'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const helper = (() => { + let srTestCapabilities = () => { // Legacy + let plugins = navigator.plugins; + let flashVer = -1; + let 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; + }; + + let getRand = () => { + return Math.round(Math.random() * 100000000); + }; + + // State variables + let env = ''; + let tag = {}; + let placementMap = {}; + + return { + ingest: function(rawBids = []) { + const adZoneAttributeKeys = ['id', 'size', 'thirdPartyClickUrl', 'dealId']; + const otherKeys = ['siteId', 'wrapper', 'referrerUrl']; + + rawBids.forEach(oneBid => { + let params = oneBid.params || {}; + + tag.auctionId = oneBid.auctionId; + tag.responseJSON = true; + + if (params.id && (/^\d+x\d+$/).test(params.size)) { + let adZoneKey = 'as' + params.id; + let zone = {}; + + zone.transactionId = oneBid.transactionId; + zone.bidId = oneBid.bidId; + tag.zones = tag.zones || {}; + tag.zones[adZoneKey] = zone; + + adZoneAttributeKeys.forEach(key => { if (params[key]) zone[key] = params[key]; }); + otherKeys.forEach(key => { if (params[key]) tag[key] = params[key]; }); + + // Check for an environment setting + if (params.env) env = params.env; + + // Update the placement map + let [x, y] = (params.size).split('x'); + placementMap[adZoneKey] = { + 'b': oneBid.bidId, + 'w': x, + 'h': y + }; + } + }); + }, + + transform: function(coxRawBids = {}) { + const pbjsBids = []; + + for (let adZoneKey in placementMap) { + let responded = coxRawBids[adZoneKey] + let ingested = placementMap[adZoneKey]; + + utils.logInfo('coxBidAdapter.transform', adZoneKey, responded, ingested); + + if (ingested && responded && responded['ad'] && responded['price'] > 0) { + pbjsBids.push({ + requestId: ingested['b'], + cpm: responded['price'], + width: ingested['w'], + height: ingested['h'], + creativeId: responded['adid'], + dealId: responded['dealid'], + currency: 'USD', + netRevenue: true, + ttl: 300, + ad: responded['ad'] + }); + } + } + return pbjsBids; + }, + + getUrl: function() { + // Bounce if the tag is invalid + if (!tag.zones) return null; + + let src = (document.location.protocol === 'https:' ? 'https://' : 'http://') + (!env || env === 'PRD' ? '' : env === 'PPE' ? 'ppe-' : env === 'STG' ? 'staging-' : '') + 'ad.afy11.net/ad' + '?mode=11' + '&ct=' + srTestCapabilities() + '&nif=0' + '&sf=0' + '&sfd=0' + '&ynw=0' + '&rand=' + getRand() + '&hb=1' + '&rk1=' + getRand() + '&rk2=' + new Date().valueOf() / 1000; + + tag.pageUrl = config.getConfig('pageUrl') || utils.getTopWindowUrl(); + tag.puTop = true; + + // Attach the serialized tag to our string + src += '&ab=' + encodeURIComponent(JSON.stringify(tag)); + + return src; + }, + + resetState: function() { + env = ''; + tag = {}; + placementMap = {}; + } + }; +})(); + +export const spec = { + code: 'cox', + supportedMediaTypes: [BANNER], + + isBidRequestValid: function(bid) { + return !!(bid.params && bid.params.id && bid.params.size); + }, + + buildRequests: function(validBidReqs) { + helper.resetState(); + helper.ingest(validBidReqs); + let url = helper.getUrl(); + + return !url ? {} : { + method: 'GET', + url: url + }; + }, + + interpretResponse: function({ body: { zones: coxRawBids } }) { + let bids = helper.transform(coxRawBids); + + utils.logInfo('coxBidAdapter.interpretResponse', bids); + return bids; + }, + + getUserSyncs: function(syncOptions, [{ body: { tpCookieSync: urls = [] } }]) { + let syncs = []; + if (syncOptions.pixelEnabled && urls.length > 0) { + syncs = urls.map((url) => ({ type: 'image', url: url })) + } + utils.logInfo('coxBidAdapter.getuserSyncs', syncs); + return syncs; + } +}; + +registerBidder(spec); diff --git a/modules/coxBidAdapter.md b/modules/coxBidAdapter.md new file mode 100644 index 00000000000..f4460b969ed --- /dev/null +++ b/modules/coxBidAdapter.md @@ -0,0 +1,41 @@ +# Overview + +``` +Module Name: Cox/COMET Bid Adapter +Module Type: Bidder Adapter +Maintainer: reynold@coxds.com +``` + +# Description + +Cox/COMET's adapter integration to the Prebid library. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'test-leaderboard', + sizes: [[728, 90]], + bids: [{ + bidder: 'cox', + params: { + size: '728x90', + id: 2000005991607, + siteId: 2000100948180, + } + }] + }, { + code: 'test-banner', + sizes: [[300, 250]], + bids: [{ + bidder: 'cox', + params: { + size: '300x250', + id: 2000005991707, + siteId: 2000100948180, + } + }] + } +] +``` \ No newline at end of file From da4c87964108ca4e8743e13f084eb1ff1fe5bb4d Mon Sep 17 00:00:00 2001 From: reynold-cox Date: Mon, 23 Apr 2018 09:27:02 -0700 Subject: [PATCH 2/4] Tests for cox bid adapter --- test/spec/modules/coxBidAdapter_spec.js | 189 ++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 test/spec/modules/coxBidAdapter_spec.js diff --git a/test/spec/modules/coxBidAdapter_spec.js b/test/spec/modules/coxBidAdapter_spec.js new file mode 100644 index 00000000000..5937f24bb3a --- /dev/null +++ b/test/spec/modules/coxBidAdapter_spec.js @@ -0,0 +1,189 @@ +import { expect } from 'chai'; +import { spec } from 'modules/coxBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; +import { deepClone } from 'src/utils'; + +describe('CoxBidAdapter', () => { + const adapter = newBidder(spec); + + describe('isBidRequestValid', () => { + const CONFIG = { + 'bidder': 'cox', + 'params': { + 'id': '8888', + 'siteId': '1000', + 'size': '300x250' + } + }; + + it('should return true when required params present', () => { + expect(spec.isBidRequestValid(CONFIG)).to.equal(true); + }); + + it('should return false when id param is missing', () => { + let config = deepClone(CONFIG); + config.params.id = null; + + expect(spec.isBidRequestValid(config)).to.equal(false); + }); + + it('should return false when size param is missing', () => { + let config = deepClone(CONFIG); + config.params.size = null; + + expect(spec.isBidRequestValid(config)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + const PROD_DOMAIN = 'ad.afy11.net'; + const PPE_DOMAIN = 'ppe-ad.afy11.net'; + const STG_DOMAIN = 'staging-ad.afy11.net'; + + const BID_INFO = [{ + 'bidder': 'cox', + 'params': { + 'id': '8888', + 'siteId': '1000', + 'size': '300x250' + }, + 'sizes': [[300, 250]], + 'transactionId': 'tId-foo', + 'bidId': 'bId-bar' + }]; + + it('should send bid request to PROD_DOMAIN via GET', () => { + let request = spec.buildRequests(BID_INFO); + expect(request.url).to.have.string(PROD_DOMAIN); + expect(request.method).to.equal('GET'); + }); + + it('should send bid request to PPE_DOMAIN when configured', () => { + let clone = deepClone(BID_INFO); + clone[0].params.env = 'PPE'; + + let request = spec.buildRequests(clone); + expect(request.url).to.have.string(PPE_DOMAIN); + }); + + it('should send bid request to STG_DOMAIN when configured', () => { + let clone = deepClone(BID_INFO); + clone[0].params.env = 'STG'; + + let request = spec.buildRequests(clone); + expect(request.url).to.have.string(STG_DOMAIN); + }); + + it('should return empty when id is invalid', () => { + let clone = deepClone(BID_INFO); + clone[0].params.id = null; + + let request = spec.buildRequests(clone); + expect(request).to.be.an('object').that.is.empty; + }); + + it('should return empty when size is invalid', () => { + let clone = deepClone(BID_INFO); + clone[0].params.size = 'FOO'; + + let request = spec.buildRequests(clone); + expect(request).to.be.an('object').that.is.empty; + }); + }) + + describe('interpretResponse', () => { + const BID_INFO = [{ + 'bidder': 'cox', + 'params': { + 'id': '2000005657007', + 'siteId': '2000101880180', + 'size': '728x90' + }, + 'transactionId': 'tId-foo', + 'bidId': 'bId-a-bar' + }]; + + const RESPONSE = { body: { + 'zones': { + 'as2000005657007': { + 'price': 1.88, + 'dealid': 'AA128460', + 'ad': '

2000005657007
728x90

', + 'adid': '7007-728-90' + }}}}; + + it('should return correct pbjs bid', () => { + let expectedBid = { + 'requestId': 'bId-a-bar', + 'cpm': 1.88, + 'width': '728', + 'height': '90', + 'creativeId': '7007-728-90', + 'dealId': 'AA128460', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'ad': '

2000005657007
728x90

' + }; + let hokey = spec.buildRequests(BID_INFO); + + let result = spec.interpretResponse(RESPONSE); + expect(result[0]).to.eql(expectedBid); + }); + + it('should return empty when price is zero', () => { + let clone = deepClone(RESPONSE); + clone.body.zones.as2000005657007.price = 0; + let hokey = spec.buildRequests(BID_INFO); + + let result = spec.interpretResponse(clone); + expect(result).to.be.an('array').that.is.empty; + }); + + it('should return empty when there is no ad', () => { + let clone = deepClone(RESPONSE); + clone.body.zones.as2000005657007.ad = null; + let hokey = spec.buildRequests(BID_INFO); + + let result = spec.interpretResponse(clone); + expect(result).to.be.an('array').that.is.empty; + }); + + it('should return empty when there is no ad unit info', () => { + let clone = deepClone(RESPONSE); + delete (clone.body.zones.as2000005657007); + let hokey = spec.buildRequests(BID_INFO); + + let result = spec.interpretResponse(clone); + expect(result).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', () => { + const RESPONSE = [{ body: { + 'zones': {}, + 'tpCookieSync': ['http://pixel.foo.com/', 'http://pixel.bar.com/'] + }}]; + + it('should return correct pbjs syncs when pixels are enabled', () => { + let syncs = spec.getUserSyncs({ pixelEnabled: true }, RESPONSE); + + expect(syncs.map(x => x.type)).to.eql(['image', 'image']); + expect(syncs.map(x => x.url)).to.have.members(['http://pixel.bar.com/', 'http://pixel.foo.com/']); + }); + + it('should return empty when pixels are not enabled', () => { + let syncs = spec.getUserSyncs({ pixelEnabled: false }, RESPONSE); + + expect(syncs).to.be.an('array').that.is.empty; + }); + + it('should return empty when response has no sync data', () => { + let clone = deepClone(RESPONSE); + delete (clone[0].body.tpCookieSync); + + let syncs = spec.getUserSyncs({ pixelEnabled: true }, clone); + expect(syncs).to.be.an('array').that.is.empty; + }); + }); +}); From 125d5d781af45332485401824be9d79cd2889fd7 Mon Sep 17 00:00:00 2001 From: reynold-cox Date: Wed, 23 May 2018 12:11:18 -0700 Subject: [PATCH 3/4] Corrected how state is handled --- modules/coxBidAdapter.js | 75 ++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/modules/coxBidAdapter.js b/modules/coxBidAdapter.js index 4a2abe384d2..eac1b2081d2 100644 --- a/modules/coxBidAdapter.js +++ b/modules/coxBidAdapter.js @@ -25,21 +25,17 @@ const helper = (() => { return Math.round(Math.random() * 100000000); }; - // State variables - let env = ''; - let tag = {}; - let placementMap = {}; - return { ingest: function(rawBids = []) { const adZoneAttributeKeys = ['id', 'size', 'thirdPartyClickUrl', 'dealId']; const otherKeys = ['siteId', 'wrapper', 'referrerUrl']; + let state = this.createState(); rawBids.forEach(oneBid => { let params = oneBid.params || {}; - tag.auctionId = oneBid.auctionId; - tag.responseJSON = true; + state.tag.auctionId = oneBid.auctionId; + state.tag.responseJSON = true; if (params.id && (/^\d+x\d+$/).test(params.size)) { let adZoneKey = 'as' + params.id; @@ -47,32 +43,33 @@ const helper = (() => { zone.transactionId = oneBid.transactionId; zone.bidId = oneBid.bidId; - tag.zones = tag.zones || {}; - tag.zones[adZoneKey] = zone; + state.tag.zones = state.tag.zones || {}; + state.tag.zones[adZoneKey] = zone; adZoneAttributeKeys.forEach(key => { if (params[key]) zone[key] = params[key]; }); - otherKeys.forEach(key => { if (params[key]) tag[key] = params[key]; }); + otherKeys.forEach(key => { if (params[key]) state.tag[key] = params[key]; }); // Check for an environment setting - if (params.env) env = params.env; + if (params.env) state.env = params.env; // Update the placement map let [x, y] = (params.size).split('x'); - placementMap[adZoneKey] = { + state.placementMap[adZoneKey] = { 'b': oneBid.bidId, 'w': x, 'h': y }; } }); + return state; }, - transform: function(coxRawBids = {}) { + transform: function(coxRawBids = {}, state) { const pbjsBids = []; - for (let adZoneKey in placementMap) { + for (let adZoneKey in state.placementMap) { let responded = coxRawBids[adZoneKey] - let ingested = placementMap[adZoneKey]; + let ingested = state.placementMap[adZoneKey]; utils.logInfo('coxBidAdapter.transform', adZoneKey, responded, ingested); @@ -94,26 +91,32 @@ const helper = (() => { return pbjsBids; }, - getUrl: function() { + getUrl: state => { // Bounce if the tag is invalid - if (!tag.zones) return null; + if (!state.tag.zones) return null; - let src = (document.location.protocol === 'https:' ? 'https://' : 'http://') + (!env || env === 'PRD' ? '' : env === 'PPE' ? 'ppe-' : env === 'STG' ? 'staging-' : '') + 'ad.afy11.net/ad' + '?mode=11' + '&ct=' + srTestCapabilities() + '&nif=0' + '&sf=0' + '&sfd=0' + '&ynw=0' + '&rand=' + getRand() + '&hb=1' + '&rk1=' + getRand() + '&rk2=' + new Date().valueOf() / 1000; + let src = (document.location.protocol === 'https:' ? 'https://' : 'http://') + + (!state.env || state.env === 'PRD' ? '' : state.env === 'PPE' ? 'ppe-' : state.env === 'STG' ? 'staging-' : '') + + 'ad.afy11.net/ad?mode=11&nif=0&sf=0&sfd=0&ynw=0&hb=1' + + '&ct=' + srTestCapabilities() + + '&rand=' + getRand() + + '&rk1=' + getRand() + + '&rk2=' + new Date().valueOf() / 1000; - tag.pageUrl = config.getConfig('pageUrl') || utils.getTopWindowUrl(); - tag.puTop = true; + state.tag.pageUrl = config.getConfig('pageUrl') || utils.getTopWindowUrl(); + state.tag.puTop = true; // Attach the serialized tag to our string - src += '&ab=' + encodeURIComponent(JSON.stringify(tag)); + src += '&ab=' + encodeURIComponent(JSON.stringify(state.tag)); return src; }, - resetState: function() { - env = ''; - tag = {}; - placementMap = {}; - } + createState: () => ({ + env: '', + tag: {}, + placementMap: {} + }) }; })(); @@ -126,24 +129,30 @@ export const spec = { }, buildRequests: function(validBidReqs) { - helper.resetState(); - helper.ingest(validBidReqs); - let url = helper.getUrl(); + let state = helper.ingest(validBidReqs); + let url = helper.getUrl(state); return !url ? {} : { method: 'GET', - url: url + url: url, + state }; }, - interpretResponse: function({ body: { zones: coxRawBids } }) { - let bids = helper.transform(coxRawBids); + interpretResponse: function({ body: { zones: coxRawBids } }, { state }) { + let bids = helper.transform(coxRawBids, state); utils.logInfo('coxBidAdapter.interpretResponse', bids); return bids; }, - getUserSyncs: function(syncOptions, [{ body: { tpCookieSync: urls = [] } }]) { + getUserSyncs: function(syncOptions, thing) { + try { + var [{ body: { tpCookieSync: urls = [] } }] = thing; + } catch (ignore) { + return []; + } + let syncs = []; if (syncOptions.pixelEnabled && urls.length > 0) { syncs = urls.map((url) => ({ type: 'image', url: url })) From 0cc09d0d2acacd235b61050d08343bef1cd12bd1 Mon Sep 17 00:00:00 2001 From: reynold-cox Date: Wed, 23 May 2018 12:14:31 -0700 Subject: [PATCH 4/4] Added test for multiple bids --- test/spec/modules/coxBidAdapter_spec.js | 102 +++++++++++++++++------- 1 file changed, 73 insertions(+), 29 deletions(-) diff --git a/test/spec/modules/coxBidAdapter_spec.js b/test/spec/modules/coxBidAdapter_spec.js index 5937f24bb3a..9dd5a5a92b4 100644 --- a/test/spec/modules/coxBidAdapter_spec.js +++ b/test/spec/modules/coxBidAdapter_spec.js @@ -92,18 +92,29 @@ describe('CoxBidAdapter', () => { }) describe('interpretResponse', () => { - const BID_INFO = [{ + const BID_INFO_1 = [{ 'bidder': 'cox', 'params': { 'id': '2000005657007', 'siteId': '2000101880180', 'size': '728x90' }, - 'transactionId': 'tId-foo', - 'bidId': 'bId-a-bar' + 'transactionId': 'foo_1', + 'bidId': 'bar_1' }]; - const RESPONSE = { body: { + const BID_INFO_2 = [{ + 'bidder': 'cox', + 'params': { + 'id': '2000005658887', + 'siteId': '2000101880180', + 'size': '300x250' + }, + 'transactionId': 'foo_2', + 'bidId': 'bar_2' + }]; + + const RESPONSE_1 = { body: { 'zones': { 'as2000005657007': { 'price': 1.88, @@ -112,49 +123,77 @@ describe('CoxBidAdapter', () => { 'adid': '7007-728-90' }}}}; + const RESPONSE_2 = { body: { + 'zones': { + 'as2000005658887': { + 'price': 2.88, + 'ad': '

2000005658887
300x250

', + 'adid': '888-88' + }}}}; + + const PBJS_BID_1 = { + 'requestId': 'bar_1', + 'cpm': 1.88, + 'width': '728', + 'height': '90', + 'creativeId': '7007-728-90', + 'dealId': 'AA128460', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'ad': '

2000005657007
728x90

' + }; + + const PBJS_BID_2 = { + 'requestId': 'bar_2', + 'cpm': 2.88, + 'width': '300', + 'height': '250', + 'creativeId': '888-88', + 'dealId': undefined, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'ad': '

2000005658887
300x250

' + }; + it('should return correct pbjs bid', () => { - let expectedBid = { - 'requestId': 'bId-a-bar', - 'cpm': 1.88, - 'width': '728', - 'height': '90', - 'creativeId': '7007-728-90', - 'dealId': 'AA128460', - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 300, - 'ad': '

2000005657007
728x90

' - }; - let hokey = spec.buildRequests(BID_INFO); - - let result = spec.interpretResponse(RESPONSE); - expect(result[0]).to.eql(expectedBid); + let result = spec.interpretResponse(RESPONSE_2, spec.buildRequests(BID_INFO_2)); + expect(result[0]).to.eql(PBJS_BID_2); + }); + + it('should handle multiple bid instances', () => { + let request1 = spec.buildRequests(BID_INFO_1); + let request2 = spec.buildRequests(BID_INFO_2); + + let result2 = spec.interpretResponse(RESPONSE_2, request2); + expect(result2[0]).to.eql(PBJS_BID_2); + + let result1 = spec.interpretResponse(RESPONSE_1, request1); + expect(result1[0]).to.eql(PBJS_BID_1); }); it('should return empty when price is zero', () => { - let clone = deepClone(RESPONSE); + let clone = deepClone(RESPONSE_1); clone.body.zones.as2000005657007.price = 0; - let hokey = spec.buildRequests(BID_INFO); - let result = spec.interpretResponse(clone); + let result = spec.interpretResponse(clone, spec.buildRequests(BID_INFO_1)); expect(result).to.be.an('array').that.is.empty; }); it('should return empty when there is no ad', () => { - let clone = deepClone(RESPONSE); + let clone = deepClone(RESPONSE_1); clone.body.zones.as2000005657007.ad = null; - let hokey = spec.buildRequests(BID_INFO); - let result = spec.interpretResponse(clone); + let result = spec.interpretResponse(clone, spec.buildRequests(BID_INFO_1)); expect(result).to.be.an('array').that.is.empty; }); it('should return empty when there is no ad unit info', () => { - let clone = deepClone(RESPONSE); + let clone = deepClone(RESPONSE_1); delete (clone.body.zones.as2000005657007); - let hokey = spec.buildRequests(BID_INFO); - let result = spec.interpretResponse(clone); + let result = spec.interpretResponse(clone, spec.buildRequests(BID_INFO_1)); expect(result).to.be.an('array').that.is.empty; }); }); @@ -185,5 +224,10 @@ describe('CoxBidAdapter', () => { let syncs = spec.getUserSyncs({ pixelEnabled: true }, clone); expect(syncs).to.be.an('array').that.is.empty; }); + + it('should return empty when response is empty', () => { + let syncs = spec.getUserSyncs({ pixelEnabled: true }, [{}]); + expect(syncs).to.be.an('array').that.is.empty; + }); }); });