Skip to content

Commit

Permalink
Core & PBS adapter: introduce bidder-level ortb2Imp; s2s-only `modu…
Browse files Browse the repository at this point in the history
…le` bids; PBS bidder-level `imp` params (#9470)

* adUnit.bid.ortb2Imp support

* Add "module" bids and PBS bidder-level imp params

* Merge branch 'master' into bid-ortb2Imp

* Update tests
  • Loading branch information
dgirardi authored Feb 15, 2023
1 parent f7b78cf commit 16166bd
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 15 deletions.
6 changes: 6 additions & 0 deletions modules/prebidServerBidAdapter/ortbConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ const PBS_CONVERTER = ortbConverter({
imp(buildImp, proxyBidRequest, context) {
Object.assign(context, proxyBidRequest.pbsData);
const imp = buildImp(proxyBidRequest, context);
(proxyBidRequest.bids || []).forEach(bid => {
if (bid.ortb2Imp && Object.keys(bid.ortb2Imp).length > 0) {
// set bidder-level imp attributes; see https://github.com/prebid/prebid-server/issues/2335
deepSetValue(imp, `ext.prebid.imp.${bid.bidder}`, bid.ortb2Imp);
}
});
if (Object.values(SUPPORTED_MEDIA_TYPES).some(mtype => imp[mtype])) {
imp.secure = context.s2sBidRequest.s2sConfig.secure;
return imp;
Expand Down
44 changes: 30 additions & 14 deletions src/adapterManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,21 @@ var _analyticsRegistry = {};

function getBids({bidderCode, auctionId, bidderRequestId, adUnits, src, metrics}) {
return adUnits.reduce((result, adUnit) => {
result.push(adUnit.bids.filter(bid => bid.bidder === bidderCode)
.reduce((bids, bid) => {
bid = Object.assign({}, bid, getDefinedParams(adUnit, [
'nativeParams',
'nativeOrtbRequest',
'ortb2Imp',
'mediaType',
'renderer'
]));
const bids = adUnit.bids.filter(bid => bid.bidder === bidderCode);
if (bidderCode == null && bids.length === 0 && adUnit.s2sBid != null) {
bids.push({bidder: null});
}
result.push(
bids.reduce((bids, bid) => {
bid = Object.assign({}, bid,
{ortb2Imp: mergeDeep({}, adUnit.ortb2Imp, bid.ortb2Imp)},
getDefinedParams(adUnit, [
'nativeParams',
'nativeOrtbRequest',
'mediaType',
'renderer'
])
);

const mediaTypes = bid.mediaTypes == null ? adUnit.mediaTypes : bid.mediaTypes

Expand Down Expand Up @@ -128,9 +134,18 @@ export const filterBidsForAdUnit = hook('sync', _filterBidsForAdUnit, 'filterBid

function getAdUnitCopyForPrebidServer(adUnits, s2sConfig) {
let adUnitsCopy = deepClone(adUnits);
let hasModuleBids = false;

adUnitsCopy.forEach((adUnit) => {
// filter out client side bids
const s2sBids = adUnit.bids.filter((b) => b.module === 'pbsBidAdapter' && b.params?.configName === s2sConfig.configName);
if (s2sBids.length === 1) {
adUnit.s2sBid = s2sBids[0];
hasModuleBids = true;
adUnit.ortb2Imp = mergeDeep({}, adUnit.s2sBid.ortb2Imp, adUnit.ortb2Imp);
} else if (s2sBids.length > 1) {
logWarn('Multiple "module" bids for the same s2s configuration; all will be ignored', s2sBids);
}
adUnit.bids = filterBidsForAdUnit(adUnit.bids, s2sConfig)
.map((bid) => {
bid.bid_id = getUniqueIdentifierStr();
Expand All @@ -140,9 +155,9 @@ function getAdUnitCopyForPrebidServer(adUnits, s2sConfig) {

// don't send empty requests
adUnitsCopy = adUnitsCopy.filter(adUnit => {
return adUnit.bids.length !== 0;
return adUnit.bids.length !== 0 || adUnit.s2sBid != null;
});
return adUnitsCopy;
return {adUnits: adUnitsCopy, hasModuleBids};
}

function getAdUnitCopyForClientAdapters(adUnits) {
Expand Down Expand Up @@ -244,11 +259,12 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a

_s2sConfigs.forEach(s2sConfig => {
if (s2sConfig && s2sConfig.enabled) {
let adUnitsS2SCopy = getAdUnitCopyForPrebidServer(adUnits, s2sConfig);
let {adUnits: adUnitsS2SCopy, hasModuleBids} = getAdUnitCopyForPrebidServer(adUnits, s2sConfig);

// uniquePbsTid is so we know which server to send which bids to during the callBids function
let uniquePbsTid = generateUUID();
serverBidders.forEach(bidderCode => {

(serverBidders.length === 0 && hasModuleBids ? [null] : serverBidders).forEach(bidderCode => {
const bidderRequestId = getUniqueIdentifierStr();
const metrics = auctionMetrics.fork();
const bidderRequest = addOrtb2({
Expand Down Expand Up @@ -279,7 +295,7 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a

bidRequests.forEach(request => {
if (request.adUnitsS2SCopy === undefined) {
request.adUnitsS2SCopy = adUnitsS2SCopy.filter(adUnitCopy => adUnitCopy.bids.length > 0);
request.adUnitsS2SCopy = adUnitsS2SCopy.filter(au => au.bids.length > 0 || au.s2sBid != null);
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ export function getKeyByValue(obj, value) {
export function getBidderCodes(adUnits = $$PREBID_GLOBAL$$.adUnits) {
// this could memoize adUnits
return adUnits.map(unit => unit.bids.map(bid => bid.bidder)
.reduce(flatten, [])).reduce(flatten, []).filter(uniques);
.reduce(flatten, [])).reduce(flatten, []).filter((bidder) => typeof bidder !== 'undefined').filter(uniques);
}

export function isGptPubadsDefined() {
Expand Down
56 changes: 56 additions & 0 deletions test/spec/modules/prebidServerBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2660,6 +2660,62 @@ describe('S2S Adapter', function () {
});
});

describe('Bidder-level ortb2Imp', () => {
beforeEach(() => {
config.setConfig({
s2sConfig: {
...CONFIG,
bidders: ['A', 'B']
}
})
})
it('should be set on imp.ext.prebid.imp', () => {
const s2sReq = utils.deepClone(REQUEST);
s2sReq.ad_units[0].ortb2Imp = {l0: 'adUnit'};
s2sReq.ad_units[0].bids = [
{
bidder: 'A',
bid_id: 1,
ortb2Imp: {
l2: 'A'
}
},
{
bidder: 'B',
bid_id: 2,
ortb2Imp: {
l2: 'B'
}
}
];
const bidderReqs = [
{
...BID_REQUESTS[0],
bidderCode: 'A',
bids: [{
bidId: 1,
bidder: 'A'
}]
},
{
...BID_REQUESTS[0],
bidderCode: 'B',
bids: [{
bidId: 2,
bidder: 'B'
}]
}
]
adapter.callBids(s2sReq, bidderReqs, addBidResponse, done, ajax);
const req = JSON.parse(server.requests[0].requestBody);
expect(req.imp[0].l0).to.eql('adUnit');
expect(req.imp[0].ext.prebid.imp).to.eql({
A: {l2: 'A'},
B: {l2: 'B'}
});
});
});

describe('ext.prebid config', function () {
it('should send \"imp.ext.prebid.storedrequest.id\" if \"ortb2Imp.ext.prebid.storedrequest.id\" is set', function () {
const consentConfig = { s2sConfig: CONFIG };
Expand Down
147 changes: 147 additions & 0 deletions test/spec/unit/core/adapterManager_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,153 @@ describe('adapterManager tests', function () {
requests.appnexus.bids.forEach((bid) => expect(bid.ortb2).to.eql(requests.appnexus.ortb2));
});

it('should merge in bid-level ortb2Imp with adUnit-level ortb2Imp', () => {
const adUnit = {
...adUnits[1],
ortb2Imp: {oneone: {twoone: 'val'}, onetwo: 'val'}
};
adUnit.bids[0].ortb2Imp = {oneone: {twotwo: 'val'}, onethree: 'val', onetwo: 'val2'};
const reqs = Object.fromEntries(
adapterManager.makeBidRequests([adUnit], 123, 'auction-id', 123, [], {})
.map((req) => [req.bidderCode, req])
);
sinon.assert.match(reqs[adUnit.bids[0].bidder].bids[0].ortb2Imp, {
oneone: {
twoone: 'val',
twotwo: 'val',
},
onetwo: 'val2',
onethree: 'val'
})
sinon.assert.match(reqs[adUnit.bids[1].bidder].bids[0].ortb2Imp, adUnit.ortb2Imp)
})

it('picks ortb2Imp from "module" when only one s2sConfig is set', () => {
config.setConfig({
s2sConfig: [
{
enabled: true,
adapter: 'mockS2S1',
}
]
});
const adUnit = {
code: 'mockau',
ortb2Imp: {
p1: 'adUnit'
},
bids: [
{
module: 'pbsBidAdapter',
ortb2Imp: {
p2: 'module'
}
}
]
};
const req = adapterManager.makeBidRequests([adUnit], 123, 'auction-id', 123, [], {})[0];
[req.adUnitsS2SCopy[0].ortb2Imp, req.bids[0].ortb2Imp].forEach(imp => {
sinon.assert.match(imp, {
p1: 'adUnit',
p2: 'module'
});
});
});

describe('with named s2s configs', () => {
beforeEach(() => {
config.setConfig({
s2sConfig: [
{
enabled: true,
adapter: 'mockS2S1',
configName: 'one',
bidders: ['A']
},
{
enabled: true,
adapter: 'mockS2S2',
configName: 'two',
bidders: ['B']
}
]
})
});

it('generates requests for "module" bids', () => {
const adUnit = {
code: 'mockau',
ortb2Imp: {
p1: 'adUnit'
},
bids: [
{
module: 'pbsBidAdapter',
params: {configName: 'one'},
ortb2Imp: {
p2: 'one'
}
},
{
module: 'pbsBidAdapter',
params: {configName: 'two'},
ortb2Imp: {
p2: 'two'
}
}
]
};
const reqs = adapterManager.makeBidRequests([adUnit], 123, 'auction-id', 123, [], {});
[reqs[0].adUnitsS2SCopy[0].ortb2Imp, reqs[0].bids[0].ortb2Imp].forEach(imp => {
sinon.assert.match(imp, {
p1: 'adUnit',
p2: 'one'
})
});
[reqs[1].adUnitsS2SCopy[0].ortb2Imp, reqs[1].bids[0].ortb2Imp].forEach(imp => {
sinon.assert.match(imp, {
p1: 'adUnit',
p2: 'two'
})
});
});

it('applies module-level ortb2Imp to "normal" s2s requests', () => {
const adUnit = {
code: 'mockau',
ortb2Imp: {
p1: 'adUnit'
},
bids: [
{
module: 'pbsBidAdapter',
params: {configName: 'one'},
ortb2Imp: {
p2: 'one'
}
},
{
bidder: 'A',
ortb2Imp: {
p3: 'bidderA'
}
}
]
};
const reqs = adapterManager.makeBidRequests([adUnit], 123, 'auction-id', 123, [], {});
expect(reqs.length).to.equal(1);
sinon.assert.match(reqs[0].adUnitsS2SCopy[0].ortb2Imp, {
p1: 'adUnit',
p2: 'one'
})
sinon.assert.match(reqs[0].bids[0].ortb2Imp, {
p1: 'adUnit',
p2: 'one',
p3: 'bidderA'
})
});
});

describe('when calling the s2s adapter', () => {
beforeEach(() => {
config.setConfig({
Expand Down

0 comments on commit 16166bd

Please sign in to comment.