diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index df4f9a9ba38..a5a93024ff9 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -8,7 +8,7 @@ const GVLID = 28; const BIDDER_CODE = 'triplelift'; const STR_ENDPOINT = 'https://tlx.3lift.com/header/auction?'; const BANNER_TIME_TO_LIVE = 300; -const INSTREAM_TIME_TO_LIVE = 3600; +const VIDEO_TIME_TO_LIVE = 3600; let gdprApplies = true; let consentString = null; export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); @@ -120,12 +120,15 @@ function _buildPostBody(bidRequests, bidderRequest) { tagid: bidRequest.params.inventoryCode, floor: _getFloor(bidRequest) }; - // remove the else to support multi-imp - if (_isInstreamBidRequest(bidRequest)) { + // Check for video bidrequest + if (_isVideoBidRequest(bidRequest)) { imp.video = _getORTBVideo(bidRequest); - } else if (bidRequest.mediaTypes.banner) { + } + // append banner if applicable and request is not for instream + if (bidRequest.mediaTypes.banner && !_isInstream(bidRequest)) { imp.banner = { format: _sizes(bidRequest.sizes) }; - }; + } + if (!isEmpty(bidRequest.ortb2Imp)) { imp.fpd = _getAdUnitFpd(bidRequest.ortb2Imp); } @@ -153,22 +156,41 @@ function _buildPostBody(bidRequests, bidderRequest) { return data; } -function _isInstreamBidRequest(bidRequest) { - if (!bidRequest.mediaTypes.video) return false; - if (!bidRequest.mediaTypes.video.context) return false; - if (bidRequest.mediaTypes.video.context.toLowerCase() === 'instream') { - return true; - } else { - return false; - } +function _isVideoBidRequest(bidRequest) { + return _isValidVideoObject(bidRequest) && (_isInstream(bidRequest) || _isOutstream(bidRequest)); +} + +function _isOutstream(bidRequest) { + return _isValidVideoObject(bidRequest) && bidRequest.mediaTypes.video.context.toLowerCase() === 'outstream'; +} + +function _isInstream(bidRequest) { + return _isValidVideoObject(bidRequest) && bidRequest.mediaTypes.video.context.toLowerCase() === 'instream'; +} + +function _isValidVideoObject(bidRequest) { + return bidRequest.mediaTypes.video && bidRequest.mediaTypes.video.context; } function _getORTBVideo(bidRequest) { // give precedent to mediaTypes.video let video = { ...bidRequest.params.video, ...bidRequest.mediaTypes.video }; - if (!video.w) video.w = video.playerSize[0][0]; - if (!video.h) video.h = video.playerSize[0][1]; + try { + if (!video.w) video.w = video.playerSize[0][0]; + if (!video.h) video.h = video.playerSize[0][1]; + } catch (err) { + logWarn('Video size not defined', err); + } if (video.context === 'instream') video.placement = 1; + if (video.context === 'outstream') { + if (!video.placement) { + video.placement = 3 + } else if ([3, 4, 5].indexOf(video.placement) === -1) { + logMessage(`video.placement value of ${video.placement} is invalid for outstream context. Setting placement to 3`) + video.placement = 3 + } + } + // clean up oRTB object delete video.playerSize; return video; @@ -180,7 +202,7 @@ function _getFloor (bid) { try { const floorInfo = bid.getFloor({ currency: 'USD', - mediaType: _isInstreamBidRequest(bid) ? 'video' : 'banner', + mediaType: _isVideoBidRequest(bid) ? 'video' : 'banner', size: '*' }); if (typeof floorInfo === 'object' && @@ -366,10 +388,10 @@ function _buildResponseObject(bidderRequest, bid) { meta: {} }; - if (_isInstreamBidRequest(breq)) { + if (_isVideoBidRequest(breq) && bid.media_type === 'video') { bidResponse.vastXml = bid.ad; bidResponse.mediaType = 'video'; - bidResponse.ttl = INSTREAM_TIME_TO_LIVE; + bidResponse.ttl = VIDEO_TIME_TO_LIVE; }; if (bid.advertiser_name) { @@ -381,7 +403,11 @@ function _buildResponseObject(bidderRequest, bid) { } if (bid.tl_source && bid.tl_source == 'hdx') { - bidResponse.meta.mediaType = 'banner'; + if (_isVideoBidRequest(breq) && bid.media_type === 'video') { + bidResponse.meta.mediaType = 'video' + } else { + bidResponse.meta.mediaType = 'banner' + } } if (bid.tl_source && bid.tl_source == 'tlx') { diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index b0269aaf077..bfcfce1ccb4 100644 --- a/test/spec/modules/tripleliftBidAdapter_spec.js +++ b/test/spec/modules/tripleliftBidAdapter_spec.js @@ -345,6 +345,209 @@ describe('triplelift adapter', function () { auctionId: '1d1a030790a475', userId: {}, schain, + }, + // outstream video only + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, + adUnitCode: 'adunit-code-outstream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // banner and incomplete outstream (missing size) + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6 + } + }, + mediaTypes: { + video: { + context: 'outstream' + }, + banner: { + sizes: [ + [970, 250], + [1, 1] + ] + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // outstream video; valid placement + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 3 + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // outstream video; valid placement + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 4 + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // outstream video; valid placement + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 5 + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // outstream video; undefined placement + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // outstream video; invalid placement + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 6 + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, } ]; @@ -415,13 +618,16 @@ describe('triplelift adapter', function () { expect(payload.imp[0].tagid).to.equal('12345'); expect(payload.imp[0].floor).to.equal(1.0); expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + // instream expect(payload.imp[1].tagid).to.equal('insteam_test'); expect(payload.imp[1].floor).to.equal(1.0); expect(payload.imp[1].video).to.exist.and.to.be.a('object'); + expect(payload.imp[1].video.placement).to.equal(1); // banner and outstream video - expect(payload.imp[2]).to.not.have.property('video'); + expect(payload.imp[2]).to.have.property('video'); expect(payload.imp[2]).to.have.property('banner'); expect(payload.imp[2].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + expect(payload.imp[2].video).to.deep.equal({'mimes': ['video/mp4'], 'maxduration': 30, 'minduration': 6, 'w': 640, 'h': 480, 'context': 'outstream', 'placement': 3}); // banner and incomplete video expect(payload.imp[3]).to.not.have.property('video'); expect(payload.imp[3]).to.have.property('banner'); @@ -434,10 +640,51 @@ describe('triplelift adapter', function () { expect(payload.imp[5]).to.not.have.property('banner'); expect(payload.imp[5]).to.have.property('video'); expect(payload.imp[5].video).to.exist.and.to.be.a('object'); + expect(payload.imp[5].video.placement).to.equal(1); // banner and outream video and native - expect(payload.imp[6]).to.not.have.property('video'); + expect(payload.imp[6]).to.have.property('video'); expect(payload.imp[6]).to.have.property('banner'); expect(payload.imp[6].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + expect(payload.imp[6].video).to.deep.equal({'mimes': ['video/mp4'], 'maxduration': 30, 'minduration': 6, 'w': 640, 'h': 480, 'context': 'outstream', 'placement': 3}); + // outstream video only + expect(payload.imp[7]).to.have.property('video'); + expect(payload.imp[7]).to.not.have.property('banner'); + expect(payload.imp[7].video).to.deep.equal({'mimes': ['video/mp4'], 'maxduration': 30, 'minduration': 6, 'w': 640, 'h': 480, 'context': 'outstream', 'placement': 3}); + // banner and incomplete outstream (missing size); video request is permitted so banner can still monetize + expect(payload.imp[8]).to.have.property('video'); + expect(payload.imp[8]).to.have.property('banner'); + expect(payload.imp[8].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + expect(payload.imp[8].video).to.deep.equal({'mimes': ['video/mp4'], 'maxduration': 30, 'minduration': 6, 'context': 'outstream', 'placement': 3}); + }); + + it('should check for valid outstream placement values', function () { + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + const payload = request.data; + // outstream video; valid placement + expect(payload.imp[9]).to.not.have.property('banner'); + expect(payload.imp[9]).to.have.property('video'); + expect(payload.imp[9].video).to.exist.and.to.be.a('object'); + expect(payload.imp[9].video.placement).to.equal(3); + // outstream video; valid placement + expect(payload.imp[10]).to.not.have.property('banner'); + expect(payload.imp[10]).to.have.property('video'); + expect(payload.imp[10].video).to.exist.and.to.be.a('object'); + expect(payload.imp[10].video.placement).to.equal(4); + // outstream video; valid placement + expect(payload.imp[11]).to.not.have.property('banner'); + expect(payload.imp[11]).to.have.property('video'); + expect(payload.imp[11].video).to.exist.and.to.be.a('object'); + expect(payload.imp[11].video.placement).to.equal(5); + // outstream video; undefined placement + expect(payload.imp[12]).to.not.have.property('banner'); + expect(payload.imp[12]).to.have.property('video'); + expect(payload.imp[12].video).to.exist.and.to.be.a('object'); + expect(payload.imp[12].video.placement).to.equal(3); + // outstream video; invalid placement + expect(payload.imp[13]).to.not.have.property('banner'); + expect(payload.imp[13]).to.have.property('video'); + expect(payload.imp[13].video).to.exist.and.to.be.a('object'); + expect(payload.imp[13].video.placement).to.equal(3); }); it('should add tdid to the payload if included', function () { @@ -913,14 +1160,40 @@ describe('triplelift adapter', function () { iurl: 'https://s.adroll.com/a/IYR/N36/IYRN366MFVDITBAGNNT5U6.jpg', tl_source: 'tlx', advertiser_name: 'fake advertiser name', - adomain: ['basspro.com', 'internetalerts.org'] + adomain: ['basspro.com', 'internetalerts.org'], + media_type: 'banner' }, { imp_id: 1, crid: '10092_76480_i2j6qm8u', cpm: 9.99, - ad: 'The Trade Desk', - tlx_source: 'hdx' + ad: 'The Trade Desk', + tl_source: 'hdx', + media_type: 'video' + }, + // video bid on banner+outstream request + { + imp_id: 2, + crid: '5989_33264_352817187', + cpm: 20, + ad: '\n \n \t', + tl_source: 'hdx', + advertiser_name: 'zennioptical.com', + adomain: ['zennioptical.com'], + media_type: 'video' + }, + // banner bid on banner+outstream request + { + imp_id: 3, + crid: '5989_33264_352817187', + cpm: 20, + width: 970, + height: 250, + ad: 'ad-markup', + tl_source: 'hdx', + advertiser_name: 'zennioptical.com', + adomain: ['zennioptical.com'], + media_type: 'banner' } ] } @@ -946,13 +1219,13 @@ describe('triplelift adapter', function () { ] } }, - bidId: '30b31c1838de1e', + bidId: '30b31c1838de1e' }, { imp_id: 1, crid: '10092_76480_i2j6qm8u', cpm: 9.99, - ad: 'The Trade Desk', + ad: 'The Trade Desk', tlx_source: 'hdx', mediaTypes: { video: { @@ -960,7 +1233,89 @@ describe('triplelift adapter', function () { playerSize: [640, 480] } }, - bidId: '30b31c1838de1e', + bidId: '30b31c1838de1e' + }, + // banner and outstream + { + bidder: 'triplelift', + params: { + inventoryCode: 'testing_desktop_outstream', + floor: 1 + }, + nativeParams: {}, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1 + }, + banner: { + sizes: [ + [728, 90], + [970, 250], + [970, 90] + ] + }, + native: {} + }, + adUnitCode: 'video-outstream', + transactionId: '135061c3-f546-4e28-8a07-44c2fb58a958', + sizes: [ + [728, 90], + [970, 250], + [970, 90] + ], + bidId: '73edc0ba8de203', + bidderRequestId: '3d81143328560b', + auctionId: 'f6427dc0-b954-4010-a76c-d498380796a2', + src: 'client', + bidRequestsCount: 2, + bidderRequestsCount: 2, + bidderWinsCount: 0 + }, + // banner and outstream + { + bidder: 'triplelift', + params: { + inventoryCode: 'testing_desktop_outstream', + floor: 1 + }, + nativeParams: {}, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1 + }, + banner: { + sizes: [ + [728, 90], + [970, 250], + [970, 90] + ] + }, + native: {} + }, + adUnitCode: 'video-outstream', + transactionId: '135061c3-f546-4e28-8a07-44c2fb58a958', + sizes: [ + [728, 90], + [970, 250], + [970, 90] + ], + bidId: '73edc0ba8de203', + bidderRequestId: '3d81143328560b', + auctionId: 'f6427dc0-b954-4010-a76c-d498380796a2', + src: 'client', + bidRequestsCount: 2, + bidderRequestsCount: 2, + bidderWinsCount: 0 } ], refererInfo: { @@ -1007,16 +1362,29 @@ describe('triplelift adapter', function () { } ]; let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); - expect(result).to.have.length(2); + expect(result).to.have.length(4); expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); expect(Object.keys(result[1])).to.have.members(Object.keys(expectedResponse[1])); expect(result[0].ttl).to.equal(300); expect(result[1].ttl).to.equal(3600); }); + it('should identify format of bid and respond accordingly', function() { + let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); + expect(result[0].meta.mediaType).to.equal('native'); + expect(result[1].mediaType).to.equal('video'); + expect(result[1].meta.mediaType).to.equal('video'); + // video bid on banner+outstream request + expect(result[2].mediaType).to.equal('video'); + expect(result[2].meta.mediaType).to.equal('video'); + expect(result[2].vastXml).to.include('aid=148508128401385324170&inv_code=testing_mobile_outstream'); + // banner bid on banner+outstream request + expect(result[3].meta.mediaType).to.equal('banner'); + }) + it('should return multiple responses to support SRA', function () { let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); - expect(result).to.have.length(2); + expect(result).to.have.length(4); }); it('should include the advertiser name in the meta field if available', function () {