mk9-prebid
Version:
Header Bidding Management Library
1,308 lines (1,215 loc) • 46.7 kB
JavaScript
import * as utils from '../src/utils.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js';
import {config} from '../src/config.js';
import { Renderer } from '../src/Renderer.js';
const BIDDER_CODE = 'pubmatic';
const LOG_WARN_PREFIX = 'PubMatic: ';
const ENDPOINT = 'https://hbopenbid.pubmatic.com/translator?source=prebid-client';
const USER_SYNC_URL_IFRAME = 'https://ads.pubmatic.com/AdServer/js/user_sync.html?kdntuid=1&p=';
const USER_SYNC_URL_IMAGE = 'https://image8.pubmatic.com/AdServer/ImgSync?p=';
const DEFAULT_CURRENCY = 'USD';
const AUCTION_TYPE = 1;
const UNDEFINED = undefined;
const DEFAULT_WIDTH = 0;
const DEFAULT_HEIGHT = 0;
const PREBID_NATIVE_HELP_LINK = 'http://prebid.org/dev-docs/show-native-ads.html';
const PUBLICATION = 'pubmatic'; // Your publication on Blue Billywig, potentially with environment (e.g. publication.bbvms.com or publication.test.bbvms.com)
const RENDERER_URL = 'https://pubmatic.bbvms.com/r/'.concat('$RENDERER', '.js'); // URL of the renderer application
const CUSTOM_PARAMS = {
'kadpageurl': '', // Custom page url
'gender': '', // User gender
'yob': '', // User year of birth
'lat': '', // User location - Latitude
'lon': '', // User Location - Longitude
'wiid': '', // OpenWrap Wrapper Impression ID
'profId': '', // OpenWrap Legacy: Profile ID
'verId': '' // OpenWrap Legacy: version ID
};
const DATA_TYPES = {
'NUMBER': 'number',
'STRING': 'string',
'BOOLEAN': 'boolean',
'ARRAY': 'array',
'OBJECT': 'object'
};
const VIDEO_CUSTOM_PARAMS = {
'mimes': DATA_TYPES.ARRAY,
'minduration': DATA_TYPES.NUMBER,
'maxduration': DATA_TYPES.NUMBER,
'startdelay': DATA_TYPES.NUMBER,
'playbackmethod': DATA_TYPES.ARRAY,
'api': DATA_TYPES.ARRAY,
'protocols': DATA_TYPES.ARRAY,
'w': DATA_TYPES.NUMBER,
'h': DATA_TYPES.NUMBER,
'battr': DATA_TYPES.ARRAY,
'linearity': DATA_TYPES.NUMBER,
'placement': DATA_TYPES.NUMBER,
'minbitrate': DATA_TYPES.NUMBER,
'maxbitrate': DATA_TYPES.NUMBER,
'skip': DATA_TYPES.NUMBER
}
const NATIVE_ASSETS = {
'TITLE': { ID: 1, KEY: 'title', TYPE: 0 },
'IMAGE': { ID: 2, KEY: 'image', TYPE: 0 },
'ICON': { ID: 3, KEY: 'icon', TYPE: 0 },
'SPONSOREDBY': { ID: 4, KEY: 'sponsoredBy', TYPE: 1 }, // please note that type of SPONSORED is also 1
'BODY': { ID: 5, KEY: 'body', TYPE: 2 }, // please note that type of DESC is also set to 2
'CLICKURL': { ID: 6, KEY: 'clickUrl', TYPE: 0 },
'VIDEO': { ID: 7, KEY: 'video', TYPE: 0 },
'EXT': { ID: 8, KEY: 'ext', TYPE: 0 },
'DATA': { ID: 9, KEY: 'data', TYPE: 0 },
'LOGO': { ID: 10, KEY: 'logo', TYPE: 0 },
'SPONSORED': { ID: 11, KEY: 'sponsored', TYPE: 1 }, // please note that type of SPONSOREDBY is also set to 1
'DESC': { ID: 12, KEY: 'data', TYPE: 2 }, // please note that type of BODY is also set to 2
'RATING': { ID: 13, KEY: 'rating', TYPE: 3 },
'LIKES': { ID: 14, KEY: 'likes', TYPE: 4 },
'DOWNLOADS': { ID: 15, KEY: 'downloads', TYPE: 5 },
'PRICE': { ID: 16, KEY: 'price', TYPE: 6 },
'SALEPRICE': { ID: 17, KEY: 'saleprice', TYPE: 7 },
'PHONE': { ID: 18, KEY: 'phone', TYPE: 8 },
'ADDRESS': { ID: 19, KEY: 'address', TYPE: 9 },
'DESC2': { ID: 20, KEY: 'desc2', TYPE: 10 },
'DISPLAYURL': { ID: 21, KEY: 'displayurl', TYPE: 11 },
'CTA': { ID: 22, KEY: 'cta', TYPE: 12 }
};
const NATIVE_ASSET_IMAGE_TYPE = {
'ICON': 1,
'LOGO': 2,
'IMAGE': 3
}
// check if title, image can be added with mandatory field default values
const NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS = [
{
id: NATIVE_ASSETS.SPONSOREDBY.ID,
required: true,
data: {
type: 1
}
},
{
id: NATIVE_ASSETS.TITLE.ID,
required: true,
},
{
id: NATIVE_ASSETS.IMAGE.ID,
required: true,
}
]
const NET_REVENUE = true;
const dealChannelValues = {
1: 'PMP',
5: 'PREF',
6: 'PMPG'
};
const FLOC_FORMAT = {
'EID': 1,
'SEGMENT': 2
}
// BB stands for Blue BillyWig
const BB_RENDERER = {
bootstrapPlayer: function(bid) {
const config = {
code: bid.adUnitCode,
};
if (bid.vastXml) config.vastXml = bid.vastXml;
else if (bid.vastUrl) config.vastUrl = bid.vastUrl;
if (!bid.vastXml && !bid.vastUrl) {
utils.logWarn(`${LOG_WARN_PREFIX}: No vastXml or vastUrl on bid, bailing...`);
return;
}
const rendererId = BB_RENDERER.getRendererId(PUBLICATION, bid.rendererCode);
const ele = document.getElementById(bid.adUnitCode); // NB convention
let renderer;
for (let rendererIndex = 0; rendererIndex < window.bluebillywig.renderers.length; rendererIndex++) {
if (window.bluebillywig.renderers[rendererIndex]._id === rendererId) {
renderer = window.bluebillywig.renderers[rendererIndex];
break;
}
}
if (renderer) renderer.bootstrap(config, ele);
else utils.logWarn(`${LOG_WARN_PREFIX}: Couldn't find a renderer with ${rendererId}`);
},
newRenderer: function(rendererCode, adUnitCode) {
var rendererUrl = RENDERER_URL.replace('$RENDERER', rendererCode);
const renderer = Renderer.install({
url: rendererUrl,
loaded: false,
adUnitCode
});
try {
renderer.setRender(BB_RENDERER.outstreamRender);
} catch (err) {
utils.logWarn(`${LOG_WARN_PREFIX}: Error tying to setRender on renderer`, err);
}
return renderer;
},
outstreamRender: function(bid) {
bid.renderer.push(function() { BB_RENDERER.bootstrapPlayer(bid) });
},
getRendererId: function(pub, renderer) {
return `${pub}-${renderer}`; // NB convention!
}
};
const MEDIATYPE = [
BANNER,
VIDEO,
NATIVE
]
let publisherId = 0;
let isInvalidNativeRequest = false;
let NATIVE_ASSET_ID_TO_KEY_MAP = {};
let NATIVE_ASSET_KEY_TO_ASSET_MAP = {};
// loading NATIVE_ASSET_ID_TO_KEY_MAP
utils._each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_ID_TO_KEY_MAP[anAsset.ID] = anAsset.KEY });
// loading NATIVE_ASSET_KEY_TO_ASSET_MAP
utils._each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_KEY_TO_ASSET_MAP[anAsset.KEY] = anAsset });
function _getDomainFromURL(url) {
let anchor = document.createElement('a');
anchor.href = url;
return anchor.hostname;
}
function _parseSlotParam(paramName, paramValue) {
if (!utils.isStr(paramValue)) {
paramValue && utils.logWarn(LOG_WARN_PREFIX + 'Ignoring param key: ' + paramName + ', expects string-value, found ' + typeof paramValue);
return UNDEFINED;
}
switch (paramName) {
case 'pmzoneid':
return paramValue.split(',').slice(0, 50).map(id => id.trim()).join();
case 'kadfloor':
return parseFloat(paramValue) || UNDEFINED;
case 'lat':
return parseFloat(paramValue) || UNDEFINED;
case 'lon':
return parseFloat(paramValue) || UNDEFINED;
case 'yob':
return parseInt(paramValue) || UNDEFINED;
default:
return paramValue;
}
}
function _cleanSlot(slotName) {
if (utils.isStr(slotName)) {
return slotName.replace(/^\s+/g, '').replace(/\s+$/g, '');
}
if (slotName) {
utils.logWarn(BIDDER_CODE + ': adSlot must be a string. Ignoring adSlot');
}
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];
}
// check if size is mentioned in sizes array. in that case do not check for @ in adslot
splits = slot.split('@');
bid.params.adUnit = splits[0];
if (splits.length > 1) {
// i.e size is specified in adslot, so consider that and ignore sizes array
splits = splits[1].split('x');
if (splits.length != 2) {
utils.logWarn(LOG_WARN_PREFIX + 'AdSlot Error: adSlot not in required format');
return;
}
bid.params.width = parseInt(splits[0], 10);
bid.params.height = parseInt(splits[1], 10);
} else if (bid.hasOwnProperty('mediaTypes') &&
bid.mediaTypes.hasOwnProperty(BANNER) &&
bid.mediaTypes.banner.hasOwnProperty('sizes')) {
var i = 0;
var sizeArray = [];
for (;i < bid.mediaTypes.banner.sizes.length; i++) {
if (bid.mediaTypes.banner.sizes[i].length === 2) { // sizes[i].length will not be 2 in case where size is set as fluid, we want to skip that entry
sizeArray.push(bid.mediaTypes.banner.sizes[i]);
}
}
bid.mediaTypes.banner.sizes = sizeArray;
if (bid.mediaTypes.banner.sizes.length >= 1) {
// set the first size in sizes array in bid.params.width and bid.params.height. These will be sent as primary size.
// The rest of the sizes will be sent in format array.
bid.params.width = bid.mediaTypes.banner.sizes[0][0];
bid.params.height = bid.mediaTypes.banner.sizes[0][1];
bid.mediaTypes.banner.sizes = bid.mediaTypes.banner.sizes.splice(1, bid.mediaTypes.banner.sizes.length - 1);
}
}
}
function _initConf(refererInfo) {
return {
pageURL: (refererInfo && refererInfo.referer) ? refererInfo.referer : window.location.href,
refURL: window.document.referrer
};
}
function _handleCustomParams(params, conf) {
if (!conf.kadpageurl) {
conf.kadpageurl = conf.pageURL;
}
var key, value, entry;
for (key in CUSTOM_PARAMS) {
if (CUSTOM_PARAMS.hasOwnProperty(key)) {
value = params[key];
if (value) {
entry = CUSTOM_PARAMS[key];
if (typeof entry === 'object') {
// will be used in future when we want to process a custom param before using
// 'keyname': {f: function() {}}
value = entry.f(value, conf);
}
if (utils.isStr(value)) {
conf[key] = value;
} else {
utils.logWarn(LOG_WARN_PREFIX + 'Ignoring param : ' + key + ' with value : ' + CUSTOM_PARAMS[key] + ', expects string-value, found ' + typeof value);
}
}
}
}
return conf;
}
function _createOrtbTemplate(conf) {
return {
id: '' + new Date().getTime(),
at: AUCTION_TYPE,
cur: [DEFAULT_CURRENCY],
imp: [],
site: {
page: conf.pageURL,
ref: conf.refURL,
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
},
user: {},
ext: {}
};
}
function _checkParamDataType(key, value, datatype) {
var errMsg = 'Ignoring param key: ' + key + ', expects ' + datatype + ', found ' + typeof value;
var functionToExecute;
switch (datatype) {
case DATA_TYPES.BOOLEAN:
functionToExecute = utils.isBoolean;
break;
case DATA_TYPES.NUMBER:
functionToExecute = utils.isNumber;
break;
case DATA_TYPES.STRING:
functionToExecute = utils.isStr;
break;
case DATA_TYPES.ARRAY:
functionToExecute = utils.isArray;
break;
}
if (functionToExecute(value)) {
return value;
}
utils.logWarn(LOG_WARN_PREFIX + errMsg);
return UNDEFINED;
}
function _commonNativeRequestObject(nativeAsset, params) {
var key = nativeAsset.KEY;
return {
id: nativeAsset.ID,
required: params[key].required ? 1 : 0,
data: {
type: nativeAsset.TYPE,
len: params[key].len,
ext: params[key].ext
}
};
}
function _createNativeRequest(params) {
var nativeRequestObject = {
assets: []
};
for (var key in params) {
if (params.hasOwnProperty(key)) {
var assetObj = {};
if (!(nativeRequestObject.assets && nativeRequestObject.assets.length > 0 && nativeRequestObject.assets.hasOwnProperty(key))) {
switch (key) {
case NATIVE_ASSETS.TITLE.KEY:
if (params[key].len || params[key].length) {
assetObj = {
id: NATIVE_ASSETS.TITLE.ID,
required: params[key].required ? 1 : 0,
title: {
len: params[key].len || params[key].length,
ext: params[key].ext
}
};
} else {
utils.logWarn(LOG_WARN_PREFIX + 'Error: Title Length is required for native ad: ' + JSON.stringify(params));
}
break;
case NATIVE_ASSETS.IMAGE.KEY:
if (params[key].sizes && params[key].sizes.length > 0) {
assetObj = {
id: NATIVE_ASSETS.IMAGE.ID,
required: params[key].required ? 1 : 0,
img: {
type: NATIVE_ASSET_IMAGE_TYPE.IMAGE,
w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED),
h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED),
wmin: params[key].wmin || params[key].minimumWidth || (params[key].minsizes ? params[key].minsizes[0] : UNDEFINED),
hmin: params[key].hmin || params[key].minimumHeight || (params[key].minsizes ? params[key].minsizes[1] : UNDEFINED),
mimes: params[key].mimes,
ext: params[key].ext,
}
};
} else {
utils.logWarn(LOG_WARN_PREFIX + 'Error: Image sizes is required for native ad: ' + JSON.stringify(params));
}
break;
case NATIVE_ASSETS.ICON.KEY:
if (params[key].sizes && params[key].sizes.length > 0) {
assetObj = {
id: NATIVE_ASSETS.ICON.ID,
required: params[key].required ? 1 : 0,
img: {
type: NATIVE_ASSET_IMAGE_TYPE.ICON,
w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED),
h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED),
}
};
} else {
utils.logWarn(LOG_WARN_PREFIX + 'Error: Icon sizes is required for native ad: ' + JSON.stringify(params));
};
break;
case NATIVE_ASSETS.VIDEO.KEY:
assetObj = {
id: NATIVE_ASSETS.VIDEO.ID,
required: params[key].required ? 1 : 0,
video: {
minduration: params[key].minduration,
maxduration: params[key].maxduration,
protocols: params[key].protocols,
mimes: params[key].mimes,
ext: params[key].ext
}
};
break;
case NATIVE_ASSETS.EXT.KEY:
assetObj = {
id: NATIVE_ASSETS.EXT.ID,
required: params[key].required ? 1 : 0,
};
break;
case NATIVE_ASSETS.LOGO.KEY:
assetObj = {
id: NATIVE_ASSETS.LOGO.ID,
required: params[key].required ? 1 : 0,
img: {
type: NATIVE_ASSET_IMAGE_TYPE.LOGO,
w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED),
h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED)
}
};
break;
case NATIVE_ASSETS.SPONSOREDBY.KEY:
case NATIVE_ASSETS.BODY.KEY:
case NATIVE_ASSETS.RATING.KEY:
case NATIVE_ASSETS.LIKES.KEY:
case NATIVE_ASSETS.DOWNLOADS.KEY:
case NATIVE_ASSETS.PRICE.KEY:
case NATIVE_ASSETS.SALEPRICE.KEY:
case NATIVE_ASSETS.PHONE.KEY:
case NATIVE_ASSETS.ADDRESS.KEY:
case NATIVE_ASSETS.DESC2.KEY:
case NATIVE_ASSETS.DISPLAYURL.KEY:
case NATIVE_ASSETS.CTA.KEY:
assetObj = _commonNativeRequestObject(NATIVE_ASSET_KEY_TO_ASSET_MAP[key], params);
break;
}
}
}
if (assetObj && assetObj.id) {
nativeRequestObject.assets[nativeRequestObject.assets.length] = assetObj;
}
};
// for native image adtype prebid has to have few required assests i.e. title,sponsoredBy, image
// if any of these are missing from the request then request will not be sent
var requiredAssetCount = NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.length;
var presentrequiredAssetCount = 0;
NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.forEach(ele => {
var lengthOfExistingAssets = nativeRequestObject.assets.length;
for (var i = 0; i < lengthOfExistingAssets; i++) {
if (ele.id == nativeRequestObject.assets[i].id) {
presentrequiredAssetCount++;
break;
}
}
});
if (requiredAssetCount == presentrequiredAssetCount) {
isInvalidNativeRequest = false;
} else {
isInvalidNativeRequest = true;
}
return nativeRequestObject;
}
function _createBannerRequest(bid) {
var sizes = bid.mediaTypes.banner.sizes;
var format = [];
var bannerObj;
if (sizes !== UNDEFINED && utils.isArray(sizes)) {
bannerObj = {};
if (!bid.params.width && !bid.params.height) {
if (sizes.length === 0) {
// i.e. since bid.params does not have width or height, and length of sizes is 0, need to ignore this banner imp
bannerObj = UNDEFINED;
utils.logWarn(LOG_WARN_PREFIX + 'Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.');
return bannerObj;
} else {
bannerObj.w = parseInt(sizes[0][0], 10);
bannerObj.h = parseInt(sizes[0][1], 10);
sizes = sizes.splice(1, sizes.length - 1);
}
} else {
bannerObj.w = bid.params.width;
bannerObj.h = bid.params.height;
}
if (sizes.length > 0) {
format = [];
sizes.forEach(function (size) {
if (size.length > 1) {
format.push({ w: size[0], h: size[1] });
}
});
if (format.length > 0) {
bannerObj.format = format;
}
}
bannerObj.pos = 0;
bannerObj.topframe = utils.inIframe() ? 0 : 1;
} else {
utils.logWarn(LOG_WARN_PREFIX + 'Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.');
bannerObj = UNDEFINED;
}
return bannerObj;
}
function _createVideoRequest(bid) {
var videoData = utils.mergeDeep(utils.deepAccess(bid.mediaTypes, 'video'), bid.params.video);
var videoObj;
if (videoData !== UNDEFINED) {
videoObj = {};
for (var key in VIDEO_CUSTOM_PARAMS) {
if (videoData.hasOwnProperty(key)) {
videoObj[key] = _checkParamDataType(key, videoData[key], VIDEO_CUSTOM_PARAMS[key]);
}
}
// read playersize and assign to h and w.
if (utils.isArray(bid.mediaTypes.video.playerSize[0])) {
videoObj.w = parseInt(bid.mediaTypes.video.playerSize[0][0], 10);
videoObj.h = parseInt(bid.mediaTypes.video.playerSize[0][1], 10);
} else if (utils.isNumber(bid.mediaTypes.video.playerSize[0])) {
videoObj.w = parseInt(bid.mediaTypes.video.playerSize[0], 10);
videoObj.h = parseInt(bid.mediaTypes.video.playerSize[1], 10);
}
} else {
videoObj = UNDEFINED;
utils.logWarn(LOG_WARN_PREFIX + 'Error: Video config params missing for adunit: ' + bid.params.adUnit + ' with mediaType set as video. Ignoring video impression in the adunit.');
}
return videoObj;
}
// support for PMP deals
function _addPMPDealsInImpression(impObj, bid) {
if (bid.params.deals) {
if (utils.isArray(bid.params.deals)) {
bid.params.deals.forEach(function(dealId) {
if (utils.isStr(dealId) && dealId.length > 3) {
if (!impObj.pmp) {
impObj.pmp = { private_auction: 0, deals: [] };
}
impObj.pmp.deals.push({ id: dealId });
} else {
utils.logWarn(LOG_WARN_PREFIX + 'Error: deal-id present in array bid.params.deals should be a strings with more than 3 charaters length, deal-id ignored: ' + dealId);
}
});
} else {
utils.logWarn(LOG_WARN_PREFIX + 'Error: bid.params.deals should be an array of strings.');
}
}
}
function _addDealCustomTargetings(imp, bid) {
var dctr = '';
var dctrLen;
if (bid.params.dctr) {
dctr = bid.params.dctr;
if (utils.isStr(dctr) && dctr.length > 0) {
var arr = dctr.split('|');
dctr = '';
arr.forEach(val => {
dctr += (val.length > 0) ? (val.trim() + '|') : '';
});
dctrLen = dctr.length;
if (dctr.substring(dctrLen, dctrLen - 1) === '|') {
dctr = dctr.substring(0, dctrLen - 1);
}
imp.ext['key_val'] = dctr.trim()
} else {
utils.logWarn(LOG_WARN_PREFIX + 'Ignoring param : dctr with value : ' + dctr + ', expects string-value, found empty or non-string value');
}
}
}
function _addJWPlayerSegmentData(imp, bid) {
var jwSegData = (bid.rtd && bid.rtd.jwplayer && bid.rtd.jwplayer.targeting) || undefined;
var jwPlayerData = '';
const jwMark = 'jw-';
if (jwSegData === undefined || jwSegData === '' || !jwSegData.hasOwnProperty('segments')) return;
var maxLength = jwSegData.segments.length;
jwPlayerData += jwMark + 'id=' + jwSegData.content.id; // add the content id first
for (var i = 0; i < maxLength; i++) {
jwPlayerData += '|' + jwMark + jwSegData.segments[i] + '=1';
}
const ext = imp.ext;
(ext && ext.key_val === undefined)
? ext.key_val = jwPlayerData
: ext.key_val += '|' + jwPlayerData;
}
function _createImpressionObject(bid, conf) {
var impObj = {};
var bannerObj;
var videoObj;
var nativeObj = {};
var sizes = bid.hasOwnProperty('sizes') ? bid.sizes : [];
var mediaTypes = '';
var format = [];
impObj = {
id: bid.bidId,
tagid: bid.params.adUnit || undefined,
bidfloor: _parseSlotParam('kadfloor', bid.params.kadfloor),
secure: 1,
ext: {
pmZoneId: _parseSlotParam('pmzoneid', bid.params.pmzoneid)
},
bidfloorcur: bid.params.currency ? _parseSlotParam('currency', bid.params.currency) : DEFAULT_CURRENCY
};
_addPMPDealsInImpression(impObj, bid);
_addDealCustomTargetings(impObj, bid);
_addJWPlayerSegmentData(impObj, bid);
if (bid.hasOwnProperty('mediaTypes')) {
for (mediaTypes in bid.mediaTypes) {
switch (mediaTypes) {
case BANNER:
bannerObj = _createBannerRequest(bid);
if (bannerObj !== UNDEFINED) {
impObj.banner = bannerObj;
}
break;
case NATIVE:
nativeObj['request'] = JSON.stringify(_createNativeRequest(bid.nativeParams));
if (!isInvalidNativeRequest) {
impObj.native = nativeObj;
} else {
utils.logWarn(LOG_WARN_PREFIX + 'Error: Error in Native adunit ' + bid.params.adUnit + '. Ignoring the adunit. Refer to ' + PREBID_NATIVE_HELP_LINK + ' for more details.');
}
break;
case VIDEO:
videoObj = _createVideoRequest(bid);
if (videoObj !== UNDEFINED) {
impObj.video = videoObj;
}
break;
}
}
} else {
// mediaTypes is not present, so this is a banner only impression
// this part of code is required for older testcases with no 'mediaTypes' to run succesfully.
bannerObj = {
pos: 0,
w: bid.params.width,
h: bid.params.height,
topframe: utils.inIframe() ? 0 : 1
};
if (utils.isArray(sizes) && sizes.length > 1) {
sizes = sizes.splice(1, sizes.length - 1);
sizes.forEach(size => {
format.push({
w: size[0],
h: size[1]
});
});
bannerObj.format = format;
}
impObj.banner = bannerObj;
}
_addImpressionFPD(impObj, bid);
_addFloorFromFloorModule(impObj, bid);
return impObj.hasOwnProperty(BANNER) ||
impObj.hasOwnProperty(NATIVE) ||
impObj.hasOwnProperty(VIDEO) ? impObj : UNDEFINED;
}
function _addImpressionFPD(imp, bid) {
const ortb2 = {...utils.deepAccess(bid, 'ortb2Imp.ext.data')};
Object.keys(ortb2).forEach(prop => {
/**
* Prebid AdSlot
* @type {(string|undefined)}
*/
if (prop === 'pbadslot') {
if (typeof ortb2[prop] === 'string' && ortb2[prop]) utils.deepSetValue(imp, 'ext.data.pbadslot', ortb2[prop]);
} else if (prop === 'adserver') {
/**
* Copy GAM AdUnit and Name to imp
*/
['name', 'adslot'].forEach(name => {
/** @type {(string|undefined)} */
const value = utils.deepAccess(ortb2, `adserver.${name}`);
if (typeof value === 'string' && value) {
utils.deepSetValue(imp, `ext.data.adserver.${name.toLowerCase()}`, value);
// copy GAM ad unit id as imp[].ext.dfp_ad_unit_code
if (name === 'adslot') {
utils.deepSetValue(imp, `ext.dfp_ad_unit_code`, value);
}
}
});
} else {
utils.deepSetValue(imp, `ext.data.${prop}`, ortb2[prop]);
}
});
}
function _addFloorFromFloorModule(impObj, bid) {
let bidFloor = -1;
// get lowest floor from floorModule
if (typeof bid.getFloor === 'function' && !config.getConfig('pubmatic.disableFloors')) {
[BANNER, VIDEO, NATIVE].forEach(mediaType => {
if (impObj.hasOwnProperty(mediaType)) {
let floorInfo = bid.getFloor({ currency: impObj.bidfloorcur, mediaType: mediaType, size: '*' });
if (typeof floorInfo === 'object' && floorInfo.currency === impObj.bidfloorcur && !isNaN(parseInt(floorInfo.floor))) {
let mediaTypeFloor = parseFloat(floorInfo.floor);
bidFloor = (bidFloor == -1 ? mediaTypeFloor : Math.min(mediaTypeFloor, bidFloor))
}
}
});
}
// get highest from impObj.bidfllor and floor from floor module
// as we are using Math.max, it is ok if we have not got any floor from floorModule, then value of bidFloor will be -1
if (impObj.bidfloor) {
bidFloor = Math.max(bidFloor, impObj.bidfloor)
}
// assign value only if bidFloor is > 0
impObj.bidfloor = ((!isNaN(bidFloor) && bidFloor > 0) ? bidFloor : UNDEFINED);
}
function _getFlocId(validBidRequests, flocFormat) {
var flocIdObject = null;
var flocId = utils.deepAccess(validBidRequests, '0.userId.flocId');
if (flocId && flocId.id) {
switch (flocFormat) {
case FLOC_FORMAT.SEGMENT:
flocIdObject = {
id: 'FLOC',
name: 'FLOC',
ext: {
ver: flocId.version
},
segment: [{
id: flocId.id,
name: 'chrome.com',
value: flocId.id.toString()
}]
}
break;
case FLOC_FORMAT.EID:
default:
flocIdObject = {
source: 'chrome.com',
uids: [
{
atype: 1,
id: flocId.id,
ext: {
ver: flocId.version
}
},
]
}
break;
}
}
return flocIdObject;
}
function _handleFlocId(payload, validBidRequests) {
var flocObject = _getFlocId(validBidRequests, FLOC_FORMAT.SEGMENT);
if (flocObject) {
if (!payload.user) {
payload.user = {};
}
if (!payload.user.data) {
payload.user.data = [];
}
payload.user.data.push(flocObject);
}
}
function _handleEids(payload, validBidRequests) {
let bidUserIdAsEids = utils.deepAccess(validBidRequests, '0.userIdAsEids');
let flocObject = _getFlocId(validBidRequests, FLOC_FORMAT.EID);
if (flocObject) {
if (!bidUserIdAsEids) {
bidUserIdAsEids = [];
}
bidUserIdAsEids.push(flocObject);
}
if (utils.isArray(bidUserIdAsEids) && bidUserIdAsEids.length > 0) {
utils.deepSetValue(payload, 'user.eids', bidUserIdAsEids);
}
}
function _checkMediaType(bid, newBid) {
// Create a regex here to check the strings
if (bid.ext && bid.ext['BidType'] != undefined) {
newBid.mediaType = MEDIATYPE[bid.ext.BidType];
} else {
utils.logInfo(LOG_WARN_PREFIX + 'bid.ext.BidType does not exist, checking alternatively for mediaType')
var adm = bid.adm;
var admStr = '';
var videoRegex = new RegExp(/VAST\s+version/);
if (adm.indexOf('span class="PubAPIAd"') >= 0) {
newBid.mediaType = BANNER;
} else if (videoRegex.test(adm)) {
newBid.mediaType = VIDEO;
} else {
try {
admStr = JSON.parse(adm.replace(/\\/g, ''));
if (admStr && admStr.native) {
newBid.mediaType = NATIVE;
}
} catch (e) {
utils.logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native reponse for ad response: ' + adm);
}
}
}
}
function _parseNativeResponse(bid, newBid) {
newBid.native = {};
if (bid.hasOwnProperty('adm')) {
var adm = '';
try {
adm = JSON.parse(bid.adm.replace(/\\/g, ''));
} catch (ex) {
utils.logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native reponse for ad response: ' + newBid.adm);
return;
}
if (adm && adm.native && adm.native.assets && adm.native.assets.length > 0) {
newBid.mediaType = NATIVE;
for (let i = 0, len = adm.native.assets.length; i < len; i++) {
switch (adm.native.assets[i].id) {
case NATIVE_ASSETS.TITLE.ID:
newBid.native.title = adm.native.assets[i].title && adm.native.assets[i].title.text;
break;
case NATIVE_ASSETS.IMAGE.ID:
newBid.native.image = {
url: adm.native.assets[i].img && adm.native.assets[i].img.url,
height: adm.native.assets[i].img && adm.native.assets[i].img.h,
width: adm.native.assets[i].img && adm.native.assets[i].img.w,
};
break;
case NATIVE_ASSETS.ICON.ID:
newBid.native.icon = {
url: adm.native.assets[i].img && adm.native.assets[i].img.url,
height: adm.native.assets[i].img && adm.native.assets[i].img.h,
width: adm.native.assets[i].img && adm.native.assets[i].img.w,
};
break;
case NATIVE_ASSETS.SPONSOREDBY.ID:
case NATIVE_ASSETS.BODY.ID:
case NATIVE_ASSETS.LIKES.ID:
case NATIVE_ASSETS.DOWNLOADS.ID:
case NATIVE_ASSETS.PRICE:
case NATIVE_ASSETS.SALEPRICE.ID:
case NATIVE_ASSETS.PHONE.ID:
case NATIVE_ASSETS.ADDRESS.ID:
case NATIVE_ASSETS.DESC2.ID:
case NATIVE_ASSETS.CTA.ID:
case NATIVE_ASSETS.RATING.ID:
case NATIVE_ASSETS.DISPLAYURL.ID:
newBid.native[NATIVE_ASSET_ID_TO_KEY_MAP[adm.native.assets[i].id]] = adm.native.assets[i].data && adm.native.assets[i].data.value;
break;
}
}
newBid.native.clickUrl = adm.native.link && adm.native.link.url;
newBid.native.clickTrackers = (adm.native.link && adm.native.link.clicktrackers) || [];
newBid.native.impressionTrackers = adm.native.imptrackers || [];
newBid.native.jstracker = adm.native.jstracker || [];
if (!newBid.width) {
newBid.width = DEFAULT_WIDTH;
}
if (!newBid.height) {
newBid.height = DEFAULT_HEIGHT;
}
}
}
}
function _blockedIabCategoriesValidation(payload, blockedIabCategories) {
blockedIabCategories = blockedIabCategories
.filter(function(category) {
if (typeof category === 'string') { // only strings
return true;
} else {
utils.logWarn(LOG_WARN_PREFIX + 'bcat: Each category should be a string, ignoring category: ' + category);
return false;
}
})
.map(category => category.trim()) // trim all
.filter(function(category, index, arr) { // more than 3 charaters length
if (category.length > 3) {
return arr.indexOf(category) === index; // unique value only
} else {
utils.logWarn(LOG_WARN_PREFIX + 'bcat: Each category should have a value of a length of more than 3 characters, ignoring category: ' + category)
}
});
if (blockedIabCategories.length > 0) {
utils.logWarn(LOG_WARN_PREFIX + 'bcat: Selected: ', blockedIabCategories);
payload.bcat = blockedIabCategories;
}
}
function _assignRenderer(newBid, request) {
let bidParams, context, adUnitCode;
if (request.bidderRequest && request.bidderRequest.bids) {
for (let bidderRequestBidsIndex = 0; bidderRequestBidsIndex < request.bidderRequest.bids.length; bidderRequestBidsIndex++) {
if (request.bidderRequest.bids[bidderRequestBidsIndex].bidId === newBid.requestId) {
bidParams = request.bidderRequest.bids[bidderRequestBidsIndex].params;
context = request.bidderRequest.bids[bidderRequestBidsIndex].mediaTypes[VIDEO].context;
adUnitCode = request.bidderRequest.bids[bidderRequestBidsIndex].adUnitCode;
}
}
if (context && context === 'outstream' && bidParams && bidParams.outstreamAU && adUnitCode) {
newBid.rendererCode = bidParams.outstreamAU;
newBid.renderer = BB_RENDERER.newRenderer(newBid.rendererCode, adUnitCode);
}
}
}
function isNonEmptyArray(test) {
if (utils.isArray(test) === true) {
if (test.length > 0) {
return true;
}
}
return false;
}
export const spec = {
code: BIDDER_CODE,
gvlid: 76,
supportedMediaTypes: [BANNER, VIDEO, NATIVE],
/**
* 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) {
if (!utils.isStr(bid.params.publisherId)) {
utils.logWarn(LOG_WARN_PREFIX + 'Error: publisherId is mandatory and cannot be numeric (wrap it in quotes in your config). Call to OpenBid will not be sent for ad unit: ' + JSON.stringify(bid));
return false;
}
// video ad validation
if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) {
// bid.mediaTypes.video.mimes OR bid.params.video.mimes should be present and must be a non-empty array
let mediaTypesVideoMimes = utils.deepAccess(bid.mediaTypes, 'video.mimes');
let paramsVideoMimes = utils.deepAccess(bid, 'params.video.mimes');
if (isNonEmptyArray(mediaTypesVideoMimes) === false && isNonEmptyArray(paramsVideoMimes) === false) {
utils.logWarn(LOG_WARN_PREFIX + 'Error: For video ads, bid.mediaTypes.video.mimes OR bid.params.video.mimes should be present and must be a non-empty array. Call to OpenBid will not be sent for ad unit:' + JSON.stringify(bid));
return false;
}
if (!bid.mediaTypes[VIDEO].hasOwnProperty('context')) {
utils.logError(`${LOG_WARN_PREFIX}: no context specified in bid. Rejecting bid: `, bid);
return false;
}
if (bid.mediaTypes[VIDEO].context === 'outstream' &&
!utils.isStr(bid.params.outstreamAU) &&
!bid.hasOwnProperty('renderer') &&
!bid.mediaTypes[VIDEO].hasOwnProperty('renderer')) {
// we are here since outstream ad-unit is provided without outstreamAU and renderer
// so it is not a valid video ad-unit
// but it may be valid banner or native ad-unit
// so if mediaType banner or Native is present then we will remove media-type video and return true
if (bid.mediaTypes.hasOwnProperty(BANNER) || bid.mediaTypes.hasOwnProperty(NATIVE)) {
delete bid.mediaTypes[VIDEO];
utils.logWarn(`${LOG_WARN_PREFIX}: for "outstream" bids either outstreamAU parameter must be provided or ad unit supplied renderer is required. Rejecting mediatype Video of bid: `, bid);
return true;
} else {
utils.logError(`${LOG_WARN_PREFIX}: for "outstream" bids either outstreamAU parameter must be provided or ad unit supplied renderer is required. Rejecting bid: `, bid);
return false;
}
}
}
return true;
}
return false;
},
/**
* 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, bidderRequest) => {
var refererInfo;
if (bidderRequest && bidderRequest.refererInfo) {
refererInfo = bidderRequest.refererInfo;
}
var conf = _initConf(refererInfo);
var payload = _createOrtbTemplate(conf);
var bidCurrency = '';
var dctrArr = [];
var bid;
var blockedIabCategories = [];
validBidRequests.forEach(originalBid => {
bid = utils.deepClone(originalBid);
bid.params.adSlot = bid.params.adSlot || '';
_parseAdSlot(bid);
if (bid.params.hasOwnProperty('video')) {
// Nothing to do
} else {
// If we have a native mediaType configured alongside banner, its ok if the banner size is not set in width and height
// The corresponding banner imp object will not be generated, but we still want the native object to be sent, hence the following check
if (!(bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(NATIVE)) && bid.params.width === 0 && bid.params.height === 0) {
utils.logWarn(LOG_WARN_PREFIX + 'Skipping the non-standard adslot: ', bid.params.adSlot, JSON.stringify(bid));
return;
}
}
conf.pubId = conf.pubId || bid.params.publisherId;
conf = _handleCustomParams(bid.params, conf);
conf.transactionId = bid.transactionId;
if (bidCurrency === '') {
bidCurrency = bid.params.currency || UNDEFINED;
} else if (bid.params.hasOwnProperty('currency') && bidCurrency !== bid.params.currency) {
utils.logWarn(LOG_WARN_PREFIX + 'Currency specifier ignored. Only one currency permitted.');
}
bid.params.currency = bidCurrency;
// check if dctr is added to more than 1 adunit
if (bid.params.hasOwnProperty('dctr') && utils.isStr(bid.params.dctr)) {
dctrArr.push(bid.params.dctr);
}
if (bid.params.hasOwnProperty('bcat') && utils.isArray(bid.params.bcat)) {
blockedIabCategories = blockedIabCategories.concat(bid.params.bcat);
}
var impObj = _createImpressionObject(bid, conf);
if (impObj) {
payload.imp.push(impObj);
}
});
if (payload.imp.length == 0) {
return;
}
payload.site.publisher.id = conf.pubId.trim();
publisherId = conf.pubId.trim();
payload.ext.wrapper = {};
payload.ext.wrapper.profile = parseInt(conf.profId) || UNDEFINED;
payload.ext.wrapper.version = parseInt(conf.verId) || UNDEFINED;
payload.ext.wrapper.wiid = conf.wiid || bidderRequest.auctionId;
// eslint-disable-next-line no-undef
payload.ext.wrapper.wv = $$REPO_AND_VERSION$$;
payload.ext.wrapper.transactionId = conf.transactionId;
payload.ext.wrapper.wp = 'pbjs';
payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED);
payload.user.geo = {};
payload.user.geo.lat = _parseSlotParam('lat', conf.lat);
payload.user.geo.lon = _parseSlotParam('lon', conf.lon);
payload.user.yob = _parseSlotParam('yob', conf.yob);
payload.device.geo = payload.user.geo;
payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim();
payload.site.domain = _getDomainFromURL(payload.site.page);
// add the content object from config in request
if (typeof config.getConfig('content') === 'object') {
payload.site.content = config.getConfig('content');
}
// merge the device from config.getConfig('device')
if (typeof config.getConfig('device') === 'object') {
payload.device = Object.assign(payload.device, config.getConfig('device'));
}
// passing transactionId in source.tid
utils.deepSetValue(payload, 'source.tid', conf.transactionId);
// test bids
if (window.location.href.indexOf('pubmaticTest=true') !== -1) {
payload.test = 1;
}
// adding schain object
if (validBidRequests[0].schain) {
utils.deepSetValue(payload, 'source.ext.schain', validBidRequests[0].schain);
}
// Attaching GDPR Consent Params
if (bidderRequest && bidderRequest.gdprConsent) {
utils.deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString);
utils.deepSetValue(payload, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0));
}
// CCPA
if (bidderRequest && bidderRequest.uspConsent) {
utils.deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent);
}
// coppa compliance
if (config.getConfig('coppa') === true) {
utils.deepSetValue(payload, 'regs.coppa', 1);
}
_handleEids(payload, validBidRequests);
_blockedIabCategoriesValidation(payload, blockedIabCategories);
_handleFlocId(payload, validBidRequests);
// First Party Data
const commonFpd = config.getConfig('ortb2') || {};
if (commonFpd.site) {
utils.mergeDeep(payload, {site: commonFpd.site});
}
if (commonFpd.user) {
utils.mergeDeep(payload, {user: commonFpd.user});
}
// Note: Do not move this block up
// if site object is set in Prebid config then we need to copy required fields from site into app and unset the site object
if (typeof config.getConfig('app') === 'object') {
payload.app = config.getConfig('app');
// not copying domain from site as it is a derived value from page
payload.app.publisher = payload.site.publisher;
payload.app.ext = payload.site.ext || UNDEFINED;
// We will also need to pass content object in app.content if app object is also set into the config;
// BUT do not use content object from config if content object is present in app as app.content
if (typeof payload.app.content !== 'object') {
payload.app.content = payload.site.content || UNDEFINED;
}
delete payload.site;
}
return {
method: 'POST',
url: ENDPOINT,
data: JSON.stringify(payload),
bidderRequest: bidderRequest
};
},
/**
* 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 = [];
var respCur = DEFAULT_CURRENCY;
let parsedRequest = JSON.parse(request.data);
let parsedReferrer = parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : '';
try {
if (response.body && response.body.seatbid && utils.isArray(response.body.seatbid)) {
// Supporting multiple bid responses for same adSize
respCur = response.body.cur || respCur;
response.body.seatbid.forEach(seatbidder => {
seatbidder.bid &&
utils.isArray(seatbidder.bid) &&
seatbidder.bid.forEach(bid => {
let newBid = {
requestId: bid.impid,
cpm: (parseFloat(bid.price) || 0).toFixed(2),
width: bid.w,
height: bid.h,
creativeId: bid.crid || bid.id,
dealId: bid.dealid,
currency: respCur,
netRevenue: NET_REVENUE,
ttl: 300,
referrer: parsedReferrer,
ad: bid.adm,
pm_seat: seatbidder.seat || null,
pm_dspid: bid.ext && bid.ext.dspid ? bid.ext.dspid : null,
partnerImpId: bid.id || '' // partner impression Id
};
if (parsedRequest.imp && parsedRequest.imp.length > 0) {
parsedRequest.imp.forEach(req => {
if (bid.impid === req.id) {
_checkMediaType(bid, newBid);
switch (newBid.mediaType) {
case BANNER:
break;
case VIDEO:
newBid.width = bid.hasOwnProperty('w') ? bid.w : req.video.w;
newBid.height = bid.hasOwnProperty('h') ? bid.h : req.video.h;
newBid.vastXml = bid.adm;
_assignRenderer(newBid, request);
break;
case NATIVE:
_parseNativeResponse(bid, newBid);
break;
}
}
});
}
if (bid.ext && bid.ext.deal_channel) {
newBid['dealChannel'] = dealChannelValues[bid.ext.deal_channel] || null;
}
newBid.meta = {};
if (bid.ext && bid.ext.dspid) {
newBid.meta.networkId = bid.ext.dspid;
}
if (bid.ext && bid.ext.advid) {
newBid.meta.buyerId = bid.ext.advid;
}
if (bid.adomain && bid.adomain.length > 0) {
newBid.meta.advertiserDomains = bid.adomain;
newBid.meta.clickUrl = bid.adomain[0];
}
// adserverTargeting
if (seatbidder.ext && seatbidder.ext.buyid) {
newBid.adserverTargeting = {
'hb_buyid_pubmatic': seatbidder.ext.buyid
};
}
bidResponses.push(newBid);
});
});
}
} catch (error) {
utils.logError(error);
}
return bidResponses;
},
/**
* Register User Sync.
*/
getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => {
let syncurl = '' + publisherId;
// Attaching GDPR Consent Params in UserSync url
if (gdprConsent) {
syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0);
syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '');
}
// CCPA
if (uspConsent) {
syncurl += '&us_privacy=' + encodeURIComponent(uspConsent);
}
// coppa compliance
if (config.getConfig('coppa') === true) {
syncurl += '&coppa=1';
}
if (syncOptions.iframeEnabled) {
return [{
type: 'iframe',
url: USER_SYNC_URL_IFRAME + syncurl
}];
} else {
return [{
type: 'image',
url: USER_SYNC_URL_IMAGE + syncurl
}];
}
},
/**
* Covert bid param types for S2S
* @param {Object} params bid params
* @param {Boolean} isOpenRtb boolean to check openrtb2 protocol
* @return {Object} params bid params
*/
transformBidParams: function (params, isOpenRtb) {
return utils.convertTypes({
'publisherId': 'string',
'adSlot': 'string'
}, params);
}
};
registerBidder(spec);