-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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 : add seat non bid handling #9696
Changes from 3 commits
ccab78d
481847d
a667e40
d4a2145
c90c7d1
d5e116f
3894aa2
0d0f275
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,7 +37,8 @@ const { | |
BIDDER_DONE, | ||
BID_TIMEOUT, | ||
BID_WON, | ||
BILLABLE_EVENT | ||
BILLABLE_EVENT, | ||
SEAT_NON_BID | ||
}, | ||
STATUS: { | ||
GOOD, | ||
|
@@ -437,12 +438,20 @@ const sizeToDimensions = size => { | |
}; | ||
} | ||
|
||
const findMatchingAdUnitFromAuctions = (matchesFunction, returnFirstMatch) => { | ||
const findMatchingAdUnitFromOrderedAuctions = (matchesFunction, returnFirstMatch) => { | ||
return findAdUnitFromAuctions(matchesFunction, returnFirstMatch, cache.auctionOrder); | ||
}; | ||
|
||
const findMatchingAdUnitFromAllAuctions = (matchesFunction, returnFirstMatch) => { | ||
return findAdUnitFromAuctions(matchesFunction, returnFirstMatch, Object.keys(cache.auctions)); | ||
}; | ||
|
||
const findAdUnitFromAuctions = (matchesFunction, returnFirstMatch, auctions) => { | ||
// finding matching adUnit / auction | ||
let matches = {}; | ||
|
||
// loop through auctions in order and adunits | ||
for (const auctionId of cache.auctionOrder) { | ||
for (const auctionId of auctions) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With other proposed change I think all of this can be reverted |
||
const auction = cache.auctions[auctionId].auction; | ||
for (const transactionId in auction.adUnits) { | ||
const adUnit = auction.adUnits[transactionId]; | ||
|
@@ -464,7 +473,7 @@ const findMatchingAdUnitFromAuctions = (matchesFunction, returnFirstMatch) => { | |
} | ||
} | ||
return matches; | ||
} | ||
}; | ||
|
||
const getRenderingIds = bidWonData => { | ||
// if bid caching off -> return the bidWon auction id | ||
|
@@ -481,7 +490,7 @@ const getRenderingIds = bidWonData => { | |
const gamHasRendered = deepAccess(cache, `auctions.${auction.auctionId}.gamRenders.${adUnit.transactionId}`); | ||
return adUnit.adUnitCode === bidWonData.adUnitCode && gamHasRendered; | ||
} | ||
let { adUnit, auction } = findMatchingAdUnitFromAuctions(matchingFunction, false); | ||
let { adUnit, auction } = findMatchingAdUnitFromOrderedAuctions(matchingFunction, false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With other proposed change I think all of this can be reverted |
||
// If no match was found, we will use the actual bid won auction id | ||
return { | ||
renderTransactionId: (adUnit && adUnit.transactionId) || bidWonData.transactionId, | ||
|
@@ -554,7 +563,7 @@ const subscribeToGamSlots = () => { | |
const gamHasRendered = deepAccess(cache, `auctions.${auction.auctionId}.gamRenders.${adUnit.transactionId}`); | ||
return matchesSlot && !gamHasRendered; | ||
} | ||
let { adUnit, auction } = findMatchingAdUnitFromAuctions(matchingFunction, true); | ||
let { adUnit, auction } = findMatchingAdUnitFromOrderedAuctions(matchingFunction, true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With other proposed change I think all of this can be reverted |
||
|
||
const slotName = `${event.slot.getAdUnitPath()} - ${event.slot.getSlotElementId()}`; | ||
|
||
|
@@ -821,10 +830,16 @@ magniteAdapter.track = ({ eventType, args }) => { | |
auctionEntry.floors.dealsEnforced = args.floorData.enforcements.floorDeals; | ||
} | ||
|
||
// Log error if no matching bid! | ||
// no-bid from server. Document it! | ||
if (!bid) { | ||
logError(`${MODULE_NAME}: Could not find associated bid request for bid response with requestId: `, args.requestId); | ||
break; | ||
bid = adUnit.bids[generateUUID()] = { | ||
bidder: 'mgnipbs', | ||
source: 'server', | ||
status: 'no-bid', | ||
bidId: args.requestId, | ||
clientLatencyMillis: args.timeToRespond || Date.now() - cache.auctions[args.auctionId].auction.auctionStart | ||
}; | ||
return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this scenario, I think we want something a bit different. This is a valid bid response, so in this scenario we actually want to create a new bid in our adUnit And then just let the rest of the bid_response processing logic do what it normally does: So something like this: // If we still do not have a matching bid object and this bidResponse has a seatBidId, lets create a default one
if (!bid && args.seatBidId) {
bid = adUnit.bids[args.seatBidId] = {
bidder: args.bidderCode,
bidId: args.seatBidId,
status: 'no-bid',
source: 'server',
unknownBid: true // just so we can see how often this happens at log level
}
}
// finally if still no bid just bail like before
if (!bid) {
logError(`${MODULE_NAME}: Could not find associated bid request for bid response with requestId: `, args.requestId);
break;
} So we just have a new check before this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of that sounds good to me, except letting regular bid response processing go. When that happens, we get this: |
||
} | ||
|
||
// set bid status | ||
|
@@ -852,6 +867,9 @@ magniteAdapter.track = ({ eventType, args }) => { | |
bid.pbsBidId = pbsBidId; | ||
} | ||
break; | ||
case SEAT_NON_BID: | ||
handleNonBidEvent(args); | ||
break; | ||
case BIDDER_DONE: | ||
const serverError = deepAccess(args, 'serverErrors.0'); | ||
const serverResponseTimeMs = args.serverResponseTimeMs; | ||
|
@@ -939,6 +957,66 @@ magniteAdapter.track = ({ eventType, args }) => { | |
} | ||
}; | ||
|
||
const handleNonBidEvent = function(args) { | ||
let {seatnonbid, auctionId} = args; | ||
seatnonbid.forEach(seatnonbid => { | ||
let {seat} = seatnonbid; | ||
seatnonbid.nonbid.forEach(nonbid => { | ||
let {status, impid} = nonbid; | ||
let matchByImpid = matchAuctionBeforeAdUnit(auctionId, impid) | ||
let {adUnit} = findMatchingAdUnitFromAllAuctions(matchByImpid, true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not think it has to loop through all auctions. Since we are given the aucitonId by the event, we can just grab the related cache auction and then find the right adUnit: Perhaps it can be simplified to something like: const handleNonBidEvent = function(args) {
let { seatnonbid, auctionId } = args;
// grab the auction
const auction = deepAccess(cache, `auctions.${auctionId}.auction`);
// grab the adUnits for this auction
const adUnits = auction.adUnits;
seatnonbid.forEach(seatnonbid => {
let {seat} = seatnonbid;
seatnonbid.nonbid.forEach(nonbid => {
try {
let { status, impid } = nonbid;
// find matching transactionId for this nonbid and use it to grab the matching adUnit object
const matchingTid = Object.keys(adUnits).find(tid => adUnits[tid].adUnitCode === impid);
const adUnit = adUnits[matchingTid];
let statusInfo = statusMap[status] || { status: 'no-bid' }
adUnit.bids[generateUUID()] = {
source: 'server',
bidder: seat,
isSeatNonBid: true,
clientLatencyMillis: Date.now() - auction.auctionStart,
...statusInfo
};
} catch (error) {
logWarn(`Unable to match nonbid to adUnit`);
}
});
});
}; Then we can remove all the other changes to the looping functions |
||
try { | ||
let statusInfo = statusMap[status] || { status: 'no-bid' } | ||
adUnit.bids[generateUUID()] = { | ||
source: 'server', | ||
bidder: seat, | ||
isSeatNonBid: true, | ||
clientLatencyMillis: Date.now() - cache.auctions[args.auctionId].auction.auctionStart, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can be simplified to clientLatencyMillis: Date.now() - auction.auctionStart, since auction is saved above |
||
...statusInfo | ||
}; | ||
} catch (error) { | ||
logWarn(`Unable to match nonbid to adUnit`); | ||
} | ||
}); | ||
}); | ||
}; | ||
|
||
const matchAuctionBeforeAdUnit = function(auctionId, impid) { | ||
return function (adUnit, auction) { | ||
if (auctionId !== auction.auctionId) return false; | ||
return adUnit.adUnitCode === impid; | ||
} | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. think this can be removed when the refactor is done |
||
|
||
const statusMap = { | ||
0: { | ||
status: 'no-bid' | ||
}, | ||
100: { | ||
status: 'error', | ||
error: { | ||
code: 'request-error', | ||
description: 'general error' | ||
} | ||
}, | ||
101: { | ||
status: 'error', | ||
error: { | ||
code: 'timeout-error', | ||
description: 'prebid server timeout' | ||
} | ||
}, | ||
200: { | ||
status: 'rejected' | ||
}, | ||
202: { | ||
status: 'rejected' | ||
}, | ||
301: { | ||
status: 'rejected-ipf' | ||
} | ||
}; | ||
|
||
adapterManager.registerAnalyticsAdapter({ | ||
adapter: magniteAdapter, | ||
code: 'magnite', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,8 @@ const { | |
BIDDER_DONE, | ||
BID_WON, | ||
BID_TIMEOUT, | ||
BILLABLE_EVENT | ||
BILLABLE_EVENT, | ||
SEAT_NON_BID | ||
} | ||
} = CONSTANTS; | ||
|
||
|
@@ -160,6 +161,23 @@ const MOCK = { | |
'status': 'rendered', | ||
getStatusCode: () => 1, | ||
}, | ||
NO_BID: { | ||
'requestId': 'fakeId', | ||
'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', | ||
'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', | ||
'adUnitCode': 'box', | ||
getStatusCode: () => 1, | ||
}, | ||
SEAT_NON_BID: { | ||
auctionId: '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', | ||
seatnonbid: [{ | ||
seat: 'rubicon', | ||
nonbid: [{ | ||
status: 1, | ||
impid: 'box' | ||
}] | ||
}] | ||
}, | ||
AUCTION_END: { | ||
'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', | ||
'auctionEnd': 1658868384019, | ||
|
@@ -2039,4 +2057,111 @@ describe('magnite analytics adapter', function () { | |
} | ||
}) | ||
}); | ||
|
||
describe('BID_RESPONSE events', () => { | ||
beforeEach(() => { | ||
magniteAdapter.enableAnalytics({ | ||
options: { | ||
endpoint: '//localhost:9999/event', | ||
accountId: 1001 | ||
} | ||
}); | ||
config.setConfig({ rubicon: { updatePageView: true } }); | ||
}); | ||
|
||
it('should add a no-bid bid to the add unit if it recieves one from the server', () => { | ||
const bidResponse = utils.deepClone(MOCK.NO_BID); | ||
const auctionInit = utils.deepClone(MOCK.AUCTION_INIT); | ||
|
||
delete auctionInit.adUnits[0].sizes; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove one of these new lines to solve the linting issue! |
||
bidResponse.requestId = 'fakeId'; | ||
events.emit(AUCTION_INIT, auctionInit); | ||
events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); | ||
events.emit(BID_RESPONSE, bidResponse) | ||
events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); | ||
events.emit(AUCTION_END, MOCK.AUCTION_END); | ||
clock.tick(rubiConf.analyticsBatchTimeout + 1000); | ||
|
||
let message = JSON.parse(server.requests[0].requestBody); | ||
expect(utils.generateUUID.called).to.equal(true); | ||
|
||
expect(message.auctions[0].adUnits[0].bids[1]).to.deep.equal( | ||
{ | ||
bidder: 'mgnipbs', | ||
source: 'server', | ||
status: 'no-bid', | ||
bidId: 'fakeId', | ||
clientLatencyMillis: -139101369960 | ||
} | ||
); | ||
}); | ||
}); | ||
|
||
describe('SEAT_NON_BID events', () => { | ||
let seatnonbid; | ||
|
||
const runNonBidAuction = () => { | ||
events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); | ||
events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); | ||
events.emit(SEAT_NON_BID, seatnonbid) | ||
events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); | ||
events.emit(AUCTION_END, MOCK.AUCTION_END); | ||
clock.tick(rubiConf.analyticsBatchTimeout + 1000); | ||
}; | ||
const checkStatusAgainstCode = (status, code, error, index) => { | ||
seatnonbid.seatnonbid[0].nonbid[0].status = code; | ||
runNonBidAuction(); | ||
let message = JSON.parse(server.requests[index].requestBody); | ||
let bid = message.auctions[0].adUnits[0].bids[1]; | ||
|
||
if (error) { | ||
expect(bid.error).to.deep.equal(error); | ||
} else { | ||
expect(bid.error).to.equal(undefined); | ||
} | ||
expect(bid.source).to.equal('server'); | ||
expect(bid.status).to.equal(status); | ||
expect(bid.isSeatNonBid).to.equal(true); | ||
}; | ||
beforeEach(() => { | ||
magniteAdapter.enableAnalytics({ | ||
options: { | ||
endpoint: '//localhost:9999/event', | ||
accountId: 1001 | ||
} | ||
}); | ||
config.setConfig({ rubicon: { updatePageView: true } }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably not needed but not a big deal |
||
seatnonbid = utils.deepClone(MOCK.SEAT_NON_BID); | ||
}); | ||
|
||
it('adds seatnonbid info to bids array', () => { | ||
runNonBidAuction(); | ||
let message = JSON.parse(server.requests[0].requestBody); | ||
|
||
expect(message.auctions[0].adUnits[0].bids[1]).to.deep.equal( | ||
{ | ||
bidder: 'rubicon', | ||
source: 'server', | ||
status: 'no-bid', | ||
isSeatNonBid: true, | ||
clientLatencyMillis: -139101369960 | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this one tests if the status is not in the status map, and then below test is all of the other ones cool. |
||
); | ||
}); | ||
|
||
it('adjusts the status according to the status map', () => { | ||
const statuses = [ | ||
{code: 0, status: 'no-bid'}, | ||
{code: 100, status: 'error', error: {code: 'request-error', description: 'general error'}}, | ||
{code: 101, status: 'error', error: {code: 'timeout-error', description: 'prebid server timeout'}}, | ||
{code: 200, status: 'rejected'}, | ||
{code: 202, status: 'rejected'}, | ||
{code: 301, status: 'rejected-ipf'} | ||
]; | ||
statuses.forEach((info, index) => { | ||
checkStatusAgainstCode(info.status, info.code, info.error, index); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 nice! |
||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With other proposed change I think all of this can be reverted