Skip to content

PubMatic Adapter : Read custom targeting from RTD providers #13705

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

Merged
merged 3 commits into from
Jul 31, 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
23 changes: 22 additions & 1 deletion modules/pubmaticBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const converter = ortbConverter({
},
imp(buildImp, bidRequest, context) {
const { kadfloor, currency, adSlot = '', deals, dctr, pmzoneid, hashedKey } = bidRequest.params;
const { adUnitCode, mediaTypes, rtd } = bidRequest;
const { adUnitCode, mediaTypes, rtd, ortb2 } = bidRequest;
const imp = buildImp(bidRequest, context);

// Check if the imp object does not have banner, video, or native
Expand All @@ -76,6 +76,16 @@ const converter = ortbConverter({
}
if (deals) addPMPDeals(imp, deals, LOG_WARN_PREFIX);
if (dctr) addDealCustomTargetings(imp, dctr, LOG_WARN_PREFIX);
const customTargetings = shouldAddDealTargeting(ortb2);
if (customTargetings) {
imp.ext = imp.ext || {};
const targetingValues = Object.values(customTargetings).filter(Boolean);
if (targetingValues.length) {
imp.ext['key_val'] = imp.ext['key_val']
? `${imp.ext['key_val']}|${targetingValues.join('|')}`
: targetingValues.join('|');
}
}
if (rtd?.jwplayer) addJWPlayerSegmentData(imp, rtd.jwplayer);
imp.bidfloor = _parseSlotParam('kadfloor', kadfloor);
imp.bidfloorcur = currency ? _parseSlotParam('currency', currency) : DEFAULT_CURRENCY;
Expand Down Expand Up @@ -159,6 +169,17 @@ const converter = ortbConverter({
}
});

export const shouldAddDealTargeting = (ortb2) => {
const imSegmentData = ortb2?.user?.ext?.data?.im_segments;
const iasBrandSafety = ortb2?.site?.ext?.data?.['ias-brand-safety'];
const hasImSegments = imSegmentData && isArray(imSegmentData) && imSegmentData.length;
const hasIasBrandSafety = typeof iasBrandSafety === 'object' && Object.keys(iasBrandSafety).length;
const result = {};
if (hasImSegments) result.im_segments = `im_segments=${imSegmentData.join(',')}`;
if (hasIasBrandSafety) result['ias-brand-safety'] = Object.entries(iasBrandSafety).map(([key, value]) => `${key}=${value}`).join('|');
return Object.keys(result).length ? result : undefined;
}

export function _calculateBidCpmAdjustment(bid) {
if (!bid) return;

Expand Down
152 changes: 94 additions & 58 deletions test/spec/modules/pubmaticBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { spec, cpmAdjustment, addViewabilityToImp } from 'modules/pubmaticBidAdapter.js';
import { spec, cpmAdjustment, addViewabilityToImp, shouldAddDealTargeting } from 'modules/pubmaticBidAdapter.js';
import * as utils from 'src/utils.js';
import { bidderSettings } from 'src/bidderSettings.js';
import { config } from 'src/config.js';
Expand Down Expand Up @@ -52,7 +52,14 @@ describe('PubMatic adapter', () => {
connectiontype: 6
},
site: {domain: 'ebay.com', page: 'https://ebay.com'},
source: {}
source: {},
user: {
ext: {
data: {
im_segments: ['segment1', 'segment2']
}
}
}
},
ortb2Imp: {
ext: {
Expand Down Expand Up @@ -150,7 +157,14 @@ describe('PubMatic adapter', () => {
connectiontype: 6
},
site: {domain: 'ebay.com', page: 'https://ebay.com'},
source: {}
source: {},
user: {
ext: {
data: {
im_segments: ['segment1', 'segment2']
}
}
}
},
timeout: 2000
};
Expand Down Expand Up @@ -273,12 +287,12 @@ describe('PubMatic adapter', () => {
expect(imp[0]).to.have.property('ext').to.have.property('key_val');
});

it('should not add key_val if dctr is absent in parameters', () => {
it('adds key_val when dctr is missing but RTD provides custom targeting via ortb2', () => {
delete validBidRequests[0].params.dctr;
const request = spec.buildRequests(validBidRequests, bidderRequest);
const { imp } = request?.data;
expect(imp).to.be.an('array');
expect(imp[0]).to.have.property('ext').to.not.have.property('key_val');
expect(imp[0]).to.have.property('ext').to.have.property('key_val');
});

it('should set w and h to the primary size for banner', () => {
Expand Down Expand Up @@ -393,6 +407,15 @@ describe('PubMatic adapter', () => {
expect(imp[0]).to.have.property('banner').to.have.property('pos').equal(0);
});

it('should include custom targeting data in imp.ext when provided by RTD', () => {
const request = spec.buildRequests(validBidRequests, bidderRequest);
const { imp } = request?.data;
expect(imp).to.be.an('array');
expect(imp[0]).to.have.property('ext');
expect(imp[0].ext).to.have.property('key_val');
expect(imp[0].ext.key_val).to.deep.equal('im_segments=segment1,segment2');
})

if (FEATURES.VIDEO) {
describe('VIDEO', () => {
beforeEach(() => {
Expand Down Expand Up @@ -513,59 +536,72 @@ describe('PubMatic adapter', () => {
});
});
}
// describe('MULTIFORMAT', () => {
// let multiFormatBidderRequest;
// it('should have both banner & video impressions', () => {
// multiFormatBidderRequest = utils.deepClone(bidderRequest);
// multiFormatBidderRequest.bids[0].mediaTypes.video = {
// skip: 1,
// mimes: ['video/mp4', 'video/x-flv'],
// minduration: 5,
// maxduration: 30,
// startdelay: 5,
// playbackmethod: [1, 3],
// api: [1, 2],
// protocols: [2, 3],
// battr: [13, 14],
// linearity: 1,
// placement: 2,
// plcmt: 1,
// minbitrate: 10,
// maxbitrate: 10,
// playerSize: [640, 480]
// }
// const request = spec.buildRequests(validBidRequests, multiFormatBidderRequest);
// const { imp } = request?.data;
// expect(imp).to.be.an('array');
// expect(imp[0]).to.have.property('banner');
// expect(imp[0].banner).to.have.property('topframe');
// expect(imp[0].banner).to.have.property('format');
// expect(imp[0]).to.have.property('video');
// });

// it('should have both banner & native impressions', () => {
// multiFormatBidderRequest = utils.deepClone(bidderRequest);
// multiFormatBidderRequest.bids[0].nativeOrtbRequest = {
// ver: '1.2',
// assets: [{
// id: 0,
// img: {
// 'type': 3,
// 'w': 300,
// 'h': 250
// },
// required: 1,
// }]
// };
// const request = spec.buildRequests(validBidRequests, multiFormatBidderRequest);
// const { imp } = request?.data;
// expect(imp).to.be.an('array');
// expect(imp[0]).to.have.property('banner');
// expect(imp[0].banner).to.have.property('topframe');
// expect(imp[0].banner).to.have.property('format');
// expect(imp[0]).to.have.property('native');
// });
// });
describe('ShouldAddDealTargeting', () => {
it('should return im_segment targeting', () => {
const ortb2 = {
user: {
ext: {
data: {
im_segments: ['segment1', 'segment2']
}
}
}
};
const result = shouldAddDealTargeting(ortb2);
expect(result).to.have.property('im_segments');
expect(result.im_segments).to.deep.equal('im_segments=segment1,segment2');
});
it('should return ias-brand-safety targeting', () => {
const ortb2 = {
site: {
ext: {
data: {
'ias-brand-safety': {
'content': 'news',
'sports': 'cricket',
'cricket': 'player'
}
}
}
}
};
const result = shouldAddDealTargeting(ortb2);
expect(result).to.have.property('ias-brand-safety');
expect(result['ias-brand-safety']).to.deep.equal('content=news|sports=cricket|cricket=player');
});
it('should return undefined if no targeting is present', () => {
const ortb2 = {};
const result = shouldAddDealTargeting(ortb2);
expect(result).to.be.undefined;
});
it('should return both im_segment and ias-brand-safety targeting', () => {
const ortb2 = {
user: {
ext: {
data: {
im_segments: ['segment1', 'segment2']
}
}
},
site: {
ext: {
data: {
'ias-brand-safety': {
'content': 'news',
'sports': 'cricket',
'cricket': 'player'
}
}
}
}
};
const result = shouldAddDealTargeting(ortb2);
expect(result).to.have.property('im_segments');
expect(result.im_segments).to.deep.equal('im_segments=segment1,segment2');
expect(result).to.have.property('ias-brand-safety');
expect(result['ias-brand-safety']).to.deep.equal('content=news|sports=cricket|cricket=player');
});
})
});

describe('rest of ORTB request', () => {
Expand Down
Loading