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
118 changes: 118 additions & 0 deletions libraries/previousAuctionInfo/previousAuctionInfo.js
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) => {
Copy link
Collaborator

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 to setConfig.

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.

Copy link
Collaborator Author

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:

import { enablePreviousAuctionInfo } from '../libraries/previousAuctionInfo/previousAuctionInfo.js';

enablePreviousAuctionInfo({ bidderCode: 'rise' });

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,
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) {}
}
172 changes: 172 additions & 0 deletions test/spec/libraries/previousAuctionInfo_spec.js
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');
});
});
});