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: Bid response filter #12147

Merged
merged 9 commits into from
Oct 8, 2024
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
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'],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this failing tests? it shouldn't need to be changed - you may need to tear down the bidResponseFilter tests. (if this was failing, more will likely be affected in the future, so it's better to try to get rid of test side effects).

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