-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New Module: MinBidToWin Notifications: Created a new module to suppor…
…t sending minbidtowin notifications to bidders (#11086) * created client side loss notifications module * addressed feedback and created tests * addressed feedback * addressed feedback, updated tests * removed some console logs and unecessary changes used for local testing * removed linebreak * added publisher opt-in logic and refactored onBidWonHandler * updated tests and prevAuctionInfo init logic * detach event listeners on deactivation --------- Co-authored-by: Demetrio Girardi <[email protected]>
- Loading branch information
Showing
2 changed files
with
302 additions
and
0 deletions.
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,135 @@ | ||
import {on as onEvent, off as offEvent} 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 = (cb = deinitHandlers) => { | ||
previousAuctionInfoEnabled = false; | ||
enabledBidders = []; | ||
auctionState = {}; | ||
cb(); | ||
}; | ||
|
||
export const initPreviousAuctionInfo = (cb = initHandlers) => { | ||
config.getConfig('previousAuctionInfo', (conf) => { | ||
if (!conf.previousAuctionInfo) { | ||
if (previousAuctionInfoEnabled) { resetPreviousAuctionInfo(); } | ||
return; | ||
} | ||
|
||
previousAuctionInfoEnabled = true; | ||
cb(); | ||
}); | ||
}; | ||
|
||
export const enablePreviousAuctionInfo = (sspConfig) => { | ||
const { bidderCode } = sspConfig; | ||
const enabledBidder = enabledBidders.find(bidder => bidder.bidderCode === bidderCode); | ||
|
||
if (!enabledBidder) enabledBidders.push({ bidderCode, maxQueueLength: sspConfig.maxQueueLength || 10 }); | ||
} | ||
|
||
export const initHandlers = () => { | ||
onEvent(EVENTS.AUCTION_END, onAuctionEndHandler); | ||
onEvent(EVENTS.BID_WON, onBidWonHandler); | ||
onEvent(EVENTS.BID_REQUESTED, onBidRequestedHandler); | ||
}; | ||
|
||
const deinitHandlers = () => { | ||
offEvent(EVENTS.AUCTION_END, onAuctionEndHandler); | ||
offEvent(EVENTS.BID_WON, onBidWonHandler); | ||
offEvent(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, | ||
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) {} | ||
} | ||
|
||
initPreviousAuctionInfo(); |
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,167 @@ | ||
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(); | ||
previousAuctionInfo.resetPreviousAuctionInfo(); | ||
initHandlersStub = sandbox.stub(); | ||
}); | ||
|
||
afterEach(() => { | ||
sandbox.restore(); | ||
}); | ||
|
||
describe('config', () => { | ||
it('should initialize the module if publisher enabled', () => { | ||
previousAuctionInfo.initPreviousAuctionInfo(initHandlersStub); | ||
config.setConfig({ previousAuctionInfo: true }); | ||
sandbox.assert.calledOnce(initHandlersStub); | ||
}); | ||
|
||
it('should not enable previous auction info if config.previousAuctionInfo is not set', () => { | ||
sandbox.restore(); | ||
previousAuctionInfo.initPreviousAuctionInfo(initHandlersStub); | ||
config.setConfig({ previousAuctionInfo: false }); | ||
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); | ||
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); | ||
previousAuctionInfo.enablePreviousAuctionInfo(config2); | ||
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); | ||
|
||
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); | ||
|
||
previousAuctionInfo.auctionState['testBidder3'] = [ | ||
{ transactionId: 'someOtherTid', rendered: 0 } | ||
]; | ||
|
||
const winningBid = { | ||
transactionId: 'trans789' | ||
}; | ||
|
||
previousAuctionInfo.onBidWonHandler(winningBid); | ||
|
||
expect(previousAuctionInfo.auctionState['testBidder3'][0]).to.include({ rendered: 0 }); | ||
}); | ||
}); | ||
}); |