From b8ac8d5513ad12eb9a267d76e59df988b71c65be Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Wed, 30 Sep 2020 11:28:21 -0400 Subject: [PATCH 1/7] Update to floors module to allow floorMin definition using setConfig({floors:...}); 1) If floorMin exists, set floorValue to new property floorRuleValue. 2) If floorMin is greater than floorValue, set floorValue to floorMin. Update to Rubicon Analytics Adapter to pass floorMin under auction.floors.floorMin if exists. Also includes update to pass floorRuleValue for each bid if floorMin exists Update to floorsModule roundup functionality to fix to one decimal place prior to roundup. This will fix issues in which JS evalutates a whole number to include a very small decimal value that forces a roundup to the next whole number. --- modules/priceFloors.js | 57 ++++++++- modules/rubiconAnalyticsAdapter.js | 6 + package-lock.json | 2 +- test/spec/modules/priceFloors_spec.js | 84 +++++++++++++ .../modules/rubiconAnalyticsAdapter_spec.js | 111 +++++++++++++++++- 5 files changed, 253 insertions(+), 7 deletions(-) diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 1b865e05c0a..d601d310ad7 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -55,7 +55,7 @@ export let _floorDataForAuction = {}; * @summary Simple function to round up to a certain decimal degree */ function roundUp(number, precision) { - return Math.ceil(parseFloat(number) * Math.pow(10, precision)) / Math.pow(10, precision); + return Math.ceil((parseFloat(number) * Math.pow(10, precision)).toFixed(1)) / Math.pow(10, precision); } let referrerHostname; @@ -98,7 +98,7 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let fieldValues = enumeratePossibleFieldValues(utils.deepAccess(floorData, 'schema.fields') || [], bidObject, responseObject); if (!fieldValues.length) return { matchingFloor: floorData.default }; - // look to see iof a request for this context was made already + // look to see if a request for this context was made already let matchingInput = fieldValues.map(field => field[0]).join('-'); // if we already have gotten the matching rule from this matching input then use it! No need to look again let previousMatch = utils.deepAccess(floorData, `matchingInputs.${matchingInput}`); @@ -113,6 +113,14 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters matchingRule }; + + // check that floorMin is greater than 0 and set values accordingly + if (utils.deepAccess(_floorsConfig, 'floorMin')) { + matchingData.floorMin = utils.deepAccess(_floorsConfig, 'floorMin'); + matchingData.floorRuleValue = matchingData.matchingFloor; + if (matchingData.floorMin > matchingData.floorRuleValue) matchingData.matchingFloor = matchingData.floorMin; + } + // save for later lookup if needed utils.deepSetValue(floorData, `matchingInputs.${matchingInput}`, {...matchingData}); return matchingData; @@ -189,10 +197,12 @@ function updateRequestParamsFromContext(bidRequest, requestParams) { export function getFloor(requestParams = {currency: 'USD', mediaType: '*', size: '*'}) { let bidRequest = this; let floorData = _floorDataForAuction[bidRequest.auctionId]; + if (!floorData || floorData.skipped) return {}; requestParams = updateRequestParamsFromContext(bidRequest, requestParams); let floorInfo = getFirstMatchingFloor(floorData.data, {...bidRequest}, {mediaType: requestParams.mediaType, size: requestParams.size}); + let currency = requestParams.currency || floorData.data.currency; // if bidder asked for a currency which is not what floors are set in convert @@ -230,6 +240,7 @@ export function getFloorsDataForAuction(floorData, adUnitCode) { auctionFloorData.values = normalizeRulesForAuction(auctionFloorData, adUnitCode); // default the currency to USD if not passed in auctionFloorData.currency = auctionFloorData.currency || 'USD'; + return auctionFloorData; } @@ -282,6 +293,37 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { } else { bid.getFloor = getFloor; } + + // information for bid and analytics adapters + bid.auctionId = auctionId; + bid.floorData = { + skipped: floorData.skipped, + skipRate: floorData.skipRate, + modelVersion: utils.deepAccess(floorData, 'data.modelVersion'), + location: utils.deepAccess(floorData, 'data.location', 'noData'), + floorProvider: floorData.floorProvider, + fetchStatus: _floorsConfig.fetchStatus + }; + + if (utils.deepAccess(floorData, 'floorMin')) { + bid.floorData.floorMin = utils.deepAccess(floorData, 'floorMin'); + } + }); + }); +} + +/** + * @summary This function takes the adUnits for the auction and update them accordingly as well as returns the rules hashmap for the auction + */ +export function updateAdUnitFloorData(adUnits, floorData, auctionId) { + adUnits.forEach((adUnit) => { + adUnit.bids.forEach(bid => { + if (floorData.skipped) { + delete bid.getFloor; + } else { + bid.getFloor = getFloor; + } + // information for bid and analytics adapters bid.auctionId = auctionId; bid.floorData = { @@ -291,6 +333,10 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { location: utils.deepAccess(floorData, 'data.location', 'noData'), floorProvider: floorData.floorProvider, fetchStatus: _floorsConfig.fetchStatus + }; + + if (utils.deepAccess(floorData, 'floorMin')) { + bid.floorData.floorMin = utils.deepAccess(floorData, 'floorMin'); } }); }); @@ -568,6 +614,7 @@ function addFieldOverrides(overrides) { */ export function handleSetFloorsConfig(config) { _floorsConfig = utils.pick(config, [ + 'floorMin', floorMin => floorMin, 'enabled', enabled => enabled !== false, // defaults to true 'auctionDelay', auctionDelay => auctionDelay || 0, 'floorProvider', floorProvider => utils.deepAccess(config, 'data.floorProvider', floorProvider), @@ -628,6 +675,11 @@ function addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm) { enforcements: {...floorData.enforcement}, matchedFields: {} }; + + if (floorInfo.floorRuleValue) { + bid.floorData.floorRuleValue = floorInfo.floorRuleValue; + } + floorData.data.schema.fields.forEach((field, index) => { let matchedValue = floorInfo.matchingData.split(floorData.data.schema.delimiter)[index]; bid.floorData.matchedFields[field] = matchedValue; @@ -650,6 +702,7 @@ function shouldFloorBid(floorData, floorInfo, bid) { */ export function addBidResponseHook(fn, adUnitCode, bid) { let floorData = _floorDataForAuction[this.bidderRequest.auctionId]; + // if no floor data or associated bidRequest then bail const matchingBidRequest = find(this.bidderRequest.bids, bidRequest => bidRequest.bidId && bidRequest.bidId === bid.requestId); if (!floorData || !bid || floorData.skipped || !matchingBidRequest) { diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 362c56b698a..12958397196 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -117,6 +117,7 @@ function sendMessage(auctionId, bidWonId) { 'dimensions', 'mediaType', 'floorValue', + 'floorRuleValue', 'floorRule' ]) : undefined ]); @@ -224,6 +225,8 @@ function sendMessage(auctionId, bidWonId) { 'dealsEnforced', () => utils.deepAccess(auctionCache.floorData, 'enforcements.floorDeals'), 'skipRate', 'fetchStatus', + 'floorMin', + 'floorRuleValue', 'floorProvider as provider' ]); } @@ -323,6 +326,7 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { if (previousBidResponse && previousBidResponse.bidPriceUSD > responsePrice) { return previousBidResponse; } + return utils.pick(bid, [ 'bidPriceUSD', () => responsePrice, 'dealId', @@ -335,6 +339,7 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { }, 'seatBidId', 'floorValue', () => utils.deepAccess(bid, 'floorData.floorValue'), + 'floorRuleValue', () => utils.deepAccess(bid, 'floorData.floorRuleValue'), 'floorRule', () => utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined ]); } @@ -499,6 +504,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.bidsWon = {}; cacheEntry.referrer = utils.deepAccess(args, 'bidderRequests.0.refererInfo.referer'); const floorData = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData'); + if (floorData) { cacheEntry.floorData = {...floorData}; } diff --git a/package-lock.json b/package-lock.json index 1784b885be9..3023392f08f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24486,4 +24486,4 @@ } } } -} +} \ No newline at end of file diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index ae45244f03d..bda43299905 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -46,6 +46,32 @@ describe('the price floors module', function () { }, data: basicFloorData } + const minFloorConfigHigh = { + enabled: true, + auctionDelay: 0, + floorMin: 7, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: basicFloorData + } + const minFloorConfigLow = { + enabled: true, + auctionDelay: 0, + floorMin: 2.3, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: basicFloorData + } const basicBidRequest = { bidder: 'rubicon', adUnitCode: 'test_div_1', @@ -181,6 +207,30 @@ describe('the price floors module', function () { matchingData: 'native', matchingRule: '*' }); + // banner with floorMin higher than matching rule + handleSetFloorsConfig({ + ...minFloorConfigHigh, + data: undefined + }); + expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 7, + floorRuleValue: 1.0, + matchingFloor: 7, + matchingData: 'banner', + matchingRule: 'banner' + }); + // banner with floorMin higher than matching rule + handleSetFloorsConfig({ + ...minFloorConfigLow, + data: undefined + }); + expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ + floorMin: 2.3, + floorRuleValue: 5, + matchingFloor: 5, + matchingData: 'video', + matchingRule: 'video' + }); }); it('does not alter cached matched input if conversion occurs', function () { let inputData = {...basicFloorData}; @@ -367,6 +417,40 @@ describe('the price floors module', function () { floorProvider: undefined }); }); + it('should use adUnit level data and minFloor should be set', function () { + handleSetFloorsConfig({ + ...minFloorConfigHigh, + data: undefined + }); + // attach floor data onto an adUnit and run an auction + let adUnitWithFloors1 = { + ...getAdUnitMock('adUnit-Div-1'), + floors: { + ...basicFloorData, + modelVersion: 'adUnit Model Version', // change the model name + } + }; + let adUnitWithFloors2 = { + ...getAdUnitMock('adUnit-Div-2'), + floors: { + ...basicFloorData, + values: { + 'banner': 5.0, + '*': 10.4 + } + } + }; + runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]); + validateBidRequests(true, { + skipped: false, + modelVersion: 'adUnit Model Version', + location: 'adUnit', + skipRate: 0, + floorMin: 7, + fetchStatus: undefined, + floorProvider: undefined + }); + }); it('bidRequests should have getFloor function and flooring meta data when setConfig occurs', function () { handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider'}); runStandardAuction(); diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 344f08823f8..5fb68bc0b09 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -113,10 +113,60 @@ const BID2 = Object.assign({}, BID, { } }); +const BID3 = Object.assign({}, BID, { + adUnitCode: '/19968336/siderail-tag1', + bidId: '5fg6hyy4r879f0', + adId: 'fake_ad_id', + requestId: '5fg6hyy4r879f0', + width: 300, + height: 250, + mediaType: 'banner', + cpm: 2.01, + source: 'server', + seatBidId: 'aaaa-bbbb-cccc-dddd', + rubiconTargeting: { + 'rpfl_elemid': '/19968336/siderail-tag1', + 'rpfl_14062': '15_tier0200' + }, + adserverTargeting: { + 'hb_bidder': 'rubicon', + 'hb_adid': '5fg6hyy4r879f0', + 'hb_pb': '2.00', + 'hb_size': '300x250', + 'hb_source': 'server' + } +}); + +const floorMinRequest = { + 'bidder': 'rubicon', + 'params': { + 'accountId': '14062', + 'siteId': '70608', + 'zoneId': '335918', + 'userId': '12346', + 'keywords': ['a', 'b', 'c'], + 'inventory': {'rating': '4-star', 'prodtype': 'tech'}, + 'visitor': {'ucat': 'new', 'lastsearch': 'iphone'}, + 'position': 'atf' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': '/19968336/siderail-tag1', + 'transactionId': 'c435626g-9e3f-401a-bee1-d56aec29a1d4', + 'sizes': [[300, 250]], + 'bidId': '5fg6hyy4r879f0', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' +}; + const MOCK = { SET_TARGETING: { [BID.adUnitCode]: BID.adserverTargeting, - [BID2.adUnitCode]: BID2.adserverTargeting + [BID2.adUnitCode]: BID2.adserverTargeting, + [BID3.adUnitCode]: BID3.adserverTargeting }, AUCTION_INIT: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', @@ -243,7 +293,8 @@ const MOCK = { }, BID_RESPONSE: [ BID, - BID2 + BID2, + BID3 ], AUCTION_END: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' @@ -254,6 +305,9 @@ const MOCK = { }), Object.assign({}, BID2, { 'status': 'rendered' + }), + Object.assign({}, BID3, { + 'status': 'rendered' }) ], BIDDER_DONE: { @@ -262,6 +316,9 @@ const MOCK = { BID, Object.assign({}, BID2, { 'serverResponseTimeMs': 42, + }), + Object.assign({}, BID3, { + 'serverResponseTimeMs': 55, }) ] }, @@ -777,14 +834,40 @@ describe('rubicon analytics adapter', function () { } }; + let floorMinResponse = { + ...BID3, + floorData: { + floorValue: 1.5, + floorRuleValue: 1, + floorRule: '12345/entertainment|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 2.00, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + matchedFields: { + gptSlot: '12345/entertainment', + mediaType: 'banner' + } + } + }; + + let bidRequest = utils.deepClone(MOCK.BID_REQUESTED); + bidRequest.bids.push(floorMinRequest) + // spoof the auction with just our duplicates events.emit(AUCTION_INIT, auctionInit); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_REQUESTED, bidRequest); events.emit(BID_RESPONSE, flooredResponse); events.emit(BID_RESPONSE, notFlooredResponse); + events.emit(BID_RESPONSE, floorMinResponse); events.emit(AUCTION_END, MOCK.AUCTION_END); events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(BID_WON, MOCK.BID_WON[2]); clock.tick(SEND_TIMEOUT + 1000); expect(server.requests.length).to.equal(1); @@ -795,7 +878,7 @@ describe('rubicon analytics adapter', function () { } it('should capture price floor information correctly', function () { - let message = performFloorAuction('rubicon') + let message = performFloorAuction('rubicon'); // verify our floor stuff is passed // top level floor info @@ -827,6 +910,16 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); + + // second adUnit's adSlot + expect(message.auctions[0].adUnits[2].gam.adSlot).to.equal('12345/entertainment'); + // top level adUnit status is success + expect(message.auctions[0].adUnits[2].status).to.equal('success'); + // second adUnits bid is success + expect(message.auctions[0].adUnits[2].bids[0].status).to.equal('success'); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorValue).to.equal(1.5); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorRuleValue).to.equal(1); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.bidPriceUSD).to.equal(2.01); }); it('should still send floor info if provider is not rubicon', function () { @@ -862,6 +955,16 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); + + // second adUnit's adSlot + expect(message.auctions[0].adUnits[2].gam.adSlot).to.equal('12345/entertainment'); + // top level adUnit status is success + expect(message.auctions[0].adUnits[2].status).to.equal('success'); + // second adUnits bid is success + expect(message.auctions[0].adUnits[2].bids[0].status).to.equal('success'); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorValue).to.equal(1.5); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorRuleValue).to.equal(1); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.bidPriceUSD).to.equal(2.01); }); describe('with session handling', function () { From 372f3b09b33907fadca4cc4507e3ebbd75c2cf04 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Wed, 30 Sep 2020 11:41:15 -0400 Subject: [PATCH 2/7] Remove extra spaces --- modules/priceFloors.js | 5 ----- modules/rubiconAnalyticsAdapter.js | 2 -- 2 files changed, 7 deletions(-) diff --git a/modules/priceFloors.js b/modules/priceFloors.js index d601d310ad7..956ad304c66 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -197,12 +197,10 @@ function updateRequestParamsFromContext(bidRequest, requestParams) { export function getFloor(requestParams = {currency: 'USD', mediaType: '*', size: '*'}) { let bidRequest = this; let floorData = _floorDataForAuction[bidRequest.auctionId]; - if (!floorData || floorData.skipped) return {}; requestParams = updateRequestParamsFromContext(bidRequest, requestParams); let floorInfo = getFirstMatchingFloor(floorData.data, {...bidRequest}, {mediaType: requestParams.mediaType, size: requestParams.size}); - let currency = requestParams.currency || floorData.data.currency; // if bidder asked for a currency which is not what floors are set in convert @@ -240,7 +238,6 @@ export function getFloorsDataForAuction(floorData, adUnitCode) { auctionFloorData.values = normalizeRulesForAuction(auctionFloorData, adUnitCode); // default the currency to USD if not passed in auctionFloorData.currency = auctionFloorData.currency || 'USD'; - return auctionFloorData; } @@ -323,7 +320,6 @@ export function updateAdUnitFloorData(adUnits, floorData, auctionId) { } else { bid.getFloor = getFloor; } - // information for bid and analytics adapters bid.auctionId = auctionId; bid.floorData = { @@ -702,7 +698,6 @@ function shouldFloorBid(floorData, floorInfo, bid) { */ export function addBidResponseHook(fn, adUnitCode, bid) { let floorData = _floorDataForAuction[this.bidderRequest.auctionId]; - // if no floor data or associated bidRequest then bail const matchingBidRequest = find(this.bidderRequest.bids, bidRequest => bidRequest.bidId && bidRequest.bidId === bid.requestId); if (!floorData || !bid || floorData.skipped || !matchingBidRequest) { diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 12958397196..373fa12d640 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -326,7 +326,6 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { if (previousBidResponse && previousBidResponse.bidPriceUSD > responsePrice) { return previousBidResponse; } - return utils.pick(bid, [ 'bidPriceUSD', () => responsePrice, 'dealId', @@ -504,7 +503,6 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.bidsWon = {}; cacheEntry.referrer = utils.deepAccess(args, 'bidderRequests.0.refererInfo.referer'); const floorData = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData'); - if (floorData) { cacheEntry.floorData = {...floorData}; } From fd96dc154b5647b2d8787a52a8a6b449e8f45142 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Thu, 1 Oct 2020 15:50:10 -0400 Subject: [PATCH 3/7] Package Lock revert --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 3023392f08f..1784b885be9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24486,4 +24486,4 @@ } } } -} \ No newline at end of file +} From 2bbe9363c2d5caa975517c45497744b804d2db8c Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Fri, 2 Oct 2020 11:59:30 -0400 Subject: [PATCH 4/7] Updates to commit --- modules/priceFloors.js | 53 +++------------- test/spec/modules/priceFloors_spec.js | 88 ++++++++++++++++++++++++--- 2 files changed, 90 insertions(+), 51 deletions(-) diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 956ad304c66..f2fc7f653cf 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -109,18 +109,14 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let matchingRule = find(allPossibleMatches, hashValue => floorData.values.hasOwnProperty(hashValue)); let matchingData = { - matchingFloor: floorData.values[matchingRule] || floorData.default, + floorMin: floorData.floorMin || 0, + floorRuleValue: floorData.values[matchingRule] || floorData.default, matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters matchingRule }; - // check that floorMin is greater than 0 and set values accordingly - if (utils.deepAccess(_floorsConfig, 'floorMin')) { - matchingData.floorMin = utils.deepAccess(_floorsConfig, 'floorMin'); - matchingData.floorRuleValue = matchingData.matchingFloor; - if (matchingData.floorMin > matchingData.floorRuleValue) matchingData.matchingFloor = matchingData.floorMin; - } - + matchingData.matchingFloor = Math.max(matchingData.floorMin, matchingData.floorRuleValue); + // save for later lookup if needed utils.deepSetValue(floorData, `matchingInputs.${matchingInput}`, {...matchingData}); return matchingData; @@ -197,7 +193,9 @@ function updateRequestParamsFromContext(bidRequest, requestParams) { export function getFloor(requestParams = {currency: 'USD', mediaType: '*', size: '*'}) { let bidRequest = this; let floorData = _floorDataForAuction[bidRequest.auctionId]; + if (!floorData || floorData.skipped) return {}; + if (floorData.hasOwnProperty('floorMin')) floorData.data.floorMin = floorData.floorMin; requestParams = updateRequestParamsFromContext(bidRequest, requestParams); let floorInfo = getFirstMatchingFloor(floorData.data, {...bidRequest}, {mediaType: requestParams.mediaType, size: requestParams.size}); @@ -296,44 +294,16 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { bid.floorData = { skipped: floorData.skipped, skipRate: floorData.skipRate, + floorMin: floorData.floorMin, modelVersion: utils.deepAccess(floorData, 'data.modelVersion'), location: utils.deepAccess(floorData, 'data.location', 'noData'), floorProvider: floorData.floorProvider, fetchStatus: _floorsConfig.fetchStatus }; - if (utils.deepAccess(floorData, 'floorMin')) { + /* if (utils.deepAccess(floorData, 'floorMin')) { bid.floorData.floorMin = utils.deepAccess(floorData, 'floorMin'); - } - }); - }); -} - -/** - * @summary This function takes the adUnits for the auction and update them accordingly as well as returns the rules hashmap for the auction - */ -export function updateAdUnitFloorData(adUnits, floorData, auctionId) { - adUnits.forEach((adUnit) => { - adUnit.bids.forEach(bid => { - if (floorData.skipped) { - delete bid.getFloor; - } else { - bid.getFloor = getFloor; - } - // information for bid and analytics adapters - bid.auctionId = auctionId; - bid.floorData = { - skipped: floorData.skipped, - skipRate: floorData.skipRate, - modelVersion: utils.deepAccess(floorData, 'data.modelVersion'), - location: utils.deepAccess(floorData, 'data.location', 'noData'), - floorProvider: floorData.floorProvider, - fetchStatus: _floorsConfig.fetchStatus - }; - - if (utils.deepAccess(floorData, 'floorMin')) { - bid.floorData.floorMin = utils.deepAccess(floorData, 'floorMin'); - } + } */ }); }); } @@ -666,16 +636,13 @@ function addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm) { bid.floorData = { floorValue: floorInfo.matchingFloor, floorRule: floorInfo.matchingRule, + floorRuleValue: floorInfo.floorRuleValue, floorCurrency: floorData.data.currency, cpmAfterAdjustments: adjustedCpm, enforcements: {...floorData.enforcement}, matchedFields: {} }; - if (floorInfo.floorRuleValue) { - bid.floorData.floorRuleValue = floorInfo.floorRuleValue; - } - floorData.data.schema.fields.forEach((field, index) => { let matchedValue = floorInfo.matchingData.split(floorData.data.schema.delimiter)[index]; bid.floorData.matchedFields[field] = matchedValue; diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index bda43299905..8c673d29701 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -34,6 +34,34 @@ describe('the price floors module', function () { '*': 2.5 } }; + const basicFloorDataHigh = { + floorMin: 7.0, + modelVersion: 'basic model', + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + 'video': 5.0, + '*': 2.5 + } + }; + const basicFloorDataLow = { + floorMin: 2.3, + modelVersion: 'basic model', + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + 'video': 5.0, + '*': 2.5 + } + }; const basicFloorConfig = { enabled: true, auctionDelay: 0, @@ -57,7 +85,7 @@ describe('the price floors module', function () { floorDeals: false, bidAdjustment: true }, - data: basicFloorData + data: basicFloorDataHigh } const minFloorConfigLow = { enabled: true, @@ -70,7 +98,7 @@ describe('the price floors module', function () { floorDeals: false, bidAdjustment: true }, - data: basicFloorData + data: basicFloorDataLow } const basicBidRequest = { bidder: 'rubicon', @@ -191,28 +219,33 @@ describe('the price floors module', function () { it('selects the right floor for different mediaTypes', function () { // banner with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.0, matchingFloor: 1.0, matchingData: 'banner', matchingRule: 'banner' }); // video with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 5.0, matchingFloor: 5.0, matchingData: 'video', matchingRule: 'video' }); // native (not in the rule list) with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'native', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 2.5, matchingFloor: 2.5, matchingData: 'native', matchingRule: '*' }); // banner with floorMin higher than matching rule handleSetFloorsConfig({ - ...minFloorConfigHigh, - data: undefined + ...minFloorConfigHigh }); - expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + expect(getFirstMatchingFloor({...basicFloorDataHigh}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ floorMin: 7, floorRuleValue: 1.0, matchingFloor: 7, @@ -221,10 +254,9 @@ describe('the price floors module', function () { }); // banner with floorMin higher than matching rule handleSetFloorsConfig({ - ...minFloorConfigLow, - data: undefined + ...minFloorConfigLow }); - expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ + expect(getFirstMatchingFloor({...basicFloorDataLow}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ floorMin: 2.3, floorRuleValue: 5, matchingFloor: 5, @@ -238,6 +270,8 @@ describe('the price floors module', function () { let result = getFirstMatchingFloor(inputData, basicBidRequest, {mediaType: 'banner', size: '*'}); // result should always be the same expect(result).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.0, matchingFloor: 1.0, matchingData: 'banner', matchingRule: 'banner' @@ -263,24 +297,32 @@ describe('the price floors module', function () { } // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: '300x250', matchingRule: '300x250' }); // video with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: '300x250', matchingRule: '300x250' }); // native (not in the rule list) with 300x600 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'native', size: [600, 300]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 4.4, matchingFloor: 4.4, matchingData: '600x300', matchingRule: '600x300' }); // n/a mediaType with a size not in file should go to catch all expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: undefined, size: [1, 1]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 5.5, matchingFloor: 5.5, matchingData: '1x1', matchingRule: '*' @@ -304,12 +346,16 @@ describe('the price floors module', function () { }; // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: 'test_div_1^banner^300x250', matchingRule: 'test_div_1^banner^300x250' }); // video with 300x250 size -> No matching rule so should use default expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0.5, matchingFloor: 0.5, matchingData: 'test_div_1^video^300x250', matchingRule: undefined @@ -317,6 +363,8 @@ describe('the price floors module', function () { // remove default and should still return the same floor as above since matches are cached delete inputFloorData.default; expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0.5, matchingFloor: 0.5, matchingData: 'test_div_1^video^300x250', matchingRule: undefined @@ -324,6 +372,8 @@ describe('the price floors module', function () { // update adUnitCode to test_div_2 with weird other params let newBidRequest = { ...basicBidRequest, adUnitCode: 'test_div_2' } expect(getFirstMatchingFloor(inputFloorData, newBidRequest, {mediaType: 'badmediatype', size: [900, 900]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 3.3, matchingFloor: 3.3, matchingData: 'test_div_2^badmediatype^900x900', matchingRule: 'test_div_2^*^*' @@ -377,6 +427,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(false, { skipped: true, + floorMin: undefined, modelVersion: undefined, location: 'noData', skipRate: 0, @@ -410,6 +461,7 @@ describe('the price floors module', function () { runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'adUnit Model Version', location: 'adUnit', skipRate: 0, @@ -456,6 +508,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -476,6 +529,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -489,6 +543,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -502,6 +557,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -524,6 +580,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 50, @@ -537,6 +594,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 10, @@ -550,6 +608,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -613,6 +672,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-1', location: 'setConfig', skipRate: 0, @@ -625,6 +685,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-2', location: 'setConfig', skipRate: 0, @@ -637,6 +698,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-3', location: 'setConfig', skipRate: 0, @@ -665,6 +727,7 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -743,6 +806,7 @@ describe('the price floors module', function () { // the exposedAdUnits should be from the fetch not setConfig level data validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -780,6 +844,7 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', location: 'fetch', skipRate: 0, @@ -816,6 +881,7 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', location: 'fetch', skipRate: 0, @@ -855,6 +921,7 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', location: 'fetch', skipRate: 95, @@ -876,6 +943,7 @@ describe('the price floors module', function () { // and fetch failed is true validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -899,6 +967,7 @@ describe('the price floors module', function () { // and fetchStatus is 'success' but location is setConfig since it had bad data validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', location: 'setConfig', skipRate: 0, @@ -1387,6 +1456,7 @@ describe('the price floors module', function () { runBidResponse(); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ + floorRuleValue: 0.3, floorValue: 0.3, floorCurrency: 'USD', floorRule: 'banner', @@ -1424,6 +1494,7 @@ describe('the price floors module', function () { expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ floorValue: 0.5, + floorRuleValue: 0.5, floorCurrency: 'USD', floorRule: 'banner|300x250', cpmAfterAdjustments: 0.5, @@ -1450,6 +1521,7 @@ describe('the price floors module', function () { }); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ + floorRuleValue: 5.5, floorValue: 5.5, floorCurrency: 'USD', floorRule: 'video|*', From e61582e1b50a08369aed73165e02133d1de89f00 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Fri, 2 Oct 2020 12:02:09 -0400 Subject: [PATCH 5/7] Remove comment --- modules/priceFloors.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/priceFloors.js b/modules/priceFloors.js index f2fc7f653cf..75effa856b1 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -300,10 +300,6 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { floorProvider: floorData.floorProvider, fetchStatus: _floorsConfig.fetchStatus }; - - /* if (utils.deepAccess(floorData, 'floorMin')) { - bid.floorData.floorMin = utils.deepAccess(floorData, 'floorMin'); - } */ }); }); } From ec1d286c9c7b7e02d973075d4409c98c3b576509 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Fri, 2 Oct 2020 12:09:40 -0400 Subject: [PATCH 6/7] Remove excess spaces --- modules/priceFloors.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 75effa856b1..e8da0196980 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -114,9 +114,7 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters matchingRule }; - matchingData.matchingFloor = Math.max(matchingData.floorMin, matchingData.floorRuleValue); - // save for later lookup if needed utils.deepSetValue(floorData, `matchingInputs.${matchingInput}`, {...matchingData}); return matchingData; @@ -193,7 +191,6 @@ function updateRequestParamsFromContext(bidRequest, requestParams) { export function getFloor(requestParams = {currency: 'USD', mediaType: '*', size: '*'}) { let bidRequest = this; let floorData = _floorDataForAuction[bidRequest.auctionId]; - if (!floorData || floorData.skipped) return {}; if (floorData.hasOwnProperty('floorMin')) floorData.data.floorMin = floorData.floorMin; @@ -288,7 +285,6 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { } else { bid.getFloor = getFloor; } - // information for bid and analytics adapters bid.auctionId = auctionId; bid.floorData = { @@ -638,7 +634,6 @@ function addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm) { enforcements: {...floorData.enforcement}, matchedFields: {} }; - floorData.data.schema.fields.forEach((field, index) => { let matchedValue = floorInfo.matchingData.split(floorData.data.schema.delimiter)[index]; bid.floorData.matchedFields[field] = matchedValue; From 7be959e024e56e7ebddaec74740837077b4ebfa5 Mon Sep 17 00:00:00 2001 From: Michael Moschovas Date: Tue, 13 Oct 2020 14:29:53 -0400 Subject: [PATCH 7/7] Update to priceFloor and rubiconAnalytics adapters --- modules/priceFloors.js | 5 +++-- modules/rubiconAnalyticsAdapter.js | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/priceFloors.js b/modules/priceFloors.js index e8da0196980..fd8a46b172f 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -192,7 +192,6 @@ export function getFloor(requestParams = {currency: 'USD', mediaType: '*', size: let bidRequest = this; let floorData = _floorDataForAuction[bidRequest.auctionId]; if (!floorData || floorData.skipped) return {}; - if (floorData.hasOwnProperty('floorMin')) floorData.data.floorMin = floorData.floorMin; requestParams = updateRequestParamsFromContext(bidRequest, requestParams); let floorInfo = getFirstMatchingFloor(floorData.data, {...bidRequest}, {mediaType: requestParams.mediaType, size: requestParams.size}); @@ -340,6 +339,8 @@ export function createFloorsDataForAuction(adUnits, auctionId) { const isSkipped = Math.random() * 100 < parseFloat(auctionSkipRate); resolvedFloorsData.skipped = isSkipped; } + // copy FloorMin to floorData.data + if (resolvedFloorsData.hasOwnProperty('floorMin')) resolvedFloorsData.data.floorMin = resolvedFloorsData.floorMin; // add floorData to bids updateAdUnitsForAuction(adUnits, resolvedFloorsData, auctionId); return resolvedFloorsData; @@ -572,7 +573,7 @@ function addFieldOverrides(overrides) { */ export function handleSetFloorsConfig(config) { _floorsConfig = utils.pick(config, [ - 'floorMin', floorMin => floorMin, + 'floorMin', 'enabled', enabled => enabled !== false, // defaults to true 'auctionDelay', auctionDelay => auctionDelay || 0, 'floorProvider', floorProvider => utils.deepAccess(config, 'data.floorProvider', floorProvider), diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 373fa12d640..44b2ee5bea9 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -226,7 +226,6 @@ function sendMessage(auctionId, bidWonId) { 'skipRate', 'fetchStatus', 'floorMin', - 'floorRuleValue', 'floorProvider as provider' ]); }