diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 1517a53eee1..8176052f9e2 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -2,6 +2,7 @@ var utils = require('src/utils.js'); var bidfactory = require('src/bidfactory.js'); var bidmanager = require('src/bidmanager.js'); var adaptermanager = require('src/adaptermanager'); +const constants = require('src/constants.json'); /** * Adapter for requesting bids from Pubmatic. @@ -9,130 +10,208 @@ var adaptermanager = require('src/adaptermanager'); * @returns {{callBids: _callBids}} * @constructor */ -function PubmaticAdapter() { - var bids; - var _pm_pub_id; - var _pm_pub_age; - var _pm_pub_gender; - var _pm_pub_kvs; - var _pm_optimize_adslots = []; +const PubmaticAdapter = function PubmaticAdapter() { + let bids; + let usersync = false; + let _secure = 0; + let _protocol = (window.location.protocol === 'https:' ? (_secure = 1, 'https') : 'http') + '://'; let iframe; - function _callBids(params) { - bids = params.bids; - _pm_optimize_adslots = []; - for (var i = 0; i < bids.length; i++) { - var bid = bids[i]; - // bidmanager.pbCallbackMap['' + bid.params.adSlot] = bid; - _pm_pub_id = _pm_pub_id || bid.params.publisherId; - _pm_pub_age = _pm_pub_age || (bid.params.age || ''); - _pm_pub_gender = _pm_pub_gender || (bid.params.gender || ''); - _pm_pub_kvs = _pm_pub_kvs || (bid.params.kvs || ''); - _pm_optimize_adslots.push(bid.params.adSlot); + let dealChannelValues = { + 1: 'PMP', + 5: 'PREF', + 6: 'PMPG' + }; + + let customPars = { + 'kadgender': 'gender', + 'age': 'kadage', + 'dctr': 'dctr', // Custom Targeting + 'wiid': 'wiid', // Wrapper Impression ID + 'profId': 'profId', // Legacy: Profile ID + 'verId': 'verId', // Legacy: version ID + 'pmzoneid': { // Zone ID + n: 'pmZoneId', + m: function(zoneId) { + if (utils.isStr(zoneId)) { + return zoneId.split(',').slice(0, 50).join(); + } else { + return undefined; + } + } + } + }; + + function _initConf() { + var conf = {}; + var currTime = new Date(); + + conf.SAVersion = '1100'; + conf.wp = 'PreBid'; + conf.js = 1; + conf.wv = constants.REPO_AND_VERSION; + _secure && (conf.sec = 1); + conf.screenResolution = screen.width + 'x' + screen.height; + conf.ranreq = Math.random(); + conf.inIframe = window != top ? '1' : '0'; + + // istanbul ignore else + if (window.navigator.cookieEnabled === false) { + conf.fpcd = '1'; } - // Load pubmatic script in an iframe, because they call document.write - _getBids(); + try { + conf.pageURL = window.top.location.href; + conf.refurl = window.top.document.referrer; + } catch (e) { + conf.pageURL = window.location.href; + conf.refurl = window.document.referrer; + } + + conf.kltstamp = currTime.getFullYear() + + '-' + (currTime.getMonth() + 1) + + '-' + currTime.getDate() + + ' ' + currTime.getHours() + + ':' + currTime.getMinutes() + + ':' + currTime.getSeconds(); + conf.timezone = currTime.getTimezoneOffset() / 60 * -1; + + return conf; } - function _getBids() { - // create the iframe - iframe = utils.createInvisibleIframe(); + function _handleCustomParams(params, conf) { + // istanbul ignore else + if (!conf.kadpageurl) { + conf.kadpageurl = conf.pageURL; + } - var elToAppend = document.getElementsByTagName('head')[0]; + var key, value, entry; + for (key in customPars) { + // istanbul ignore else + if (customPars.hasOwnProperty(key)) { + value = params[key]; + // istanbul ignore else + if (value) { + entry = customPars[key]; + + if (typeof entry === 'object') { + value = entry.m(value, conf); + key = entry.n; + } else { + key = customPars[key]; + } + + if (utils.isStr(value)) { + conf[key] = value; + } else { + utils.logWarn('PubMatic: Ignoring param key: ' + customPars[key] + ', expects string-value, found ' + typeof value); + } + } + } + } + return conf; + } - // insert the iframe into document - elToAppend.insertBefore(iframe, elToAppend.firstChild); + function _cleanSlot(slotName) { + // istanbul ignore else + if (utils.isStr(slotName)) { + return slotName.replace(/^\s+/g, '').replace(/\s+$/g, ''); + } + return ''; + } + function _legacyExecution(conf, slots) { + var url = _generateLegacyCall(conf, slots); + iframe = utils.createInvisibleIframe(); + var elToAppend = document.getElementsByTagName('head')[0]; + elToAppend.insertBefore(iframe, elToAppend.firstChild); var iframeDoc = utils.getIframeDocument(iframe); - iframeDoc.write(_createRequestContent()); + var content = utils.createContentToExecuteExtScriptInFriendlyFrame(url); + content = content.replace(``, ``); + iframeDoc.write(content); iframeDoc.close(); } - function _createRequestContent() { - var content = 'inDapIF=true;'; - content += ''; - content += ''; - content += '' + - 'window.pm_pub_id = "%%PM_PUB_ID%%";' + - 'window.pm_optimize_adslots = [%%PM_OPTIMIZE_ADSLOTS%%];' + - 'window.kaddctr = "%%PM_ADDCTR%%";' + - 'window.kadgender = "%%PM_GENDER%%";' + - 'window.kadage = "%%PM_AGE%%";' + - 'window.pm_async_callback_fn = "window.parent.$$PREBID_GLOBAL$$.handlePubmaticCallback";'; - - content += ''; - - var map = {}; - map.PM_PUB_ID = _pm_pub_id; - map.PM_ADDCTR = _pm_pub_kvs; - map.PM_GENDER = _pm_pub_gender; - map.PM_AGE = _pm_pub_age; - map.PM_OPTIMIZE_ADSLOTS = _pm_optimize_adslots.map(function (adSlot) { - return "'" + adSlot + "'"; - }).join(','); - - content += ''; - content += ''; - content += ''; - content += ''; - content = utils.replaceTokenInString(content, map, '%%'); - - return content; + function _generateLegacyCall(conf, slots) { + var request_url = 'gads.pubmatic.com/AdServer/AdCallAggregator'; + return _protocol + request_url + '?' + utils.parseQueryStringParameters(conf) + 'adslots=' + encodeURIComponent('[' + slots.join(',') + ']'); } - $$PREBID_GLOBAL$$.handlePubmaticCallback = function () { - let bidDetailsMap = {}; - let progKeyValueMap = {}; - try { - bidDetailsMap = iframe.contentWindow.bidDetailsMap; - progKeyValueMap = iframe.contentWindow.progKeyValueMap; - } catch (e) { - utils.logError(e, 'Error parsing Pubmatic response'); + function _initUserSync(pubId) { + // istanbul ignore else + if (!usersync) { + var iframe = utils.createInvisibleIframe(); + iframe.src = _protocol + 'ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p=' + pubId; + utils.insertElement(iframe, document); + usersync = true; + } + } + + function _callBids(params) { + var conf = _initConf(); + var slots = []; + + conf.pubId = 0; + bids = params.bids || []; + + for (var i = 0; i < bids.length; i++) { + var bid = bids[i]; + conf.pubId = conf.pubId || bid.params.publisherId; + conf = _handleCustomParams(bid.params, conf); + bid.params.adSlot = _cleanSlot(bid.params.adSlot); + bid.params.adSlot.length && slots.push(bid.params.adSlot); + } + + // istanbul ignore else + if (conf.pubId && slots.length > 0) { + _legacyExecution(conf, slots); } + _initUserSync(conf.pubId); + } + + $$PREBID_GLOBAL$$.handlePubmaticCallback = function(bidDetailsMap, progKeyValueMap) { var i; var adUnit; var adUnitInfo; var bid; - var bidResponseMap = bidDetailsMap || {}; - var bidInfoMap = progKeyValueMap || {}; - var dimensions; + var bidResponseMap = bidDetailsMap; + var bidInfoMap = progKeyValueMap; + + if (!bidResponseMap || !bidInfoMap) { + return; + } for (i = 0; i < bids.length; i++) { var adResponse; bid = bids[i].params; - adUnit = bidResponseMap[bid.adSlot] || {}; - // adUnitInfo example: bidstatus=0;bid=0.0000;bidid=39620189@320x50;wdeal= - // if using DFP GPT, the params string comes in the format: // "bidstatus;1;bid;5.0000;bidid;hb_test@468x60;wdeal;" // the code below detects and handles this. + // istanbul ignore else if (bidInfoMap[bid.adSlot] && bidInfoMap[bid.adSlot].indexOf('=') === -1) { bidInfoMap[bid.adSlot] = bidInfoMap[bid.adSlot].replace(/([a-z]+);(.[^;]*)/ig, '$1=$2'); } - adUnitInfo = (bidInfoMap[bid.adSlot] || '').split(';').reduce(function (result, pair) { + adUnitInfo = (bidInfoMap[bid.adSlot] || '').split(';').reduce(function(result, pair) { var parts = pair.split('='); result[parts[0]] = parts[1]; return result; }, {}); if (adUnitInfo.bidstatus === '1') { - dimensions = adUnitInfo.bidid.split('@')[1].split('x'); adResponse = bidfactory.createBid(1); adResponse.bidderCode = 'pubmatic'; adResponse.adSlot = bid.adSlot; adResponse.cpm = Number(adUnitInfo.bid); adResponse.ad = unescape(adUnit.creative_tag); adResponse.ad += utils.createTrackPixelIframeHtml(decodeURIComponent(adUnit.tracking_url)); - adResponse.width = dimensions[0]; - adResponse.height = dimensions[1]; + adResponse.width = adUnit.width; + adResponse.height = adUnit.height; adResponse.dealId = adUnitInfo.wdeal; + adResponse.dealChannel = dealChannelValues[adUnit.deal_channel] || null; bidmanager.addBidResponse(bids[i].placementCode, adResponse); } else { @@ -147,7 +226,7 @@ function PubmaticAdapter() { return { callBids: _callBids }; -} +}; adaptermanager.registerBidAdapter(new PubmaticAdapter(), 'pubmatic'); diff --git a/src/utils.js b/src/utils.js index 9efa4f53c57..9e481c9aceb 100644 --- a/src/utils.js +++ b/src/utils.js @@ -730,6 +730,19 @@ export function deepAccess(obj, path) { return obj; } +/** + * Returns content for a friendly iframe to execute a URL in script tag + * @param {url} URL to be executed in a script tag in a friendly iframe + * and are macros left to be replaced if required + */ +export function createContentToExecuteExtScriptInFriendlyFrame(url) { + if (!url) { + return ''; + } + + return ``; +} + /** * Build an object consisting of only defined parameters to avoid creating an * object with defined keys and undefined values. diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js new file mode 100644 index 00000000000..c7b8cd5cd8e --- /dev/null +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -0,0 +1,276 @@ +import { + expect +} from 'chai'; +import * as utils from 'src/utils'; +import PubMaticAdapter from 'modules/pubmaticBidAdapter'; +import bidmanager from 'src/bidmanager'; +import constants from 'src/constants.json'; + +let getDefaultBidRequest = () => { + return { + bidderCode: 'pubmatic', + requestId: 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', + bidderRequestId: '7101db09af0db2', + start: new Date().getTime(), + bids: [{ + bidder: 'pubmatic', + bidId: '84ab500420319d', + bidderRequestId: '7101db09af0db2', + requestId: 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', + placementCode: 'DIV_1', + params: { + placement: 1234567, + network: '9599.1' + } + }] + }; +}; + +describe('PubMaticAdapter', () => { + let adapter; + + function createBidderRequest({ + bids, + params + } = {}) { + var bidderRequest = getDefaultBidRequest(); + if (bids && Array.isArray(bids)) { + bidderRequest.bids = bids; + } + if (params) { + bidderRequest.bids.forEach(bid => bid.params = params); + } + return bidderRequest; + } + + beforeEach(() => adapter = new PubMaticAdapter()); + + describe('callBids()', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + + describe('user syncup', () => { + beforeEach(() => { + sinon.stub(utils, 'insertElement'); + }); + + afterEach(() => { + utils.insertElement.restore(); + }); + + it('usersync is initiated', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9999, + adSlot: 'abcd@728x90', + age: '20' + } + })); + utils.insertElement.calledOnce.should.be.true; + expect(utils.insertElement.getCall(0).args[0].src).to.equal('http://ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p=9999'); + }); + }); + + describe('bid request', () => { + beforeEach(() => { + sinon.stub(utils, 'createContentToExecuteExtScriptInFriendlyFrame', function() { + return ''; + }); + }); + + afterEach(() => { + utils.createContentToExecuteExtScriptInFriendlyFrame.restore(); + }); + + it('requires parameters to be made', () => { + adapter.callBids({}); + utils.createContentToExecuteExtScriptInFriendlyFrame.calledOnce.should.be.false; + }); + + it('for publisherId 9990 call is made to gads.pubmatic.com', () => { + var bidRequest = createBidderRequest({ + params: { + publisherId: 9990, + adSlot: ' abcd@728x90', + age: '20', + wiid: 'abcdefghijk', + profId: '1234', + verId: '12', + pmzoneid: 'abcd123, efg345', + dctr: 'key=1234,5678' + } + }); + adapter.callBids(bidRequest); + var callURL = utils.createContentToExecuteExtScriptInFriendlyFrame.getCall(0).args[0]; + expect(bidRequest.bids[0].params.adSlot).to.equal('abcd@728x90'); + expect(callURL).to.contain('gads.pubmatic.com/AdServer/AdCallAggregator?'); + expect(callURL).to.contain('SAVersion=1100'); + expect(callURL).to.contain('wp=PreBid'); + expect(callURL).to.contain('js=1'); + expect(callURL).to.contain('screenResolution='); + expect(callURL).to.contain('wv=' + constants.REPO_AND_VERSION); + expect(callURL).to.contain('ranreq='); + expect(callURL).to.contain('inIframe='); + expect(callURL).to.contain('pageURL='); + expect(callURL).to.contain('refurl='); + expect(callURL).to.contain('kltstamp='); + expect(callURL).to.contain('timezone='); + expect(callURL).to.contain('age=20'); + expect(callURL).to.contain('adslots=%5Babcd%40728x90%5D'); + expect(callURL).to.contain('kadpageurl='); + expect(callURL).to.contain('wiid=abcdefghijk'); + expect(callURL).to.contain('profId=1234'); + expect(callURL).to.contain('verId=12'); + expect(callURL).to.contain('pmZoneId=abcd123%2C%20efg345'); + expect(callURL).to.contain('dctr=key%3D1234%2C5678'); + }); + + it('for publisherId 9990 call is made to gads.pubmatic.com, age passed as int not being passed ahead', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9990, + adSlot: 'abcd@728x90', + age: 20, + wiid: 'abcdefghijk', + profId: '1234', + verId: '12', + pmzoneid: {}, + dctr: 1234 + } + })); + var callURL = utils.createContentToExecuteExtScriptInFriendlyFrame.getCall(0).args[0]; + expect(callURL).to.contain('gads.pubmatic.com/AdServer/AdCallAggregator?'); + expect(callURL).to.not.contain('age=20'); + expect(callURL).to.not.contain('dctr=1234'); + }); + + it('for publisherId 9990 call is made to gads.pubmatic.com, invalid data for pmzoneid', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9990, + adSlot: 'abcd@728x90', + age: '20', + wiid: 'abcdefghijk', + profId: '1234', + verId: '12', + pmzoneid: {}, + dctr: 1234 + } + })); + var callURL = utils.createContentToExecuteExtScriptInFriendlyFrame.getCall(0).args[0]; + expect(callURL).to.contain('gads.pubmatic.com/AdServer/AdCallAggregator?'); + expect(callURL).to.not.contain('pmZoneId='); + }); + }); + + describe('#handlePubmaticCallback: ', () => { + beforeEach(() => { + sinon.stub(utils, 'createContentToExecuteExtScriptInFriendlyFrame', function() { + return ''; + }); + sinon.stub(bidmanager, 'addBidResponse'); + }); + + afterEach(() => { + utils.createContentToExecuteExtScriptInFriendlyFrame.restore(); + bidmanager.addBidResponse.restore(); + }); + + it('exists and is a function', () => { + expect($$PREBID_GLOBAL$$.handlePubmaticCallback).to.exist.and.to.be.a('function'); + }); + + it('empty response, arguments not passed', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9999, + adSlot: 'abcd@728x90', + age: '20' + } + })); + $$PREBID_GLOBAL$$.handlePubmaticCallback(); + expect(bidmanager.addBidResponse.callCount).to.equal(0); + }); + + it('empty response', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9999, + adSlot: 'abcd@728x90', + age: '20' + } + })); + $$PREBID_GLOBAL$$.handlePubmaticCallback({}, {}); + sinon.assert.called(bidmanager.addBidResponse); + expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal('DIV_1'); + var theBid = bidmanager.addBidResponse.firstCall.args[1]; + expect(theBid.bidderCode).to.equal('pubmatic'); + expect(theBid.getStatusCode()).to.equal(2); + }); + + it('not empty response', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9999, + adSlot: 'abcd@728x90:0', + age: '20' + } + })); + $$PREBID_GLOBAL$$.handlePubmaticCallback({ + 'abcd@728x90:0': { + 'ecpm': 10, + 'creative_tag': 'hello', + 'tracking_url': 'http%3a%2f%2fhaso.pubmatic.com%2fads%2f9999%2fGRPBID%2f2.gif%3ftrackid%3d12345', + 'width': 728, + 'height': 90, + 'deal_channel': 5 + } + }, { + 'abcd@728x90:0': 'bidstatus;1;bid;10.0000;bidid;abcd@728x90:0;wdeal;PMERW36842' + }); + sinon.assert.called(bidmanager.addBidResponse); + expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal('DIV_1'); + var theBid = bidmanager.addBidResponse.firstCall.args[1]; + expect(theBid.bidderCode).to.equal('pubmatic'); + expect(theBid.adSlot).to.equal('abcd@728x90:0'); + expect(theBid.cpm).to.equal(10); + expect(theBid.width).to.equal(728); + expect(theBid.height).to.equal(90); + expect(theBid.dealId).to.equal('PMERW36842'); + expect(theBid.dealChannel).to.equal('PREF'); + }); + + it('not empty response, without dealChannel', () => { + adapter.callBids(createBidderRequest({ + params: { + publisherId: 9999, + adSlot: 'abcd@728x90', + age: '20' + } + })); + $$PREBID_GLOBAL$$.handlePubmaticCallback({ + 'abcd@728x90': { + 'ecpm': 10, + 'creative_tag': 'hello', + 'tracking_url': 'http%3a%2f%2fhaso.pubmatic.com%2fads%2f9999%2fGRPBID%2f2.gif%3ftrackid%3d12345', + 'width': 728, + 'height': 90 + } + }, { + 'abcd@728x90': 'bidstatus;1;bid;10.0000;bidid;abcd@728x90:0;wdeal;PMERW36842' + }); + sinon.assert.called(bidmanager.addBidResponse); + expect(bidmanager.addBidResponse.firstCall.args[0]).to.equal('DIV_1'); + var theBid = bidmanager.addBidResponse.firstCall.args[1]; + expect(theBid.bidderCode).to.equal('pubmatic'); + expect(theBid.adSlot).to.equal('abcd@728x90'); + expect(theBid.cpm).to.equal(10); + expect(theBid.width).to.equal(728); + expect(theBid.height).to.equal(90); + expect(theBid.dealId).to.equal('PMERW36842'); + expect(theBid.dealChannel).to.equal(null); + }); + }); + }); +}); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index ad2645b2351..a08abaee847 100755 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -658,6 +658,20 @@ describe('Utils', function () { }); }); + describe('createContentToExecuteExtScriptInFriendlyFrame', function () { + it('should return empty string if url is not passed', function () { + var output = utils.createContentToExecuteExtScriptInFriendlyFrame(); + assert.equal(output, ''); + }); + + it('should have URL in returned value if url is passed', function () { + var url = 'https://abcd.com/service?a=1&b=2&c=3'; + var output = utils.createContentToExecuteExtScriptInFriendlyFrame(url); + var expected = ``; + assert.equal(output, expected); + }); + }); + describe('getDefinedParams', () => { it('builds an object consisting of defined params', () => { const adUnit = {