diff --git a/adapters.json b/adapters.json index 7f4ded12c11..0f0da3c2342 100644 --- a/adapters.json +++ b/adapters.json @@ -7,6 +7,7 @@ "aol", "appnexus", "appnexusAst", + "conversant", "getintent", "hiromedia", "indexExchange", diff --git a/src/adapters/conversant.js b/src/adapters/conversant.js new file mode 100644 index 00000000000..207e5cb2226 --- /dev/null +++ b/src/adapters/conversant.js @@ -0,0 +1,223 @@ +'use strict'; +var VERSION = '2.0.1', + CONSTANTS = require('../constants.json'), + utils = require('../utils.js'), + bidfactory = require('../bidfactory.js'), + bidmanager = require('../bidmanager.js'), + adloader = require('../adloader'); + +/** + * Adapter for requesting bids from Conversant + */ +var ConversantAdapter = function () { + var w = window, + n = navigator; + + // production endpoint + var conversantUrl = '//media.msg.dotomi.com/s2s/header?callback=$$PREBID_GLOBAL$$.conversantResponse'; + + // SSAPI returns JSONP with window.pbjs.conversantResponse as the cb + var appendScript = function (code) { + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.className = 'cnvr-response'; + + try { + script.appendChild(document.createTextNode(code)); + document.getElementsByTagName('head')[0].appendChild(script); + } catch (e) { + script.text = code; + document.getElementsByTagName('head')[0].appendChild(script); + } + }; + + var httpPOSTAsync = function (url, data) { + var xmlHttp = new w.XMLHttpRequest(); + + xmlHttp.onload = function () { + appendScript(xmlHttp.responseText); + }; + xmlHttp.open('POST', url, true); // true for asynchronous + xmlHttp.withCredentials = true; + xmlHttp.send(data); + }; + + var getDNT = function () { + return n.doNotTrack === '1' || w.doNotTrack === '1' || n.msDoNotTrack === '1' || n.doNotTrack === 'yes'; + }; + + var getDevice = function () { + return { + h: screen.height, + w: screen.width, + dnt: getDNT() ? 1 : 0, + language: n.language.split('-')[0], + make: n.vendor ? n.vendor : '', + ua: n.userAgent + }; + }; + + var callBids = function (params) { + var conversantBids = params.bids || []; + requestBids(conversantBids); + }; + + var requestBids = function (bidReqs) { + // build bid request object + var page = location.pathname + location.search + location.hash, + siteId = '', + conversantImps = [], + conversantBidReqs, + secure = 0; + + //build impression array for conversant + utils._each(bidReqs, function (bid) { + var bidfloor = utils.getBidIdParamater('bidloor', bid.params), + sizeArrayLength = bid.sizes.length, + adW = 0, + adH = 0, + imp; + + secure = utils.getBidIdParamater('secure', bid.params) ? 1 : secure; + siteId = utils.getBidIdParamater('site_id', bid.params); + + if (sizeArrayLength === 2 && typeof bid.sizes[0] === 'number' && typeof bid.sizes[1] === 'number') { + adW = bid.sizes[0]; + adH = bid.sizes[1]; + } else { + adW = bid.sizes[0][0]; + adH = bid.sizes[0][1]; + } + + imp = { + id: bid.bidId, + banner: { + w: adW, + h: adH + }, + secure: secure, + bidfloor: bidfloor ? bidfloor : 0, + displaymanager: 'Prebid.js', + displaymanagerver: VERSION + }; + + conversantImps.push(imp); + }); + + conversantBidReqs = { + 'id': utils.getUniqueIdentifierStr(), + 'imp': conversantImps, + + 'site': { + 'id': siteId, + 'mobile': document.querySelector('meta[name="viewport"][content*="width=device-width"]') !== null ? 1 : 0, + 'page': page + }, + + 'device': getDevice(), + 'at': 1 + }; + + var url = secure ? 'https:' + conversantUrl : location.protocol + conversantUrl; + httpPOSTAsync(url, JSON.stringify(conversantBidReqs)); + }; + + var addEmptyBidResponses = function (placementsWithBidsBack) { + var allConversantBidRequests = $$PREBID_GLOBAL$$._bidsRequested.find(bidSet => bidSet.bidderCode === 'conversant'); + + if (allConversantBidRequests && allConversantBidRequests.bids){ + utils._each(allConversantBidRequests.bids, function (conversantBid) { + if (!utils.contains(placementsWithBidsBack, conversantBid.placementCode)) { + // Add a no-bid response for this placement. + var bid = bidfactory.createBid(2, conversantBid); + bid.bidderCode = 'conversant'; + bidmanager.addBidResponse(conversantBid.placementCode, bid); + } + }); + } + }; + + var parseSeatbid = function (bidResponse) { + var placementsWithBidsBack = []; + utils._each(bidResponse.bid, function (conversantBid) { + var responseCPM, + placementCode = '', + id = conversantBid.impid, + bid = {}, + responseAd, + responseNurl, + sizeArrayLength; + + // Bid request we sent Conversant + var bidRequested = $$PREBID_GLOBAL$$._bidsRequested.find(bidSet => bidSet.bidderCode === 'conversant').bids.find(bid => bid.bidId === id); + + if (bidRequested) { + placementCode = bidRequested.placementCode; + bidRequested.status = CONSTANTS.STATUS.GOOD; + responseCPM = parseFloat(conversantBid.price); + + if (responseCPM !== 0.0) { + conversantBid.placementCode = placementCode; + placementsWithBidsBack.push(placementCode); + conversantBid.size = bidRequested.sizes; + responseAd = conversantBid.adm || ''; + responseNurl = conversantBid.nurl || ''; + + // Our bid! + bid = bidfactory.createBid(1, bidRequested); + bid.creative_id = conversantBid.id || ''; + bid.bidderCode = 'conversant'; + + bid.cpm = responseCPM; + + // Track impression image onto returned html + bid.ad = responseAd + ''; + + sizeArrayLength = bidRequested.sizes.length; + if (sizeArrayLength === 2 && typeof bidRequested.sizes[0] === 'number' && typeof bidRequested.sizes[1] === 'number') { + bid.width = bidRequested.sizes[0]; + bid.height = bidRequested.sizes[1]; + } else { + bid.width = bidRequested.sizes[0][0]; + bid.height = bidRequested.sizes[0][1]; + } + + bidmanager.addBidResponse(placementCode, bid); + } + } + }); + addEmptyBidResponses(placementsWithBidsBack); + }; + + // Register our callback to the global object: + $$PREBID_GLOBAL$$.conversantResponse = function (conversantResponseObj, path) { + // valid object? + if (conversantResponseObj && conversantResponseObj.id) { + if (conversantResponseObj.seatbid && conversantResponseObj.seatbid.length > 0 && conversantResponseObj.seatbid[0].bid && conversantResponseObj.seatbid[0].bid.length > 0) { + utils._each(conversantResponseObj.seatbid, parseSeatbid); + } else { + //no response data for any placements + addEmptyBidResponses([]); + } + } else { + //no response data for any placements + addEmptyBidResponses([]); + } + // for debugging purposes + if (path){ + adloader.loadScript(path, function () { + var allConversantBidRequests = $$PREBID_GLOBAL$$._bidsRequested.find(bidSet => bidSet.bidderCode === 'conversant'); + + if ($$PREBID_GLOBAL$$.conversantDebugResponse){ + $$PREBID_GLOBAL$$.conversantDebugResponse(allConversantBidRequests); + } + }); + } + }; // conversantResponse + + return { + callBids: callBids + }; +}; + +module.exports = ConversantAdapter; diff --git a/test/spec/adapters/conversant_spec.js b/test/spec/adapters/conversant_spec.js new file mode 100644 index 00000000000..1f6a37c5410 --- /dev/null +++ b/test/spec/adapters/conversant_spec.js @@ -0,0 +1,260 @@ +var expect = require('chai').expect; +var Adapter = require('src/adapters/conversant'); +var bidManager = require('src/bidmanager'); + +describe('Conversant adapter tests', function () { + + var addBidResponseSpy; + var adapter; + + var bidderRequest = { + bidderCode: 'conversant', + bids: [ + { + bidId: 'bidId1', + bidder: 'conversant', + placementCode: 'div1', + sizes: [[300, 600]], + params: { + site_id: '87293', + secure: false + } + }, + { + bidId: 'bidId2', + bidder: 'conversant', + placementCode: 'div2', + sizes: [[300, 600]], + params: { + site_id: '87293', + secure: false + } + }, + { + bidId: 'bidId3', + bidder: 'conversant', + placementCode: 'div3', + sizes: [[300, 600], [160, 600]], + params: { + site_id: '87293', + secure: false + } + } + ] + }; + + + it('The Conversant response should exist and be a function', function () { + expect(pbjs.conversantResponse).to.exist.and.to.be.a('function'); + }); + + describe('Should submit bid responses correctly', function () { + beforeEach(function () { + addBidResponseSpy = sinon.stub(bidManager, 'addBidResponse'); + pbjs._bidsRequested.push(bidderRequest); + adapter = new Adapter(); + }); + + afterEach(function () { + addBidResponseSpy.restore(); + }); + + it('Should correctly submit valid and empty bids to the bid manager', function () { + var bidResponse = { + id: 123, + seatbid: [{ + bid: [{ + id: 1111111, + impid: 'bidId1', + price: 0 + },{ + id: 2345, + impid: 'bidId2', + price: 0.22, + nurl: '', + adm: 'adm2', + h:300, + w:600 + }] + }] + }; + + pbjs.conversantResponse(bidResponse); + + // in this case, the valid bid (div2) is submitted before the empty bids (div1, div3) + var firstBid = addBidResponseSpy.getCall(0).args[1]; + var secondBid = addBidResponseSpy.getCall(1).args[1]; + var thirdBid = addBidResponseSpy.getCall(2).args[1]; + var placementCode1 = addBidResponseSpy.getCall(0).args[0]; + var placementCode2 = addBidResponseSpy.getCall(1).args[0]; + var placementCode3 = addBidResponseSpy.getCall(2).args[0]; + + expect(firstBid.getStatusCode()).to.equal(1); + expect(firstBid.bidderCode).to.equal('conversant'); + expect(firstBid.cpm).to.equal(0.22); + expect(firstBid.ad).to.equal('adm2' + ''); + expect(placementCode1).to.equal('div2'); + + expect(secondBid.getStatusCode()).to.equal(2); + expect(secondBid.bidderCode).to.equal('conversant'); + expect(placementCode2).to.equal('div1'); + + expect(thirdBid.getStatusCode()).to.equal(2); + expect(thirdBid.bidderCode).to.equal('conversant'); + expect(placementCode3).to.equal('div3'); + + expect(addBidResponseSpy.getCalls().length).to.equal(3); + }); + + it('Should submit bids with statuses of 2 to the bid manager for empty bid responses', function () { + pbjs.conversantResponse({id: 1, seatbid: []}); + + var placementCode1 = addBidResponseSpy.getCall(0).args[0]; + var firstBid = addBidResponseSpy.getCall(0).args[1]; + var placementCode2 = addBidResponseSpy.getCall(1).args[0]; + var secondBid = addBidResponseSpy.getCall(1).args[1]; + var placementCode3 = addBidResponseSpy.getCall(2).args[0]; + var thirdBid = addBidResponseSpy.getCall(2).args[1]; + + expect(placementCode1).to.equal('div1'); + expect(firstBid.getStatusCode()).to.equal(2); + expect(firstBid.bidderCode).to.equal('conversant'); + + expect(placementCode2).to.equal('div2'); + expect(secondBid.getStatusCode()).to.equal(2); + expect(secondBid.bidderCode).to.equal('conversant'); + + expect(placementCode3).to.equal('div3'); + expect(thirdBid.getStatusCode()).to.equal(2); + expect(thirdBid.bidderCode).to.equal('conversant'); + + expect(addBidResponseSpy.getCalls().length).to.equal(3); + }); + + it('Should submit valid bids to the bid manager', function () { + var bidResponse = { + id: 123, + seatbid: [{ + bid: [{ + id: 1111111, + impid: 'bidId1', + price: 0.11, + nurl : '', + adm: 'adm', + h: 250, + w: 300, + ext : {} + },{ + id: 2345, + impid: 'bidId2', + price: 0.22, + nurl: '', + adm: 'adm2', + h:300, + w:600 + }, + { + id: 33333, + impid: 'bidId3', + price: 0.33, + nurl: '', + adm: 'adm3', + h: 160, + w: 600 + }] + }] + }; + + pbjs.conversantResponse(bidResponse); + + var firstBid = addBidResponseSpy.getCall(0).args[1]; + var secondBid = addBidResponseSpy.getCall(1).args[1]; + var thirdBid = addBidResponseSpy.getCall(2).args[1]; + var placementCode1 = addBidResponseSpy.getCall(0).args[0]; + var placementCode2 = addBidResponseSpy.getCall(1).args[0]; + var placementCode3 = addBidResponseSpy.getCall(2).args[0]; + + expect(firstBid.getStatusCode()).to.equal(1); + expect(firstBid.bidderCode).to.equal('conversant'); + expect(firstBid.cpm).to.equal(0.11); + expect(firstBid.ad).to.equal('adm'+ ''); + expect(placementCode1).to.equal('div1'); + + expect(secondBid.getStatusCode()).to.equal(1); + expect(secondBid.bidderCode).to.equal('conversant'); + expect(secondBid.cpm).to.equal(0.22); + expect(secondBid.ad).to.equal('adm2' + ''); + expect(placementCode2).to.equal('div2'); + + expect(thirdBid.getStatusCode()).to.equal(1); + expect(thirdBid.bidderCode).to.equal('conversant'); + expect(thirdBid.cpm).to.equal(0.33); + expect(thirdBid.ad).to.equal('adm3' + ''); + expect(placementCode3).to.equal('div3'); + + expect(addBidResponseSpy.getCalls().length).to.equal(3); + }); + }); + + + describe('Should submit the correct headers in the xhr', function () { + var server, + adapter; + + var bidResponse = { + id: 123, + seatbid: [{ + bid: [{ + id: 1111, + impid: 'bidId1', + price: 0.11, + nurl : '', + adm: 'adm', + h: 250, + w: 300, + ext : {} + },{ + id: 2222, + impid: 'bidId2', + price: 0.22, + nurl: '', + adm: 'adm2', + h:300, + w:600 + }, + { + id: 3333, + impid: 'bidId3', + price: 0.33, + nurl: '', + adm: 'adm3', + h: 160, + w: 600 + }] + }] + }; + + beforeEach(function () { + server = sinon.fakeServer.create(); + adapter = new Adapter(); + }); + + afterEach(function () { + server.restore(); + }); + + beforeEach(function () { + var resp = [200, {'Content-type': 'text/javascript'}, 'pbjs.conversantResponse(\'' + JSON.stringify(bidResponse) + '\')']; + server.respondWith('POST', new RegExp('media.msg.dotomi.com/s2s/header'), resp); + }); + + it('Should contain valid request header properties', function () { + adapter.callBids(bidderRequest); + server.respond(); + + var request = server.requests[0]; + expect(request.requestBody).to.not.be.empty; + expect(request.withCredentials).to.equal(true); // allows for request cookies + }); + }); +});