Skip to content

Commit

Permalink
New Module: MinBidToWin Notifications: Created a new module to suppor…
Browse files Browse the repository at this point in the history
…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
jlquaccia and dgirardi authored Feb 18, 2025
1 parent b1d3f71 commit 73ce266
Show file tree
Hide file tree
Showing 2 changed files with 302 additions and 0 deletions.
135 changes: 135 additions & 0 deletions libraries/previousAuctionInfo/previousAuctionInfo.js
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();
167 changes: 167 additions & 0 deletions test/spec/libraries/previousAuctionInfo_spec.js
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 });
});
});
});

0 comments on commit 73ce266

Please sign in to comment.