diff --git a/modules/synacormediaBidAdapter.js b/modules/synacormediaBidAdapter.js index dcaa2c35f4f..3339ecfb04f 100644 --- a/modules/synacormediaBidAdapter.js +++ b/modules/synacormediaBidAdapter.js @@ -2,18 +2,26 @@ import { getAdUnitSizes, logWarn } from '../src/utils'; import { registerBidder } from '../src/adapters/bidderFactory'; -import { BANNER } from '../src/mediaTypes'; +import { BANNER, VIDEO } from '../src/mediaTypes'; +import includes from 'core-js/library/fn/array/includes'; const BID_HOST = '//prebid.technoratimedia.com'; const USER_SYNC_HOST = '//ad-cdn.technoratimedia.com'; +const VIDEO_PARAMS = [ 'minduration', 'maxduration' ]; + export const spec = { code: 'synacormedia', - supportedMediaTypes: [ BANNER ], + supportedMediaTypes: [ BANNER, VIDEO ], sizeMap: {}, + isVideoBid: function(bid) { + return bid.mediaTypes !== undefined && + bid.mediaTypes.hasOwnProperty('video'); + }, isBidRequestValid: function(bid) { return !!(bid && bid.params && bid.params.placementId && bid.params.seatId); }, + buildRequests: function(validBidReqs, bidderRequest) { if (!validBidReqs || !validBidReqs.length || !bidderRequest) { return; @@ -49,20 +57,33 @@ export const spec = { logWarn(`Synacormedia: there is an invalid POS: ${bid.params.pos}`); pos = 0; } + let videoOrBannerKey = this.isVideoBid(bid) ? 'video' : 'banner'; getAdUnitSizes(bid).forEach((size, i) => { - let request = { - id: bid.bidId + '~' + size[0] + 'x' + size[1], - tagid: placementId, - banner: { - w: size[0], - h: size[1], - pos - } + if (!size || size.length != 2) { + return; + } + let size0 = size[0]; + let size1 = size[1]; + let imp = { + id: `${videoOrBannerKey.substring(0, 1)}${bid.bidId}-${size0}x${size1}`, + tagid: placementId }; if (bidFloor !== null && !isNaN(bidFloor)) { - request.bidfloor = bidFloor; + imp.bidfloor = bidFloor; + } + + let videoOrBannerValue = { + w: size0, + h: size1, + pos + }; + if (videoOrBannerKey === 'video' && bid.params.video) { + Object.keys(bid.params.video) + .filter(param => includes(VIDEO_PARAMS, param) && !isNaN(parseInt(bid.params.video[param], 10))) + .forEach(param => videoOrBannerValue[param] = parseInt(bid.params.video[param], 10)); } - openRtbBidRequest.imp.push(request); + imp[videoOrBannerKey] = videoOrBannerValue; + openRtbBidRequest.imp.push(imp); }); }); @@ -79,41 +100,43 @@ export const spec = { } }, interpretResponse: function(serverResponse) { + var updateMacros = (bid, r) => { + return r ? r.replace(/\${AUCTION_PRICE}/g, bid.price) : r; + }; + 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 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; - }); - let [, impid, width, height] = bid.impid.match(/^(.*)~(.*)x(.*)$/); - bids.push({ + let creative = updateMacros(bid, bid.adm); + let nurl = updateMacros(bid, bid.nurl); + let [, impType, impid, width, height] = bid.impid.match(/^([vb])(.*)-(.*)x(.*)$/); + let isVideo = impType == 'v'; + let bidObj = { requestId: impid, adId: bid.id.replace(/~/g, '-'), - cpm: price, + cpm: parseFloat(bid.price), width: parseInt(width, 10), height: parseInt(height, 10), - creativeId: seatbid.seat + '~' + bid.crid, + creativeId: `${seatbid.seat}_${bid.crid}`, currency: 'USD', netRevenue: true, - mediaType: BANNER, + mediaType: isVideo ? VIDEO : BANNER, ad: creative, ttl: 60 - }); + }; + if (isVideo) { + let [, uuid] = nurl.match(/ID=([^&]*)&?/); + bidObj.videoCacheKey = encodeURIComponent(uuid); + bidObj.vastUrl = nurl; + } + bids.push(bidObj); }); }); } diff --git a/modules/synacormediaBidAdapter.md b/modules/synacormediaBidAdapter.md index 279cc48bcc1..1f225aa0b2a 100644 --- a/modules/synacormediaBidAdapter.md +++ b/modules/synacormediaBidAdapter.md @@ -11,6 +11,13 @@ Maintainer: eng-demand@synacor.com The Synacor Media adapter requires setup and approval from Synacor. Please reach out to your account manager for more information. +### DFP Video Creative +To use video, setup a `VAST redirect` creative within Google AdManager (DFP) with the following VAST tag URL: + +``` +https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_cache_id_synacorm%%&AUCTION_PRICE=%%PATTERN:hb_pb_synacormedia%% +``` + # Test Parameters ## Web @@ -24,23 +31,32 @@ Please reach out to your account manager for more information. bidder: "synacormedia", params: { seatId: "prebid", - placementId: "81416", - bidfloor: "0.10", + placementId: "demo1", + bidfloor: 0.10, pos: 1 } }] },{ code: 'test-div2', - sizes: [ - [300, 250] - ], + mediaType: { + video: { + context: 'instream', + playerSizes: [ + [300, 250] + ], + } + }, bids: [{ bidder: "synacormedia", params: { seatId: "prebid", - placementId: "demo2" - bidfloor: "0.10", - pos: 1 + placementId: "demo1" + bidfloor: 0.20, + pos: 1, + video: { + minduration: 15, + maxduration: 30 + } } }] }]; diff --git a/test/pages/video.html b/test/pages/video.html index c6a72b6e26b..e040b65fe23 100644 --- a/test/pages/video.html +++ b/test/pages/video.html @@ -32,7 +32,7 @@ code: 'video1', sizes: [640,480], mediaTypes: { - video: {context: 'instream'} + video: {context: 'instream', playerSize: [640, 480]} }, bids: [ { diff --git a/test/spec/modules/synacormediaBidAdapter_spec.js b/test/spec/modules/synacormediaBidAdapter_spec.js index b91e8cc9fc1..e86bf81a6a7 100644 --- a/test/spec/modules/synacormediaBidAdapter_spec.js +++ b/test/spec/modules/synacormediaBidAdapter_spec.js @@ -32,7 +32,114 @@ describe('synacormediaBidAdapter ', function () { assert.isFalse(spec.isBidRequestValid(null)); }); }); + + describe('impression type', function() { + let nonVideoReq = { + bidId: '9876abcd', + sizes: [[300, 250], [300, 600]], + params: { + seatId: 'prebid', + placementId: '1234', + bidfloor: '0.50' + } + }; + + let bannerReq = { + bidId: '9876abcd', + sizes: [[300, 250], [300, 600]], + params: { + seatId: 'prebid', + placementId: '1234', + bidfloor: '0.50' + }, + mediaTypes: { + banner: { + h: 600, + pos: 0, + w: 300, + } + }, + }; + + let videoReq = { + bidId: '9876abcd', + sizes: [[640, 480]], + params: { + seatId: 'prebid', + placementId: '1234', + bidfloor: '0.50' + }, + mediaTypes: { + video: { + context: 'instream', + playerSize: [ + [ + 640, + 480 + ] + ] + } + }, + }; + it('should return correct impression type video/banner', function() { + assert.isFalse(spec.isVideoBid(nonVideoReq)); + assert.isFalse(spec.isVideoBid(bannerReq)); + assert.isTrue(spec.isVideoBid(videoReq)); + }); + }); describe('buildRequests', function () { + let validBidRequestVideo = { + bidder: 'synacormedia', + params: { + seatId: 'prebid', + placementId: '1234', + video: { + minduration: 30 + } + }, + mediaTypes: { + video: { + context: 'instream', + playerSize: [[ 640, 480 ]] + } + }, + adUnitCode: 'video1', + transactionId: '93e5def8-29aa-4fe8-bd3a-0298c39f189a', + sizes: [[ 640, 480 ]], + bidId: '2624fabbb078e8', + bidderRequestId: '117954d20d7c9c', + auctionId: 'defd525f-4f1e-4416-a4cb-ae53be90e706', + src: 'client', + bidRequestsCount: 1 + }; + + let bidderRequestVideo = { + bidderCode: 'synacormedia', + auctionId: 'VideoAuctionId124', + bidderRequestId: '117954d20d7c9c', + auctionStart: 1553624929697, + timeout: 700, + refererInfo: { + referer: 'http://localhost:9999/test/pages/video.html?pbjs_debug=true', + reachedTop: true, + numIframes: 0, + stack: [ 'http://localhost:9999/test/pages/video.html?pbjs_debug=true' ] + }, + start: 1553624929700 + }; + + bidderRequestVideo.bids = validBidRequestVideo; + let expectedDataVideo1 = { + id: 'v2624fabbb078e8-640x480', + tagid: '1234', + video: { + w: 640, + h: 480, + pos: 0, + minduration: 30 + } + }; + let validBidRequest = { bidId: '9876abcd', sizes: [[300, 250], [300, 600]], @@ -56,7 +163,7 @@ describe('synacormediaBidAdapter ', function () { pos: 0, w: 300, }, - id: '9876abcd~300x250', + id: 'b9876abcd-300x250', tagid: '1234', bidfloor: 0.5 }; @@ -66,12 +173,13 @@ describe('synacormediaBidAdapter ', function () { pos: 0, w: 300, }, - id: '9876abcd~300x600', + id: 'b9876abcd-300x600', tagid: '1234', bidfloor: 0.5 }; it('should return valid request when valid bids are used', function () { + // banner test let req = spec.buildRequests([validBidRequest], bidderRequest); expect(req).be.an('object'); expect(req).to.have.property('method', 'POST'); @@ -80,6 +188,16 @@ describe('synacormediaBidAdapter ', function () { expect(req.data).to.exist.and.to.be.an('object'); expect(req.data.id).to.equal('xyz123'); expect(req.data.imp).to.eql([expectedDataImp1, expectedDataImp2]); + + // video test + let reqVideo = spec.buildRequests([validBidRequestVideo], bidderRequestVideo); + expect(reqVideo).be.an('object'); + expect(reqVideo).to.have.property('method', 'POST'); + expect(reqVideo).to.have.property('url'); + expect(reqVideo.url).to.contain('//prebid.technoratimedia.com/openrtb/bids/prebid?'); + expect(reqVideo.data).to.exist.and.to.be.an('object'); + expect(reqVideo.data.id).to.equal('VideoAuctionId124'); + expect(reqVideo.data.imp).to.eql([expectedDataVideo1]); }); it('should return multiple bids when multiple valid requests with the same seatId are used', function () { @@ -104,7 +222,7 @@ describe('synacormediaBidAdapter ', function () { pos: 0, w: 300, }, - id: 'foobar~300x600', + id: 'bfoobar-300x600', tagid: '5678', bidfloor: 0.5 }]); @@ -132,7 +250,7 @@ describe('synacormediaBidAdapter ', function () { pos: 0, w: 300, }, - id: 'foobar~300x250', + id: 'bfoobar-300x250', tagid: '5678', bidfloor: 0.5 } @@ -161,7 +279,7 @@ describe('synacormediaBidAdapter ', function () { pos: 0, w: 300, }, - id: '9876abcd~300x250', + id: 'b9876abcd-300x250', tagid: '1234', } ]); @@ -188,7 +306,7 @@ describe('synacormediaBidAdapter ', function () { pos: 0, w: 300, }, - id: '9876abcd~300x250', + id: 'b9876abcd-300x250', tagid: '1234', } ]); @@ -216,7 +334,7 @@ describe('synacormediaBidAdapter ', function () { w: 300, pos: 1, }, - id: '9876abcd~300x250', + id: 'b9876abcd-300x250', tagid: '1234' } ]); @@ -243,7 +361,7 @@ describe('synacormediaBidAdapter ', function () { w: 300, pos: 0, }, - id: '9876abcd~300x250', + id: 'b9876abcd-300x250', tagid: '1234' } ]); @@ -252,22 +370,58 @@ describe('synacormediaBidAdapter ', function () { expect(spec.buildRequests([], bidderRequest)).to.be.undefined; expect(spec.buildRequests([validBidRequest], null)).to.be.undefined; }); + + it('should return empty impression when there is no valid sizes in bidrequest', function() { + let validBidReqWithoutSize = { + bidId: '9876abcd', + sizes: [], + params: { + seatId: 'prebid', + placementId: '1234', + bidfloor: '0.50' + } + }; + + let validBidReqInvalidSize = { + bidId: '9876abcd', + sizes: [[300]], + params: { + seatId: 'prebid', + placementId: '1234', + bidfloor: '0.50' + } + }; + + let bidderRequest = { + auctionId: 'xyz123', + refererInfo: { + referer: 'https://test.com/foo/bar' + } + }; + + let req = spec.buildRequests([validBidReqWithoutSize], bidderRequest); + assert.isUndefined(req); + req = spec.buildRequests([validBidReqInvalidSize], bidderRequest); + assert.isUndefined(req); + }); }); describe('interpretResponse', function () { let bidResponse = { id: '10865933907263896~9998~0', - impid: '9876abcd~300x250', + impid: 'b9876abcd-300x250', price: 0.13, crid: '1022-250', - adm: '' + adm: '', + nurl: '//uat-net.technoratimedia.com/openrtb/tags?ID=k5JkFVQ1RJT05fSU1QX0lEPXYyZjczN&AUCTION_PRICE=${AUCTION_PRICE}' }; let bidResponse2 = { id: '10865933907263800~9999~0', - impid: '9876abcd~300x600', + impid: 'b9876abcd-300x600', price: 1.99, crid: '9993-013', - adm: '' + adm: '', + nurl: '//uat-net.technoratimedia.com/openrtb/tags?ID=OTk5OX4wJkFVQ1RJT05fU0VBVF9JR&AUCTION_PRICE=${AUCTION_PRICE}' }; let serverResponse; @@ -282,6 +436,52 @@ describe('synacormediaBidAdapter ', function () { } }; }); + + it('should return 1 video bid when 1 bid is in the video response', function () { + let serverRespVideo = { + body: { + id: 'abcd1234', + seatbid: [ + { + bid: [ + { + id: '11339128001692337~9999~0', + impid: 'v2da7322b2df61f-640x480', + price: 0.45, + nurl: 'https://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=${AUCTION_PRICE}', + adm: '\n\n\n\nSynacor Media Ad Server - 9999\nhttps://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=${AUCTION_PRICE}\n\n\n', + adomain: [ 'psacentral.org' ], + cid: 'bidder-crid', + crid: 'bidder-cid', + cat: [] + } + ], + seat: '9999' + } + ] + } + }; + + // serverResponse.body.seatbid[0].bid.push(bidResponse); + let resp = spec.interpretResponse(serverRespVideo); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.eql({ + requestId: '2da7322b2df61f', + adId: '11339128001692337-9999-0', + cpm: 0.45, + width: 640, + height: 480, + creativeId: '9999_bidder-cid', + currency: 'USD', + netRevenue: true, + mediaType: 'video', + ad: '\n\n\n\nSynacor Media Ad Server - 9999\nhttps://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45\n\n\n', + ttl: 60, + videoCacheKey: 'QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk', + vastUrl: 'https://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45' + }); + }); + it('should return 1 bid when 1 bid is in the response', function () { serverResponse.body.seatbid[0].bid.push(bidResponse); let resp = spec.interpretResponse(serverResponse); @@ -292,11 +492,11 @@ describe('synacormediaBidAdapter ', function () { cpm: 0.13, width: 300, height: 250, - creativeId: '9998~1022-250', + creativeId: '9998_1022-250', currency: 'USD', netRevenue: true, mediaType: BANNER, - ad: '', + ad: '', ttl: 60 }); }); @@ -315,31 +515,34 @@ describe('synacormediaBidAdapter ', function () { cpm: 0.13, width: 300, height: 250, - creativeId: '9998~1022-250', + creativeId: '9998_1022-250', currency: 'USD', netRevenue: true, mediaType: BANNER, - ad: '', + ad: '', ttl: 60 }); + expect(resp[1]).to.eql({ requestId: '9876abcd', adId: '10865933907263800-9999-0', cpm: 1.99, width: 300, height: 600, - creativeId: '9999~9993-013', + creativeId: '9999_9993-013', currency: 'USD', netRevenue: true, mediaType: BANNER, - ad: '', + 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;