UNPKG

mk9-prebid

Version:

Header Bidding Management Library

509 lines (479 loc) 15.3 kB
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); }