From 744c3471df1ee8c4f01b642ffd19ebc59f59c6a6 Mon Sep 17 00:00:00 2001 From: rizhang Date: Thu, 12 Jan 2017 11:06:46 -0800 Subject: [PATCH] Add Sharethrough adapter (#865) * Rz/submit to prebid (#4) Added Sharethrough Adapter * fix warnings * added beacons * made compatible with chrome 37. other minor changes * win beacon fired in analytics adapter * specs for new analytics adapter * add try catch blocks. misc refactor * removed test page * remove debugger * refactor analytics adapter * removed test endpoint * analytics url parameter is empty * removed bidwon listener on adapter * removed analytics from package.json --- adapters.json | 1 + .../analytics/sharethrough_analytics.js | 63 +++++++ src/adapters/sharethrough.js | 127 +++++++++++++ test/spec/adapters/sharethrough_spec.js | 178 ++++++++++++++++++ .../analytics/sharethrough_analytics_spec.js | 99 ++++++++++ 5 files changed, 468 insertions(+) create mode 100644 src/adapters/analytics/sharethrough_analytics.js create mode 100644 src/adapters/sharethrough.js create mode 100644 test/spec/adapters/sharethrough_spec.js create mode 100644 test/spec/unit/adapters/analytics/sharethrough_analytics_spec.js diff --git a/adapters.json b/adapters.json index 4750f3df7f48..6b0e885c6efb 100644 --- a/adapters.json +++ b/adapters.json @@ -39,6 +39,7 @@ "underdogmedia", "memeglobal", "centro", + "sharethrough", "roxot", "vertoz", "widespace", diff --git a/src/adapters/analytics/sharethrough_analytics.js b/src/adapters/analytics/sharethrough_analytics.js new file mode 100644 index 000000000000..6b4f92973462 --- /dev/null +++ b/src/adapters/analytics/sharethrough_analytics.js @@ -0,0 +1,63 @@ +import adapter from 'AnalyticsAdapter'; +const utils = require('../../utils'); + +const emptyUrl = ''; +const analyticsType = 'endpoint'; +const STR_BIDDER_CODE = "sharethrough"; +const STR_VERSION = "0.1.0"; + +export default utils.extend(adapter( + { + emptyUrl, + analyticsType + } +), + { + STR_BEACON_HOST: document.location.protocol + "//b.sharethrough.com/butler?", + placementCodeSet: {}, + + track({ eventType, args }) { + if(eventType === 'bidRequested' && args.bidderCode === 'sharethrough') { + var bids = args.bids; + var keys = Object.keys(bids); + for(var i = 0; i < keys.length; i++) { + this.placementCodeSet[bids[keys[i]].placementCode] = args.bids[keys[i]]; + } + } + + if(eventType === 'bidWon') { + this.bidWon(args); + } + }, + + bidWon(args) { + const curBidderCode = args.bidderCode; + + if(curBidderCode !== STR_BIDDER_CODE && (args.adUnitCode in this.placementCodeSet)) { + let strBid = this.placementCodeSet[args.adUnitCode]; + this.fireLoseBeacon(curBidderCode, args.cpm, strBid.adserverRequestId, "headerBidLose"); + } + }, + + fireLoseBeacon(winningBidderCode, winningCPM, arid, type) { + let loseBeaconUrl = this.STR_BEACON_HOST; + loseBeaconUrl = utils.tryAppendQueryString(loseBeaconUrl, "winnerBidderCode", winningBidderCode); + loseBeaconUrl = utils.tryAppendQueryString(loseBeaconUrl, "winnerCpm", winningCPM); + loseBeaconUrl = utils.tryAppendQueryString(loseBeaconUrl, "arid", arid); + loseBeaconUrl = utils.tryAppendQueryString(loseBeaconUrl, "type", type); + loseBeaconUrl = this.appendEnvFields(loseBeaconUrl); + + this.fireBeacon(loseBeaconUrl); + }, + appendEnvFields(url) { + url = utils.tryAppendQueryString(url, 'hbVersion', '$prebid.version$'); + url = utils.tryAppendQueryString(url, 'strVersion', STR_VERSION); + url = utils.tryAppendQueryString(url, 'hbSource', 'prebid'); + + return url; + }, + fireBeacon(theUrl) { + const img = new Image(); + img.src = theUrl; + } +}); diff --git a/src/adapters/sharethrough.js b/src/adapters/sharethrough.js new file mode 100644 index 000000000000..2732d7144791 --- /dev/null +++ b/src/adapters/sharethrough.js @@ -0,0 +1,127 @@ +var utils = require('../utils.js'); +var bidmanager = require('../bidmanager.js'); +var bidfactory = require('../bidfactory.js'); + +const STR_BIDDER_CODE = "sharethrough"; +const STR_VERSION = "0.1.0"; //Need to check analytics too for version + +var SharethroughAdapter = function SharethroughAdapter() { + + const str = {}; + str.STR_BTLR_HOST = document.location.protocol + "//btlr.sharethrough.com"; + str.STR_BEACON_HOST = document.location.protocol + "//b.sharethrough.com/butler?"; + str.placementCodeSet = {}; + + function _callBids(params) { + const bids = params.bids; + + addEventListener("message", _receiveMessage, false); + + // cycle through bids + for (let i = 0; i < bids.length; i += 1) { + const bidRequest = bids[i]; + str.placementCodeSet[bidRequest.placementCode] = bidRequest; + const scriptUrl = _buildSharethroughCall(bidRequest); + str.loadIFrame(scriptUrl); + } + } + + function _buildSharethroughCall(bid) { + const testPkey = 'test'; + const pkey = utils.getBidIdParameter('pkey', bid.params); + + let host = str.STR_BTLR_HOST; + + let url = host + "/header-bid/v1?"; + url = utils.tryAppendQueryString(url, 'bidId', bid.bidId); + + if(pkey !== testPkey) { + url = utils.tryAppendQueryString(url, 'placement_key', pkey); + url = utils.tryAppendQueryString(url, 'ijson', '$$PREBID_GLOBAL$$.strcallback'); + url = appendEnvFields(url); + } else { + url = url.substring(0, url.length - 1); + } + + return url; + } + + str.loadIFrame = function(url) { + const iframe = document.createElement("iframe"); + iframe.src = url; + iframe.style.cssText = 'display:none;'; + + document.body.appendChild(iframe); + }; + + function _receiveMessage(event) { + if(event.origin === str.STR_BTLR_HOST) { + try { + $$PREBID_GLOBAL$$.strcallback(JSON.parse(event.data).response); + } catch(e) { + console.log(e); + } + } + } + + $$PREBID_GLOBAL$$.strcallback = function(bidResponse) { + const bidId = bidResponse.bidId; + const bidObj = utils.getBidRequest(bidId); + try { + const bid = bidfactory.createBid(1, bidObj); + bid.bidderCode = STR_BIDDER_CODE; + bid.cpm = bidResponse.creatives[0].cpm; + const size = bidObj.sizes[0]; + bid.width = size[0]; + bid.height = size[1]; + bid.adserverRequestId = bidResponse.adserverRequestId; + str.placementCodeSet[bidObj.placementCode].adserverRequestId = bidResponse.adserverRequestId; + + bid.pkey = utils.getBidIdParameter('pkey', bidObj.params); + + const windowLocation = `str_response_${bidId}`; + const bidJsonString = JSON.stringify(bidResponse); + bid.ad = `
+
+ + + `; + bidmanager.addBidResponse(bidObj.placementCode, bid); + } catch (e) { + _handleInvalidBid(bidObj); + } + }; + + function _handleInvalidBid(bidObj) { + const bid = bidfactory.createBid(2, bidObj); + bidmanager.addBidResponse(bidObj.placementCode, bid); + } + + function appendEnvFields(url) { + url = utils.tryAppendQueryString(url, 'hbVersion', '$prebid.version$'); + url = utils.tryAppendQueryString(url, 'strVersion', STR_VERSION); + url = utils.tryAppendQueryString(url, 'hbSource', 'prebid'); + + return url; + } + + return { + callBids: _callBids, + str : str, + }; +}; + +module.exports = SharethroughAdapter; + diff --git a/test/spec/adapters/sharethrough_spec.js b/test/spec/adapters/sharethrough_spec.js new file mode 100644 index 000000000000..83e207e0199f --- /dev/null +++ b/test/spec/adapters/sharethrough_spec.js @@ -0,0 +1,178 @@ +import { expect } from 'chai'; +import Adapter from '../../../src/adapters/sharethrough'; +import bidManager from '../../../src/bidmanager'; + +describe('sharethrough adapter', () => { + + let adapter; + let sandbox; + let bidsRequestedOriginal; + + const bidderRequest = { + bidderCode: 'sharethrough', + bids: [ + { + bidder: 'sharethrough', + bidId: 'bidId1', + sizes: [[600,300]], + placementCode: 'foo', + params: { + pkey: 'aaaa1111' + } + }, + { + bidder: 'sharethrough', + bidId: 'bidId2', + sizes: [[700,400]], + placementCode: 'bar', + params: { + pkey: 'bbbb2222' + } + } + ] + }; + + beforeEach(() => { + adapter = new Adapter(); + sandbox = sinon.sandbox.create(); + bidsRequestedOriginal = pbjs._bidsRequested; + pbjs._bidsRequested = []; + }); + + afterEach(() => { + sandbox.restore(); + + pbjs._bidsRequested = bidsRequestedOriginal; + }); + + describe('callBids', () => { + + let firstBidUrl; + let secondBidUrl; + + beforeEach(() => { + sandbox.spy(adapter.str, 'loadIFrame'); + }); + + it('should call loadIFrame on the adloader for each bid', () => { + + adapter.callBids(bidderRequest); + + firstBidUrl = adapter.str.loadIFrame.firstCall.args[0]; + secondBidUrl = adapter.str.loadIFrame.secondCall.args[0]; + + sinon.assert.calledTwice(adapter.str.loadIFrame); + + expect(firstBidUrl).to.contain(adapter.str.STR_BTLR_HOST + '/header-bid/v1?bidId=bidId1&placement_key=aaaa1111&ijson=pbjs.strcallback&hbVersion=%24prebid.version%24&strVersion=0.1.0&hbSource=prebid&'); + expect(secondBidUrl).to.contain(adapter.str.STR_BTLR_HOST + '/header-bid/v1?bidId=bidId2&placement_key=bbbb2222&ijson=pbjs.strcallback&hbVersion=%24prebid.version%24&strVersion=0.1.0&hbSource=prebid&'); + }); + + }); + + describe('strcallback', () => { + + it('should exist and be a function', () => { + let shit = sandbox.stub(pbjs, 'strcallback'); + expect(pbjs.strcallback).to.exist.and.to.be.a('function'); + }); + + }); + + describe('bid requests', () => { + + let firstBid; + let secondBid; + + beforeEach(() => { + sandbox.stub(bidManager, 'addBidResponse'); + + pbjs._bidsRequested.push(bidderRequest); + adapter.str.placementCodeSet['foo'] = {}; + adapter.str.placementCodeSet['bar'] = {}; + // respond + + let bidderReponse1 = { + "adserverRequestId": "40b6afd5-6134-4fbb-850a-bb8972a46994", + "bidId": "bidId1", + "creatives": [ + { + "cpm": 12.34, + "auctionWinId": "b2882d5e-bf8b-44da-a91c-0c11287b8051", + "version": 1 + } + ], + "stxUserId": "" + }; + + let bidderReponse2 = { + "adserverRequestId": "40b6afd5-6134-4fbb-850a-bb8972a46994", + "bidId": "bidId2", + "creatives": [ + { + "cpm": 12.35, + "auctionWinId": "b2882d5e-bf8b-44da-a91c-0c11287b8051", + "version": 1 + } + ], + "stxUserId": "" + }; + + pbjs.strcallback(bidderReponse1); + pbjs.strcallback(bidderReponse2); + + firstBid = bidManager.addBidResponse.firstCall.args[1]; + secondBid = bidManager.addBidResponse.secondCall.args[1]; + }); + + it('should add a bid object for each bid', () => { + sinon.assert.calledTwice(bidManager.addBidResponse); + }); + + it('should pass the correct placement code as first param', () => { + let firstPlacementCode = bidManager.addBidResponse.firstCall.args[0]; + let secondPlacementCode = bidManager.addBidResponse.secondCall.args[0]; + + expect(firstPlacementCode).to.eql('foo'); + expect(secondPlacementCode).to.eql('bar'); + }); + + it('should include the bid request bidId as the adId', () => { + expect(firstBid).to.have.property('adId', 'bidId1'); + expect(secondBid).to.have.property('adId', 'bidId2'); + }); + + it('should have a good statusCode', () => { + expect(firstBid.getStatusCode()).to.eql(1); + expect(secondBid.getStatusCode()).to.eql(1); + }); + + it('should add the CPM to the bid object', () => { + expect(firstBid).to.have.property('cpm', 12.34); + expect(secondBid).to.have.property('cpm', 12.35); + }); + + it('should add the bidder code to the bid object', () => { + expect(firstBid).to.have.property('bidderCode', 'sharethrough'); + expect(secondBid).to.have.property('bidderCode', 'sharethrough'); + }); + + it('should include the ad on the bid object', () => { + expect(firstBid).to.have.property('ad'); + expect(secondBid).to.have.property('ad'); + }); + + it('should include the size on the bid object', () => { + expect(firstBid).to.have.property('width', 600); + expect(firstBid).to.have.property('height', 300); + expect(secondBid).to.have.property('width', 700); + expect(secondBid).to.have.property('height', 400); + }); + + it('should include the pkey', () => { + expect(firstBid).to.have.property('pkey', 'aaaa1111'); + expect(secondBid).to.have.property('pkey', 'bbbb2222'); + }); + + }); + +}); diff --git a/test/spec/unit/adapters/analytics/sharethrough_analytics_spec.js b/test/spec/unit/adapters/analytics/sharethrough_analytics_spec.js new file mode 100644 index 000000000000..81a46205d889 --- /dev/null +++ b/test/spec/unit/adapters/analytics/sharethrough_analytics_spec.js @@ -0,0 +1,99 @@ +import sharethroughAnalytics from 'src/adapters/analytics/sharethrough_analytics'; +import { expect } from 'chai'; + +describe('sharethrough analytics adapter', () => { + let sandbox; + + beforeEach(() =>{ + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('track', () => { + + describe('when event type is bidRequested', () => { + + beforeEach(() => { + let eventType = 'bidRequested'; + let args = {"bidderCode" : "sharethrough", "bids":{"0" : {"placementCode" : "fake placement Code"}}}; + sharethroughAnalytics.track({eventType, args}) + }); + + it('placementCodeSet contains a value', () => { + expect(sharethroughAnalytics.placementCodeSet["fake placement Code"] == undefined).to.equal(false) + }); + }); + + }); + + describe('bid won handler', () => { + + let fireLoseBeaconStub; + + beforeEach(() => { + fireLoseBeaconStub = sandbox.stub(sharethroughAnalytics, 'fireLoseBeacon'); + }); + + describe('when bidderCode is not sharethrough and sharethrough is in bid', () => { + beforeEach(() => { + sharethroughAnalytics.placementCodeSet["div-gpt-ad-1460505748561-0"] = {"adserverRequestId" : "0eca470d-fcac-48e6-845a-c86483ccaa0c"} + var args = { + "bidderCode": "someoneelse", + "width": 600, + "height": 300, + "statusMessage": "Bid available", + "adId": "23fbe93a90c924", + "cpm": 3.984986853301525, + "adserverRequestId": "0eca470d-fcac-48e6-845a-c86483ccaa0c", + "winId": "1c404469-f7bb-4e50-b6f6-a8eaf0808999", + "pkey": "xKcxTTHyndFyVx7T8GKSzxPE", + "ad": "
", + "requestId": "dd2420bd-cdc2-4c66-8479-f3499ece73da", + "responseTimestamp": 1473983655565, + "requestTimestamp": 1473983655458, + "bidder": "sharethrough", + "adUnitCode": "div-gpt-ad-1460505748561-0", + "timeToRespond": 107, + "pbLg": "3.50", + "pbMg": "3.90", + "pbHg": "3.98", + "pbAg": "3.95", + "pbDg": "3.95", + "size": "600x300", + "adserverTargeting": { + "hb_bidder": "sharethrough", + "hb_adid": "23fbe93a90c924", + "hb_pb": "3.90", + "hb_size": "600x300" + } + }; + + sharethroughAnalytics.bidWon(args); + + }); + + it('should fire lose beacon', () => { + sinon.assert.calledOnce(fireLoseBeaconStub); + }); + + }); + + }); + + describe('lose beacon is fired', () => { + + beforeEach(() => { + sandbox.stub(sharethroughAnalytics, 'fireBeacon'); + sharethroughAnalytics.fireLoseBeacon('someoneelse', 10.0, 'arid', 'losebeacontype'); + }); + + it('should call correct url', () => { + let winUrl = sharethroughAnalytics.fireBeacon.firstCall.args[0]; + expect(winUrl).to.contain(sharethroughAnalytics.STR_BEACON_HOST + 'winnerBidderCode=someoneelse&winnerCpm=10&arid=arid&type=losebeacontype&hbVersion=%24prebid.version%24&strVersion=0.1.0&hbSource=prebid&'); + }); + }); + +}); \ No newline at end of file