Skip to content
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 9 commits into from
Feb 18, 2025
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 });
});
});
});