Skip to content

Commit

Permalink
New Module: Bid response filter (#12147)
Browse files Browse the repository at this point in the history
* initial commit

* update

* review changes

* + auction index

* unit tests to ortb converter

* review changes

* battr setting

* improvements

* fix log message

---------

Co-authored-by: Marcin Komorski <marcinkomorski@Marcins-MacBook-Pro.local>
Co-authored-by: Marcin Komorski <marcinkomorski@marcins-mbp.home>
Co-authored-by: Demetrio Girardi <dgirardi@prebid.org>
  • Loading branch information
4 people authored Oct 8, 2024
1 parent df04741 commit 7ac768f
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 3 deletions.
7 changes: 7 additions & 0 deletions libraries/ortbConverter/processors/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ export const DEFAULT_PROCESSORS = {
if (bid.ext?.dsa) {
bidResponse.meta.dsa = bid.ext.dsa;
}
if (bid.cat) {
bidResponse.meta.primaryCatId = bid.cat[0];
bidResponse.meta.secondaryCatIds = bid.cat.slice(1);
}
if (bid.attr) {
bidResponse.meta.attr = bid.attr;
}
}
}
}
Expand Down
40 changes: 40 additions & 0 deletions modules/bidResponseFilter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { auctionManager } from '../../src/auctionManager.js';
import { config } from '../../src/config.js';
import { getHook } from '../../src/hook.js';

export const MODULE_NAME = 'bidResponseFilter';
export const BID_CATEGORY_REJECTION_REASON = 'Category is not allowed';
export const BID_ADV_DOMAINS_REJECTION_REASON = 'Adv domain is not allowed';
export const BID_ATTR_REJECTION_REASON = 'Attr is not allowed';

function init() {
getHook('addBidResponse').before(addBidResponseHook);
};

export function addBidResponseHook(next, adUnitCode, bid, reject, index = auctionManager.index) {
const {bcat = [], badv = []} = index.getOrtb2(bid) || {};
const battr = index.getBidRequest(bid)?.ortb2Imp[bid.mediaType]?.battr || index.getAdUnit(bid)?.ortb2Imp[bid.mediaType]?.battr || [];
const moduleConfig = config.getConfig(MODULE_NAME);

const catConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.cat || {})};
const advConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.adv || {})};
const attrConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.attr || {})};

const { primaryCatId, secondaryCatIds = [], advertiserDomains = [], attr: metaAttr } = bid.meta || {};

// checking if bid fulfills ortb2 fields rules
if ((catConfig.enforce && bcat.some(category => [primaryCatId, ...secondaryCatIds].includes(category))) ||
(catConfig.blockUnknown && !primaryCatId)) {
reject(BID_CATEGORY_REJECTION_REASON);
} else if ((advConfig.enforce && badv.some(domain => advertiserDomains.includes(domain))) ||
(advConfig.blockUnknown && !advertiserDomains.length)) {
reject(BID_ADV_DOMAINS_REJECTION_REASON);
} else if ((attrConfig.enforce && battr.includes(metaAttr)) ||
(attrConfig.blockUnknown && !metaAttr)) {
reject(BID_ATTR_REJECTION_REASON);
} else {
return next(adUnitCode, bid, reject);
}
}

init();
3 changes: 3 additions & 0 deletions src/auctionIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export function AuctionIndex(getAuctions) {
.flatMap(ber => ber.bids)
.find(br => br && br.bidId === requestId);
}
},
getOrtb2(bid) {
return this.getBidderRequest(bid)?.ortb2 || this.getAuction(bid)?.getFPD()?.global?.ortb2
}
});
}
19 changes: 19 additions & 0 deletions src/prebid.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,22 @@ function validateSizes(sizes, targLength) {
return cleanSizes;
}

export function setBattrForAdUnit(adUnit, mediaType) {
const ortb2Imp = adUnit.ortb2Imp || {};
const mediaTypes = adUnit.mediaTypes || {};

if (ortb2Imp[mediaType]?.battr && mediaTypes[mediaType]?.battr && (ortb2Imp[mediaType]?.battr !== mediaTypes[mediaType]?.battr)) {
logWarn(`Ad unit ${adUnit.code} specifies conflicting ortb2Imp.${mediaType}.battr and mediaTypes.${mediaType}.battr, the latter will be ignored`, adUnit);
}

const battr = ortb2Imp[mediaType]?.battr || mediaTypes[mediaType]?.battr;

if (battr != null) {
deepSetValue(adUnit, `ortb2Imp.${mediaType}.battr`, battr);
deepSetValue(adUnit, `mediaTypes.${mediaType}.battr`, battr);
}
}

