diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index cb37e4735c5..97341fbfd78 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -44,12 +44,20 @@ function bidToBannerImp(bid) { */ function bidToVideoImp(bid) { const imp = bidToImp(bid); + const videoAdUnitRef = utils.deepAccess(bid, 'mediaTypes.video'); + const context = utils.deepAccess(bid, 'mediaTypes.video.context'); + const videoAdUnitWhitelist = [ + 'mimes', 'minduration', 'maxduration', 'protocols', 'protocol', + 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', + 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', + 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', + 'delivery', 'pos', 'companionad', 'api', 'companiontype', 'ext' + ]; imp.video = utils.deepClone(bid.params.video) imp.video.w = bid.params.size[0]; imp.video.h = bid.params.size[1]; - const context = utils.deepAccess(bid, 'mediaTypes.video.context'); if (context) { if (context === 'instream') { imp.video.placement = 1; @@ -60,6 +68,12 @@ function bidToVideoImp(bid) { } } + for (let adUnitProperty in videoAdUnitRef) { + if (videoAdUnitWhitelist.indexOf(adUnitProperty) !== -1 && !imp.video.hasOwnProperty(adUnitProperty)) { + imp.video[adUnitProperty] = videoAdUnitRef[adUnitProperty]; + } + } + return imp; } @@ -285,6 +299,12 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r.ext.source = 'prebid'; r.ext.ixdiag = {}; + // getting ixdiags for adunits of the video, outstream & multi format (MF) style + let ixdiag = buildIXDiag(validBidRequests); + for (var key in ixdiag) { + r.ext.ixdiag[key] = ixdiag[key]; + } + // if an schain is provided, send it along if (validBidRequests[0].schain) { r.source = { @@ -382,7 +402,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { data: payload }; - const BASE_REQ_SIZE = new Blob([`${request.url}${utils.parseQueryStringParameters({...request.data, r: JSON.stringify(r)})}`]).size; + const BASE_REQ_SIZE = new Blob([`${request.url}${utils.parseQueryStringParameters({ ...request.data, r: JSON.stringify(r) })}`]).size; let currReqSize = BASE_REQ_SIZE; const MAX_REQ_SIZE = 8000; @@ -466,6 +486,66 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { return requests; } + +/** + * Calculates IX diagnostics values and packages them into an object + * + * @param {array} validBidRequests The valid bid requests from prebid + * @return {Object} IX diag values for ad units + */ +function buildIXDiag(validBidRequests) { + var adUnitMap = validBidRequests + .map(bidRequest => bidRequest.transactionId) + .filter((value, index, arr) => arr.indexOf(value) === index) + + var ixdiag = { + mfu: 0, + bu: 0, + iu: 0, + nu: 0, + ou: 0, + allU: 0, + ren: false + }; + + // create ad unit map and collect the required diag properties + for (let i = 0; i < adUnitMap.length; i++) { + var bid = validBidRequests.filter(bidRequest => bidRequest.transactionId === adUnitMap[i])[0]; + + if (utils.deepAccess(bid, 'mediaTypes')) { + if (Object.keys(bid.mediaTypes).length > 1) { + ixdiag.mfu++; + } + + if (utils.deepAccess(bid, 'mediaTypes.native')) { + ixdiag.nu++; + } + + if (utils.deepAccess(bid, 'mediaTypes.banner')) { + ixdiag.bu++; + } + + if (utils.deepAccess(bid, 'mediaTypes.video.context') === 'outstream') { + ixdiag.ou++; + // renderer only needed for outstream + + const hasRenderer = typeof (utils.deepAccess(bid, 'renderer') || utils.deepAccess(bid, 'mediaTypes.video.renderer')) === 'object'; + + // if any one ad unit is missing renderer, set ren status to false in diag + ixdiag.ren = ixdiag.ren && hasRenderer ? (utils.deepAccess(ixdiag, 'ren')) : hasRenderer; + } + + if (utils.deepAccess(bid, 'mediaTypes.video.context') === 'instream') { + ixdiag.iu++; + } + + ixdiag.allU++; + } + } + + return ixdiag; +} + /** * * @param {Object} impressions containing ixImps and possibly missingImps @@ -525,7 +605,8 @@ function updateMissingSizes(validBidRequest, missingBannerSizes, imp) { if (utils.deepAccess(validBidRequest, 'mediaTypes.banner.sizes')) { let sizeList = utils.deepClone(validBidRequest.mediaTypes.banner.sizes); removeFromSizes(sizeList, validBidRequest.params.size); - let newAdUnitEntry = { 'missingSizes': sizeList, + let newAdUnitEntry = { + 'missingSizes': sizeList, 'impression': imp }; missingBannerSizes[transactionID] = newAdUnitEntry; @@ -560,32 +641,57 @@ export const spec = { * @return {boolean} True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { + const paramsVideoRef = utils.deepAccess(bid, 'params.video'); + const paramsSize = utils.deepAccess(bid, 'params.size'); + const mediaTypeBannerSizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes'); + const mediaTypeVideoRef = utils.deepAccess(bid, 'mediaTypes.video'); + const mediaTypeVideoPlayerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); + const hasBidFloor = bid.params.hasOwnProperty('bidFloor'); + const hasBidFloorCur = bid.params.hasOwnProperty('bidFloorCur'); + if (!isValidSize(bid.params.size)) { utils.logError('ix bidder params: bid size has invalid format.'); return false; } - if (!includesSize(bid.sizes, bid.params.size)) { - utils.logError('ix bidder params: bid size is not included in ad unit sizes.'); + if (bid.hasOwnProperty('mediaType') && !(utils.contains(SUPPORTED_AD_TYPES, bid.mediaType))) { return false; } - if (bid.hasOwnProperty('mediaType') && !(utils.contains(SUPPORTED_AD_TYPES, bid.mediaType))) { + if (bid.hasOwnProperty('mediaTypes') && !(mediaTypeBannerSizes || mediaTypeVideoPlayerSize)) { return false; } - if (bid.hasOwnProperty('mediaTypes') && !(utils.deepAccess(bid, 'mediaTypes.banner.sizes') || utils.deepAccess(bid, 'mediaTypes.video.playerSize'))) { + if (!includesSize(bid.sizes, paramsSize) && !((mediaTypeVideoPlayerSize && includesSize(mediaTypeVideoPlayerSize, paramsSize)) || + (mediaTypeBannerSizes && includesSize(mediaTypeBannerSizes, paramsSize)))) { + utils.logError('ix bidder params: bid size is not included in ad unit sizes or player size.'); return false; } + if (mediaTypeVideoRef && paramsVideoRef) { + const requiredIXParams = ['mimes', 'minduration', 'maxduration', 'protocols']; + let isParamsLevelValid = true; + for (let property of requiredIXParams) { + if (!mediaTypeVideoRef.hasOwnProperty(property) && !paramsVideoRef.hasOwnProperty(property)) { + const isProtocolsValid = (property === 'protocols' && (mediaTypeVideoRef.hasOwnProperty('protocol') || paramsVideoRef.hasOwnProperty('protocol'))); + if (isProtocolsValid) { + continue; + } + utils.logError('ix bidder params: ' + property + ' is not included in either the adunit or params level'); + isParamsLevelValid = false; + } + } + + if (!isParamsLevelValid) { + return false; + } + } + if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { utils.logError('ix bidder params: siteId must be string or number value.'); return false; } - const hasBidFloor = bid.params.hasOwnProperty('bidFloor'); - const hasBidFloorCur = bid.params.hasOwnProperty('bidFloorCur'); - if (hasBidFloor || hasBidFloorCur) { if (!(hasBidFloor && hasBidFloorCur && isValidBidFloorParams(bid.params.bidFloor, bid.params.bidFloorCur))) { utils.logError('ix bidder params: bidFloor / bidFloorCur parameter has invalid format.'); @@ -616,7 +722,7 @@ export const spec = { detectMissingSizes: true, }; - const ixConfig = {...DEFAULT_IX_CONFIG, ...config.getConfig('ix')}; + const ixConfig = { ...DEFAULT_IX_CONFIG, ...config.getConfig('ix') }; for (let i = 0; i < validBidRequests.length; i++) { validBidRequest = validBidRequests[i]; @@ -631,11 +737,10 @@ export const spec = { } videoImps[validBidRequest.transactionId].ixImps.push(bidToVideoImp(validBidRequest)); - } else { - utils.logError('Bid size is not included in video playerSize') } } - if (validBidRequest.mediaType === BANNER || utils.deepAccess(validBidRequest, 'mediaTypes.banner') || + if (validBidRequest.mediaType === BANNER || + (utils.deepAccess(validBidRequest, 'mediaTypes.banner') && includesSize(utils.deepAccess(validBidRequest, 'mediaTypes.banner.sizes'), validBidRequest.params.size)) || (!validBidRequest.mediaType && !validBidRequest.mediaTypes)) { let imp = bidToBannerImp(validBidRequest); @@ -726,7 +831,7 @@ export const spec = { * @param {Boolean} isOpenRtb boolean to check openrtb2 protocol * @return {Object} params bid params */ - transformBidParams: function(params, isOpenRtb) { + transformBidParams: function (params, isOpenRtb) { return utils.convertTypes({ 'siteID': 'number' }, params); diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index 5b9903c91d2..c358b19a0a2 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -87,7 +87,7 @@ object are detailed here. | --- | --- | --- | --- | siteId | Required | String | An IX-specific identifier that is associated with a specific size on this ad unit. This is similar to a placement ID or an ad unit ID that some other modules have. Examples: `'3723'`, `'6482'`, `'3639'` | size | Required | Number[] | The single size associated with the site ID. It should be one of the sizes listed in the ad unit under `adUnits[].sizes` or `adUnits[].mediaTypes.video.playerSize`. Examples: `[300, 250]`, `[300, 600]` -| video | Required | Hash | The video object will serve as the properties of the video ad. You can create any field under the video object that is mentioned in the `OpenRTB Spec v2.5`. Some fields like `mimes, protocols, minduration, maxduration` are required. +| video | Required | Hash | The video object will serve as the properties of the video ad. You can create any field under the video object that is mentioned in the `OpenRTB Spec v2.5`. Some fields like `mimes, protocols, minduration, maxduration` are required. Properties not defined at this level, will be pulled from the Adunit level. | video.mimes | Required | String[] | Array list of content MIME types supported. Popular MIME types include, but are not limited to, `"video/x-ms- wmv"` for Windows Media and `"video/x-flv"` for Flash Video. |video.minduration| Required | Integer | Minimum video ad duration in seconds. |video.maxduration| Required | Integer | Maximum video ad duration in seconds. diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 7ac4bd94f9d..edbcc5725ac 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -126,7 +126,9 @@ describe('IndexexchangeAdapter', function () { 'video/mp4', 'video/webm' ], - minduration: 0 + minduration: 0, + maxduration: 60, + protocols: [2] }, size: [400, 100] }, @@ -146,6 +148,68 @@ describe('IndexexchangeAdapter', function () { } ]; + const DEFAULT_MULTIFORMAT_BANNER_VALID_BID = [ + { + bidder: 'ix', + params: { + siteId: '123', + size: [300, 250] + }, + sizes: [[300, 250], [300, 600]], + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[400, 100]] + }, + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + adUnitCode: 'div-gpt-ad-1460505748562-0', + transactionId: '173f49a8-7549-4218-a23c-e7ba59b47230', + bidId: '1a2b3c4e', + bidderRequestId: '11a22b33c44e', + auctionId: '1aa2bb3cc4de', + schain: SAMPLE_SCHAIN + } + ]; + + const DEFAULT_MULTIFORMAT_VIDEO_VALID_BID = [ + { + bidder: 'ix', + params: { + siteId: '456', + video: { + skippable: false, + mimes: [ + 'video/mp4', + 'video/webm' + ], + minduration: 0, + maxduration: 60, + protocols: [1] + }, + size: [400, 100] + }, + sizes: [[300, 250], [300, 600]], + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[400, 100]] + }, + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + adUnitCode: 'div-gpt-ad-1460505748562-0', + transactionId: '173f49a8-7549-4218-a23c-e7ba59b47230', + bidId: '1a2b3c4e', + bidderRequestId: '11a22b33c44e', + auctionId: '1aa2bb3cc4de', + schain: SAMPLE_SCHAIN + } + ]; + const DEFAULT_BANNER_BID_RESPONSE = { cur: 'USD', id: '11a22b33c44d', @@ -436,6 +500,16 @@ describe('IndexexchangeAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); + it('should return true for banner bid when there are multiple mediaTypes (banner, outstream)', function () { + const bid = utils.deepClone(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0]); + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true for video bid when there are multiple mediaTypes (banner, outstream)', function () { + const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false when there is only bidFloor', function () { const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); bid.params.bidFloor = 50; @@ -461,6 +535,43 @@ describe('IndexexchangeAdapter', function () { bid.params.bidFloorCur = 70; expect(spec.isBidRequestValid(bid)).to.equal(false); }); + + it('should return false when required video properties are missing on both adunit & param levels', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + delete bid.params.video.mimes; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when required video properties are at the adunit level', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + delete bid.params.video.mimes; + bid.mediaTypes.video.mimes = ['video/mp4']; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true if protocols exists but protocol doesn\'t', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + expect(spec.isBidRequestValid(bid)).to.equal(true); + delete bid.params.video.protocols; + bid.mediaTypes.video.protocols = 1; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true if protocol exists but protocols doesn\'t', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + delete bid.params.video.protocols; + bid.params.video.protocol = 1; + expect(spec.isBidRequestValid(bid)).to.equal(true); + delete bid.params.video.protocol; + bid.mediaTypes.video.protocol = 1; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if both protocol/protocols are missing', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + delete bid.params.video.protocols; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); }); describe('buildRequestsIdentity', function () { @@ -1292,6 +1403,101 @@ describe('IndexexchangeAdapter', function () { expect(impression.video.placement).to.exist; expect(impression.video.placement).to.equal(4); }); + + it('should not override video properties if they are already configured at the params video level', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + bid.mediaTypes.video.context = 'outstream'; + bid.mediaTypes.video.protocols = [1]; + bid.mediaTypes.video.mimes = ['video/override']; + const request = spec.buildRequests([bid])[0]; + const impression = JSON.parse(request.data.r).imp[0]; + + expect(impression.video.protocols[0]).to.equal(2); + expect(impression.video.mimes[0]).to.not.equal('video/override'); + }); + + it('should not add video adunit level properties in imp object if they are not whitelisted', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + bid.mediaTypes.video.context = 'outstream'; + bid.mediaTypes.video.random = true; + const request = spec.buildRequests([bid])[0]; + const impression = JSON.parse(request.data.r).imp[0]; + + expect(impression.video.random).to.not.exist; + }); + + it('should add whitelisted adunit level video properties in imp object if they are not configured at params level', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + bid.mediaTypes.video.context = 'outstream'; + delete bid.params.video.protocols; + delete bid.params.video.mimes; + bid.mediaTypes.video.protocols = [6]; + bid.mediaTypes.video.mimes = ['video/mp4']; + bid.mediaTypes.video.api = 2; + const request = spec.buildRequests([bid])[0]; + const impression = JSON.parse(request.data.r).imp[0]; + + expect(impression.video.protocols[0]).to.equal(6); + expect(impression.video.api).to.equal(2); + expect(impression.video.mimes[0]).to.equal('video/mp4'); + }); + }); + + describe('buildRequestMultiFormat', function () { + describe('only banner bidder params set', function () { + const request = spec.buildRequests(DEFAULT_MULTIFORMAT_BANNER_VALID_BID) + + const bannerImp = JSON.parse(request[0].data.r).imp[0]; + expect(JSON.parse(request[0].data.r).imp).to.have.lengthOf(2); + expect(JSON.parse(request[0].data.v)).to.equal(BANNER_ENDPOINT_VERSION); + expect(bannerImp.id).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].bidId); + expect(bannerImp.banner).to.exist; + expect(bannerImp.banner.w).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].params.size[0]); + expect(bannerImp.banner.h).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].params.size[1]); + }); + + describe('only video bidder params set', function () { + const request = spec.buildRequests(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID); + + const videoImp = JSON.parse(request[0].data.r).imp[0]; + expect(JSON.parse(request[0].data.r).imp).to.have.lengthOf(1); + expect(JSON.parse(request[0].data.v)).to.equal(VIDEO_ENDPOINT_VERSION); + expect(videoImp.id).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].bidId); + expect(videoImp.video).to.exist; + expect(videoImp.video.w).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].params.size[0]); + expect(videoImp.video.h).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].params.size[1]); + }); + describe('both banner and video bidder params set', function () { + const request = spec.buildRequests([DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0], DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]]); + + it('should return valid banner and video requests', function () { + const bannerImp = JSON.parse(request[0].data.r).imp[0]; + expect(JSON.parse(request[0].data.r).imp).to.have.lengthOf(2); + expect(JSON.parse(request[0].data.v)).to.equal(BANNER_ENDPOINT_VERSION); + expect(bannerImp.id).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].bidId); + expect(bannerImp.banner).to.exist; + expect(bannerImp.banner.w).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].params.size[0]); + expect(bannerImp.banner.h).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].params.size[1]); + + const videoImp = JSON.parse(request[1].data.r).imp[0]; + expect(JSON.parse(request[1].data.r).imp).to.have.lengthOf(1); + expect(JSON.parse(request[1].data.v)).to.equal(VIDEO_ENDPOINT_VERSION); + expect(videoImp.id).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].bidId); + expect(videoImp.video).to.exist; + expect(videoImp.video.w).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].params.size[0]); + expect(videoImp.video.h).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].params.size[1]); + }); + + it('should contain all correct IXdiag properties', function () { + const diagObj = JSON.parse(request[0].data.r).ext.ixdiag; + expect(diagObj.iu).to.equal(0); + expect(diagObj.nu).to.equal(0); + expect(diagObj.ou).to.equal(1); + expect(diagObj.ren).to.equal(false); + expect(diagObj.mfu).to.equal(1); + expect(diagObj.allU).to.equal(1); + }); + }); }); describe('interpretResponse', function () {