mk9-prebid
Version:
Header Bidding Management Library
509 lines (479 loc) • 15.3 kB
JavaScript
import * as utils from '../src/utils.js';
import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import find from 'core-js-pure/features/array/find.js';
import includes from 'core-js-pure/features/array/includes.js';
import {config} from '../src/config.js';
/*
* In case you're AdKernel whitelable platform's client who needs branded adapter to
* work with Adkernel platform - DO NOT COPY THIS ADAPTER UNDER NEW NAME
*
* Please contact prebid@adkernel.com and we'll add your adapter as an alias.
*/
const VIDEO_TARGETING = Object.freeze(['mimes', 'minduration', 'maxduration', 'protocols',
'startdelay', 'linearity', 'boxingallowed', 'playbackmethod', 'delivery',
'pos', 'api', 'ext']);
const VERSION = '1.6';
const SYNC_IFRAME = 1;
const SYNC_IMAGE = 2;
const SYNC_TYPES = Object.freeze({
1: 'iframe',
2: 'image'
});
const GVLID = 14;
const NATIVE_MODEL = [
{name: 'title', assetType: 'title'},
{name: 'icon', assetType: 'img', type: 1},
{name: 'image', assetType: 'img', type: 3},
{name: 'body', assetType: 'data', type: 2},
{name: 'body2', assetType: 'data', type: 10},
{name: 'sponsoredBy', assetType: 'data', type: 1},
{name: 'phone', assetType: 'data', type: 8},
{name: 'address', assetType: 'data', type: 9},
{name: 'price', assetType: 'data', type: 6},
{name: 'salePrice', assetType: 'data', type: 7},
{name: 'cta', assetType: 'data', type: 12},
{name: 'rating', assetType: 'data', type: 3},
{name: 'downloads', assetType: 'data', type: 5},
{name: 'likes', assetType: 'data', type: 4},
{name: 'displayUrl', assetType: 'data', type: 11}
];
const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => {
acc[val.name] = {id: idx, ...val};
return acc;
}, {});
/**
* Adapter for requesting bids from AdKernel white-label display platform
*/
export const spec = {
code: 'adkernel',
gvlid: GVLID,
aliases: [
{code: 'headbidding'},
{code: 'adsolut'},
{code: 'oftmediahb'},
{code: 'audiencemedia'},
{code: 'waardex_ak'},
{code: 'roqoon'},
{code: 'andbeyond'},
{code: 'adbite'},
{code: 'houseofpubs'},
{code: 'torchad'},
{code: 'stringads'},
{code: 'bcm'},
{code: 'engageadx'},
{code: 'converge', gvlid: 248},
{code: 'adomega'},
{code: 'denakop'},
{code: 'rtbanalytica'}
],
supportedMediaTypes: [BANNER, VIDEO, NATIVE],
/**
* Validates bid request for adunit
* @param bidRequest {BidRequest}
* @returns {boolean}
*/
isBidRequestValid: function (bidRequest) {
return 'params' in bidRequest &&
typeof bidRequest.params.host !== 'undefined' &&
'zoneId' in bidRequest.params &&
!isNaN(Number(bidRequest.params.zoneId)) &&
bidRequest.params.zoneId > 0 &&
bidRequest.mediaTypes &&
(bidRequest.mediaTypes.banner || bidRequest.mediaTypes.video || (bidRequest.mediaTypes.native && validateNativeAdUnit(bidRequest.mediaTypes.native)));
},
/**
* Builds http request for each unique combination of adkernel host/zone
* @param bidRequests {BidRequest[]}
* @param bidderRequest {BidderRequest}
* @returns {ServerRequest[]}
*/
buildRequests: function (bidRequests, bidderRequest) {
let impDispatch = dispatchImps(bidRequests, bidderRequest.refererInfo);
let requests = [];
let schain = bidRequests[0].schain;
Object.keys(impDispatch).forEach(host => {
Object.keys(impDispatch[host]).forEach(zoneId => {
const request = buildRtbRequest(impDispatch[host][zoneId], bidderRequest, schain);
requests.push({
method: 'POST',
url: `https://${host}/hb?zone=${zoneId}&v=${VERSION}`,
data: JSON.stringify(request)
});
});
});
return requests;
},
/**
* Parse response from adkernel backend
* @param serverResponse {ServerResponse}
* @param serverRequest {ServerRequest}
* @returns {Bid[]}
*/
interpretResponse: function (serverResponse, serverRequest) {
let response = serverResponse.body;
if (!response.seatbid) {
return [];
}
let rtbRequest = JSON.parse(serverRequest.data);
let rtbBids = response.seatbid
.map(seatbid => seatbid.bid)
.reduce((a, b) => a.concat(b), []);
return rtbBids.map(rtbBid => {
let imp = find(rtbRequest.imp, imp => imp.id === rtbBid.impid);
let prBid = {
requestId: rtbBid.impid,
cpm: rtbBid.price,
creativeId: rtbBid.crid,
currency: response.cur || 'USD',
ttl: 360,
netRevenue: true
};
if ('banner' in imp) {
prBid.mediaType = BANNER;
prBid.width = rtbBid.w;
prBid.height = rtbBid.h;
prBid.ad = formatAdMarkup(rtbBid);
} else if ('video' in imp) {
prBid.mediaType = VIDEO;
prBid.vastUrl = rtbBid.nurl;
prBid.width = imp.video.w;
prBid.height = imp.video.h;
} else if ('native' in imp) {
prBid.mediaType = NATIVE;
prBid.native = buildNativeAd(JSON.parse(rtbBid.adm));
}
if (utils.isStr(rtbBid.dealid)) {
prBid.dealId = rtbBid.dealid;
}
if (utils.isArray(rtbBid.adomain)) {
utils.deepSetValue(prBid, 'meta.advertiserDomains', rtbBid.adomain);
}
if (utils.isArray(rtbBid.cat)) {
utils.deepSetValue(prBid, 'meta.secondaryCatIds', rtbBid.cat);
}
if (utils.isPlainObject(rtbBid.ext)) {
if (utils.isNumber(rtbBid.ext.advertiser_id)) {
utils.deepSetValue(prBid, 'meta.advertiserId', rtbBid.ext.advertiser_id);
}
if (utils.isStr(rtbBid.ext.advertiser_name)) {
utils.deepSetValue(prBid, 'meta.advertiserName', rtbBid.ext.advertiser_name);
}
if (utils.isStr(rtbBid.ext.agency_name)) {
utils.deepSetValue(prBid, 'meta.agencyName', rtbBid.ext.agency_name);
}
}
return prBid;
});
},
/**
* Extracts user-syncs information from server response
* @param syncOptions {SyncOptions}
* @param serverResponses {ServerResponse[]}
* @returns {UserSync[]}
*/
getUserSyncs: function (syncOptions, serverResponses) {
if (!serverResponses || serverResponses.length === 0 || (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled)) {
return [];
}
return serverResponses.filter(rsp => rsp.body && rsp.body.ext && rsp.body.ext.adk_usersync)
.map(rsp => rsp.body.ext.adk_usersync)
.reduce((a, b) => a.concat(b), [])
.map(({url, type}) => ({type: SYNC_TYPES[type], url: url}));
}
};
registerBidder(spec);
/**
* Dispatch impressions by ad network host and zone
* @param bidRequests {BidRequest[]}
* @param refererInfo {refererInfo}
*/
function dispatchImps(bidRequests, refererInfo) {
let secure = (refererInfo && refererInfo.referer.indexOf('https:') === 0);
return bidRequests.map(bidRequest => buildImp(bidRequest, secure))
.reduce((acc, curr, index) => {
let bidRequest = bidRequests[index];
let {zoneId, host} = bidRequest.params;
acc[host] = acc[host] || {};
acc[host][zoneId] = acc[host][zoneId] || [];
acc[host][zoneId].push(curr);
return acc;
}, {});
}
function getBidFloor(bid, mediaType, sizes) {
var floor;
var size = sizes.length === 1 ? sizes[0] : '*';
if (typeof bid.getFloor === 'function') {
const floorInfo = bid.getFloor({currency: 'USD', mediaType, size});
if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) {
floor = parseFloat(floorInfo.floor);
}
}
return floor;
}
/**
* Builds rtb imp object for single adunit
* @param bidRequest {BidRequest}
* @param secure {boolean}
*/
function buildImp(bidRequest, secure) {
const imp = {
'id': bidRequest.bidId,
'tagid': bidRequest.adUnitCode
};
var mediaType;
var sizes = [];
if (utils.deepAccess(bidRequest, 'mediaTypes.banner')) {
sizes = utils.getAdUnitSizes(bidRequest);
imp.banner = {
format: sizes.map(wh => utils.parseGPTSingleSizeArrayToRtbSize(wh)),
topframe: 0
};
mediaType = BANNER;
} else if (utils.deepAccess(bidRequest, 'mediaTypes.video')) {
let video = utils.deepAccess(bidRequest, 'mediaTypes.video');
imp.video = {};
if (video.playerSize) {
sizes = video.playerSize[0];
imp.video = Object.assign(imp.video, utils.parseGPTSingleSizeArrayToRtbSize(sizes) || {});
}
if (bidRequest.params.video) {
Object.keys(bidRequest.params.video)
.filter(key => includes(VIDEO_TARGETING, key))
.forEach(key => imp.video[key] = bidRequest.params.video[key]);
}
mediaType = VIDEO;
} else if (utils.deepAccess(bidRequest, 'mediaTypes.native')) {
let nativeRequest = buildNativeRequest(bidRequest.mediaTypes.native);
imp.native = {
ver: '1.1',
request: JSON.stringify(nativeRequest)
};
mediaType = NATIVE;
} else {
throw new Error('Unsupported bid received');
}
let floor = getBidFloor(bidRequest, mediaType, sizes);
if (floor) {
imp.bidfloor = floor;
}
if (secure) {
imp.secure = 1;
}
return imp;
}
/**
* Builds native request from native adunit
*/
function buildNativeRequest(nativeReq) {
let request = {ver: '1.1', assets: []};
for (let k of Object.keys(nativeReq)) {
let v = nativeReq[k];
let desc = NATIVE_INDEX[k];
if (desc === undefined) {
continue;
}
let assetRoot = {
id: desc.id,
required: ~~v.required,
};
if (desc.assetType === 'img') {
assetRoot[desc.assetType] = buildImageAsset(desc, v);
} else if (desc.assetType === 'data') {
assetRoot.data = utils.cleanObj({type: desc.type, len: v.len});
} else if (desc.assetType === 'title') {
assetRoot.title = {len: v.len || 90};
} else {
return;
}
request.assets.push(assetRoot);
}
return request;
}
/**
* Builds image asset request
*/
function buildImageAsset(desc, val) {
let img = {
type: desc.type
};
if (val.sizes) {
[img.w, img.h] = val.sizes;
} else if (val.aspect_ratios) {
img.wmin = val.aspect_ratios[0].min_width;
img.hmin = val.aspect_ratios[0].min_height;
}
return utils.cleanObj(img);
}
/**
* Checks if configuration allows specified sync method
* @param syncRule {Object}
* @param bidderCode {string}
* @returns {boolean}
*/
function isSyncMethodAllowed(syncRule, bidderCode) {
if (!syncRule) {
return false;
}
let bidders = utils.isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode];
let rule = syncRule.filter === 'include';
return utils.contains(bidders, bidderCode) === rule;
}
/**
* Get preferred user-sync method based on publisher configuration
* @param bidderCode {string}
* @returns {number|undefined}
*/
function getAllowedSyncMethod(bidderCode) {
if (!config.getConfig('userSync.syncEnabled')) {
return;
}
let filterConfig = config.getConfig('userSync.filterSettings');
if (isSyncMethodAllowed(filterConfig.all, bidderCode) || isSyncMethodAllowed(filterConfig.iframe, bidderCode)) {
return SYNC_IFRAME;
} else if (isSyncMethodAllowed(filterConfig.image, bidderCode)) {
return SYNC_IMAGE;
}
}
/**
* Builds complete rtb request
* @param imps {Object} Collection of rtb impressions
* @param bidderRequest {BidderRequest}
* @param schain {Object=} Supply chain config
* @return {Object} Complete rtb request
*/
function buildRtbRequest(imps, bidderRequest, schain) {
let {bidderCode, gdprConsent, auctionId, refererInfo, timeout, uspConsent} = bidderRequest;
let coppa = config.getConfig('coppa');
let req = {
'id': auctionId,
'imp': imps,
'site': createSite(refererInfo),
'at': 1,
'device': {
'ip': 'caller',
'ipv6': 'caller',
'ua': 'caller',
'js': 1,
'language': getLanguage()
},
'tmax': parseInt(timeout)
};
if (utils.getDNT()) {
req.device.dnt = 1;
}
if (gdprConsent) {
if (gdprConsent.gdprApplies !== undefined) {
utils.deepSetValue(req, 'regs.ext.gdpr', ~~gdprConsent.gdprApplies);
}
if (gdprConsent.consentString !== undefined) {
utils.deepSetValue(req, 'user.ext.consent', gdprConsent.consentString);
}
}
if (uspConsent) {
utils.deepSetValue(req, 'regs.ext.us_privacy', uspConsent);
}
if (coppa) {
utils.deepSetValue(req, 'regs.coppa', 1);
}
let syncMethod = getAllowedSyncMethod(bidderCode);
if (syncMethod) {
utils.deepSetValue(req, 'ext.adk_usersync', syncMethod);
}
if (schain) {
utils.deepSetValue(req, 'source.ext.schain', schain);
}
let eids = getExtendedUserIds(bidderRequest);
if (eids) {
utils.deepSetValue(req, 'user.ext.eids', eids);
}
return req;
}
/**
* Get browser language
* @returns {String}
*/
function getLanguage() {
const language = navigator.language ? 'language' : 'userLanguage';
return navigator[language].split('-')[0];
}
/**
* Creates site description object
*/
function createSite(refInfo) {
let url = utils.parseUrl(refInfo.referer);
let site = {
'domain': url.hostname,
'page': `${url.protocol}://${url.hostname}${url.pathname}`
};
if (self === top && document.referrer) {
site.ref = document.referrer;
}
let keywords = document.getElementsByTagName('meta')['keywords'];
if (keywords && keywords.content) {
site.keywords = keywords.content;
}
return site;
}
function getExtendedUserIds(bidderRequest) {
let eids = utils.deepAccess(bidderRequest, 'bids.0.userIdAsEids');
if (utils.isArray(eids)) {
return eids;
}
}
/**
* Format creative with optional nurl call
* @param bid rtb Bid object
*/
function formatAdMarkup(bid) {
let adm = bid.adm;
if ('nurl' in bid) {
adm += utils.createTrackPixelHtml(`${bid.nurl}&px=1`);
}
return adm;
}
/**
* Basic validates to comply with platform requirements
*/
function validateNativeAdUnit(adUnit) {
return validateNativeImageSize(adUnit.image) && validateNativeImageSize(adUnit.icon) &&
!utils.deepAccess(adUnit, 'privacyLink.required') && // not supported yet
!utils.deepAccess(adUnit, 'privacyIcon.required'); // not supported yet
}
/**
* Validates image asset size definition
*/
function validateNativeImageSize(img) {
if (!img) {
return true;
}
if (img.sizes) {
return utils.isArrayOfNums(img.sizes, 2);
}
if (utils.isArray(img.aspect_ratios)) {
return img.aspect_ratios.length > 0 && img.aspect_ratios[0].min_height && img.aspect_ratios[0].min_width;
}
return true;
}
/**
* Creates native ad for native 1.1 response
*/
function buildNativeAd(nativeResp) {
const {assets, link, imptrackers, jstracker, privacy} = nativeResp.native;
let nativeAd = {
clickUrl: link.url,
impressionTrackers: imptrackers,
javascriptTrackers: jstracker ? [jstracker] : undefined,
privacyLink: privacy,
};
utils._each(assets, asset => {
let assetName = NATIVE_MODEL[asset.id].name;
let assetType = NATIVE_MODEL[asset.id].assetType;
nativeAd[assetName] = asset[assetType].text || asset[assetType].value || utils.cleanObj({
url: asset[assetType].url,
width: asset[assetType].w,
height: asset[assetType].h
});
});
return utils.cleanObj(nativeAd);
}