function validateBannerMediaType(adUnit) {
const validatedAdUnit = deepClone(adUnit);
const banner = validatedAdUnit.mediaTypes.banner;
Expand All @@ -112,6 +128,7 @@ function validateBannerMediaType(adUnit) {
logError('Detected a mediaTypes.banner object without a proper sizes field. Please ensure the sizes are listed like: [[300, 250], ...]. Removing invalid mediaTypes.banner object from request.');
delete validatedAdUnit.mediaTypes.banner
}
setBattrForAdUnit(validatedAdUnit, 'banner');
return validatedAdUnit;
}

Expand All @@ -135,6 +152,7 @@ function validateVideoMediaType(adUnit) {
}
}
validateOrtbVideoFields(validatedAdUnit);
setBattrForAdUnit(validatedAdUnit, 'video');
return validatedAdUnit;
}

Expand Down Expand Up @@ -184,6 +202,7 @@ function validateNativeMediaType(adUnit) {
logError('Please use an array of sizes for native.icon.sizes field. Removing invalid mediaTypes.native.icon.sizes property from request.');
delete validatedAdUnit.mediaTypes.native.icon.sizes;
}
setBattrForAdUnit(validatedAdUnit, 'native');
return validatedAdUnit;
}

Expand Down
135 changes: 135 additions & 0 deletions test/spec/modules/bidResponseFilter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { BID_ADV_DOMAINS_REJECTION_REASON, BID_ATTR_REJECTION_REASON, BID_CATEGORY_REJECTION_REASON, MODULE_NAME, PUBLISHER_FILTER_REJECTION_REASON, addBidResponseHook } from '../../../modules/bidResponseFilter';
import { config } from '../../../src/config';

