-
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
Changes from 7 commits
8a8929c
ac11a00
f9534c8
97b13e4
ca9d3d6
505521a
8c593db
d5c47f6
4394e85
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 |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import {on as onEvent} from '../../src/events.js'; | ||
import { EVENTS } from '../../src/constants.js'; | ||
import { config } from '../../src/config.js'; | ||
|
||
export let previousAuctionInfoEnabled = false; | ||
let enabledBidders = []; | ||
|
||
export let auctionState = {}; | ||
|
||
export const resetPreviousAuctionInfo = () => { | ||
previousAuctionInfoEnabled = false; | ||
enabledBidders = []; | ||
auctionState = {}; | ||
}; | ||
|
||
export const enablePreviousAuctionInfo = (sspConfig, cb = initHandlers) => { | ||
config.getConfig('previousAuctionInfo', (conf) => { | ||
if (!conf.previousAuctionInfo) return; | ||
|
||
const { bidderCode } = sspConfig; | ||
const enabledBidder = enabledBidders.find(bidder => bidder.bidderCode === bidderCode); | ||
|
||
if (!enabledBidder) enabledBidders.push({ bidderCode, maxQueueLength: sspConfig.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) {} | ||
} | ||
|
||
export const onBidWonHandler = (winningBid) => { | ||
const winningTid = winningBid.transactionId; | ||
|
||
Object.values(auctionState).flat().forEach(prevAuctPayload => { | ||
if (prevAuctPayload.transactionId === winningTid) { | ||
prevAuctPayload.rendered = 1; | ||
} | ||
}); | ||
}; | ||
|
||
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 (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) {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import * as previousAuctionInfo from 'libraries/previousAuctionInfo/previousAuctionInfo.js'; | ||
import sinon from 'sinon'; | ||
import { expect } from 'chai'; | ||
import { config } from 'src/config.js'; | ||
|
||
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(() => { | ||
sandbox = sinon.createSandbox(); | ||
let origGetConfig = config.getConfig; | ||
sandbox.stub(config, 'getConfig').callsFake((key, callback) => { | ||
if (key === 'previousAuctionInfo') { | ||
// eslint-disable-next-line standard/no-callback-literal | ||
callback({ previousAuctionInfo: true }); | ||
} else { | ||
return origGetConfig.apply(config, arguments); | ||
} | ||
}); | ||
|
||
previousAuctionInfo.resetPreviousAuctionInfo(); | ||
initHandlersStub = sandbox.stub(); | ||
}); | ||
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. It's better if the test just calls 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. thanks! i switched to using setConfig instead. |
||
|
||
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); | ||
}); | ||
|
||
it('should not enable previous auction info if config.previousAuctionInfo is not set', () => { | ||
sandbox.restore(); | ||
|
||
sandbox.stub(config, 'getConfig').callsFake((key, callback) => { | ||
if (key === 'previousAuctionInfo') { | ||
// eslint-disable-next-line standard/no-callback-literal | ||
callback({ previousAuctionInfo: false }); | ||
} | ||
}); | ||
|
||
const configData = { bidderCode: 'testBidder' }; | ||
previousAuctionInfo.enablePreviousAuctionInfo(configData, initHandlersStub); | ||
|
||
expect(previousAuctionInfo.previousAuctionInfoEnabled).to.be.false; | ||
}); | ||
}); | ||
|
||
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('onBidWonHandler', () => { | ||
it('should update the rendered field in auctionState when a pbjs bid wins', () => { | ||
const config = { bidderCode: 'testBidder3' }; | ||
previousAuctionInfo.enablePreviousAuctionInfo(config, initHandlersStub); | ||
|
||
previousAuctionInfo.auctionState['testBidder3'] = [ | ||
{ transactionId: 'trans789', rendered: 0 } | ||
]; | ||
|
||
const winningBid = { | ||
transactionId: 'trans789' | ||
}; | ||
|
||
previousAuctionInfo.onBidWonHandler(winningBid); | ||
|
||
expect(previousAuctionInfo.auctionState['testBidder3'][0]).to.include({ rendered: 1 }); | ||
}); | ||
|
||
it('should not update the rendered field if no matching transactionId is found', () => { | ||
const config = { bidderCode: 'testBidder3' }; | ||
previousAuctionInfo.enablePreviousAuctionInfo(config, initHandlersStub); | ||
|
||
previousAuctionInfo.auctionState['testBidder3'] = [ | ||
{ transactionId: 'someOtherTid', rendered: 0 } | ||
]; | ||
|
||
const winningBid = { | ||
transactionId: 'trans789' | ||
}; | ||
|
||
previousAuctionInfo.onBidWonHandler(winningBid); | ||
|
||
expect(previousAuctionInfo.auctionState['testBidder3'][0]).to.include({ rendered: 0 }); | ||
}); | ||
}); | ||
}); |
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.
this should disable if it was previously enabled.
It might be easier to check the configuration flag later with a simpler
if (config.getConfig('previousAuctionInfo'))
, when you're about to update auction info state. This version adds a new listener for config changes (which I'm not sure would work, see my comment on the test), once per bidder. So if 100 bidders register themselves this runs 100 times every time the configuration flag changes.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.
this makes sense. i went back and refactored things.. i am still using this listener
config.getConfig('previousAuctionInfo', (conf) => {
but i removed it from theenablePreviousAuctionInfo
function. now i believe it should only get called once. does this approach sound good? otherwise i can refactor further if you want