From 6237a0afea79e3f0fc98bda1fa1729fd1013db76 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Sun, 10 Dec 2017 20:10:58 -0800 Subject: [PATCH 1/5] first commit --- modules/pubmaticBidAdapter.js | 288 +++++++++++++++++++ test/spec/modules/pubmaticBidAdapter_spec.js | 119 ++++++++ 2 files changed, 407 insertions(+) create mode 100644 modules/pubmaticBidAdapter.js create mode 100644 test/spec/modules/pubmaticBidAdapter_spec.js diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js new file mode 100644 index 00000000000..eb5b8d0fe46 --- /dev/null +++ b/modules/pubmaticBidAdapter.js @@ -0,0 +1,288 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +const constants = require('src/constants.json'); + +const BIDDER_CODE = 'pubmatic'; +const ENDPOINT = '//openbid.pubmatic.com/translator?source=prebid-server'; +const USYNCURL = '//ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p='; +const CURRENCY = 'USD'; +const AUCTION_TYPE = 2; //todo ?? is auction type correct ? second price auction +//todo: now what is significance of value ? +const CUSTOM_PARAMS = { + 'kadpageurl': 'kadpageurl', + 'gender': 'gender', + 'yob': 'yob', + 'dctr': 'dctr', // Custom Targeting + 'lat': 'lat', // Location - Latitude + 'lon': 'lon', // Location - Longitude + 'wiid': 'wiid', // Wrapper Impression ID + 'profId': 'profId', // Legacy: Profile ID + 'verId': 'verId' // Legacy: version ID +}; + +let publisherId = 0; + +function _processPmZoneId(zoneId) { + if (utils.isStr(zoneId)) { + return zoneId.split(',').slice(0, 50).join(); + } else { + return undefined; + } +} + +function _cleanSlot(slotName) { + if (utils.isStr(slotName)) { + return slotName.replace(/^\s+/g, '').replace(/\s+$/g, ''); + } + return ''; +} + +function _parseAdSlot(bid){ + + bid.params.adUnit = ''; + bid.params.adUnitIndex = '0'; + bid.params.width = 0; + bid.params.height = 0; + + bid.params.adSlot = _cleanSlot(bid.params.adSlot); + + var slot = bid.params.adSlot; + var splits = slot.split(':'); + + slot = splits[0]; + if(splits.length == 2){ + bid.params.adUnitIndex = splits[1]; + } + splits = slot.split('@'); + if(splits.length != 2){ + return; + } + bid.params.adUnit = splits[0]; + splits = splits[1].split('x'); + if(splits.length != 2){ + return; + } + bid.params.width = parseInt(splits[0]); + bid.params.height = parseInt(splits[1]); +} + +//todo: remove commented code +function _initConf() { + var conf = {}; + var currTime = new Date(); + conf.sec = 0; + //todo remove + let _protocol = (window.location.protocol === 'https:' ? (conf.sec = 1, 'https') : 'http') + '://'; + conf.wp = 'PreBid';//todo : do we need to send this ? + conf.wv = constants.REPO_AND_VERSION; + // istanbul ignore else + //if (window.navigator.cookieEnabled === false) { + // conf.fpcd = '1'; + //} + try { + conf.pageURL = window.top.location.href; + conf.hostname = window.top.location.hostname; + conf.refurl = window.top.document.referrer; + } catch (e) { + conf.pageURL = window.location.href; + conf.hostname = window.location.hostname; + 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 _handleCustomParams(params, conf) { + // istanbul ignore else + if (!conf.kadpageurl) { + conf.kadpageurl = conf.pageURL; + } + + var key, value, entry; + for (key in CUSTOM_PARAMS) { + // istanbul ignore else + if (CUSTOM_PARAMS.hasOwnProperty(key)) { + value = params[key]; + // istanbul ignore else + if (value) { + entry = CUSTOM_PARAMS[key]; + + if (typeof entry === 'object') { + value = entry.m(value, conf); + key = entry.n; + } else { + key = CUSTOM_PARAMS[key]; + } + + if (utils.isStr(value)) { + conf[key] = value; + } else { + utils.logWarn('PubMatic: Ignoring param key: ' + CUSTOM_PARAMS[key] + ', expects string-value, found ' + typeof value); + } + } + } + } + return conf; +} + +function _createOrtbTemplate(conf){ + return { + id : '' + new Date().getTime(), + at: AUCTION_TYPE, + cur: [CURRENCY], + imp: [], + site: { + domain: conf.hostname, + page: conf.pageURL, + publisher: {} + }, + device: { + ua: navigator.userAgent, + js: 1, + dnt: (navigator.doNotTrack == "yes" || navigator.doNotTrack == "1" || navigator.msDoNotTrack == "1") ? 1 : 0, + h: screen.height, + w: screen.width, + language: navigator.language, + ip: "123.4.12.34" //todo : REMOVE :: Mandatory w/o this we are not getting bids, isnt it retrieved from header ? + }, + user: {}, + ext: {} + }; +} + +function _createImpressionObject(bid, conf){ + return { + id: bid.bidId, + tagid: bid.params.adUnit, + bidfloor: bid.params.kadfloor || undefined, + secure: conf.sec, + banner: { + pos: 0, + w: bid.params.width, + h: bid.params.height, + topframe: 1, //todo: may need to change for postbid : check with open bid + }, + ext: { + pmZoneId: _processPmZoneId(bid.params.pmzoneid) + } + }; +} + +export const spec = { + code: BIDDER_CODE, + + /** + * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: bid => { + //if(bid && bid.params && bid.params.publisherId){ + //_parseAdSlot(bid); + //return !!(bid.params.adSlot && bid.params.adUnit && bid.params.adUnitIndex && bid.params.width && bid.params.height); + //}else{ + // return false; + //} + return !!(bid && bid.params && bid.params.publisherId && bid.params.adSlot); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: validBidRequests => { + var conf = _initConf(); + var payload = _createOrtbTemplate(conf); + validBidRequests.forEach(bid => { + _parseAdSlot(bid); + if(! (bid.params.adSlot && bid.params.adUnit && bid.params.adUnitIndex && bid.params.width && bid.params.height)){ + utils.logWarn('PubMatic: Skipping the non-standard adslot:', bid.params.adSlot, bid); + return; + } + conf.pubId = conf.pubId || bid.params.publisherId; + conf = _handleCustomParams(bid.params, conf); + conf.transactionId = bid.transactionId; + payload.imp.push(_createImpressionObject(bid, conf)); + }); + + if(payload.imp.length == 0){ + return; + } + + payload.site.publisher.id = conf.pubId; + publisherId = conf.pubId; + payload.ext.wrapper = {}; + payload.ext.wrapper.profile = conf.profId || undefined; + payload.ext.wrapper.version = conf.verId || undefined; + payload.ext.wrapper.wiid = conf.wiid || undefined; + payload.ext.wrapper.wv = conf.wv || undefined; + payload.ext.wrapper.transactionId = conf.transactionId; + payload.user.gender = conf.gender || undefined; + payload.user.lat = conf.lat || undefined; + payload.user.lon = conf.lon || undefined; + payload.user.yob = conf.yob || undefined; + payload.site.page = conf.kadpageurl || payload.site.page; + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(payload) + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} response A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: (response, request) => { + const bidResponses = []; + try { + if (response.body && response.body.seatbid && response.body.seatbid[0] && response.body.seatbid[0].bid) { + response.body.seatbid[0].bid.forEach(bid => { + let newBid = { + requestId: bid.impid, + cpm: bid.price, // Can we round to min precision ? + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.id, + dealId: bid.dealid, + currency: CURRENCY, + netRevenue: true, // todo: mandatory: Mike to confirm + ttl: 300, + referrer: utils.getTopWindowUrl(), + ad: bid.adm + }; + bidResponses.push(newBid); + }); + } + } catch (error) { + utils.logError(error); + } + return bidResponses; + }, + + /** + * Register User Sync. + */ + getUserSyncs: syncOptions => { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: USYNCURL + publisherId + }]; + } + } +}; + +registerBidder(spec); \ No newline at end of file diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js new file mode 100644 index 00000000000..04d5cd843cf --- /dev/null +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -0,0 +1,119 @@ +import {expect} from 'chai'; +import {spec} from 'modules/pubmaticBidAdapter'; +const constants = require('src/constants.json'); + +describe('PubMatic adapter', () => { + let bidRequests; + + beforeEach(() => { + bidRequests = [ + { + bidder: 'pubmatic', + params: { + publisherId: '301', + adSlot: '/15671365/DMDemo@300x250:0', + kadfloor: '1.2', + pmzoneid: 'aabc, ddef', + kadpageurl: 'www.publisher.com', + yob: '1986', + gender: 'M', + lat: '12.3', + lon: '23.7', + wiid: '1234567890', + profId: '100', + verId: '200' + }, + placementCode: '/19968336/header-bid-tag-1', + sizes: [[300, 250], [300, 600]], + bidId: '23acc48ad47af5', + requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' + } + ]; + }); + + describe('implementation', () => { + + describe('Bid validations', () => { + + it('valid bid case', () => { + let validBid = { + bidder: 'pubmatic', + params: { + publisherId: '301', + adSlot: '/15671365/DMDemo@300x250:0' + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + }); + + it('invalid bid case: publisherId not passed', () => { + let validBid = { + bidder: 'pubmatic', + params: { + adSlot: '/15671365/DMDemo@300x250:0' + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }); + + it('invalid bid case: adSlot not passed', () => { + let validBid = { + bidder: 'pubmatic', + params: { + publisherId: '301' + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }); + }); + + describe('Request formation', () => { + it('Endpoint checking', () => { + let request = spec.buildRequests(bidRequests); + expect(request.url).to.equal('//openbid.pubmatic.com/translator?source=prebid-server'); + expect(request.method).to.equal('POST'); + }); + + it('Request params check', () => { + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.at).to.equal(2); //auction type + expect(data.cur[0]).to.equal("USD"); //currency + expect(data.site.domain).to.be.a('string'); // domain should be set + expect(data.site.page).to.equal(bidRequests[0].params.kadpageurl); // forced pageURL + expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id + expect(data.user.yob).to.equal(bidRequests[0].params.yob); // YOB + expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender + expect(data.user.lat).to.equal(bidRequests[0].params.lat); // Latitude + expect(data.user.lon).to.equal(bidRequests[0].params.lon); // Lognitude + expect(data.ext.wrapper.wv).to.equal(constants.REPO_AND_VERSION); // Wrapper Version + expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].transactionId); // Prebid TransactionId + expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID + expect(data.ext.wrapper.profile).to.equal(bidRequests[0].params.profId); // OpenWrap: Wrapper Profile ID + expect(data.ext.wrapper.version).to.equal(bidRequests[0].params.verId); // OpenWrap: Wrapper Profile Version ID + + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id + expect(data.imp[0].bidfloor).to.equal(bidRequests[0].params.kadfloor); // kadfloor + expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid + expect(data.imp[0].banner.w).to.equal(300); // width + expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid); // pmzoneid + }); + + it('invalid adslot', () => { + bidRequests[0].params.adSlot = '/15671365/DMDemo'; + let request = spec.buildRequests(bidRequests); + expect(request).to.equal(undefined); + }); + }); + + }); + + + +}); \ No newline at end of file From d713a4e168965e06bcbf1862a2b96d8cc8161533 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 11 Dec 2017 13:02:26 -0800 Subject: [PATCH 2/5] ip field removed, added comments to params, netRevenue set to true also changed surce of end-point to prebid-client from prebid-server --- modules/pubmaticBidAdapter.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index eb5b8d0fe46..8aa257a7d31 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -3,21 +3,21 @@ import { registerBidder } from 'src/adapters/bidderFactory'; const constants = require('src/constants.json'); const BIDDER_CODE = 'pubmatic'; -const ENDPOINT = '//openbid.pubmatic.com/translator?source=prebid-server'; +const ENDPOINT = '//openbid.pubmatic.com/translator?source=prebid-client'; const USYNCURL = '//ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p='; const CURRENCY = 'USD'; const AUCTION_TYPE = 2; //todo ?? is auction type correct ? second price auction //todo: now what is significance of value ? const CUSTOM_PARAMS = { 'kadpageurl': 'kadpageurl', - 'gender': 'gender', - 'yob': 'yob', - 'dctr': 'dctr', // Custom Targeting - 'lat': 'lat', // Location - Latitude - 'lon': 'lon', // Location - Longitude - 'wiid': 'wiid', // Wrapper Impression ID - 'profId': 'profId', // Legacy: Profile ID - 'verId': 'verId' // Legacy: version ID + 'gender': 'gender', // User gender + 'yob': 'yob', // User year of birth + 'dctr': 'dctr', // Custom Targeting //todo : remove ???? + 'lat': 'lat', // User location - Latitude + 'lon': 'lon', // User Location - Longitude + 'wiid': 'wiid', // OpenWrap Wrapper Impression ID + 'profId': 'profId', // OpenWrap Legacy: Profile ID + 'verId': 'verId' // OpenWrap Legacy: version ID }; let publisherId = 0; @@ -149,8 +149,7 @@ function _createOrtbTemplate(conf){ dnt: (navigator.doNotTrack == "yes" || navigator.doNotTrack == "1" || navigator.msDoNotTrack == "1") ? 1 : 0, h: screen.height, w: screen.width, - language: navigator.language, - ip: "123.4.12.34" //todo : REMOVE :: Mandatory w/o this we are not getting bids, isnt it retrieved from header ? + language: navigator.language }, user: {}, ext: {} @@ -258,7 +257,7 @@ export const spec = { creativeId: bid.crid || bid.id, dealId: bid.dealid, currency: CURRENCY, - netRevenue: true, // todo: mandatory: Mike to confirm + netRevenue: true, ttl: 300, referrer: utils.getTopWindowUrl(), ad: bid.adm From 10a8fa0862f93b0b1b7420e65eb1abf5ffd5ff0f Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 11 Dec 2017 15:01:41 -0800 Subject: [PATCH 3/5] added _processFloor --- modules/pubmaticBidAdapter.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 8aa257a7d31..f5e3556c640 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -26,6 +26,16 @@ function _processPmZoneId(zoneId) { if (utils.isStr(zoneId)) { return zoneId.split(',').slice(0, 50).join(); } else { + utils.logWarn('PubMatic: Ignoring param key: pmzoneid, expects string-value, found ' + typeof zoneId); + return undefined; + } +} + +function _processFloor(floor){ + if (utils.isStr(floor)) { + return parseFloat(floor) || undefined; + } else { + utils.logWarn('PubMatic: Ignoring param key: kadfloor, expects string-value, found ' + typeof floor); return undefined; } } @@ -160,7 +170,7 @@ function _createImpressionObject(bid, conf){ return { id: bid.bidId, tagid: bid.params.adUnit, - bidfloor: bid.params.kadfloor || undefined, + bidfloor: _processFloor(bid.params.kadfloor), secure: conf.sec, banner: { pos: 0, From 3a8cbe4a517ddb2b3b4c24112c0695ae43ceeabc Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 11 Dec 2017 15:30:09 -0800 Subject: [PATCH 4/5] removed comments --- modules/pubmaticBidAdapter.js | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index f5e3556c640..e81a30fe97d 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -76,19 +76,12 @@ function _parseAdSlot(bid){ bid.params.height = parseInt(splits[1]); } -//todo: remove commented code function _initConf() { var conf = {}; var currTime = new Date(); - conf.sec = 0; - //todo remove - let _protocol = (window.location.protocol === 'https:' ? (conf.sec = 1, 'https') : 'http') + '://'; + conf.sec = window.location.protocol === 'https:' ? 1 : 0; conf.wp = 'PreBid';//todo : do we need to send this ? conf.wv = constants.REPO_AND_VERSION; - // istanbul ignore else - //if (window.navigator.cookieEnabled === false) { - // conf.fpcd = '1'; - //} try { conf.pageURL = window.top.location.href; conf.hostname = window.top.location.hostname; @@ -98,14 +91,6 @@ function _initConf() { conf.hostname = window.location.hostname; 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; } @@ -194,12 +179,6 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: bid => { - //if(bid && bid.params && bid.params.publisherId){ - //_parseAdSlot(bid); - //return !!(bid.params.adSlot && bid.params.adUnit && bid.params.adUnitIndex && bid.params.width && bid.params.height); - //}else{ - // return false; - //} return !!(bid && bid.params && bid.params.publisherId && bid.params.adSlot); }, From b6e7805d4ab75dfbac14da64e08b7bf4849a624b Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 11 Dec 2017 15:47:05 -0800 Subject: [PATCH 5/5] fixed unit test cases --- test/spec/modules/pubmaticBidAdapter_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 04d5cd843cf..fd440387a80 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -75,7 +75,7 @@ describe('PubMatic adapter', () => { describe('Request formation', () => { it('Endpoint checking', () => { let request = spec.buildRequests(bidRequests); - expect(request.url).to.equal('//openbid.pubmatic.com/translator?source=prebid-server'); + expect(request.url).to.equal('//openbid.pubmatic.com/translator?source=prebid-client'); expect(request.method).to.equal('POST'); }); @@ -98,7 +98,7 @@ describe('PubMatic adapter', () => { expect(data.ext.wrapper.version).to.equal(bidRequests[0].params.verId); // OpenWrap: Wrapper Profile Version ID expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id - expect(data.imp[0].bidfloor).to.equal(bidRequests[0].params.kadfloor); // kadfloor + expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height