diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 999daa09eff..0b746985088 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -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 @@ -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; @@ -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; diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index e0a52420cd0..84963985ab8 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -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'; @@ -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: { @@ -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 }; @@ -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', () => { @@ -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(() => { @@ -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', () => {