diff --git a/modules/synacormediaBidAdapter.js b/modules/synacormediaBidAdapter.js new file mode 100644 index 00000000000..ba5fb0cb23b --- /dev/null +++ b/modules/synacormediaBidAdapter.js @@ -0,0 +1,121 @@ +'use strict'; + +import { getAdUnitSizes, logWarn } from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { BANNER } from 'src/mediaTypes'; +import { REPO_AND_VERSION } from 'src/constants'; + +const SYNACOR_URL = '//prebid.technoratimedia.com'; +export const spec = { + code: 'synacormedia', + supportedMediaTypes: [ BANNER ], + sizeMap: {}, + + isBidRequestValid: function(bid) { + return !!(bid && bid.params && bid.params.placementId && bid.params.seatId); + }, + buildRequests: function(validBidReqs, bidderRequest) { + if (!validBidReqs || !validBidReqs.length || !bidderRequest) { + return; + } + let refererInfo = bidderRequest.refererInfo; + let openRtbBidRequest = { + id: bidderRequest.auctionId, + site: { + domain: location.hostname, + page: refererInfo.referer, + ref: document.referrer + }, + device: { + ua: navigator.userAgent + }, + imp: [] + }; + let seatId = null; + validBidReqs.forEach((bid, i) => { + if (seatId && seatId !== bid.params.seatId) { + logWarn(`Synacormedia: there is an inconsistent seatId: ${bid.params.seatId} but only sending bid requests for ${seatId}, you should double check your configuration`); + return; + } else { + seatId = bid.params.seatId; + } + let placementId = bid.params.placementId; + let size = getAdUnitSizes(bid)[0]; + this.sizeMap[bid.bidId] = size; + openRtbBidRequest.imp.push({ + id: bid.bidId, + tagid: placementId, + banner: { + w: size[0], + h: size[1], + pos: 0 + } + }); + }); + if (openRtbBidRequest.imp.length && seatId) { + return { + method: 'POST', + url: `${SYNACOR_URL}/openrtb/bids/${seatId}?src=${REPO_AND_VERSION}`, + data: openRtbBidRequest, + options: { + contentType: 'application/json', + withCredentials: true + } + }; + } + }, + interpretResponse: function(serverResponse) { + if (!serverResponse.body || typeof serverResponse.body != 'object') { + logWarn('Synacormedia: server returned empty/non-json response: ' + JSON.stringify(serverResponse.body)); + return; + } + const {id, seatbid: seatbids} = serverResponse.body; + let bids = []; + if (id && seatbids) { + seatbids.forEach(seatbid => { + seatbid.bid.forEach(bid => { + let size = this.sizeMap[bid.impid] || [0, 0]; + let price = parseFloat(bid.price); + let creative = bid.adm.replace(/\${([^}]*)}/g, (match, key) => { + switch (key) { + case 'AUCTION_SEAT_ID': return seatbid.seat; + case 'AUCTION_ID': return id; + case 'AUCTION_BID_ID': return bid.id; + case 'AUCTION_IMP_ID': return bid.impid; + case 'AUCTION_PRICE': return price; + case 'AUCTION_CURRENCY': return 'USD'; + } + return match; + }); + bids.push({ + requestId: bid.impid, + cpm: price, + width: size[0], + height: size[1], + creativeId: seatbid.seat + '~' + bid.crid, + currency: 'USD', + netRevenue: true, + mediaType: BANNER, + ad: creative, + ttl: 60 + }); + }); + }); + } + return bids; + }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `${SYNACOR_URL}/usersync/html?src=${REPO_AND_VERSION}` + }); + } else { + logWarn('Synacormedia: Please enable iframe based user sync.'); + } + return syncs; + } +}; + +registerBidder(spec); diff --git a/modules/synacormediaBidAdapter.md b/modules/synacormediaBidAdapter.md new file mode 100644 index 00000000000..813e14f6be6 --- /dev/null +++ b/modules/synacormediaBidAdapter.md @@ -0,0 +1,43 @@ +# Overview + +``` +Module Name: Synacor Media Bidder Adapter +Module Type: Bidder Adapter +Maintainer: eng-demand@synacor.com +``` + +# Description + +The Synacor Media adapter requires setup and approval from Synacor. +Please reach out to your account manager for more information. + +# Test Parameters + +## Web +``` + var adUnits = [{ + code: 'test-div', + sizes: [ + [300, 250] + ], + bids: [{ + bidder: "synacormedia", + params: { + seatId: "prebid", + placementId: "81416" + } + }] + },{ + code: 'test-div2', + sizes: [ + [300, 250] + ], + bids: [{ + bidder: "synacormedia", + params: { + seatId: "prebid", + placementId: "demo2" + } + }] + }]; +``` \ No newline at end of file diff --git a/test/spec/modules/synacormediaBidAdapter_spec.js b/test/spec/modules/synacormediaBidAdapter_spec.js new file mode 100644 index 00000000000..9a95d1377e8 --- /dev/null +++ b/test/spec/modules/synacormediaBidAdapter_spec.js @@ -0,0 +1,198 @@ +import { assert, expect } from 'chai'; +import { BANNER } from 'src/mediaTypes'; +import { spec } from 'modules/synacormediaBidAdapter'; + +describe('synacormediaBidAdapter ', function () { + describe('isBidRequestValid', function () { + let bid; + beforeEach(function () { + bid = { + params: { + seatId: 'prebid', + placementId: '1234' + } + }; + }); + + it('should return true when params placementId and seatId are truthy', function () { + assert(spec.isBidRequestValid(bid)); + }); + + it('should return false when seatId param is missing', function () { + delete bid.params.seatId; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + it('should return false when placementId param is missing', function () { + delete bid.params.placementId; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + it('should return false when params is missing or null', function () { + assert.isFalse(spec.isBidRequestValid({ params: null })); + assert.isFalse(spec.isBidRequestValid({})); + assert.isFalse(spec.isBidRequestValid(null)); + }); + }); + describe('buildRequests', function () { + let validBidRequest = { + bidId: '9876abcd', + sizes: [[300, 250]], + params: { + seatId: 'prebid', + placementId: '1234' + } + }; + + let bidderRequest = { + auctionId: 'xyz123', + refererInfo: { + referer: 'https://test.com/foo/bar' + } + }; + + let expectedDataImp = { + banner: { + h: 250, + pos: 0, + w: 300, + }, + id: '9876abcd', + tagid: '1234' + }; + + it('should return valid request when valid bids are used', function () { + let req = spec.buildRequests([validBidRequest], bidderRequest); + expect(req).be.an('object'); + expect(req).to.have.property('method', 'POST'); + expect(req).to.have.property('url'); + expect(req.url).to.contain('//prebid.technoratimedia.com/openrtb/bids/prebid?'); + expect(req.data).to.exist.and.to.be.an('object'); + expect(req.data.id).to.equal('xyz123'); + expect(req.data.imp).to.eql([expectedDataImp]); + }); + + it('should return multiple bids when multiple valid requests with the same seatId are used', function () { + let secondBidRequest = { + bidId: 'foobar', + sizes: [[300, 600]], + params: { + seatId: validBidRequest.params.seatId, + placementId: '5678' + } + }; + let req = spec.buildRequests([validBidRequest, secondBidRequest], bidderRequest); + expect(req).to.exist.and.be.an('object'); + expect(req).to.have.property('method', 'POST'); + expect(req).to.have.property('url'); + expect(req.url).to.contain('//prebid.technoratimedia.com/openrtb/bids/prebid?'); + expect(req.data.id).to.equal('xyz123'); + expect(req.data.imp).to.eql([expectedDataImp, { + banner: { + h: 600, + pos: 0, + w: 300, + }, + id: 'foobar', + tagid: '5678' + }]); + }); + + it('should return only first bid when different seatIds are used', function () { + let mismatchedSeatBidRequest = { + bidId: 'foobar', + sizes: [[300, 250]], + params: { + seatId: 'somethingelse', + placementId: '5678' + } + }; + let req = spec.buildRequests([mismatchedSeatBidRequest, validBidRequest], bidderRequest); + expect(req).to.have.property('method', 'POST'); + expect(req).to.have.property('url'); + expect(req.url).to.contain('//prebid.technoratimedia.com/openrtb/bids/somethingelse?'); + expect(req.data.id).to.equal('xyz123'); + expect(req.data.imp).to.eql([ + { + banner: { + h: 250, + pos: 0, + w: 300, + }, + id: 'foobar', + tagid: '5678' + } + ]); + }); + + it('should not return a request when no valid bid request used', function () { + expect(spec.buildRequests([], bidderRequest)).to.be.undefined; + expect(spec.buildRequests([validBidRequest], null)).to.be.undefined; + }); + }); + + describe('interpretResponse', function () { + let bidResponse = { + id: '10865933907263896~9998~0', + impid: '9876abcd', + price: 0.13, + crid: '1022-250', + adm: '' + }; + spec.sizeMap['9876abcd'] = [300, 250]; + + let serverResponse; + beforeEach(function() { + serverResponse = { + body: { + id: 'abc123', + seatbid: [{ + seat: '9998', + bid: [], + }] + } + }; + }); + it('should return a bid when bid is in the response', function () { + serverResponse.body.seatbid[0].bid.push(bidResponse); + let resp = spec.interpretResponse(serverResponse); + expect(resp).to.be.an('array').that.is.not.empty; + expect(resp[0]).to.eql({ + requestId: '9876abcd', + cpm: 0.13, + width: 300, + height: 250, + creativeId: '9998~1022-250', + currency: 'USD', + netRevenue: true, + mediaType: BANNER, + ad: '', + ttl: 60 + }); + }); + it('should not return a bid when no bid is in the response', function () { + let resp = spec.interpretResponse(serverResponse); + expect(resp).to.be.an('array').that.is.empty; + }); + it('should not return a bid when there is no response body', function () { + expect(spec.interpretResponse({ body: null })).to.not.exist; + expect(spec.interpretResponse({ body: 'some error text' })).to.not.exist; + }); + }); + describe('getUserSyncs', function () { + it('should return a usersync when iframes is enabled', function () { + let usersyncs = spec.getUserSyncs({ + iframeEnabled: true + }, null); + expect(usersyncs).to.be.an('array').that.is.not.empty; + expect(usersyncs[0]).to.have.property('type', 'iframe'); + expect(usersyncs[0]).to.have.property('url'); + expect(usersyncs[0].url).to.contain('//prebid.technoratimedia.com/usersync/html'); + }); + + it('should not return a usersync when iframes are not enabled', function () { + let usersyncs = spec.getUserSyncs({ + pixelEnabled: true + }, null); + expect(usersyncs).to.be.an('array').that.is.empty; + }); + }); +});