Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

priceFloors & PBS adapter: support mediaType and size specific floors #12690

Merged
merged 4 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 42 additions & 14 deletions modules/prebidServerBidAdapter/ortbConverter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ortbConverter} from '../../libraries/ortbConverter/converter.js';
import {deepSetValue, getBidRequest, logError, logWarn, mergeDeep, timestamp} from '../../src/utils.js';
import {deepClone, deepSetValue, getBidRequest, logError, logWarn, mergeDeep, timestamp} from '../../src/utils.js';
import {config} from '../../src/config.js';
import {S2S, STATUS} from '../../src/constants.js';
import {createBid} from '../../src/bidfactory.js';
Expand All @@ -17,12 +17,25 @@ import {currencyCompare} from '../../libraries/currencyUtils/currency.js';
import {minimum} from '../../src/utils/reducers.js';
import {s2sDefaultConfig} from './index.js';
import {premergeFpd} from './bidderConfig.js';
import {ALL_MEDIATYPES, BANNER} from '../../src/mediaTypes.js';

const DEFAULT_S2S_TTL = 60;
const DEFAULT_S2S_CURRENCY = 'USD';
const DEFAULT_S2S_NETREVENUE = true;
const BIDDER_SPECIFIC_REQUEST_PROPS = new Set(['bidderCode', 'bidderRequestId', 'uniquePbsTid', 'bids', 'timeout']);

const getMinimumFloor = (() => {
const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur]));
return function(candidates) {
let min;
for (const candidate of candidates) {
if (candidate?.bidfloorcur == null || candidate?.bidfloor == null) return null;
min = min == null ? candidate : getMin(min, candidate);
}
return min;
}
})();

