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