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

PubMatic adapter #1707

Merged
merged 8 commits into from
Nov 10, 2017
235 changes: 157 additions & 78 deletions modules/pubmaticBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,137 +2,216 @@ 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.
*
* @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);
Copy link
Member

Choose a reason for hiding this comment

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

I'm not clear why this is still loaded into an iframe. Can you not use the ajax method here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Our server response uses global namespace also our server is not supporting CORS.
By calling our server from a friendly-iframe we can get better priority than calling from a script tag thus we are calling our server using an iframe.

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(`<!--POST_SCRIPT_TAG_MACRO-->`, `<script>window.parent.$$PREBID_GLOBAL$$.handlePubmaticCallback(window.bidDetailsMap, window.progKeyValueMap);</script>`);
iframeDoc.write(content);
iframeDoc.close();
}

function _createRequestContent() {
var content = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"' +
' "http://www.w3.org/TR/html4/loose.dtd"><html><head><base target="_top" /><scr' +
'ipt>inDapIF=true;</scr' + 'ipt></head>';
content += '<body>';
content += '<scr' + 'ipt>';
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 += '</scr' + 'ipt>';

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 += '<scr' + 'ipt src="https://ads.pubmatic.com/AdServer/js/gshowad.js"></scr' + 'ipt>';
content += '<scr' + 'ipt>';
content += '</scr' + 'ipt>';
content += '</body></html>';
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);
Copy link
Member

Choose a reason for hiding this comment

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

it looks like the legacy path is currently the only supported path (ie it still uses an iframe) is that correct? Is this optimized vs the last version?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Our older implementation used to include an external js library, now we do not include it.
_legacyExecution is the only flow available now, we are planning to add a new flow later.

}

_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 {
Expand All @@ -147,7 +226,7 @@ function PubmaticAdapter() {
return {
callBids: _callBids
};
}
};

adaptermanager.registerBidAdapter(new PubmaticAdapter(), 'pubmatic');

Expand Down
13 changes: 13 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <!--PRE_SCRIPT_TAG_MACRO--> and <!--POST_SCRIPT_TAG_MACRO--> are macros left to be replaced if required
*/
export function createContentToExecuteExtScriptInFriendlyFrame(url) {
if (!url) {
return '';
}

return `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head><base target="_top" /><script>inDapIF=true;</script></head><body><!--PRE_SCRIPT_TAG_MACRO--><script src="${url}"></script><!--POST_SCRIPT_TAG_MACRO--></body></html>`;
}

/**
* Build an object consisting of only defined parameters to avoid creating an
* object with defined keys and undefined values.
Expand Down
Loading