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 all commits
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
133 changes: 61 additions & 72 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,59 @@ 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;
const latencies = getLatencies(args, auctionEntry.auctionStart);
bid.clientLatencyMillis = latencies.total;
bid.httpLatencyMillis = latencies.net;
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;
}
}

const getLatencies = (args, auctionStart) => {
try {
const metrics = args.metrics.getMetrics();
Expand Down Expand Up @@ -837,70 +885,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'
};
}
const latencies = getLatencies(args, auctionEntry.auctionStart);
bid.clientLatencyMillis = latencies.total;
bid.httpLatencyMillis = latencies.net;
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 === CONSTANTS.REJECTION_REASON.FLOOR_NOT_MET ? BID_REJECTED_IPF : 'rejected';
handleBidResponse(args, bidStatus);
break;
case SEAT_NON_BID:
handleNonBidEvent(args);
Expand Down
94 changes: 93 additions & 1 deletion test/spec/modules/magniteAnalyticsAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const {
BID_WON,
BID_TIMEOUT,
BILLABLE_EVENT,
SEAT_NON_BID
SEAT_NON_BID,
BID_REJECTED
}
} = CONSTANTS;

Expand Down Expand Up @@ -2147,4 +2148,95 @@ 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,
httpLatencyMillis: 240,
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,
httpLatencyMillis: 240,
bidResponse: {
bidPriceUSD: 3.4,
mediaType: 'banner',
dimensions: {
width: 300,
height: 250
},
rejectionReason: 'this bid is rejected'
}
});
});
});
});