diff --git a/adapters.json b/adapters.json index 6f165edaf9d..beb0ec44be9 100644 --- a/adapters.json +++ b/adapters.json @@ -30,6 +30,7 @@ "openx", "piximedia", "pubmatic", + "pubgears", "pulsepoint", "pulsepointLite", "rhythmone", diff --git a/src/adapters/pubgears.js b/src/adapters/pubgears.js new file mode 100644 index 00000000000..6d487826a75 --- /dev/null +++ b/src/adapters/pubgears.js @@ -0,0 +1,151 @@ +var bidfactory = require('../bidfactory.js'); +var bidmanager = require('../bidmanager.js'); +var consts = require('../constants.json'); +var utils = require('../utils.js'); +var d = document; +var SCRIPT = 'script'; +var PARAMS = 'params'; +var SIZES = 'sizes'; +var SIZE = 'size'; +var CPM = 'cpm'; +var AD = 'ad'; +var WIDTH = 'width'; +var HEIGHT = 'height'; +var PUB_ZONE = 'pub_zone'; +var GROSS_PRICE = 'gross_price'; +var RESOURCE = 'resource'; +var DETAIL = 'detail'; +var BIDDER_CODE_RESPONSE_KEY = 'bidderCode'; +var BIDDER_CODE = 'pubgears'; +var SCRIPT_ID = 'pg-header-tag'; +var ATTRIBUTE_PREFIX = 'data-bsm-'; +var SLOT_LIST_ATTRIBUTE = 'slot-list'; +var PUBLISHER_ATTRIBUTE = 'pub'; +var FLAG_ATTRIBUTE = 'flag'; +var PLACEMENT_CODE = 'placementCode'; +var BID_ID = 'bidId'; +var PUBLISHER_PARAM = 'publisherName'; +var PUB_ZONE_PARAM = 'pubZone'; +var BID_RECEIVED_EVENT_NAME = 'onBidResponse'; +var SLOT_READY_EVENT_NAME = 'onResourceComplete'; +var CREATIVE_TEMPLATE = decodeURIComponent("%3Cscript%3E%0A(function(define)%7B%0Adefine(function(a)%7B%0A%09var%20id%3D%20%22pg-ad-%22%20%2B%20Math.floor(Math.random()%20*%201e10)%2C%20d%3D%20document%0A%09d.write(\'%3Cdiv%20id%3D%22\'%2Bid%2B\'%22%3E%3C%2Fdiv%3E\')%0A%09a.push(%7B%0A%09%09pub%3A%20\'%25%25PUBLISHER_NAME%25%25\'%2C%0A%09%09pub_zone%3A%20\'%25%25PUB_ZONE%25%25\'%2C%0A%09%09sizes%3A%20%5B\'%25%25SIZE%25%25\'%5D%2C%0A%09%09flag%3A%20true%2C%0A%09%09container%3A%20d.getElementById(id)%2C%0A%09%7D)%3B%0A%7D)%7D)(function(f)%7Bvar%20key%3D\'uber_imps\'%2Ca%3Dthis%5Bkey%5D%3Dthis%5Bkey%5D%7C%7C%5B%5D%3Bf(a)%3B%7D)%3B%0A%3C%2Fscript%3E%0A%3Cscript%20src%3D%22%2F%2Fc.pubgears.com%2Ftags%2Fb%22%3E%3C%2Fscript%3E%0A"); +var TAG_URL = '//c.pubgears.com/tags/h'; +var publisher = ''; + +module.exports = PubGearsAdapter; + +function PubGearsAdapter() { + var proxy = null; + var pendingSlots = {}; + var initialized = false; + + this.callBids = callBids; + + function callBids(params) { + var bids = params[consts.JSON_MAPPING.PL_BIDS]; + var slots = bids.map(getSlotFromBidParam) ; + if(slots.length <= 0) + return; + publisher = bids[0][PARAMS][PUBLISHER_PARAM]; + + bids.forEach(function(bid) { + + var name = getSlotFromBidParam(bid); + pendingSlots[ name ] = bid; + }); + + proxy = proxy || getScript(SCRIPT_ID) || makeScript(slots, publisher, SCRIPT_ID, TAG_URL); + if(!initialized) + registerEventListeners(proxy); + initialized = true; + } + function loadScript(script) { + var anchor = (function(scripts) { + return scripts[ scripts.length - 1 ]; + })(d.getElementsByTagName(SCRIPT)); + + return anchor.parentNode.insertBefore(script, anchor); + } + function getSlotFromBidParam(bid) { + var size = getSize(bid); + var params = bid[PARAMS]; + var slotName = params[PUB_ZONE_PARAM]; + return [ slotName, size ].join('@'); + } + function getSlotFromResource(resource) { + var size = resource[SIZE]; + var key = [ resource[PUB_ZONE], size ].join('@'); + return key; + } + function getSize(bid) { + var sizes = bid[SIZES]; + var size = Array.isArray(sizes[0]) ? sizes[0] : sizes; + return size.join('x'); + } + function makeScript(slots, publisher, id, url) { + var script = d.createElement(SCRIPT); + script.src = url; + script.id = id ; + script.setAttribute(ATTRIBUTE_PREFIX + SLOT_LIST_ATTRIBUTE, slots.join(' ')); + script.setAttribute(ATTRIBUTE_PREFIX + FLAG_ATTRIBUTE, 'true'); + script.setAttribute(ATTRIBUTE_PREFIX + PUBLISHER_ATTRIBUTE, publisher); + + return loadScript(script); + } + function getScript(id) { + return d.getElementById(id); + } + function registerEventListeners(script) { + script.addEventListener(BID_RECEIVED_EVENT_NAME, onBid, true); + script.addEventListener(SLOT_READY_EVENT_NAME, onComplete, true); + } + function onBid(event) { + var data = event[DETAIL]; + var slotKey = getSlotFromResource(data[RESOURCE]); + var bidRequest = pendingSlots[slotKey]; + var adUnitCode = bidRequest[PLACEMENT_CODE]; + var bid = null; + + if(bidRequest) { + bid = buildResponse(data, bidRequest); + bidmanager.addBidResponse(adUnitCode, bid); + utils.logMessage('adding bid respoonse to "' + adUnitCode + '" for bid request "' + bidRequest[BID_ID] + '"'); + }else { + utils.logError('Cannot get placement id for slot "' + slotKey + '"'); + } + } + function buildResponse(eventData, bidRequest) { + var resource = eventData[RESOURCE]; + var dims = resource[SIZE].split('x'); + var price = Number(eventData[GROSS_PRICE]); + var status = isNaN(price) || price <= 0 ? 2 : 1; + + var response = bidfactory.createBid(status, bidRequest); + response[BIDDER_CODE_RESPONSE_KEY] = BIDDER_CODE; + + if(status !== 1) + return response; + + response[AD] = getCreative(resource); + + response[CPM] = price / 1e3 ; + response[WIDTH] = dims[0]; + response[HEIGHT] = dims[1]; + return response; + } + function getCreative(resource) { + var token = '%%'; + var creative = CREATIVE_TEMPLATE; + var replacementValues = { + publisher_name: publisher, + pub_zone: resource[PUB_ZONE], + size: resource[SIZE] + }; + return utils.replaceTokenInString(creative, replacementValues, token); + } + function onComplete(event) { + var data = event[DETAIL]; + var slotKey = getSlotFromResource(data[RESOURCE]); + delete pendingSlots[slotKey]; + } +} diff --git a/test/spec/adapters/pubgears_spec.js b/test/spec/adapters/pubgears_spec.js new file mode 100644 index 00000000000..329d74bb5b3 --- /dev/null +++ b/test/spec/adapters/pubgears_spec.js @@ -0,0 +1,298 @@ +import { expect } from 'chai'; +import Adapter from 'src/adapters/pubgears' +import bidmanager from 'src/bidmanager' + +describe('PubGearsAdapter', () => { + + var adapter, mockScript + , params = { + bids: [] + } + + beforeEach( () => { + adapter = new Adapter() + mockScript = document.createElement('script') + sinon.spy(mockScript, 'setAttribute') + } ) + + describe('request function', () => { + + beforeEach( () => { + sinon.spy(document, 'createElement') + }) + + afterEach( () => { + document.createElement.restore && document.createElement.restore() + var s = document.getElementById('pg-header-tag') + if(s) + s.parentNode.removeChild(s) + } ) + + it('has `#callBids()` method', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function') + }) + + it('requires bids to make script', () => { + + adapter.callBids({bids: []}) + expect(document.createElement.notCalled).to.be.ok + }) + + it('creates script when passed bids', () => { + + adapter.callBids({ + bidderCode: "pubgears", + bids: [ + { + bidder: "pubgears", + sizes: [ [300,250] ], + adUnitCode: "foo123/header-bid-tag", + params: { + publisherName: "integration", + pubZone: "testpub.com/combined" + } + } + ] + }) + + sinon.assert.calledWith(document.createElement, 'script') + }) + + it('should assign attributes to script', () => { + + adapter.callBids({ + bidderCode: "pubgears", + bids: [ + { + bidder: "pubgears", + sizes: [ [300,250] ], + adUnitCode: "foo123/header-bid-tag", + params: { + publisherName: "integration", + pubZone: "testpub.com/combined" + } + }, + { + bidder: "pubgears", + sizes: [ [160,600] ], + adUnitCode: "foo123/header-bid-tag", + params: { + publisherName: "integration", + pubZone: "testpub.com/combined" + } + } + ] + }) + var script = document.createElement.returnValues[0] + var slots = script.getAttribute('data-bsm-slot-list') + expect(slots).to.equal('testpub.com/combined@300x250 testpub.com/combined@160x600') + expect(script.getAttribute('data-bsm-flag')).to.equal('true') + expect(script.getAttribute('data-bsm-pub')).to.equal('integration') + expect(script.getAttribute('src')).to.equal('//c.pubgears.com/tags/h') + expect(script.id).to.equal('pg-header-tag') + }) + + it('should reuse existing script when called twice', () => { + + var params = { + bidderCode: "pubgears", + bids: [ + { + bidder: "pubgears", + sizes: [ [300,250] ], + adUnitCode: "foo123/header-bid-tag", + params: { + publisherName: "integration", + pubZone: "testpub.com/combined" + } + }, + { + bidder: "pubgears", + sizes: [ [160,600] ], + adUnitCode: "foo123/header-bid-tag", + params: { + publisherName: "integration", + pubZone: "testpub.com/combined" + } + } + ] + } + adapter.callBids(params) + expect(document.createElement.calledOnce).to.be.true + adapter.callBids(params) + expect(document.createElement.calledOnce).to.be.true + }) + + it('should register event listeners', () => { + + var script = document.createElement('script') + script.id = 'pg-header-tag' + var spy = sinon.spy(script, 'addEventListener') + document.body.appendChild(script) + var params = { + bidderCode: "pubgears", + bids: [ + { + bidder: "pubgears", + sizes: [ [300,250] ], + adUnitCode: "foo123/header-bid-tag", + params: { + publisherName: "integration", + pubZone: "testpub.com/combined" + } + }, + { + bidder: "pubgears", + sizes: [ [160,600] ], + adUnitCode: "foo123/header-bid-tag", + params: { + publisherName: "integration", + pubZone: "testpub.com/combined" + } + } + ] + } + adapter.callBids(params) + + expect(spy.calledWith('onBidResponse')).to.be.ok + expect(spy.calledWith('onResourceComplete')).to.be.ok + }) + }) + + describe('bids received', () => { + + beforeEach(() => { + sinon.spy(bidmanager, 'addBidResponse') + }) + + afterEach(() => { + bidmanager.addBidResponse.restore() + }) + + it('should call bidManager.addBidResponse() when bid received', () => { + + var options = { + bubbles: false, + cancelable: false, + detail: { + gross_price: 1000, + resource: { + position: 'atf', + pub_zone: 'testpub.com/combined', + size: '300x250' + } + } + } + + adapter.callBids({ + bidderCode: "pubgears", + bids: [ + { + bidder: "pubgears", + sizes: [ [300,250] ], + adUnitCode: "foo123/header-bid-tag", + params: { + publisherName: "integration", + pubZone: "testpub.com/combined" + } + }, + { + bidder: "pubgears", + sizes: [ [160,600] ], + adUnitCode: "foo123/header-bid-tag", + params: { + publisherName: "integration", + pubZone: "testpub.com/combined" + } + } + ] + + }) + var script = document.getElementById('pg-header-tag') + var event = new CustomEvent('onBidResponse', options) + script.dispatchEvent(event) + + expect(bidmanager.addBidResponse.calledOnce).to.be.ok + }) + + it('should send correct bid response object when receiving onBidResponse event', () => { + expect(bidmanager.addBidResponse.calledOnce).to.not.be.ok + var bid = { + bidder: "pubgears", + sizes: [ [300,250] ], + adUnitCode: "foo123/header-bid-tag", + params: { + publisherName: "integration", + pubZone: "testpub.com/combined" + } + } + + adapter.callBids({ + bidderCode: "pubgears", + bids: [ bid ] + }) + + var options = { + bubbles: false, + cancelable: false, + detail: { + gross_price: 1000, + resource: { + position: 'atf', + pub_zone: 'testpub.com/combined', + size: '300x250' + } + } + } + var script = document.getElementById('pg-header-tag') + var event = new CustomEvent('onBidResponse', options) + script.dispatchEvent(event) + + var args = bidmanager.addBidResponse.getCall(1).args + expect(args).to.have.length(2) + var bidResponse = args[1] + expect(bidResponse.ad).to.contain(bid.params.pubZone) + }) + + it('should send $0 bid as no-bid response', () => { + + var bid = { + bidder: "pubgears", + sizes: [ [300,250] ], + adUnitCode: "foo123/header-bid-tag", + params: { + publisherName: "integration", + pubZone: "testpub.com/combined" + } + } + + adapter.callBids({ + bidderCode: "pubgears", + bids: [ bid ] + }) + + var options = { + bubbles: false, + cancelable: false, + detail: { + gross_price: 0, + resource: { + position: 'atf', + pub_zone: 'testpub.com/combined', + size: '300x250' + } + } + } + var script = document.getElementById('pg-header-tag') + var event = new CustomEvent('onBidResponse', options) + + bidmanager.addBidResponse.reset() + script.dispatchEvent(event) + + var args = bidmanager.addBidResponse.getCall(1).args + var bidResponse = args[1] + expect(bidResponse).to.be.a('object') + expect(bidResponse.getStatusCode()).to.equal(2) + }) + }) +})