const PBS_CONVERTER = ortbConverter({
processors: pbsExtensions,
context: {
Expand Down Expand Up @@ -126,24 +139,39 @@ const PBS_CONVERTER = ortbConverter({
}
}
},
// for bid floors, we pass each bidRequest associated with this imp through normal bidfloor/extBidfloor processing,
// and aggregate all of them into a single, minimum floor to put in the request
bidfloor(orig, imp, proxyBidRequest, context) {
// for bid floors, we pass each bidRequest associated with this imp through normal bidfloor processing,
// and aggregate all of them into a single, minimum floor to put in the request
const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur]));
let min;
for (const req of context.actualBidRequests.values()) {
const floor = {};
orig(floor, req, context);
// if any bid does not have a valid floor, do not attempt to send any to PBS
if (floor.bidfloorcur == null || floor.bidfloor == null) {
min = null;
break;
const min = getMinimumFloor((function * () {
for (const req of context.actualBidRequests.values()) {
const floor = {};
orig(floor, req, context);
yield floor;
}
min = min == null ? floor : getMin(min, floor);
}
})())
if (min != null) {
Object.assign(imp, min);
}
},
extBidfloor(orig, imp, proxyBidRequest, context) {
function setExtFloor(target, minFloor) {
if (minFloor != null) {
deepSetValue(target, 'ext.bidfloor', minFloor.bidfloor);
deepSetValue(target, 'ext.bidfloorcur', minFloor.bidfloorcur);
}
}
const imps = Array.from(context.actualBidRequests.values())
.map(request => {
const requestImp = deepClone(imp);
orig(requestImp, request, context);
return requestImp;
});
Object.values(ALL_MEDIATYPES).forEach(mediaType => {
setExtFloor(imp[mediaType], getMinimumFloor(imps.map(imp => imp[mediaType]?.ext)))
});
(imp[BANNER]?.format || []).forEach((format, i) => {
setExtFloor(format, getMinimumFloor(imps.map(imp => imp[BANNER].format[i]?.ext)))
})
}
},
[REQUEST]: {
Expand Down
80 changes: 63 additions & 17 deletions modules/priceFloors.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {adjustCpm} from '../src/utils/cpm.js';
import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js';
import {convertCurrency} from '../libraries/currencyUtils/currency.js';
import { timeoutQueue } from '../libraries/timeoutQueue/timeoutQueue.js';
import {ALL_MEDIATYPES, BANNER} from '../src/mediaTypes.js';

export const FLOOR_SKIPPED_REASON = {
NOT_FOUND: 'not_found',
Expand Down Expand Up @@ -264,7 +265,10 @@ export function getFloor(requestParams = {currency: 'USD', mediaType: '*', size:
// pub provided inverse function takes precedence, otherwise do old adjustment stuff
const inverseFunction = bidderSettings.get(bidRequest.bidder, 'inverseBidAdjustment');
if (inverseFunction) {
floorInfo.matchingFloor = inverseFunction(floorInfo.matchingFloor, bidRequest);
const definedParams = Object.fromEntries(
Object.entries(requestParams).filter(([key, val]) => val !== '*' && ['mediaType', 'size'].includes(key))
);
floorInfo.matchingFloor = inverseFunction(floorInfo.matchingFloor, bidRequest, definedParams);
} else {
let cpmAdjustment = getBiddersCpmAdjustment(floorInfo.matchingFloor, null, bidRequest);
floorInfo.matchingFloor = cpmAdjustment ? calculateAdjustedFloor(floorInfo.matchingFloor, cpmAdjustment) : floorInfo.matchingFloor;
Expand Down Expand Up @@ -801,30 +805,70 @@ export const addBidResponseHook = timedBidResponseHook('priceFloors', function a

config.getConfig('floors', config => handleSetFloorsConfig(config.floors));

/**
* Sets bidfloor and bidfloorcur for ORTB imp objects
*/
export function setOrtbImpBidFloor(imp, bidRequest, context) {
function tryGetFloor(bidRequest, {currency = config.getConfig('currency.adServerCurrency') || 'USD', mediaType = '*', size = '*'}, fn) {
if (typeof bidRequest.getFloor === 'function') {
let currency, floor;
let floor;
try {
({currency, floor} = bidRequest.getFloor({
currency: context.currency || config.getConfig('currency.adServerCurrency') || 'USD',
mediaType: context.mediaType || '*',
size: '*'
}) || {});
floor = bidRequest.getFloor({
currency,
mediaType,
size
}) || {};
} catch (e) {
logWarn('Cannot compute floor for bid', bidRequest);
return;
}
floor = parseFloat(floor);
if (currency != null && floor != null && !isNaN(floor)) {
Object.assign(imp, {
bidfloor: floor,
bidfloorcur: currency
});
floor.floor = parseFloat(floor.floor);
if (floor.currency != null && floor.floor && !isNaN(floor.floor)) {
fn(floor.floor, floor.currency);
}
}
}

/**
* Sets bidfloor and bidfloorcur for ORTB imp objects
*/
export function setOrtbImpBidFloor(imp, bidRequest, context) {
tryGetFloor(bidRequest, {
currency: context.currency,
mediaType: context.mediaType || '*',
size: '*'
}, (bidfloor, bidfloorcur) => {
Object.assign(imp, {
bidfloor,
bidfloorcur
});
})
}

/**
* Set per-mediatype and per-format bidfloor
*/
export function setGranularBidfloors(imp, bidRequest, context) {
function setIfDifferent(bidfloor, bidfloorcur) {
if (bidfloor !== imp.bidfloor || bidfloorcur !== imp.bidfloorcur) {
deepSetValue(this, 'ext.bidfloor', bidfloor);
deepSetValue(this, 'ext.bidfloorcur', bidfloorcur);
}
}

Object.values(ALL_MEDIATYPES)
.filter(mediaType => imp[mediaType] != null)
.forEach(mediaType => {
tryGetFloor(bidRequest, {
currency: imp.bidfloorcur || context?.currency,
mediaType
}, setIfDifferent.bind(imp[mediaType]))
});
(imp[BANNER]?.format || [])
.filter(({w, h}) => w != null && h != null)
.forEach(format => {
tryGetFloor(bidRequest, {
currency: imp.bidfloorcur || context?.currency,
mediaType: BANNER,
size: [format.w, format.h]
}, setIfDifferent.bind(format))
})
}

export function setImpExtPrebidFloors(imp, bidRequest, context) {
Expand Down Expand Up @@ -867,5 +911,7 @@ export function setOrtbExtPrebidFloors(ortbRequest, bidderRequest, context) {
}

registerOrtbProcessor({type: IMP, name: 'bidfloor', fn: setOrtbImpBidFloor});
// granular floors should be set after both "normal" bidfloors and mediaypes
registerOrtbProcessor({type: IMP, name: 'extBidfloor', fn: setGranularBidfloors, priority: -10})
registerOrtbProcessor({type: IMP, name: 'extPrebidFloors', fn: setImpExtPrebidFloors, dialects: [PBS], priority: -1});
registerOrtbProcessor({type: REQUEST, name: 'extPrebidFloors', fn: setOrtbExtPrebidFloors, dialects: [PBS]});
2 changes: 2 additions & 0 deletions src/mediaTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ export const VIDEO = 'video';
export const BANNER = 'banner';
/** @type {VideoContext} */
export const ADPOD = 'adpod';

export const ALL_MEDIATYPES = [NATIVE, VIDEO, BANNER];
124 changes: 78 additions & 46 deletions test/spec/modules/prebidServerBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1372,65 +1372,97 @@ describe('S2S Adapter', function () {
updateBid(BID_REQUESTS[1].bids[0]);
adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax);
const pbsReq = JSON.parse(server.requests[server.requests.length - 1].requestBody);
expect(pbsReq.imp[0].bidfloor).to.be.undefined;
expect(pbsReq.imp[0].bidfloorcur).to.be.undefined;
[pbsReq.imp[0], pbsReq.imp[0].banner, pbsReq.imp[0].banner.format[0]].forEach(obj => {
expect(obj.bidfloor).to.be.undefined;
expect(obj.bidfloorcur).to.be.undefined;
})
});
})

Object.entries({
'is available': {
expectDesc: 'minimum after conversion',
expectedFloor: 10,
expectedCur: '0.1',
conversionFn: (amount, from, to) => {
from = parseFloat(from);
to = parseFloat(to);
return amount * from / to;
},
'imp level floors': {
target: 'imp.0'
},
'is not available': {
expectDesc: 'absolute minimum',
expectedFloor: 1,
expectedCur: '10',
conversionFn: null
'mediaType level floors': {
target: 'imp.0.banner.ext',
floorFilter: ({mediaType, size}) => size === '*' && mediaType !== '*'
},
'is not working': {
expectDesc: 'absolute minimum',
expectedFloor: 1,
expectedCur: '10',
conversionFn: () => {
throw new Error();
}
'format level floors': {
target: 'imp.0.banner.format.0.ext',
floorFilter: ({size}) => size !== '*'
}
}).forEach(([t, {expectDesc, expectedFloor, expectedCur, conversionFn}]) => {
describe(`and currency conversion ${t}`, () => {
let mockConvertCurrency;
const origConvertCurrency = getGlobal().convertCurrency;
}).forEach(([t, {target, floorFilter}]) => {
describe(t, () => {
beforeEach(() => {
if (conversionFn) {
getGlobal().convertCurrency = mockConvertCurrency = sinon.stub().callsFake(conversionFn)
} else {
mockConvertCurrency = null;
delete getGlobal().convertCurrency;
if (floorFilter != null) {
BID_REQUESTS
.flatMap(req => req.bids)
.forEach(req => {
req.getFloor = ((orig) => (params) => {
if (floorFilter(params)) {
return orig(params);
}
})(req.getFloor);
})
}
});
})

afterEach(() => {
if (origConvertCurrency != null) {
getGlobal().convertCurrency = origConvertCurrency;
} else {
delete getGlobal().convertCurrency;
Object.entries({
'is available': {
expectDesc: 'minimum after conversion',
expectedFloor: 10,
expectedCur: '0.1',
conversionFn: (amount, from, to) => {
from = parseFloat(from);
to = parseFloat(to);
return amount * from / to;
},
},
'is not available': {
expectDesc: 'absolute minimum',
expectedFloor: 1,
expectedCur: '10',
conversionFn: null
},
'is not working': {
expectDesc: 'absolute minimum',
expectedFloor: 1,
expectedCur: '10',
conversionFn: () => {
throw new Error();
}
}
});
}).forEach(([t, {expectDesc, expectedFloor, expectedCur, conversionFn}]) => {
describe(`and currency conversion ${t}`, () => {
let mockConvertCurrency;
const origConvertCurrency = getGlobal().convertCurrency;
beforeEach(() => {
if (conversionFn) {
getGlobal().convertCurrency = mockConvertCurrency = sinon.stub().callsFake(conversionFn)
} else {
mockConvertCurrency = null;
delete getGlobal().convertCurrency;
}
});

it(`should pick the ${expectDesc}`, () => {
adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax);
const pbsReq = JSON.parse(server.requests[server.requests.length - 1].requestBody);
expect(pbsReq.imp[0].bidfloor).to.eql(expectedFloor);
expect(pbsReq.imp[0].bidfloorcur).to.eql(expectedCur);
afterEach(() => {
if (origConvertCurrency != null) {
getGlobal().convertCurrency = origConvertCurrency;
} else {
delete getGlobal().convertCurrency;
}
});

it(`should pick the ${expectDesc}`, () => {
adapter.callBids(s2sReq, BID_REQUESTS, addBidResponse, done, ajax);
const pbsReq = JSON.parse(server.requests[server.requests.length - 1].requestBody);
expect(deepAccess(pbsReq, `${target}.bidfloor`)).to.eql(expectedFloor);
expect(deepAccess(pbsReq, `${target}.bidfloorcur`)).to.eql(expectedCur)
});
});
});
});
});
})
})
});
});

Expand Down
Loading
Loading