describe('bidResponseFilter', () => {
let mockAuctionIndex
beforeEach(() => {
config.resetConfig();
mockAuctionIndex = {
getBidRequest: () => {},
getAdUnit: () => {}
};
});

it('should pass the bid after successful ortb2 rules validation', () => {
const call = sinon.stub();

mockAuctionIndex.getOrtb2 = () => ({
badv: [], bcat: ['BANNED_CAT1', 'BANNED_CAT2']
});

const bid = {
meta: {
advertiserDomains: ['domain1.com', 'domain2.com'],
primaryCatId: 'EXAMPLE-CAT-ID',
attr: 'attr'
}
};

addBidResponseHook(call, 'adcode', bid, () => {}, mockAuctionIndex);
sinon.assert.calledOnce(call);
});

it('should reject the bid after failed ortb2 cat rule validation', () => {
const reject = sinon.stub();
const call = sinon.stub();
const bid = {
meta: {
advertiserDomains: ['domain1.com', 'domain2.com'],
primaryCatId: 'BANNED_CAT1',
attr: 'attr'
}
};
mockAuctionIndex.getOrtb2 = () => ({
badv: [], bcat: ['BANNED_CAT1', 'BANNED_CAT2']
});

addBidResponseHook(call, 'adcode', bid, reject, mockAuctionIndex);
sinon.assert.calledWith(reject, BID_CATEGORY_REJECTION_REASON);
});

it('should reject the bid after failed ortb2 adv domains rule validation', () => {
const rejection = sinon.stub();
const call = sinon.stub();
const bid = {
meta: {
advertiserDomains: ['domain1.com', 'domain2.com'],
primaryCatId: 'VALID_CAT',
attr: 'attr'
}
};
mockAuctionIndex.getOrtb2 = () => ({
badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2']
});

addBidResponseHook(call, 'adcode', bid, rejection, mockAuctionIndex);
sinon.assert.calledWith(rejection, BID_ADV_DOMAINS_REJECTION_REASON);
});

it('should reject the bid after failed ortb2 attr rule validation', () => {
const reject = sinon.stub();
const call = sinon.stub();
const bid = {
meta: {
advertiserDomains: ['validdomain1.com', 'validdomain2.com'],
primaryCatId: 'VALID_CAT',
attr: 'BANNED_ATTR'
},
mediaType: 'video'
};
mockAuctionIndex.getOrtb2 = () => ({
badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2']
});

mockAuctionIndex.getBidRequest = () => ({
ortb2Imp: {
video: {
battr: 'BANNED_ATTR'
}
}
})

addBidResponseHook(call, 'adcode', bid, reject, mockAuctionIndex);
sinon.assert.calledWith(reject, BID_ATTR_REJECTION_REASON);
});

it('should omit the validation if the flag is set to false', () => {
const call = sinon.stub();
const bid = {
meta: {
advertiserDomains: ['validdomain1.com', 'validdomain2.com'],
primaryCatId: 'BANNED_CAT1',
attr: 'valid_attr'
}
};

mockAuctionIndex.getOrtb2 = () => ({
badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2']
});

config.setConfig({[MODULE_NAME]: {cat: {enforce: false}}});

addBidResponseHook(call, 'adcode', bid, () => {}, mockAuctionIndex);
sinon.assert.calledOnce(call);
});

it('should allow bid for unknown flag set to false', () => {
const call = sinon.stub();
const bid = {
meta: {
advertiserDomains: ['validdomain1.com', 'validdomain2.com'],
primaryCatId: undefined,
attr: 'valid_attr'
}
};

mockAuctionIndex.getOrtb2 = () => ({
badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2']
});

config.setConfig({[MODULE_NAME]: {cat: {blockUnknown: false}}});

addBidResponseHook(call, 'adcode', bid, () => {});
sinon.assert.calledOnce(call);
});
})
4 changes: 3 additions & 1 deletion test/spec/modules/dsp_genieeBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ describe('Geniee adapter tests', () => {
currency: 'JPY',
mediaType: 'banner',
meta: {
advertiserDomains: ['geniee.co.jp']
advertiserDomains: ['geniee.co.jp'],
primaryCatId: 'IAB1',
secondaryCatIds: []
},
netRevenue: true,
requestId: 'bid-id',
Expand Down
10 changes: 10 additions & 0 deletions test/spec/ortbConverter/banner_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ describe('pbjs -> ortb banner conversion', () => {
expect(imp.banner.someParam).to.eql('someValue');
});

it('should keep ortb2Imp.banner.battr', () => {
const imp = {
banner: {
battr: 'battr'
}
};
fillBannerImp(imp, {mediaTypes: {banner: {sizes: [1, 2]}}}, {});
expect(imp.banner.battr).to.eql('battr');
});

it('does nothing if context.mediaType is set but is not BANNER', () => {
const imp = {};
fillBannerImp(imp, {mediaTypes: {banner: {sizes: [1, 2]}}}, {mediaType: VIDEO});
Expand Down
10 changes: 10 additions & 0 deletions test/spec/ortbConverter/native_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ describe('pbjs -> ortb native requests', () => {
expect(imp.native.something).to.eql('orother')
});

it('should keep ortb2Imp.native.battr', () => {
const imp = {
native: {
battr: 'battr'
}
};
fillNativeImp(imp, {mediaTypes: {native: {sizes: [1, 2]}}}, {});
expect(imp.native.battr).to.eql('battr');
});

it('should do nothing if there are no assets', () => {
const imp = {};
fillNativeImp(imp, {nativeOrtbRequest: {assets: []}}, {});
Expand Down
10 changes: 10 additions & 0 deletions test/spec/ortbConverter/video_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ describe('pbjs -> ortb video conversion', () => {
expect(imp.video.someParam).to.eql('someValue');
});

it('should keep ortb2Imp.video.battr', () => {
const imp = {
video: {
battr: 'battr'
}
};
fillVideoImp(imp, {mediaTypes: {video: {sizes: [1, 2]}}}, {});
expect(imp.video.battr).to.eql('battr');
});

it('does nothing is context.mediaType is set but is not VIDEO', () => {
const imp = {};
fillVideoImp(imp, {mediaTypes: {video: {playerSize: [[1, 2]]}}}, {mediaType: BANNER});
Expand Down
30 changes: 28 additions & 2 deletions test/spec/unit/core/auctionIndex_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import {AuctionIndex} from '../../../../src/auctionIndex.js';
describe('auction index', () => {
let index, auctions;

function mockAuction(id, adUnits, bidderRequests) {
function mockAuction(id, adUnits, bidderRequests, ortb2) {
return {
getAuctionId() { return id },
getAdUnits() { return adUnits; },
getBidRequests() { return bidderRequests; }
getBidRequests() { return bidderRequests; },
getFPD() {
return { global: { ortb2 } }
}
}
}

Expand Down Expand Up @@ -126,4 +129,27 @@ describe('auction index', () => {
});
})
});

describe('getOrtb2', () => {
let bidderRequests, adUnits = [];
beforeEach(() => {
bidderRequests = [
{bidderRequestId: 'ber1', ortb2: {}, bids: [{bidId: 'b1', adUnitId: 'au1'}, {}]},
{bidderRequestId: 'ber2', bids: [{bidId: 'b2', adUnitId: 'au2'}]}
]
auctions = [
mockAuction('a1', [adUnits[0]], [bidderRequests[0], {}]),
mockAuction('a2', [adUnits[1]], [bidderRequests[1]], {ortb2Field: true})
]
});
it('should return ortb2 for bid if exists on bidder request', () => {
const ortb2 = index.getOrtb2({bidderRequestId: 'ber1'});
expect(ortb2).to.be.a('object');
})

it('should return ortb2 from auction if does not exist on bidder request', () => {
const ortb2 = index.getOrtb2({bidderRequestId: 'ber2', auctionId: 'a2'});
expect(ortb2).to.be.deep.equals({ortb2Field: true});
})
})
});
Loading

0 comments on commit 7ac768f

Please sign in to comment.