-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Added tremor bid adapter for prebid.js * - Fixed some tests. * - Added some comments and changed a few variable names to be in line with what we use at tremor. - Changed a test to look for document.location.href instead of a hardcoded localhost url. * - Some formatting. * - Removed the vastUrl field from the adapter.
- Loading branch information
1 parent
598817f
commit 27944ac
Showing
3 changed files
with
341 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
/* | ||
* Tremor Video bid Adapter for prebid.js | ||
* */ | ||
|
||
import Adapter from 'src/adapter'; | ||
import bidfactory from 'src/bidfactory'; | ||
import bidmanager from 'src/bidmanager'; | ||
import * as utils from 'src/utils'; | ||
import {ajax} from 'src/ajax'; | ||
import {STATUS} from 'src/constants'; | ||
import adaptermanager from 'src/adaptermanager'; | ||
|
||
const ENDPOINT = '.ads.tremorhub.com/ad/tag'; | ||
|
||
const OPTIONAL_PARAMS = [ | ||
'mediaId', 'mediaUrl', 'mediaTitle', 'contentLength', 'floor', | ||
'efloor', 'custom', 'categories', 'keywords', 'blockDomains', | ||
'c2', 'c3', 'c4', 'skip', 'skipmin', 'skipafter', 'delivery', | ||
'placement', 'videoMinBitrate', 'videoMaxBitrate' | ||
]; | ||
|
||
/** | ||
* Bidder adapter Tremor Video. Given the list of all ad unit tag IDs, | ||
* sends out a bid request. When a bid response is back, registers the bid | ||
* to Prebid.js. | ||
* Steps: | ||
* - Format and send the bid request | ||
* - Evaluate and handle the response | ||
* - Store potential VAST markup | ||
* - Send request to ad server | ||
* - intercept ad server response | ||
* - Check if the vast wrapper URL is http://cdn.tremorhub.com/static/dummy.xml | ||
* - If yes: then render the locally stored VAST markup by directly passing it to your player | ||
* - Else: give the player the VAST wrapper from your ad server | ||
*/ | ||
function TremorAdapter() { | ||
let baseAdapter = new Adapter('tremor'); | ||
|
||
/* Prebid executes this function when the page asks to send out bid requests */ | ||
baseAdapter.callBids = function (bidRequest) { | ||
const bids = bidRequest.bids || []; | ||
bids.filter(bid => valid(bid)) | ||
.map(bid => { | ||
let url = generateUrl(bid); | ||
if (url) { | ||
ajax(url, response => { | ||
handleResponse(bid, response); | ||
}, null, {method: 'GET', withCredentials: true}); | ||
} | ||
}); | ||
}; | ||
|
||
/** | ||
* Generates the url based on the parameters given. Sizes are required. | ||
* The format is: [L,W] or [[L1,W1],...] | ||
* @param bid | ||
* @returns {string} | ||
*/ | ||
function generateUrl(bid) { | ||
// get the sizes | ||
let width, height; | ||
if (utils.isArray(bid.sizes) && bid.sizes.length === 2 && (!isNaN(bid.sizes[0]) && !isNaN(bid.sizes[1]))) { | ||
width = bid.sizes[0]; | ||
height = bid.sizes[1]; | ||
} else if (typeof bid.sizes === 'object') { | ||
// take the primary (first) size from the array | ||
width = bid.sizes[0][0]; | ||
height = bid.sizes[0][1]; | ||
} | ||
if (width && height) { | ||
let scheme = ((document.location.protocol === 'https:') ? 'https' : 'http') + '://'; | ||
let url = scheme + bid.params.supplyCode + ENDPOINT + '?adCode=' + bid.params.adCode; | ||
|
||
url += ('&playerWidth=' + width); | ||
url += ('&playerHeight=' + height); | ||
url += ('&srcPageUrl=' + encodeURIComponent(document.location.href)); | ||
|
||
OPTIONAL_PARAMS.forEach(param => { | ||
if (bid.params[param]) { | ||
url += ('&' + param + '=' + bid.params[param]); | ||
} | ||
}); | ||
|
||
url = (url + '&fmt=json'); | ||
|
||
return url; | ||
} | ||
} | ||
|
||
/* Notify Prebid of bid responses so bids can get in the auction */ | ||
function handleResponse(bidReq, response) { | ||
let bidResult; | ||
|
||
try { | ||
bidResult = JSON.parse(response); | ||
} catch (error) { | ||
utils.logError(error); | ||
} | ||
|
||
if (!bidResult || bidResult.error) { | ||
let errorMessage = `in response for ${baseAdapter.getBidderCode()} adapter`; | ||
if (bidResult && bidResult.error) { | ||
errorMessage += `: ${bidResult.error}`; | ||
} | ||
utils.logError(errorMessage); | ||
|
||
// signal this response is complete | ||
bidmanager.addBidResponse(bidReq.placementCode, createBid(STATUS.NO_BID)); | ||
} | ||
|
||
if (bidResult.seatbid && bidResult.seatbid.length > 0) { | ||
bidResult.seatbid[0].bid.forEach(tag => { | ||
let status = STATUS.GOOD; | ||
const bid = createBid(status, bidReq, tag); | ||
bidmanager.addBidResponse(bidReq.placementCode, bid); | ||
}); | ||
} else { | ||
// signal this response is complete with no bid | ||
bidmanager.addBidResponse(bidReq.placementCode, createBid(STATUS.NO_BID)); | ||
} | ||
} | ||
|
||
/** | ||
* We require the ad code and the supply code to generate a tag url | ||
* @param bid | ||
* @returns {*} | ||
*/ | ||
function valid(bid) { | ||
if (bid.params.adCode && bid.params.supplyCode) { | ||
return bid; | ||
} else { | ||
utils.logError('missing bid params'); | ||
} | ||
} | ||
|
||
/** | ||
* Create and return a bid object based on status and tag | ||
* @param status | ||
* @param reqBid | ||
* @param response | ||
*/ | ||
function createBid(status, reqBid, response) { | ||
let bid = bidfactory.createBid(status, reqBid); | ||
bid.code = baseAdapter.getBidderCode(); | ||
bid.bidderCode = baseAdapter.getBidderCode(); | ||
|
||
if (response) { | ||
bid.cpm = response.price; | ||
bid.crid = response.crid; | ||
bid.vastXml = response.adm; | ||
bid.mediaType = 'video'; | ||
} | ||
|
||
return bid; | ||
} | ||
|
||
return Object.assign(this, { | ||
callBids: baseAdapter.callBids, | ||
setBidderCode: baseAdapter.setBidderCode, | ||
}); | ||
} | ||
|
||
adaptermanager.registerBidAdapter(new TremorAdapter(), 'tremor', { | ||
supportedMediaTypes: ['video'] | ||
}); | ||
|
||
module.exports = TremorAdapter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import {expect} from 'chai'; | ||
import Adapter from 'modules/tremorBidAdapter'; | ||
import bidmanager from 'src/bidmanager'; | ||
|
||
const AD_CODE = 'ssp-!demo!-lufip'; | ||
const SUPPLY_CODE = 'ssp-%21demo%21-rm6rh'; | ||
const SIZES = [640, 480]; | ||
const REQUEST = { | ||
'code': 'video1', | ||
'sizes': [640, 480], | ||
'mediaType': 'video', | ||
'bids': [{ | ||
'bidder': 'tremor', | ||
'params': { | ||
'mediaId': 'MyCoolVideo', | ||
'mediaUrl': '', | ||
'mediaTitle': '', | ||
'contentLength': '', | ||
'floor': '', | ||
'efloor': '', | ||
'custom': '', | ||
'categories': '', | ||
'keywords': '', | ||
'blockDomains': '', | ||
'c2': '', | ||
'c3': '', | ||
'c4': '', | ||
'skip': '', | ||
'skipmin': '', | ||
'skipafter': '', | ||
'delivery': '', | ||
'placement': '', | ||
'videoMinBitrate': '', | ||
'videoMaxBitrate': '' | ||
} | ||
}] | ||
}; | ||
|
||
const RESPONSE = { | ||
'cur': 'USD', | ||
'id': '3dba13e35f3d42f998bc7e65fd871889', | ||
'seatbid': [{ | ||
'seat': 'TremorVideo', | ||
'bid': [{ | ||
'adomain': [], | ||
'price': 0.50000, | ||
'id': '3dba13e35f3d42f998bc7e65fd871889', | ||
'adm': '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<VAST version="2.0"> <Ad id="defaultText"> <InLine> <AdSystem version="1.0">Tremor Video</AdSystem> <AdTitle>Test MP4 Creative</AdTitle> <Error><![CDATA[https://events.tremorhub.com/diag?rid=3dba13e35f3d42f998bc7e65fd871889&req_ts=1503951950395&pbid=47376&seatid=60858&aid=348453&asid=null&lid=null&rid=3dba13e35f3d42f998bc7e65fd871889&rtype=VAST_ERR&vastError=[ERRORCODE]&sec=true&adcode=ssp-!demo!-lufip&seatId=60858&pbid=47376&brid=141046&sid=149810&sdom=console.tremorhub.com&aid=348453]]></Error>\n<Impression id="TV"><![CDATA[https://events.tremorhub.com/evt?rid=3dba13e35f3d42f998bc7e65fd871889&req_ts=1503951950395&pbid=47376&seatid=60858&aid=348453&asid=null&lid=null&tuid=97e0d10a4b504700b578e4f7d22cac35&evt=IMP&tvssa=false]]></Impression>\n<Impression/> <Creatives> <Creative> <Linear> <Duration><![CDATA[ 00:00:30 ]]></Duration> <AdParameters><![CDATA[ &referer=- ]]></AdParameters> <MediaFiles> <MediaFile delivery="progressive" height="360" type="video/mp4" width="640"> <![CDATA[https://cdn.tremorhub.com/adUnitTest/tremor_video_test_ad_30sec_640x360.mp4]]> </MediaFile> </MediaFiles> <TrackingEvents>\n<Tracking event="start"><![CDATA[https://events.tremorhub.com/evt?rid=3dba13e35f3d42f998bc7e65fd871889&req_ts=1503951950395&pbid=47376&seatid=60858&aid=348453&asid=null&lid=null&tuid=97e0d10a4b504700b578e4f7d22cac35&evt=start&vastcrtype=linear&crid=]]></Tracking>\n<Tracking event="firstQuartile"><![CDATA[https://events.tremorhub.com/evt?rid=3dba13e35f3d42f998bc7e65fd871889&req_ts=1503951950395&pbid=47376&seatid=60858&aid=348453&asid=null&lid=null&tuid=97e0d10a4b504700b578e4f7d22cac35&evt=firstQuartile&vastcrtype=linear&crid=]]></Tracking>\n<Tracking event="midpoint"><![CDATA[https://events.tremorhub.com/evt?rid=3dba13e35f3d42f998bc7e65fd871889&req_ts=1503951950395&pbid=47376&seatid=60858&aid=348453&asid=null&lid=null&tuid=97e0d10a4b504700b578e4f7d22cac35&evt=midpoint&vastcrtype=linear&crid=]]></Tracking>\n<Tracking event="thirdQuartile"><![CDATA[https://events.tremorhub.com/evt?rid=3dba13e35f3d42f998bc7e65fd871889&req_ts=1503951950395&pbid=47376&seatid=60858&aid=348453&asid=null&lid=null&tuid=97e0d10a4b504700b578e4f7d22cac35&evt=thirdQuartile&vastcrtype=linear&crid=]]></Tracking>\n<Tracking event="complete"><![CDATA[https://events.tremorhub.com/evt?rid=3dba13e35f3d42f998bc7e65fd871889&req_ts=1503951950395&pbid=47376&seatid=60858&aid=348453&asid=null&lid=null&tuid=97e0d10a4b504700b578e4f7d22cac35&evt=complete&vastcrtype=linear&crid=]]></Tracking>\n</TrackingEvents> <VideoClicks>\n<ClickTracking id="TV"><![CDATA[https://events.tremorhub.com/evt?rid=3dba13e35f3d42f998bc7e65fd871889&req_ts=1503951950395&pbid=47376&seatid=60858&aid=348453&asid=null&lid=null&tuid=97e0d10a4b504700b578e4f7d22cac35&evt=click&vastcrtype=linear&crid=]]></ClickTracking>\n</VideoClicks> </Linear> </Creative> </Creatives> <Extensions/> </InLine> </Ad>\n</VAST>\n', | ||
'impid': '1' | ||
}] | ||
}] | ||
}; | ||
|
||
describe('TremorBidAdapter', () => { | ||
let adapter; | ||
|
||
beforeEach(() => adapter = new Adapter()); | ||
|
||
describe('request function', () => { | ||
let xhr; | ||
let requests; | ||
|
||
beforeEach(() => { | ||
xhr = sinon.useFakeXMLHttpRequest(); | ||
requests = []; | ||
xhr.onCreate = request => requests.push(request); | ||
}); | ||
|
||
afterEach(() => xhr.restore()); | ||
|
||
it('exists and is a function', () => { | ||
expect(adapter.callBids).to.exist.and.to.be.a('function'); | ||
}); | ||
|
||
it('requires paramters to make request', () => { | ||
adapter.callBids({}); | ||
expect(requests).to.be.empty; | ||
}); | ||
|
||
it('requires adCode && supplyCode', () => { | ||
let backup = REQUEST.bids[0].params; | ||
REQUEST.bids[0].params = {adCode: AD_CODE}; | ||
adapter.callBids(REQUEST); | ||
expect(requests).to.be.empty; | ||
REQUEST.bids[0].params = backup; | ||
}); | ||
|
||
it('requires proper sizes to make a bid request', () => { | ||
let backupBid = REQUEST; | ||
backupBid.sizes = []; | ||
adapter.callBids(backupBid); | ||
expect(requests).to.be.empty; | ||
}); | ||
|
||
it('generates a proper ad call URL', () => { | ||
REQUEST.bids[0].params.adCode = AD_CODE; | ||
REQUEST.bids[0].params.supplyCode = SUPPLY_CODE; | ||
REQUEST.bids[0].sizes = SIZES; | ||
adapter.callBids(REQUEST); | ||
const requestUrl = requests[0].url; | ||
let srcPageURl = ('&srcPageUrl=' + encodeURIComponent(document.location.href)); | ||
expect(requestUrl).to.equal('http://ssp-%21demo%21-rm6rh.ads.tremorhub.com/ad/tag?adCode=ssp-!demo!-lufip&playerWidth=640&playerHeight=480' + srcPageURl + '&mediaId=MyCoolVideo&fmt=json'); | ||
}); | ||
|
||
it('generates a proper ad call URL given a different size format', () => { | ||
REQUEST.bids[0].params.adCode = AD_CODE; | ||
REQUEST.bids[0].params.supplyCode = SUPPLY_CODE; | ||
REQUEST.bids[0].sizes = [SIZES]; | ||
adapter.callBids(REQUEST); | ||
const requestUrl = requests[0].url; | ||
let srcPageURl = ('&srcPageUrl=' + encodeURIComponent(document.location.href)); | ||
expect(requestUrl).to.equal('http://ssp-%21demo%21-rm6rh.ads.tremorhub.com/ad/tag?adCode=ssp-!demo!-lufip&playerWidth=640&playerHeight=480' + srcPageURl + '&mediaId=MyCoolVideo&fmt=json'); | ||
}); | ||
}); | ||
|
||
describe('response handler', () => { | ||
let server; | ||
|
||
beforeEach(() => { | ||
server = sinon.fakeServer.create(); | ||
sinon.stub(bidmanager, 'addBidResponse'); | ||
}); | ||
|
||
afterEach(() => { | ||
server.restore(); | ||
bidmanager.addBidResponse.restore(); | ||
}); | ||
|
||
it('registers bids', () => { | ||
server.respondWith(JSON.stringify(RESPONSE)); | ||
|
||
adapter.callBids(REQUEST); | ||
server.respond(); | ||
sinon.assert.calledOnce(bidmanager.addBidResponse); | ||
|
||
const response = bidmanager.addBidResponse.firstCall.args[1]; | ||
expect(response).to.have.property('statusMessage', 'Bid available'); | ||
expect(response).to.have.property('cpm', 0.50000); | ||
}); | ||
|
||
it('handles nobid responses', () => { | ||
server.respondWith(JSON.stringify({ | ||
'cur': 'USD', | ||
'id': 'ff83ce7e00df41c9bce79b651afc7c51', | ||
'seatbid': [] | ||
})); | ||
|
||
adapter.callBids(REQUEST); | ||
server.respond(); | ||
sinon.assert.calledOnce(bidmanager.addBidResponse); | ||
|
||
const response = bidmanager.addBidResponse.firstCall.args[1]; | ||
expect(response).to.have.property( | ||
'statusMessage', | ||
'Bid returned empty or error response' | ||
); | ||
}); | ||
|
||
it('handles JSON.parse errors', () => { | ||
server.respondWith(''); | ||
|
||
adapter.callBids(REQUEST); | ||
server.respond(); | ||
sinon.assert.calledOnce(bidmanager.addBidResponse); | ||
|
||
const response = bidmanager.addBidResponse.firstCall.args[1]; | ||
expect(response).to.have.property( | ||
'statusMessage', | ||
'Bid returned empty or error response' | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters