Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Magnite Analytics Adapter : do not rely on BID_RESPONSE 0cpm rejected bids #9933

Merged
merged 3 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 60 additions & 70 deletions modules/magniteAnalyticsAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -282,6 +276,7 @@ export const parseBidResponse = (bid, previousBidResponse) => {
'conversionError', conversionError => conversionError === true || undefined, // only pass if exactly true
'ogCurrency',
'ogPrice',
'rejectionReason'
]);
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could do a simple compare against CONSTANTS.REJECTION_REASON.FLOOR_NOT_MET. Not sure if core should provide that as an error code as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhh much better!

updated

handleBidResponse(args, bidStatus);
break;
case SEAT_NON_BID:
handleNonBidEvent(args);
Expand Down
92 changes: 91 additions & 1 deletion test/spec/modules/magniteAnalyticsAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const {
BID_WON,
BID_TIMEOUT,
BILLABLE_EVENT,
SEAT_NON_BID
SEAT_NON_BID,
BID_REJECTED
}
} = CONSTANTS;

Expand Down Expand Up @@ -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'
}
});
});
});
});