From d2a036054571515c75d114a7766f8e5e192694c4 Mon Sep 17 00:00:00 2001 From: punkiller Date: Wed, 24 Mar 2021 01:40:53 -0700 Subject: [PATCH] IX Bid Adapter: add priceFloors support and an integration example (#6390) * Adding priceFloors Support to IX bid adapter and some refactoring * refactoring the getfloor logic, and removing dead code * newline at end of integration example * spacing issues lint fix * try catch around getFloor, in case the priceFloors module throws an exception * minor rename allU -> allu * falsey values fix, log prefix added --- integrationExamples/gpt/adUnitFloors.html | 112 ++++++++++++ modules/ixBidAdapter.js | 104 +++++++++-- test/spec/modules/ixBidAdapter_spec.js | 210 +++++++++++++++++++++- 3 files changed, 404 insertions(+), 22 deletions(-) create mode 100644 integrationExamples/gpt/adUnitFloors.html diff --git a/integrationExamples/gpt/adUnitFloors.html b/integrationExamples/gpt/adUnitFloors.html new file mode 100644 index 00000000000..bb48a20ef78 --- /dev/null +++ b/integrationExamples/gpt/adUnitFloors.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+ + + + diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 7ddad134451..f3514f66b9b 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -14,11 +14,14 @@ const CENT_TO_DOLLAR_FACTOR = 100; const BANNER_TIME_TO_LIVE = 300; const VIDEO_TIME_TO_LIVE = 3600; // 1hr const NET_REVENUE = true; + const PRICE_TO_DOLLAR_FACTOR = { JPY: 1 }; const USER_SYNC_URL = 'https://js-sec.indexww.com/um/ixmatch.html'; +const FLOOR_SOURCE = { PBJS: 'p', IX: 'x' }; + /** * Transform valid bid request config object to banner impression object that will be sent to ad server. * @@ -33,6 +36,8 @@ function bidToBannerImp(bid) { imp.banner.h = bid.params.size[1]; imp.banner.topframe = utils.inIframe() ? 0 : 1; + _applyFloor(bid, imp, BANNER); + return imp; } @@ -46,7 +51,7 @@ function bidToVideoImp(bid) { const imp = bidToImp(bid); const videoAdUnitRef = utils.deepAccess(bid, 'mediaTypes.video'); const context = utils.deepAccess(bid, 'mediaTypes.video.context'); - const videoAdUnitWhitelist = [ + const videoAdUnitAllowlist = [ 'mimes', 'minduration', 'maxduration', 'protocols', 'protocol', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', @@ -68,12 +73,14 @@ function bidToVideoImp(bid) { } } - for (let adUnitProperty in videoAdUnitRef) { - if (videoAdUnitWhitelist.indexOf(adUnitProperty) !== -1 && !imp.video.hasOwnProperty(adUnitProperty)) { + for (const adUnitProperty in videoAdUnitRef) { + if (videoAdUnitAllowlist.indexOf(adUnitProperty) !== -1 && !imp.video.hasOwnProperty(adUnitProperty)) { imp.video[adUnitProperty] = videoAdUnitRef[adUnitProperty]; } } + _applyFloor(bid, imp, VIDEO); + return imp; } @@ -92,12 +99,73 @@ function bidToImp(bid) { imp.ext.sid = `${bid.params.size[0]}x${bid.params.size[1]}`; } - if (bid.params.hasOwnProperty('bidFloor') && bid.params.hasOwnProperty('bidFloorCur')) { - imp.bidfloor = bid.params.bidFloor; - imp.bidfloorcur = bid.params.bidFloorCur; + return imp; +} + +/** + * Gets priceFloors floors and IX adapter floors, + * Validates and sets the higher one on the impression + * @param {object} bid bid object + * @param {object} imp impression object + * @param {string} mediaType the impression ad type, one of the SUPPORTED_AD_TYPES + */ +function _applyFloor(bid, imp, mediaType) { + let adapterFloor = null; + let moduleFloor = null; + + if (bid.params.bidFloor && bid.params.bidFloorCur) { + adapterFloor = { floor: bid.params.bidFloor, currency: bid.params.bidFloorCur }; } - return imp; + if (utils.isFn(bid.getFloor)) { + let _mediaType = '*'; + let _size = '*'; + + if (mediaType && utils.contains(SUPPORTED_AD_TYPES, mediaType)) { + const { w: width, h: height } = imp[mediaType]; + _mediaType = mediaType; + _size = [width, height]; + } + try { + moduleFloor = bid.getFloor({ + mediaType: _mediaType, + size: _size + }); + } catch (err) { + // continue with no module floors + utils.logWarn('priceFloors module call getFloor failed, error : ', err); + } + } + + if (adapterFloor && moduleFloor) { + if (adapterFloor.currency !== moduleFloor.currency) { + utils.logWarn('The bid floor currency mismatch between IX params and priceFloors module config'); + return; + } + + if (adapterFloor.floor > moduleFloor.floor) { + imp.bidfloor = adapterFloor.floor; + imp.bidfloorcur = adapterFloor.currency; + imp.ext.fl = FLOOR_SOURCE.IX; + } else { + imp.bidfloor = moduleFloor.floor; + imp.bidfloorcur = moduleFloor.currency; + imp.ext.fl = FLOOR_SOURCE.PBJS; + } + return; + } + + if (moduleFloor) { + imp.bidfloor = moduleFloor.floor; + imp.bidfloorcur = moduleFloor.currency; + imp.ext.fl = FLOOR_SOURCE.PBJS; + } else if (adapterFloor) { + imp.bidfloor = adapterFloor.floor; + imp.bidfloorcur = adapterFloor.currency; + imp.ext.fl = FLOOR_SOURCE.IX; + } else { + utils.logInfo('IX Bid Adapter: No floors available, no floors applied'); + } } /** @@ -270,7 +338,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { if (identityInfo.hasOwnProperty(partnerName)) { let response = identityInfo[partnerName]; if (!response.responsePending && response.data && typeof response.data === 'object' && - Object.keys(response.data).length && !eidInfo.seenSources[response.data.source]) { + Object.keys(response.data).length && !eidInfo.seenSources[response.data.source]) { userEids.push(response.data); } } @@ -498,7 +566,7 @@ function buildIXDiag(validBidRequests) { iu: 0, nu: 0, ou: 0, - allU: 0, + allu: 0, ren: false, version: '$prebid.version$' }; @@ -534,7 +602,7 @@ function buildIXDiag(validBidRequests) { ixdiag.iu++; } - ixdiag.allU++; + ixdiag.allu++; } } @@ -610,16 +678,19 @@ function updateMissingSizes(validBidRequest, missingBannerSizes, imp) { } /** - * + * @param {object} bid ValidBidRequest object, used to adjust floor * @param {object} imp Impression object to be modified * @param {array} newSize The new size to be applied * @return {object} newImp Updated impression object */ -function createMissingBannerImp(imp, newSize) { +function createMissingBannerImp(bid, imp, newSize) { const newImp = utils.deepClone(imp); newImp.ext.sid = `${newSize[0]}x${newSize[1]}`; newImp.banner.w = newSize[0]; newImp.banner.h = newSize[1]; + + _applyFloor(bid, newImp, BANNER); + return newImp; } @@ -658,7 +729,7 @@ export const spec = { } if (!includesSize(bid.sizes, paramsSize) && !((mediaTypeVideoPlayerSize && includesSize(mediaTypeVideoPlayerSize, paramsSize)) || - (mediaTypeBannerSizes && includesSize(mediaTypeBannerSizes, paramsSize)))) { + (mediaTypeBannerSizes && includesSize(mediaTypeBannerSizes, paramsSize)))) { utils.logError('ix bidder params: bid size is not included in ad unit sizes or player size.'); return false; } @@ -730,13 +801,12 @@ export const spec = { if (!videoImps[validBidRequest.transactionId].hasOwnProperty('ixImps')) { videoImps[validBidRequest.transactionId].ixImps = []; } - videoImps[validBidRequest.transactionId].ixImps.push(bidToVideoImp(validBidRequest)); } } if (validBidRequest.mediaType === BANNER || - (utils.deepAccess(validBidRequest, 'mediaTypes.banner') && includesSize(utils.deepAccess(validBidRequest, 'mediaTypes.banner.sizes'), validBidRequest.params.size)) || - (!validBidRequest.mediaType && !validBidRequest.mediaTypes)) { + (utils.deepAccess(validBidRequest, 'mediaTypes.banner') && includesSize(utils.deepAccess(validBidRequest, 'mediaTypes.banner.sizes'), validBidRequest.params.size)) || + (!validBidRequest.mediaType && !validBidRequest.mediaTypes)) { let imp = bidToBannerImp(validBidRequest); if (!bannerImps.hasOwnProperty(validBidRequest.transactionId)) { @@ -767,7 +837,7 @@ export const spec = { let origImp = missingBannerSizes[transactionId].impression; for (let i = 0; i < missingSizes.length; i++) { - let newImp = createMissingBannerImp(origImp, missingSizes[i]); + let newImp = createMissingBannerImp(validBidRequest, origImp, missingSizes[i]); bannerImps[transactionId].missingImps.push(newImp); bannerImps[transactionId].missingCount++; } diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 6d7b3a397bd..6b7f35e7fd4 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -26,7 +26,7 @@ describe('IndexexchangeAdapter', function () { } ] }; - var div_many_sizes = [ + const div_many_sizes = [ [300, 250], [600, 410], [336, 280], @@ -94,6 +94,62 @@ describe('IndexexchangeAdapter', function () { [600, 40], [600, 30] ]; + + const ONE_VIDEO = [ + { + bidder: 'ix', + params: { + siteId: '456', + video: { + skippable: false, + mimes: [ + 'video/mp4', + 'video/webm' + ], + minduration: 0, + maxduration: 60, + protocols: [2] + }, + size: [400, 100] + }, + sizes: [[400, 100]], + mediaTypes: { + video: { + context: 'instream', + playerSize: [[400, 100]] + } + }, + adUnitCode: 'div-gpt-ad-1460505748562-0', + transactionId: '173f49a8-7549-4218-a23c-e7ba59b47230', + bidId: '1a2b3c4e', + bidderRequestId: '11a22b33c44e', + auctionId: '1aa2bb3cc4de', + schain: SAMPLE_SCHAIN + } + ]; + + const ONE_BANNER = [ + { + bidder: 'ix', + params: { + siteId: '123', + size: [300, 250] + }, + sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + adUnitCode: 'div-gpt-ad-1460505748561-0', + transactionId: '173f49a8-7549-4218-a23c-e7ba59b47229', + bidId: '1a2b3c4d', + bidderRequestId: '11a22b33c44d', + auctionId: '1aa2bb3cc4dd', + schain: SAMPLE_SCHAIN + } + ]; + const DEFAULT_BANNER_VALID_BID = [ { bidder: 'ix', @@ -1057,6 +1113,84 @@ describe('IndexexchangeAdapter', function () { expect(impression.ext.sid).to.equal(sidValue); }); + it('video impression has #priceFloors floors', function () { + const bid = utils.deepClone(ONE_VIDEO[0]); + const flr = 5.5 + const floorInfo = {floor: flr, currency: 'USD'}; + bid.getFloor = function () { + return floorInfo; + } + + // check if floors are in imp + const requestBidFloor = spec.buildRequests([bid])[0]; + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + expect(imp1.bidfloor).to.equal(flr); + expect(imp1.bidfloorcur).to.equal('USD'); + expect(imp1.ext.fl).to.equal('p'); + }); + + it('banner imp has floors from #priceFloors module', function () { + const floor300x250 = 3.25 + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]) + + const floorInfo = { floor: floor300x250, currency: 'USD' }; + bid.getFloor = function () { + return floorInfo; + }; + + // check if floors are in imp + const requestBidFloor = spec.buildRequests([bid])[0]; + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + + expect(imp1.bidfloorcur).to.equal('USD'); + expect(imp1.bidfloor).to.equal(floor300x250); + expect(imp1.ext.fl).to.equal('p'); + }); + + it('ix adapter floors chosen over #priceFloors ', function () { + const bid = utils.deepClone(ONE_BANNER[0]); + + const floorhi = 4.5 + const floorlow = 3.5 + + bid.params.bidFloor = floorhi + bid.params.bidFloorCur = 'USD' + + const floorInfo = { floor: floorlow, currency: 'USD' }; + bid.getFloor = function () { + return floorInfo; + }; + + // check if floors are in imp + const requestBidFloor = spec.buildRequests([bid])[0]; + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + expect(imp1.bidfloor).to.equal(floorhi); + expect(imp1.bidfloorcur).to.equal(bid.params.bidFloorCur); + expect(imp1.ext.fl).to.equal('x'); + }); + + it(' #priceFloors floors chosen over ix adapter floors', function () { + const bid = utils.deepClone(ONE_BANNER[0]); + + const floorhi = 4.5 + const floorlow = 3.5 + + bid.params.bidFloor = floorlow + bid.params.bidFloorCur = 'USD' + + const floorInfo = { floor: floorhi, currency: 'USD' }; + bid.getFloor = function () { + return floorInfo; + }; + + // check if floors are in imp + const requestBidFloor = spec.buildRequests([bid])[0]; + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + expect(imp1.bidfloor).to.equal(floorhi); + expect(imp1.bidfloorcur).to.equal(bid.params.bidFloorCur); + expect(imp1.ext.fl).to.equal('p'); + }); + it('impression should have bidFloor and bidFloorCur if configured', function () { const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); bid.params.bidFloor = 50; @@ -1066,6 +1200,73 @@ describe('IndexexchangeAdapter', function () { expect(impression.bidfloor).to.equal(bid.params.bidFloor); expect(impression.bidfloorcur).to.equal(bid.params.bidFloorCur); + expect(impression.ext.fl).to.equal('x'); + }); + + it('missing sizes #priceFloors ', function () { + const bid = utils.deepClone(ONE_BANNER[0]); + bid.mediaTypes.banner.sizes.push([500, 400]) + + const floorInfo = { floor: 3.25, currency: 'USD' }; + bid.getFloor = function () { + return floorInfo; + }; + + sinon.spy(bid, 'getFloor'); + + const requestBidFloor = spec.buildRequests([bid])[0]; + // called getFloor with 300 x 250 + expect(bid.getFloor.getCall(0).args[0].mediaType).to.equal('banner'); + expect(bid.getFloor.getCall(0).args[0].size[0]).to.equal(300); + expect(bid.getFloor.getCall(0).args[0].size[1]).to.equal(250); + + // called getFloor with 500 x 400 + expect(bid.getFloor.getCall(1).args[0].mediaType).to.equal('banner'); + expect(bid.getFloor.getCall(1).args[0].size[0]).to.equal(500); + expect(bid.getFloor.getCall(1).args[0].size[1]).to.equal(400); + + // both will have same floors due to mock getFloor + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + expect(imp1.bidfloor).to.equal(3.25); + expect(imp1.bidfloorcur).to.equal('USD'); + + const imp2 = JSON.parse(requestBidFloor.data.r).imp[1]; + expect(imp2.bidfloor).to.equal(3.25); + expect(imp2.bidfloorcur).to.equal('USD'); + }); + + it('#pricefloors inAdUnit, banner impressions should have floors', function () { + const bid = utils.deepClone(ONE_BANNER[0]); + + const flr = 4.3 + bid.floors = { + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType', 'size'] + }, + values: { + 'banner|300x250': flr, + 'banner|600x500': 6.5, + 'banner|*': 7.5 + } + }; + const floorInfo = { floor: flr, currency: 'USD' }; + bid.getFloor = function () { + return floorInfo; + }; + + sinon.spy(bid, 'getFloor'); + + const requestBidFloor = spec.buildRequests([bid])[0]; + // called getFloor with 300 x 250 + expect(bid.getFloor.getCall(0).args[0].mediaType).to.equal('banner'); + expect(bid.getFloor.getCall(0).args[0].size[0]).to.equal(300); + expect(bid.getFloor.getCall(0).args[0].size[1]).to.equal(250); + + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + expect(imp1.bidfloor).to.equal(flr); + expect(imp1.bidfloorcur).to.equal('USD'); }); it('payload without mediaType should have correct format and value', function () { @@ -1145,7 +1346,6 @@ describe('IndexexchangeAdapter', function () { const requestWithFirstPartyData = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION)[0]; const pageUrl = JSON.parse(requestWithFirstPartyData.data.r).site.page; const expectedPageUrl = DEFAULT_OPTION.refererInfo.referer + '?ab=123&cd=123%23ab&e%2Ff=456&h%3Fg=456%23cd'; - expect(pageUrl).to.equal(expectedPageUrl); }); @@ -1492,7 +1692,7 @@ describe('IndexexchangeAdapter', function () { 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 () { + it('should not add video adunit level properties in imp object if they are not allowlisted', function () { const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); bid.mediaTypes.video.context = 'outstream'; bid.mediaTypes.video.random = true; @@ -1502,7 +1702,7 @@ describe('IndexexchangeAdapter', function () { 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 () { + it('should add allowlisted 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; @@ -1571,7 +1771,7 @@ describe('IndexexchangeAdapter', function () { expect(diagObj.ou).to.equal(1); expect(diagObj.ren).to.equal(false); expect(diagObj.mfu).to.equal(1); - expect(diagObj.allU).to.equal(1); + expect(diagObj.allu).to.equal(1); expect(diagObj.version).to.equal('$prebid.version$'); }); });