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
121 changes: 121 additions & 0 deletions libraries/previousAuctionInfo/previousAuctionInfo.js
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;
Copy link
Collaborator

@dgirardi dgirardi Feb 13, 2025

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.

Copy link
Collaborator Author

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 the enablePreviousAuctionInfo function. now i believe it should only get called once. does this approach sound good? otherwise i can refactor further if you want


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,
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) {}
}
188 changes: 188 additions & 0 deletions test/spec/libraries/previousAuctionInfo_spec.js
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();
});
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 better if the test just calls setConfig the way a publisher would. I am not sure that a listener as you have would work for single flags (every instance of that pattern expects a configuration object).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 });
});
});
});