mk9-prebid
Version:
Header Bidding Management Library
618 lines (593 loc) • 20.8 kB
JavaScript
import {registerBidder} from '../src/adapters/bidderFactory.js';
import * as utils from '../src/utils.js';
import {BANNER, NATIVE} from '../src/mediaTypes.js';
import {config} from '../src/config.js';
import { getStorageManager } from '../src/storageManager.js';
const GVLID = 358;
const DEFAULT_CUR = 'USD';
const BIDDER_CODE = 'mgid';
export const storage = getStorageManager(GVLID, BIDDER_CODE);
const ENDPOINT_URL = 'https://prebid.mgid.com/prebid/';
const LOG_WARN_PREFIX = '[MGID warn]: ';
const LOG_INFO_PREFIX = '[MGID info]: ';
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
'DESC': { ID: 5, KEY: 'data', TYPE: 2 }, // please note that type of BODY is also set to 2
'PRICE': { ID: 6, KEY: 'price', TYPE: 6 },
'SALEPRICE': { ID: 7, KEY: 'saleprice', TYPE: 7 },
'DISPLAYURL': { ID: 8, KEY: 'displayurl', TYPE: 11 },
'CTA': { ID: 9, KEY: 'cta', TYPE: 12 },
'BODY': { ID: 10, KEY: 'body', TYPE: 2 }, // please note that type of DESC is also set to 2
'SPONSORED': { ID: 11, KEY: 'sponsored', TYPE: 1 }, // please note that type of SPONSOREDBY is also set to 1
};
const NATIVE_ASSET_IMAGE_TYPE = {
'ICON': 1,
'IMAGE': 3
};
const DEFAULT_IMAGE_WIDTH = 492;
const DEFAULT_IMAGE_HEIGHT = 328;
const DEFAULT_ICON_WIDTH = 50;
const DEFAULT_ICON_HEIGHT = 50;
const DEFAULT_TITLE_LENGTH = 80;
let isInvalidNativeRequest = false;
// 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,
}
];
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 });
export const spec = {
VERSION: '1.5',
code: BIDDER_CODE,
supportedMediaTypes: [BANNER, NATIVE],
reId: /^[1-9][0-9]*$/,
NATIVE_ASSET_ID_TO_KEY_MAP: _NATIVE_ASSET_ID_TO_KEY_MAP,
NATIVE_ASSET_KEY_TO_ASSET_MAP: _NATIVE_ASSET_KEY_TO_ASSET_MAP,
/**
* Determines whether or not the given bid request is valid.
*
* @param {BidRequest} bid The bid params to validate.
* @return boolean True if this is a valid bid, and false otherwise.
*/
isBidRequestValid: (bid) => {
const banner = utils.deepAccess(bid, 'mediaTypes.banner');
const native = utils.deepAccess(bid, 'mediaTypes.native');
let nativeOk = utils.isPlainObject(native);
if (nativeOk) {
const nativeParams = utils.deepAccess(bid, 'nativeParams');
let assetsCount = 0;
if (utils.isPlainObject(nativeParams)) {
for (let k in nativeParams) {
let v = nativeParams[k];
const supportProp = spec.NATIVE_ASSET_KEY_TO_ASSET_MAP.hasOwnProperty(k);
if (supportProp) {
assetsCount++
}
if (!utils.isPlainObject(v) || (!supportProp && utils.deepAccess(v, 'required'))) {
nativeOk = false;
break;
}
}
}
nativeOk = nativeOk && (assetsCount > 0);
}
let bannerOk = utils.isPlainObject(banner);
if (bannerOk) {
const sizes = utils.deepAccess(banner, 'sizes');
bannerOk = utils.isArray(sizes) && sizes.length > 0;
for (let f = 0; bannerOk && f < sizes.length; f++) {
bannerOk = sizes[f].length === 2;
}
}
let acc = Number(bid.params.accountId);
let plcmt = Number(bid.params.placementId);
return (bannerOk || nativeOk) && utils.isPlainObject(bid.params) && !!bid.adUnitCode && utils.isStr(bid.adUnitCode) && (plcmt > 0 ? bid.params.placementId.toString().search(spec.reId) === 0 : true) &&
!!acc && acc > 0 && bid.params.accountId.toString().search(spec.reId) === 0;
},
/**
* 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) => {
utils.logInfo(LOG_INFO_PREFIX + `buildRequests`);
if (validBidRequests.length === 0) {
return;
}
const info = pageInfo();
const page = info.location || utils.deepAccess(bidderRequest, 'refererInfo.referer') || utils.deepAccess(bidderRequest, 'refererInfo.canonicalUrl');
const hostname = utils.parseUrl(page).hostname;
let domain = extractDomainFromHost(hostname) || hostname;
const accountId = setOnAny(validBidRequests, 'params.accountId');
const muid = getLocalStorageSafely('mgMuidn');
let url = (setOnAny(validBidRequests, 'params.bidUrl') || ENDPOINT_URL) + accountId;
if (utils.isStr(muid) && muid.length > 0) {
url += '?muid=' + muid;
}
const cur = setOnAny(validBidRequests, 'params.currency') || setOnAny(validBidRequests, 'params.cur') || config.getConfig('currency.adServerCurrency') || DEFAULT_CUR;
const secure = window.location.protocol === 'https:' ? 1 : 0;
let imp = [];
validBidRequests.forEach(bid => {
let tagid = utils.deepAccess(bid, 'params.placementId') || 0;
tagid = !tagid ? bid.adUnitCode : tagid + '/' + bid.adUnitCode;
let impObj = {
id: bid.bidId,
tagid,
secure,
};
const floorData = getBidFloor(bid, cur);
if (floorData.floor) {
impObj.bidfloor = floorData.floor;
}
if (floorData.cur) {
impObj.bidfloorcur = floorData.cur
}
for (let mediaTypes in bid.mediaTypes) {
switch (mediaTypes) {
case BANNER:
impObj.banner = createBannerRequest(bid);
imp.push(impObj);
break;
case NATIVE:
const native = createNativeRequest(bid.nativeParams);
if (!isInvalidNativeRequest) {
impObj.native = {
'request': native
};
imp.push(impObj);
}
break;
}
}
});
if (imp.length === 0) {
return;
}
let request = {
id: utils.deepAccess(bidderRequest, 'bidderRequestId'),
site: {domain, page},
cur: [cur],
geo: {utcoffset: info.timeOffset},
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: getLanguage()
},
ext: {mgid_ver: spec.VERSION, prebid_ver: '$prebid.version$'},
imp
};
if (bidderRequest && bidderRequest.gdprConsent) {
request.user = {ext: {consent: bidderRequest.gdprConsent.consentString}};
request.regs = {ext: {gdpr: (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)}}
}
if (info.referrer) {
request.site.ref = info.referrer
}
utils.logInfo(LOG_INFO_PREFIX + `buildRequest:`, request);
return {
method: 'POST',
url: url,
data: JSON.stringify(request),
};
},
/**
* Unpack the response from the server into a list of bids.
*
* @param {ServerResponse} serverResponse A successful response from the server.
* @return {Bid[]} An array of bids which were nested inside the server.
*/
interpretResponse: (serverResponse, bidRequests) => {
utils.logInfo(LOG_INFO_PREFIX + `interpretResponse`, serverResponse);
if (serverResponse == null || serverResponse.body == null || serverResponse.body === '' || !utils.isArray(serverResponse.body.seatbid) || !serverResponse.body.seatbid.length) {
return;
}
const returnedBids = [];
const muidn = utils.deepAccess(serverResponse.body, 'ext.muidn')
if (utils.isStr(muidn) && muidn.length > 0) {
setLocalStorageSafely('mgMuidn', muidn)
}
serverResponse.body.seatbid.forEach((bids) => {
bids.bid.forEach((bid) => {
const pbid = prebidBid(bid, serverResponse.body.cur);
if (pbid.mediaType === NATIVE && utils.isEmpty(pbid.native)) {
return;
}
returnedBids.push(pbid);
})
});
utils.logInfo(LOG_INFO_PREFIX + `interpretedResponse`, returnedBids);
return returnedBids;
},
onBidWon: (bid) => {
const cpm = utils.deepAccess(bid, 'adserverTargeting.hb_pb') || '';
if (utils.isStr(bid.nurl) && bid.nurl !== '') {
bid.nurl = bid.nurl.replace(
/\${AUCTION_PRICE}/,
cpm
);
utils.triggerPixel(bid.nurl);
}
if (bid.isBurl) {
if (bid.mediaType === BANNER) {
bid.ad = bid.ad.replace(
/\${AUCTION_PRICE}/,
cpm
)
} else {
bid.burl = bid.burl.replace(
/\${AUCTION_PRICE}/,
cpm
);
utils.triggerPixel(bid.burl);
}
}
utils.logInfo(LOG_INFO_PREFIX + `onBidWon`);
},
getUserSyncs: (syncOptions, serverResponses) => {
utils.logInfo(LOG_INFO_PREFIX + `getUserSyncs`);
}
};
registerBidder(spec);
function setOnAny(collection, key) {
for (let i = 0, result; i < collection.length; i++) {
result = utils.deepAccess(collection[i], key);
if (result) {
return result;
}
}
}
/**
* Unpack the Server's Bid into a Prebid-compatible one.
* @param serverBid
* @return Bid
*/
function prebidBid(serverBid, cur) {
if (!utils.isStr(cur) || cur === '') {
cur = DEFAULT_CUR;
}
const bid = {
requestId: serverBid.impid,
ad: serverBid.adm,
cpm: serverBid.price,
creativeId: serverBid.adid,
currency: cur,
dealId: serverBid.dealid || '',
width: serverBid.w,
height: serverBid.h,
mediaType: 'banner',
netRevenue: true,
ttl: serverBid.ttl || 300,
nurl: serverBid.nurl || '',
burl: serverBid.burl || '',
isBurl: utils.isStr(serverBid.burl) && serverBid.burl.length > 0,
meta: { advertiserDomains: (utils.isArray(serverBid.adomain) && serverBid.adomain.length > 0 ? serverBid.adomain : []) },
};
setMediaType(serverBid, bid);
switch (bid.mediaType) {
case BANNER:
break;
case NATIVE:
parseNativeResponse(serverBid, bid);
break;
}
return bid;
}
function setMediaType(bid, newBid) {
if (utils.deepAccess(bid, 'ext.crtype') === 'native') {
newBid.mediaType = NATIVE;
} else {
newBid.mediaType = BANNER;
}
}
function extractDomainFromHost(pageHost) {
if (pageHost == 'localhost') {
return 'localhost'
}
let domain = null;
try {
let domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost);
if (domains != null && domains.length > 0) {
domain = domains[0];
for (let i = 1; i < domains.length; i++) {
if (domains[i].length > domain.length) {
domain = domains[i];
}
}
}
} catch (e) {
domain = null;
}
return domain;
}
function getLanguage() {
const language = navigator.language ? 'language' : 'userLanguage';
const lang2 = navigator[language].split('-')[0];
if (lang2.length === 2 || lang2.length === 3) {
return lang2;
}
return '';
}
function getLocalStorageSafely(key) {
try {
return storage.getDataFromLocalStorage(key);
} catch (e) {
return null;
}
}
function setLocalStorageSafely(key, val) {
try {
return storage.setDataInLocalStorage(key, val);
} catch (e) {
return null;
}
}
function createBannerRequest(bid) {
const sizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes');
let format = [];
if (sizes.length > 1) {
for (let f = 0; f < sizes.length; f++) {
if (sizes[f].length === 2) {
format.push({w: sizes[f][0], h: sizes[f][1]});
}
}
}
let r = {
w: sizes && sizes[0][0],
h: sizes && sizes[0][1],
};
if (format.length) {
r.format = format
}
const pos = utils.deepAccess(bid, 'mediaTypes.banner.pos') || 0
if (pos) {
r.pos = pos
}
return r
}
function createNativeRequest(params) {
let nativeRequestObject = {
plcmtcnt: 1,
assets: []
};
for (let key in params) {
let assetObj = {};
if (params.hasOwnProperty(key)) {
if (!(nativeRequestObject.assets && nativeRequestObject.assets.length > 0 && nativeRequestObject.assets.hasOwnProperty(key))) {
switch (key) {
case NATIVE_ASSETS.TITLE.KEY:
assetObj = {
id: NATIVE_ASSETS.TITLE.ID,
required: params[key].required ? 1 : 0,
title: {
len: params[key].len || params[key].length || DEFAULT_TITLE_LENGTH
}
};
break;
case NATIVE_ASSETS.IMAGE.KEY:
const wmin = params[key].wmin || params[key].minimumWidth || (utils.isArray(params[key].minsizes) && params[key].minsizes.length > 0 ? params[key].minsizes[0] : 0);
const hmin = params[key].hmin || params[key].minimumHeight || (utils.isArray(params[key].minsizes) && params[key].minsizes.length > 1 ? params[key].minsizes[1] : 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 || (utils.isArray(params[key].sizes) && params[key].sizes.length > 0 ? params[key].sizes[0] : 0),
h: params[key].h || params[key].height || (utils.isArray(params[key].sizes) && params[key].sizes.length > 1 ? params[key].sizes[1] : 0),
mimes: params[key].mimes,
ext: params[key].ext,
}
};
if (wmin > 0) {
assetObj.img.wmin = wmin;
}
if (hmin > 0) {
assetObj.img.hmin = hmin;
}
if (!assetObj.img.w) {
assetObj.img.w = DEFAULT_IMAGE_WIDTH;
}
if (!assetObj.img.h) {
assetObj.img.h = DEFAULT_IMAGE_HEIGHT;
}
break;
case NATIVE_ASSETS.ICON.KEY:
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 || (utils.isArray(params[key].sizes) && params[key].sizes.length > 0 ? params[key].sizes[0] : 0),
h: params[key].h || params[key].height || (utils.isArray(params[key].sizes) && params[key].sizes.length > 0 ? params[key].sizes[1] : 0),
}
};
if (!assetObj.img.w) {
assetObj.img.w = DEFAULT_ICON_WIDTH;
}
if (!assetObj.img.h) {
assetObj.img.h = DEFAULT_ICON_HEIGHT;
}
break;
case NATIVE_ASSETS.SPONSORED.KEY:
case NATIVE_ASSETS.SPONSOREDBY.KEY:
case NATIVE_ASSETS.PRICE.KEY:
case NATIVE_ASSETS.SALEPRICE.KEY:
case NATIVE_ASSETS.DESC.KEY:
case NATIVE_ASSETS.BODY.KEY:
case NATIVE_ASSETS.DISPLAYURL.KEY:
case NATIVE_ASSETS.CTA.KEY:
assetObj = commonNativeRequestObject(spec.NATIVE_ASSET_KEY_TO_ASSET_MAP[key], params);
break;
default:
if (params[key].required) {
isInvalidNativeRequest = true;
return;
}
}
}
}
if (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
let requiredAssetCount = NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.length;
let presentrequiredAssetCount = 0;
NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.forEach(ele => {
let lengthOfExistingAssets = nativeRequestObject.assets.length;
for (let i = 0; i < lengthOfExistingAssets; i++) {
if (ele.id === nativeRequestObject.assets[i].id) {
presentrequiredAssetCount++;
break;
} else {
if (ele.id === 4 && nativeRequestObject.assets[i].id === 11) {
if (utils.deepAccess(nativeRequestObject.assets[i], 'data.type') === ele.data.type) {
presentrequiredAssetCount++;
break;
}
}
}
}
});
isInvalidNativeRequest = requiredAssetCount !== presentrequiredAssetCount;
return nativeRequestObject;
}
function commonNativeRequestObject(nativeAsset, params) {
const 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 parseNativeResponse(bid, newBid) {
newBid.native = {};
if (bid.hasOwnProperty('adm')) {
let adm = '';
try {
adm = JSON.parse(bid.adm);
} catch (ex) {
utils.logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native response 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.SPONSORED.ID:
case NATIVE_ASSETS.PRICE:
case NATIVE_ASSETS.SALEPRICE.ID:
case NATIVE_ASSETS.DESC.ID:
case NATIVE_ASSETS.BODY.ID:
case NATIVE_ASSETS.DISPLAYURL.ID:
case NATIVE_ASSETS.CTA.ID:
newBid.native[spec.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 || [];
newBid.width = 0;
newBid.height = 0;
}
}
}
function pageInfo() {
var w, d, l, r, m, p, t;
for (w = window, d = w.document, l = d.location.href, r = d.referrer, m = 0, t = new Date(); w !== w.parent;) {
try {
p = w.parent; l = p.location.href; r = p.document.referrer; w = p;
} catch (e) {
m = top !== w.parent ? 2 : 1;
break
}
}
return {
location: l,
referrer: r || '',
masked: m,
wWidth: w.innerWidth,
wHeight: w.innerHeight,
date: t.toUTCString(),
timeOffset: t.getTimezoneOffset()
};
}
/**
* Get the floor price from bid.params for backward compatibility.
* If not found, then check floor module.
* @param bid A valid bid object
* @returns {*|number} floor price
*/
function getBidFloor(bid, cur) {
let bidFloor = utils.getBidIdParameter('bidfloor', bid.params) || utils.getBidIdParameter('bidFloor', bid.params) || 0;
const reqCur = cur
if (!bidFloor && utils.isFn(bid.getFloor)) {
const floorObj = bid.getFloor({
currency: '*',
mediaType: '*',
size: '*'
});
if (utils.isPlainObject(floorObj) && utils.isNumber(floorObj.floor)) {
if (!floorObj.currency && reqCur !== DEFAULT_CUR) {
floorObj.currency = DEFAULT_CUR
}
if (floorObj.currency && reqCur !== floorObj.currency) {
cur = floorObj.currency
}
bidFloor = floorObj.floor;
}
}
if (reqCur === cur) {
cur = ''
}
return {floor: bidFloor, cur: cur}
}