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
+ });
+ });
+});