-
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
New Module: MinBidToWin Notifications: Created a new module to support sending minbidtowin notifications to bidders #11086
Merged
Merged
Changes from 6 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
8a8929c
created client side loss notifications module
jlquaccia ac11a00
addressed feedback and created tests
jlquaccia f9534c8
addressed feedback
jlquaccia 97b13e4
addressed feedback, updated tests
jlquaccia ca9d3d6
removed some console logs and unecessary changes used for local testing
jlquaccia 505521a
removed linebreak
jlquaccia 8c593db
added publisher opt-in logic and refactored onBidWonHandler
jlquaccia d5c47f6
updated tests and prevAuctionInfo init logic
jlquaccia 4394e85
detach event listeners on deactivation
dgirardi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import {on as onEvent} from '../../src/events.js'; | ||
import { EVENTS } from '../../src/constants.js'; | ||
|
||
export let previousAuctionInfoEnabled = false; | ||
let enabledBidders = []; | ||
export let winningBidsMap = {}; | ||
|
||
export let auctionState = {}; | ||
|
||
export const resetPreviousAuctionInfo = () => { | ||
previousAuctionInfoEnabled = false; | ||
enabledBidders = []; | ||
winningBidsMap = {}; | ||
auctionState = {}; | ||
}; | ||
|
||
export const enablePreviousAuctionInfo = (config, cb = initHandlers) => { | ||
const { bidderCode } = config; | ||
const enabledBidder = enabledBidders.find(bidder => bidder.bidderCode === bidderCode); | ||
if (!enabledBidder) enabledBidders.push({ bidderCode, maxQueueLength: config.maxQueueLength || 10 }); | ||
if (previousAuctionInfoEnabled) return; | ||
previousAuctionInfoEnabled = true; | ||
cb(); | ||
} | ||
|
||
export const initHandlers = () => { | ||
onEvent(EVENTS.AUCTION_END, onAuctionEndHandler); | ||
onEvent(EVENTS.BID_WON, onBidWonHandler); | ||
onEvent(EVENTS.BID_REQUESTED, onBidRequestedHandler); | ||
}; | ||
|
||
export const onAuctionEndHandler = (auctionDetails) => { | ||
try { | ||
const receivedBidsMap = {}; | ||
const rejectedBidsMap = {}; | ||
const highestBidsByAdUnitCode = {}; | ||
|
||
if (auctionDetails.bidsReceived?.length) { | ||
auctionDetails.bidsReceived.forEach((bid) => { | ||
receivedBidsMap[bid.requestId] = bid; | ||
if (!highestBidsByAdUnitCode[bid.adUnitCode] || bid.cpm > highestBidsByAdUnitCode[bid.adUnitCode].cpm) { | ||
highestBidsByAdUnitCode[bid.adUnitCode] = bid; | ||
} | ||
}); | ||
} | ||
|
||
if (auctionDetails.bidsRejected?.length) { | ||
auctionDetails.bidsRejected.forEach(bidRejected => { | ||
rejectedBidsMap[bidRejected.requestId] = bidRejected; | ||
}); | ||
} | ||
|
||
if (auctionDetails.bidderRequests?.length) { | ||
auctionDetails.bidderRequests.forEach(bidderRequest => { | ||
const enabledBidder = enabledBidders.find(bidder => bidder.bidderCode === bidderRequest.bidderCode); | ||
|
||
if (enabledBidder) { | ||
auctionState[bidderRequest.bidderCode] = auctionState[bidderRequest.bidderCode] || []; | ||
|
||
bidderRequest.bids.forEach(bid => { | ||
const previousAuctionInfoPayload = { | ||
bidderRequestId: bidderRequest.bidderRequestId, | ||
patmmccann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
bidId: bid.bidId, | ||
rendered: 0, | ||
source: 'pbjs', | ||
adUnitCode: bid.adUnitCode, | ||
highestTargetedBidCpm: highestBidsByAdUnitCode[bid.adUnitCode]?.adserverTargeting?.hb_pb || '', | ||
targetedBidCpm: receivedBidsMap[bid.bidId]?.adserverTargeting?.hb_pb || '', | ||
highestBidCpm: highestBidsByAdUnitCode[bid.adUnitCode]?.cpm || 0, | ||
bidderCpm: receivedBidsMap[bid.bidId]?.cpm || 'nobid', | ||
bidderOriginalCpm: receivedBidsMap[bid.bidId]?.originalCpm || 'nobid', | ||
bidderCurrency: receivedBidsMap[bid.bidId]?.currency || 'nobid', | ||
bidderOriginalCurrency: receivedBidsMap[bid.bidId]?.originalCurrency || 'nobid', | ||
bidderErrorCode: rejectedBidsMap[bid.bidId] ? rejectedBidsMap[bid.bidId].rejectionReason : -1, | ||
timestamp: auctionDetails.timestamp, | ||
transactionId: bid.transactionId, // this field gets removed before injecting previous auction info into the bid stream | ||
} | ||
|
||
if (auctionState[bidderRequest.bidderCode].length > enabledBidder.maxQueueLength) { | ||
auctionState[bidderRequest.bidderCode].shift(); | ||
} | ||
|
||
auctionState[bidderRequest.bidderCode].push(previousAuctionInfoPayload); | ||
}); | ||
} | ||
}); | ||
} | ||
} catch (error) {} | ||
} | ||
|
||
const onBidWonHandler = (winningBid) => { | ||
winningBidsMap[winningBid.transactionId] = winningBid; | ||
} | ||
|
||
export const onBidRequestedHandler = (bidRequest) => { | ||
try { | ||
const enabledBidder = enabledBidders.find(bidder => bidder.bidderCode === bidRequest.bidderCode); | ||
if (enabledBidder && auctionState[bidRequest.bidderCode]) { | ||
auctionState[bidRequest.bidderCode].forEach(prevAuctPayload => { | ||
if (winningBidsMap[prevAuctPayload.transactionId]) { | ||
prevAuctPayload.rendered = 1; | ||
delete winningBidsMap[prevAuctPayload.transactionId]; | ||
} | ||
|
||
if (prevAuctPayload.transactionId) { | ||
delete prevAuctPayload.transactionId; | ||
} | ||
}); | ||
|
||
bidRequest.ortb2 = Object.assign({}, bidRequest.ortb2); | ||
bidRequest.ortb2.ext = Object.assign({}, bidRequest.ortb2.ext); | ||
bidRequest.ortb2.ext.prebid = Object.assign({}, bidRequest.ortb2.ext.prebid); | ||
|
||
bidRequest.ortb2.ext.prebid.previousauctioninfo = auctionState[bidRequest.bidderCode]; | ||
delete auctionState[bidRequest.bidderCode]; | ||
} | ||
} catch (error) {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import * as previousAuctionInfo from 'libraries/previousAuctionInfo/previousAuctionInfo.js'; | ||
import sinon from 'sinon'; | ||
import { expect } from 'chai'; | ||
|
||
describe('previous auction info', () => { | ||
let sandbox; | ||
let initHandlersStub; | ||
|
||
const auctionDetails = { | ||
auctionId: 'auction123', | ||
bidsReceived: [ | ||
{ requestId: 'bid123', bidderCode: 'testBidder1', cpm: 1, adUnitCode: 'adUnit1', currency: 'USD', originalCpm: 1.1, originalCurrency: 'USD' }, | ||
{ requestId: 'bidabc', bidderCode: 'testBidder2', cpm: 2, adUnitCode: 'adUnit1', currency: 'EUR', originalCpm: 2.1, originalCurrency: 'EUR' }, | ||
{ requestId: 'bidxyz', bidderCode: 'testBidder3', cpm: 3, adUnitCode: 'adUnit2', currency: 'USD', originalCpm: 3.2, originalCurrency: 'USD' } | ||
], | ||
bidsRejected: [ | ||
{ requestId: 'bid456', rejectionReason: 1 }, | ||
{ requestId: 'bid789', rejectionReason: 2 } | ||
], | ||
bidderRequests: [ | ||
{ | ||
bidderCode: 'testBidder1', | ||
bidderRequestId: 'req1', | ||
bids: [ | ||
{ bidId: 'bid123', ortb2: { cur: ['USD'] }, ortb2Imp: { ext: { tid: 'trans123' } }, adUnitCode: 'adUnit1' } | ||
] | ||
}, | ||
{ | ||
bidderCode: 'testBidder2', | ||
bidderRequestId: 'req2', | ||
bids: [ | ||
{ bidId: 'bidabc', ortb2: { cur: ['EUR'] }, ortb2Imp: { ext: { tid: 'trans456' } }, adUnitCode: 'adUnit1' } | ||
] | ||
}, | ||
{ | ||
bidderCode: 'testBidder3', | ||
bidderRequestId: 'req3', | ||
bids: [ | ||
{ bidId: 'bidxyz', ortb2: { cur: ['USD'] }, ortb2Imp: { ext: { tid: 'trans789' } }, adUnitCode: 'adUnit2' } | ||
] | ||
} | ||
], | ||
timestamp: Date.now(), | ||
}; | ||
|
||
beforeEach(() => { | ||
previousAuctionInfo.resetPreviousAuctionInfo(); | ||
sandbox = sinon.createSandbox(); | ||
initHandlersStub = sandbox.stub(); | ||
}); | ||
|
||
afterEach(() => { | ||
sandbox.restore(); | ||
}); | ||
|
||
describe('config', () => { | ||
it('should only be initialized once', () => { | ||
const config = { bidderCode: 'testBidder' }; | ||
previousAuctionInfo.enablePreviousAuctionInfo(config, initHandlersStub); | ||
sandbox.assert.calledOnce(initHandlersStub); | ||
previousAuctionInfo.enablePreviousAuctionInfo(config, initHandlersStub); | ||
sandbox.assert.calledOnce(initHandlersStub); | ||
}); | ||
}); | ||
|
||
describe('onAuctionEndHandler', () => { | ||
it('should store auction data for enabled bidders in auctionState', () => { | ||
const config = { bidderCode: 'testBidder2' }; | ||
previousAuctionInfo.enablePreviousAuctionInfo(config, initHandlersStub); | ||
previousAuctionInfo.onAuctionEndHandler(auctionDetails); | ||
|
||
expect(previousAuctionInfo.auctionState).to.have.property('testBidder2'); | ||
expect(previousAuctionInfo.auctionState['testBidder2']).to.be.an('array').with.lengthOf(1); | ||
|
||
const storedData = previousAuctionInfo.auctionState['testBidder2'][0]; | ||
|
||
expect(storedData).to.include({ | ||
bidderRequestId: 'req2', | ||
bidId: 'bidabc', | ||
rendered: 0, | ||
source: 'pbjs', | ||
adUnitCode: 'adUnit1', | ||
highestBidCpm: 2, | ||
bidderCpm: 2, | ||
bidderOriginalCpm: 2.1, | ||
bidderCurrency: 'EUR', | ||
bidderOriginalCurrency: 'EUR', | ||
bidderErrorCode: -1, | ||
timestamp: auctionDetails.timestamp | ||
}); | ||
}); | ||
|
||
it('should store auction data for multiple bidders correctly', () => { | ||
const config1 = { bidderCode: 'testBidder1' }; | ||
const config2 = { bidderCode: 'testBidder3' }; | ||
previousAuctionInfo.enablePreviousAuctionInfo(config1, initHandlersStub); | ||
previousAuctionInfo.enablePreviousAuctionInfo(config2, initHandlersStub); | ||
previousAuctionInfo.onAuctionEndHandler(auctionDetails); | ||
|
||
expect(previousAuctionInfo.auctionState).to.have.property('testBidder1'); | ||
expect(previousAuctionInfo.auctionState).to.have.property('testBidder3'); | ||
|
||
expect(previousAuctionInfo.auctionState['testBidder1'][0]).to.include({ | ||
bidId: 'bid123', | ||
highestBidCpm: 2, | ||
adUnitCode: 'adUnit1', | ||
bidderCpm: 1, | ||
bidderCurrency: 'USD' | ||
}); | ||
|
||
expect(previousAuctionInfo.auctionState['testBidder3'][0]).to.include({ | ||
bidId: 'bidxyz', | ||
highestBidCpm: 3, | ||
adUnitCode: 'adUnit2', | ||
bidderCpm: 3, | ||
bidderCurrency: 'USD' | ||
}); | ||
}); | ||
|
||
it('should not store auction data for disabled bidders', () => { | ||
previousAuctionInfo.onAuctionEndHandler(auctionDetails); | ||
expect(previousAuctionInfo.auctionState).to.not.have.property('testBidder2'); | ||
}); | ||
}); | ||
|
||
describe('onBidRequestedHandler', () => { | ||
it('should update the rendered field if a pbjs bid wins', () => { | ||
const config = { bidderCode: 'testBidder3' }; | ||
previousAuctionInfo.enablePreviousAuctionInfo(config, initHandlersStub); | ||
|
||
const bidRequest = { | ||
bidderCode: 'testBidder3', | ||
ortb2: { ext: { prebid: {} } }, | ||
}; | ||
|
||
previousAuctionInfo.winningBidsMap['trans789'] = { | ||
transactionId: 'trans789', | ||
rendered: 1 | ||
}; | ||
|
||
previousAuctionInfo.auctionState['testBidder3'] = [ | ||
{ transactionId: 'trans789', rendered: 0 } | ||
]; | ||
|
||
previousAuctionInfo.onBidRequestedHandler(bidRequest); | ||
const updatedInfo = bidRequest.ortb2.ext.prebid.previousauctioninfo; | ||
|
||
expect(updatedInfo).to.be.an('array').with.lengthOf(1); | ||
expect(updatedInfo[0]).to.include({ rendered: 1 }); | ||
}); | ||
|
||
it('should remove winning bid entry from winningBidsMap after updating auctionState', () => { | ||
const config = { bidderCode: 'testBidder3' }; | ||
previousAuctionInfo.enablePreviousAuctionInfo(config, initHandlersStub); | ||
|
||
previousAuctionInfo.winningBidsMap['trans789'] = { | ||
cpm: 3.5, | ||
transactionId: 'trans789', | ||
adserverTargeting: { hb_pb: '3.50' } | ||
}; | ||
|
||
previousAuctionInfo.auctionState['testBidder3'] = [ | ||
{ transactionId: 'trans789', highestBidCpm: 0, rendered: 0 } | ||
]; | ||
|
||
const bidRequest = { bidderCode: 'testBidder3', ortb2: { ext: { prebid: {} } } }; | ||
previousAuctionInfo.onBidRequestedHandler(bidRequest); | ||
|
||
expect(previousAuctionInfo.winningBidsMap).to.not.have.property('trans789'); | ||
}); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
It's unclear to me how this is intended to be used.
If it's an optional module chosen by the publisher, this should be in the
modules
folder and listen for changes tosetConfig
.Alternatively we could let individual bidders opt in, in which case the only parameter needed should be the bidder code, and this should allow for being called multiple times with different bidders.
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.
Yes, that latter is what we are aiming for. i was thinking any bid adapter that wants to enable the feature could simply import the previousAuctionInfo module and enable it with their bidder code: