From 283823e519c9d240cf5b9862ee6c4ba543933730 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Wed, 10 May 2023 10:59:50 -0700 Subject: [PATCH 1/2] Magnite Analytics - Do not rely on BID_RESPONSE for rejected bids --- modules/magniteAnalyticsAdapter.js | 130 ++++++++---------- .../modules/magniteAnalyticsAdapter_spec.js | 92 ++++++++++++- 2 files changed, 151 insertions(+), 71 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 7cede6af38d..36ee0b16743 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -57,13 +57,7 @@ const { BID_TIMEOUT, BID_WON, BILLABLE_EVENT, - SEAT_NON_BID - }, - STATUS: { - GOOD, - NO_BID - }, - BID_STATUS: { + SEAT_NON_BID, BID_REJECTED } } = CONSTANTS; @@ -220,7 +214,7 @@ const getBidPrice = bid => { // get the cpm from bidResponse let cpm; let currency; - if (bid.status === BID_REJECTED && typeof deepAccess(bid, 'floorData.cpmAfterAdjustments') === 'number') { + if (typeof deepAccess(bid, 'floorData.cpmAfterAdjustments') === 'number') { // if bid was rejected and bid.floorData.cpmAfterAdjustments use it cpm = bid.floorData.cpmAfterAdjustments; currency = bid.floorData.floorCurrency; @@ -282,6 +276,7 @@ export const parseBidResponse = (bid, previousBidResponse) => { 'conversionError', conversionError => conversionError === true || undefined, // only pass if exactly true 'ogCurrency', 'ogPrice', + 'rejectionReason' ]); } @@ -702,6 +697,58 @@ magniteAdapter.onDataDeletionRequest = function () { magniteAdapter.MODULE_INITIALIZED_TIME = Date.now(); magniteAdapter.referrerHostname = ''; +const handleBidResponse = (args, bidStatus) => { + const auctionEntry = deepAccess(cache, `auctions.${args.auctionId}.auction`); + const adUnit = deepAccess(auctionEntry, `adUnits.${args.transactionId}`); + let bid = adUnit.bids[args.requestId]; + + // if this came from multibid, there might now be matching bid, so check + // THIS logic will change when we support multibid per bid request + if (!bid && args.originalRequestId) { + let ogBid = adUnit.bids[args.originalRequestId]; + // create new bid + adUnit.bids[args.requestId] = { + ...ogBid, + bidId: args.requestId, + bidderDetail: args.targetingBidder + }; + bid = adUnit.bids[args.requestId]; + } + + // if we have not set enforcements yet set it (This is hidden from bidders until now so we have to get from here) + if (typeof deepAccess(auctionEntry, 'floors.enforcement') !== 'boolean' && deepAccess(args, 'floorData.enforcements')) { + deepSetValue(auctionEntry, 'floors.enforcement', args.floorData.enforcements.enforceJS); + deepSetValue(auctionEntry, 'floors.dealsEnforced', args.floorData.enforcements.floorDeals); + } + + // no-bid from server. report it! + if (!bid && args.seatBidId) { + bid = adUnit.bids[args.seatBidId] = { + bidder: args.bidderCode, + source: 'server', + bidId: args.seatBidId, + unknownBid: true + }; + } + + if (!bid) { + logError(`${MODULE_NAME}: Could not find associated bid request for bid response with requestId: `, args.requestId); + return; + } + + // set bid status + bid.status = bidStatus; + + bid.clientLatencyMillis = args.timeToRespond || Date.now() - cache.auctions[args.auctionId].auction.auctionStart; + bid.bidResponse = parseBidResponse(args, bid.bidResponse); + + // if pbs gave us back a bidId, we need to use it and update our bidId to PBA + const pbsBidId = (args.pbsBidId == 0 ? generateUUID() : args.pbsBidId) || (args.seatBidId == 0 ? generateUUID() : args.seatBidId); + if (pbsBidId) { + bid.pbsBidId = pbsBidId; + } +} + let browser; magniteAdapter.track = ({ eventType, args }) => { switch (eventType) { @@ -818,68 +865,11 @@ magniteAdapter.track = ({ eventType, args }) => { }); break; case BID_RESPONSE: - const auctionEntry = deepAccess(cache, `auctions.${args.auctionId}.auction`); - const adUnit = deepAccess(auctionEntry, `adUnits.${args.transactionId}`); - let bid = adUnit.bids[args.requestId]; - - // if this came from multibid, there might now be matching bid, so check - // THIS logic will change when we support multibid per bid request - if (!bid && args.originalRequestId) { - let ogBid = adUnit.bids[args.originalRequestId]; - // create new bid - adUnit.bids[args.requestId] = { - ...ogBid, - bidId: args.requestId, - bidderDetail: args.targetingBidder - }; - bid = adUnit.bids[args.requestId]; - } - - // if we have not set enforcements yet set it (This is hidden from bidders until now so we have to get from here) - if (typeof deepAccess(auctionEntry, 'floors.enforcement') !== 'boolean' && deepAccess(args, 'floorData.enforcements')) { - auctionEntry.floors.enforcement = args.floorData.enforcements.enforceJS; - auctionEntry.floors.dealsEnforced = args.floorData.enforcements.floorDeals; - } - - // no-bid from server. report it! - if (!bid && args.seatBidId) { - bid = adUnit.bids[args.seatBidId] = { - bidder: args.bidderCode, - source: 'server', - bidId: args.seatBidId, - unknownBid: true - }; - } - - if (!bid) { - logError(`${MODULE_NAME}: Could not find associated bid request for bid response with requestId: `, args.requestId); - break; - } - - // set bid status - switch (args.getStatusCode()) { - case GOOD: - bid.status = 'success'; - delete bid.error; // it's possible for this to be set by a previous timeout - break; - case NO_BID: - bid.status = args.status === BID_REJECTED ? BID_REJECTED_IPF : 'no-bid'; - delete bid.error; - break; - default: - bid.status = 'error'; - bid.error = { - code: 'request-error' - }; - } - bid.clientLatencyMillis = args.timeToRespond || Date.now() - cache.auctions[args.auctionId].auction.auctionStart; - bid.bidResponse = parseBidResponse(args, bid.bidResponse); - - // if pbs gave us back a bidId, we need to use it and update our bidId to PBA - const pbsBidId = (args.pbsBidId == 0 ? generateUUID() : args.pbsBidId) || (args.seatBidId == 0 ? generateUUID() : args.seatBidId); - if (pbsBidId) { - bid.pbsBidId = pbsBidId; - } + handleBidResponse(args, 'success'); + break; + case BID_REJECTED: + const bidStatus = args.rejectionReason && args.rejectionReason.includes('floor') ? BID_REJECTED_IPF : 'rejected'; + handleBidResponse(args, bidStatus); break; case SEAT_NON_BID: handleNonBidEvent(args); diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index bf9c3050bf6..dd42f1449a1 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -29,7 +29,8 @@ const { BID_WON, BID_TIMEOUT, BILLABLE_EVENT, - SEAT_NON_BID + SEAT_NON_BID, + BID_REJECTED } } = CONSTANTS; @@ -2167,4 +2168,93 @@ describe('magnite analytics adapter', function () { }); }); }); + + describe('BID_REJECTED events', () => { + let bidRejectedArgs; + + const runBidRejectedAuction = () => { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_REJECTED, bidRejectedArgs) + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + clock.tick(rubiConf.analyticsBatchTimeout + 1000); + }; + beforeEach(() => { + magniteAdapter.enableAnalytics({ + options: { + endpoint: '//localhost:9999/event', + accountId: 1001 + } + }); + bidRejectedArgs = utils.deepClone(MOCK.BID_RESPONSE); + }); + + it('updates the bid to be rejected by floors', () => { + bidRejectedArgs.floorData = { + floorValue: 0.5, + floorRule: 'banner', + floorRuleValue: 0.5, + floorCurrency: 'USD', + cpmAfterAdjustments: 0.15, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + matchedFields: { + mediaType: 'banner' + } + } + bidRejectedArgs.rejectionReason = 'Bid does not meet price floor'; + + runBidRejectedAuction(); + let message = JSON.parse(server.requests[0].requestBody); + + expect(message.auctions[0].adUnits[0].bids[0]).to.deep.equal({ + bidder: 'rubicon', + bidId: '23fcd8cf4bf0d7', + source: 'client', + status: 'rejected-ipf', + clientLatencyMillis: 271, + bidResponse: { + bidPriceUSD: 0.15, + mediaType: 'banner', + dimensions: { + width: 300, + height: 250 + }, + floorValue: 0.5, + floorRuleValue: 0.5, + rejectionReason: 'Bid does not meet price floor' + } + }); + }); + + it('does general rejection', () => { + bidRejectedArgs + bidRejectedArgs.rejectionReason = 'this bid is rejected'; + + runBidRejectedAuction(); + let message = JSON.parse(server.requests[0].requestBody); + + expect(message.auctions[0].adUnits[0].bids[0]).to.deep.equal({ + bidder: 'rubicon', + bidId: '23fcd8cf4bf0d7', + source: 'client', + status: 'rejected', + clientLatencyMillis: 271, + bidResponse: { + bidPriceUSD: 3.4, + mediaType: 'banner', + dimensions: { + width: 300, + height: 250 + }, + rejectionReason: 'this bid is rejected' + } + }); + }); + }); }); From 02b58e4e2b8f8157f701121f8ad0290d30daae71 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 11 May 2023 11:00:58 -0700 Subject: [PATCH 2/2] use rejection floor constant! --- modules/magniteAnalyticsAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 36ee0b16743..7e3da64ea72 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -868,7 +868,7 @@ magniteAdapter.track = ({ eventType, args }) => { handleBidResponse(args, 'success'); break; case BID_REJECTED: - const bidStatus = args.rejectionReason && args.rejectionReason.includes('floor') ? BID_REJECTED_IPF : 'rejected'; + const bidStatus = args.rejectionReason === CONSTANTS.REJECTION_REASON.FLOOR_NOT_MET ? BID_REJECTED_IPF : 'rejected'; handleBidResponse(args, bidStatus); break; case SEAT_NON_BID: