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

PBS adapter: fix bug with priceFloors sometimes not being set in request #8309

Merged
merged 5 commits into from
Apr 22, 2022
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
72 changes: 58 additions & 14 deletions modules/prebidServerBidAdapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {find, includes} from '../../src/polyfill.js';
import { S2S_VENDORS } from './config.js';
import { ajax } from '../../src/ajax.js';
import {hook} from '../../src/hook.js';
import {getGlobal} from '../../src/prebidGlobal.js';

const getConfig = config.getConfig;

Expand Down Expand Up @@ -755,21 +756,64 @@ Object.assign(ORTB2.prototype, {
deepSetValue(imp, 'ext.prebid.storedauctionresponse.id', storedAuctionResponseBid.storedAuctionResponse.toString());
}

const getFloorBid = find(firstBidRequest.bids, bid => bid.adUnitCode === adUnit.code && typeof bid.getFloor === 'function');
const floor = (() => {
// we have to pick a floor for the imp - here we attempt to find the minimum floor
// across all bids for this adUnit

const convertCurrency = typeof getGlobal().convertCurrency !== 'function'
? (amount) => amount
: (amount, from, to) => {
if (from === to) return amount;
let result = null;
try {
result = getGlobal().convertCurrency(amount, from, to);
} catch (e) {
}
return result;
}
const s2sCurrency = config.getConfig('currency.adServerCurrency') || DEFAULT_S2S_CURRENCY;

return adUnit.bids
.map((bid) => this.getBidRequest(imp.id, bid.bidder))
.map((bid) => {
if (!bid || typeof bid.getFloor !== 'function') return;
try {
const {currency, floor} = bid.getFloor({
currency: s2sCurrency
});
return {
currency,
floor: parseFloat(floor)
}
} catch (e) {
logError('PBS: getFloor threw an error: ', e);
}
})
.reduce((min, floor) => {
// if any bid does not have a valid floor, do not attempt to send any to PBS
if (floor == null || floor.currency == null || floor.floor == null || isNaN(floor.floor)) {
min.min = null;
}
if (min.min === null) {
return min;
}
// otherwise, pick the minimum one (or, in some strange confluence of circumstances, the one in the best currency)
if (min.ref == null) {
min.ref = min.min = floor;
} else {
const value = convertCurrency(floor.floor, floor.currency, min.ref.currency);
if (value != null && value < min.ref.floor) {
min.ref.floor = value;
min.min = floor;
}
}
return min;
}, {}).min
})();

if (getFloorBid) {
let floorInfo;
try {
floorInfo = getFloorBid.getFloor({
currency: config.getConfig('currency.adServerCurrency') || DEFAULT_S2S_CURRENCY,
});
} catch (e) {
logError('PBS: getFloor threw an error: ', e);
}
if (floorInfo && floorInfo.currency && !isNaN(parseFloat(floorInfo.floor))) {
imp.bidfloor = parseFloat(floorInfo.floor);
imp.bidfloorcur = floorInfo.currency
}
if (floor) {
imp.bidfloor = floor.floor;
imp.bidfloorcur = floor.currency
}

if (imp.banner || imp.video || imp.native) {
Expand Down
200 changes: 200 additions & 0 deletions test/spec/modules/prebidServerBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { decorateAdUnitsWithNativeParams } from '../../../src/native.js';
import { auctionManager } from '../../../src/auctionManager.js';
import { stubAuctionIndex } from '../../helpers/indexStub.js';
import { registerBidder } from 'src/adapters/bidderFactory.js';
import {getGlobal} from '../../../src/prebidGlobal.js';

let CONFIG = {
accountId: '1',
Expand Down Expand Up @@ -1038,6 +1039,205 @@ describe('S2S Adapter', function () {
})
).to.be.true;
});

it('should find the floor when not all bidderRequests contain it', () => {
config.setConfig({
s2sConfig: {
...CONFIG,
bidders: ['b1', 'b2']
},
});
const bidderRequests = [
{
...BID_REQUESTS[0],
bidderCode: 'b1',
bids: [{
bidder: 'b1',
bidId: 1,
}]
},
{
...BID_REQUESTS[0],
bidderCode: 'b2',
bids: [{
bidder: 'b2',
bidId: 2,
getFloor: () => ({
currency: 'CUR',
floor: 123
})
}],
}
];
const adUnits = [
{
code: 'au1',
transactionId: 't1',
mediaTypes: {
banner: {sizes: [1, 1]}
},
bids: [{bidder: 'b1', bid_id: 1}]
},
{
code: 'au2',
transactionId: 't2',
bids: [{bidder: 'b2', bid_id: 2}],
mediaTypes: {
banner: {sizes: [1, 1]}
}
}
];
const s2sReq = {
...REQUEST,
ad_units: adUnits
}

adapter.callBids(s2sReq, bidderRequests, addBidResponse, done, ajax);

const pbsReq = JSON.parse(server.requests[server.requests.length - 1].requestBody);
const [imp1, imp2] = pbsReq.imp;

expect(imp1.bidfloor).to.be.undefined;
expect(imp1.bidfloorcur).to.be.undefined;

expect(imp2.bidfloor).to.eql(123);
expect(imp2.bidfloorcur).to.eql('CUR');
});

describe('when different bids have different floors', () => {
let s2sReq;
beforeEach(() => {
config.setConfig({
s2sConfig: {
...CONFIG,
bidders: ['b1', 'b2', 'b3']
},
});
BID_REQUESTS = [
{
...BID_REQUESTS[0],
bidderCode: 'b2',
bids: [{
bidder: 'b2',
bidId: 2,
getFloor: () => ({
currency: '1',
floor: 2
})
}],
},
{
...BID_REQUESTS[0],
bidderCode: 'b1',
bids: [{
bidder: 'b1',
bidId: 1,
getFloor: () => ({
floor: 10,
currency: '0.1'
})
}]
},
{
...BID_REQUESTS[0],
bidderCode: 'b3',
bids: [{
bidder: 'b3',
bidId: 3,
getFloor: () => ({
currency: '10',
floor: 1
})
}],
}
];
s2sReq = {
...REQUEST,
ad_units: [
{
code: 'au1',
transactionId: 't1',
mediaTypes: {
banner: {sizes: [1, 1]}
},
bids: [
{bidder: 'b2', bid_id: 2},
{bidder: 'b3', bid_id: 3},
{bidder: 'b1', bid_id: 1},
]
}
]
};
});

Object.entries({
'cannot compute a floor': (bid) => { bid.getFloor = () => { throw new Error() } },
'does not set a floor': (bid) => { delete bid.getFloor; },
}).forEach(([t, updateBid]) => {
it(`should not set pricefloor if any one of them ${t}`, () => {
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;
});
})

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: 'first',
expectedFloor: 2,
expectedCur: '1',
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;
}
});

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(pbsReq.imp[0].bidfloor).to.eql(expectedFloor);
expect(pbsReq.imp[0].bidfloorcur).to.eql(expectedCur);
});
});
});
});
});

it('adds device.w and device.h even if the config lacks a device object', function () {
Expand Down