UNPKG

mk9-prebid

Version:

Header Bidding Management Library

695 lines (566 loc) 20.1 kB
import {Renderer} from '../src/Renderer.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {isStr, isArray, isNumber, isPlainObject, isBoolean, logError, replaceAuctionPrice} from '../src/utils.js'; import find from 'core-js-pure/features/array/find.js'; import { config } from '../src/config.js'; const ADAPTER_VERSION = 'v1.0.0'; const BID_METHOD = 'POST'; const BIDDER_URL = 'https://dsp.adotmob.com/headerbidding{PUBLISHER_PATH}/bidrequest'; const FIRST_PRICE = 1; const NET_REVENUE = true; // eslint-disable-next-line no-template-curly-in-string const AUCTION_PRICE = '${AUCTION_PRICE}'; const TTL = 10; const SUPPORTED_VIDEO_CONTEXTS = ['instream', 'outstream']; const SUPPORTED_INSTREAM_CONTEXTS = ['pre-roll', 'mid-roll', 'post-roll']; const SUPPORTED_VIDEO_MIMES = ['video/mp4']; const BID_SUPPORTED_MEDIA_TYPES = ['banner', 'video', 'native']; const DOMAIN_REGEX = new RegExp('//([^/]*)'); const OUTSTREAM_VIDEO_PLAYER_URL = 'https://adserver.adotmob.com/video/player.min.js'; const NATIVE_PLACEMENTS = { title: {id: 1, name: 'title'}, icon: {id: 2, type: 1, name: 'img'}, image: {id: 3, type: 3, name: 'img'}, sponsoredBy: {id: 4, name: 'data', type: 1}, body: {id: 5, name: 'data', type: 2}, cta: {id: 6, type: 12, name: 'data'} }; const NATIVE_ID_MAPPING = {1: 'title', 2: 'icon', 3: 'image', 4: 'sponsoredBy', 5: 'body', 6: 'cta'}; const NATIVE_PRESET_FORMATTERS = { image: formatNativePresetImage } function isNone(value) { return (value === null) || (value === undefined); } function groupBy(values, key) { const groups = values.reduce((acc, value) => { const groupId = value[key]; if (!acc[groupId]) acc[groupId] = []; acc[groupId].push(value); return acc; }, {}); return Object .keys(groups) .map(id => ({id, key, values: groups[id]})); } function validateMediaTypes(mediaTypes, allowedMediaTypes) { if (!isPlainObject(mediaTypes)) return false; if (!allowedMediaTypes.some(mediaType => mediaType in mediaTypes)) return false; if (isBanner(mediaTypes)) { if (!validateBanner(mediaTypes.banner)) return false; } if (isVideo(mediaTypes)) { if (!validateVideo(mediaTypes.video)) return false; } return true; } function isBanner(mediaTypes) { return isPlainObject(mediaTypes) && isPlainObject(mediaTypes.banner); } function isVideo(mediaTypes) { return isPlainObject(mediaTypes) && 'video' in mediaTypes; } function validateBanner(banner) { return isPlainObject(banner) && isArray(banner.sizes) && (banner.sizes.length > 0) && banner.sizes.every(validateMediaSizes); } function validateVideo(video) { if (!isPlainObject(video)) return false; if (!isStr(video.context)) return false; if (SUPPORTED_VIDEO_CONTEXTS.indexOf(video.context) === -1) return false; if (!video.playerSize) return true; if (!isArray(video.playerSize)) return false; return video.playerSize.every(validateMediaSizes); } function validateMediaSizes(mediaSize) { return isArray(mediaSize) && (mediaSize.length === 2) && mediaSize.every(size => (isNumber(size) && size >= 0)); } function validateParameters(parameters, adUnit) { if (isVideo(adUnit.mediaTypes)) { if (!isPlainObject(parameters)) return false; if (!isPlainObject(adUnit.mediaTypes.video)) return false; if (!validateVideoParameters(parameters.video, adUnit)) return false; } return true; } function validateVideoParameters(videoParams, adUnit) { const video = adUnit.mediaTypes.video; if (!video) return false; if (!isArray(video.mimes)) return false; if (video.mimes.length === 0) return false; if (!video.mimes.every(isStr)) return false; if (video.minDuration && !isNumber(video.minDuration)) return false; if (video.maxDuration && !isNumber(video.maxDuration)) return false; if (!isArray(video.protocols)) return false; if (video.protocols.length === 0) return false; if (!video.protocols.every(isNumber)) return false; if (isInstream(video)) { if (!videoParams.instreamContext) return false; if (SUPPORTED_INSTREAM_CONTEXTS.indexOf(videoParams.instreamContext) === -1) return false; } return true; } function validateServerRequest(serverRequest) { return isPlainObject(serverRequest) && isPlainObject(serverRequest.data) && isArray(serverRequest.data.imp) && isPlainObject(serverRequest._adot_internal) && isArray(serverRequest._adot_internal.impressions) } function createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) { const publisherPath = config.getConfig('adot.publisherPath') === undefined ? '' : '/' + config.getConfig('adot.publisherPath'); return { method: BID_METHOD, url: BIDDER_URL.replace('{PUBLISHER_PATH}', publisherPath), data: generateBidRequestsFromAdUnits(adUnits, bidRequestId, adUnitContext), _adot_internal: generateAdotInternal(adUnits) } } function generateAdotInternal(adUnits) { const impressions = adUnits.reduce((acc, adUnit) => { const {bidId, mediaTypes, adUnitCode, params} = adUnit; const base = {bidId, adUnitCode, container: params.video && params.video.container}; const imps = Object .keys(mediaTypes) .reduce((acc, mediaType, index) => { const data = mediaTypes[mediaType]; const impressionId = `${bidId}_${index}`; if (mediaType !== 'banner') return acc.concat({...base, impressionId}); const bannerImps = data.sizes.map((item, i) => ({...base, impressionId: `${impressionId}_${i}`})); return acc.concat(bannerImps); }, []); return acc.concat(imps); }, []); return {impressions}; } function generateBidRequestsFromAdUnits(adUnits, bidRequestId, adUnitContext) { return { id: bidRequestId, imp: adUnits.reduce(generateImpressionsFromAdUnit, []), site: generateSiteFromAdUnitContext(adUnitContext), device: getDeviceInfo(), user: getUserInfoFromAdUnitContext(adUnitContext), regs: getRegulationFromAdUnitContext(adUnitContext), at: FIRST_PRICE, ext: generateBidRequestExtension() }; } function generateImpressionsFromAdUnit(acc, adUnit) { const {bidId, mediaTypes, params} = adUnit; const {placementId} = params; const pmp = {}; const ext = {placementId}; if (placementId) pmp.deals = [{id: placementId}] const imps = Object .keys(mediaTypes) .reduce((acc, mediaType, index) => { const data = mediaTypes[mediaType]; const impId = `${bidId}_${index}`; if (mediaType === 'banner') return acc.concat(generateBannerFromAdUnit(impId, data, params)); if (mediaType === 'video') return acc.concat({id: impId, video: generateVideoFromAdUnit(data, params), pmp, ext}); if (mediaType === 'native') return acc.concat({id: impId, native: generateNativeFromAdUnit(data), pmp, ext}); }, []); return acc.concat(imps); } function isImpressionAVideo(impression) { return isPlainObject(impression) && isPlainObject(impression.video); } function generateBannerFromAdUnit(impId, data, params) { const {position, placementId} = params; const pos = position || 0; const pmp = {}; const ext = {placementId}; if (placementId) pmp.deals = [{id: placementId}] return data.sizes.map(([w, h], index) => ({id: `${impId}_${index}`, banner: {format: [{w, h}], w, h, pos}, pmp, ext})); } function generateVideoFromAdUnit(data, params) { const {playerSize} = data; const video = data const hasPlayerSize = isArray(playerSize) && playerSize.length > 0; const {minDuration, maxDuration, protocols} = video; const size = {width: hasPlayerSize ? playerSize[0][0] : null, height: hasPlayerSize ? playerSize[0][1] : null}; const duration = {min: isNumber(minDuration) ? minDuration : null, max: isNumber(maxDuration) ? maxDuration : null}; const startdelay = computeStartDelay(data, params); return { mimes: SUPPORTED_VIDEO_MIMES, skip: video.skippable || 0, w: size.width, h: size.height, startdelay: startdelay, linearity: video.linearity || null, minduration: duration.min, maxduration: duration.max, protocols, api: getApi(protocols), format: hasPlayerSize ? playerSize.map(s => { return {w: s[0], h: s[1]}; }) : null, pos: video.position || 0 }; } function getApi(protocols) { let defaultValue = [2]; let listProtocols = [ {key: 'VPAID_1_0', value: 1}, {key: 'VPAID_2_0', value: 2}, {key: 'MRAID_1', value: 3}, {key: 'ORMMA', value: 4}, {key: 'MRAID_2', value: 5}, {key: 'MRAID_3', value: 6}, ]; if (protocols) { return listProtocols.filter(p => { return protocols.indexOf(p.key) !== -1; }).map(p => p.value) } else { return defaultValue; } } function isInstream(video) { return isPlainObject(video) && (video.context === 'instream'); } function isOutstream(video) { return isPlainObject(video) && (video.startdelay === null) } function computeStartDelay(data, params) { if (isInstream(data)) { if (params.video.instreamContext === 'pre-roll') return 0; if (params.video.instreamContext === 'mid-roll') return -1; if (params.video.instreamContext === 'post-roll') return -2; } return null; } function generateNativeFromAdUnit(data) { const {type} = data; const presetFormatter = type && NATIVE_PRESET_FORMATTERS[data.type]; const nativeFields = presetFormatter ? presetFormatter(data) : data; const assets = Object .keys(nativeFields) .reduce((acc, placement) => { const placementData = nativeFields[placement]; const assetInfo = NATIVE_PLACEMENTS[placement]; if (!assetInfo) return acc; const {id, name, type} = assetInfo; const {required, len, sizes = []} = placementData; let wmin; let hmin; if (isArray(sizes[0])) { wmin = sizes[0][0]; hmin = sizes[0][1]; } else { wmin = sizes[0]; hmin = sizes[1]; } const content = {}; if (type) content.type = type; if (len) content.len = len; if (wmin) content.wmin = wmin; if (hmin) content.hmin = hmin; acc.push({id, required, [name]: content}); return acc; }, []); return { request: JSON.stringify({assets}) }; } function formatNativePresetImage(data) { const sizes = data.sizes; return { image: { required: true, sizes }, title: { required: true }, sponsoredBy: { required: true }, body: { required: false }, cta: { required: false }, icon: { required: false } }; } function generateSiteFromAdUnitContext(adUnitContext) { if (!adUnitContext || !adUnitContext.refererInfo) return null; const domain = extractSiteDomainFromURL(adUnitContext.refererInfo.referer); const publisherId = config.getConfig('adot.publisherId'); if (!domain) return null; return { page: adUnitContext.refererInfo.referer, domain: domain, name: domain, publisher: { id: publisherId } }; } function extractSiteDomainFromURL(url) { if (!url || !isStr(url)) return null; const domain = url.match(DOMAIN_REGEX); if (isArray(domain) && domain.length === 2) return domain[1]; return null; } function getDeviceInfo() { return {ua: navigator.userAgent, language: navigator.language}; } function getUserInfoFromAdUnitContext(adUnitContext) { if (!adUnitContext || !adUnitContext.gdprConsent) return null; if (!isStr(adUnitContext.gdprConsent.consentString)) return null; return { ext: { consent: adUnitContext.gdprConsent.consentString } }; } function getRegulationFromAdUnitContext(adUnitContext) { if (!adUnitContext || !adUnitContext.gdprConsent) return null; if (!isBoolean(adUnitContext.gdprConsent.gdprApplies)) return null; return { ext: { gdpr: adUnitContext.gdprConsent.gdprApplies } }; } function generateBidRequestExtension() { return { adot: {adapter_version: ADAPTER_VERSION}, should_use_gzip: true }; } function validateServerResponse(serverResponse) { return isPlainObject(serverResponse) && isPlainObject(serverResponse.body) && isStr(serverResponse.body.cur) && isArray(serverResponse.body.seatbid); } function seatBidsToAds(seatBid, bidResponse, serverRequest) { return seatBid.bid .filter(bid => validateBids(bid, serverRequest)) .map(bid => generateAdFromBid(bid, bidResponse, serverRequest)); } function validateBids(bid, serverRequest) { if (!isPlainObject(bid)) return false; if (!isStr(bid.impid)) return false; if (!isStr(bid.crid)) return false; if (!isNumber(bid.price)) return false; if (!isPlainObject(bid.ext)) return false; if (!isPlainObject(bid.ext.adot)) return false; if (!isStr(bid.ext.adot.media_type)) return false; if (BID_SUPPORTED_MEDIA_TYPES.indexOf(bid.ext.adot.media_type) === -1) return false; if (!bid.adm && !bid.nurl) return false; if (bid.adm) { if (!isStr(bid.adm)) return false; if (bid.adm.indexOf(AUCTION_PRICE) === -1) return false; } if (bid.nurl) { if (!isStr(bid.nurl)) return false; if (bid.nurl.indexOf(AUCTION_PRICE) === -1) return false; } if (isBidABanner(bid)) { if (!isNumber(bid.h)) return false; if (!isNumber(bid.w)) return false; } if (isBidAVideo(bid)) { if (!(isNone(bid.h) || isNumber(bid.h))) return false; if (!(isNone(bid.w) || isNumber(bid.w))) return false; } const impression = getImpressionData(serverRequest, bid.impid); if (!isPlainObject(impression.openRTB)) return false; if (!isPlainObject(impression.internal)) return false; if (!isStr(impression.internal.adUnitCode)) return false; if (isBidABanner(bid)) { if (!isPlainObject(impression.openRTB.banner)) return false; } if (isBidAVideo(bid)) { if (!isPlainObject(impression.openRTB.video)) return false; } if (isBidANative(bid)) { if (!isPlainObject(impression.openRTB.native) || !tryParse(bid.adm)) return false; } return true; } function isBidABanner(bid) { return isPlainObject(bid) && isPlainObject(bid.ext) && isPlainObject(bid.ext.adot) && bid.ext.adot.media_type === 'banner'; } function isBidAVideo(bid) { return isPlainObject(bid) && isPlainObject(bid.ext) && isPlainObject(bid.ext.adot) && bid.ext.adot.media_type === 'video'; } function isBidANative(bid) { return isPlainObject(bid) && isPlainObject(bid.ext) && isPlainObject(bid.ext.adot) && bid.ext.adot.media_type === 'native'; } function getImpressionData(serverRequest, impressionId) { const openRTBImpression = find(serverRequest.data.imp, imp => imp.id === impressionId); const internalImpression = find(serverRequest._adot_internal.impressions, imp => imp.impressionId === impressionId); return { id: impressionId, openRTB: openRTBImpression || null, internal: internalImpression || null }; } function generateAdFromBid(bid, bidResponse, serverRequest) { const impressionData = getImpressionData(serverRequest, bid.impid); const isVideo = isBidAVideo(bid); const base = { requestId: impressionData.internal.bidId, cpm: bid.price, currency: bidResponse.cur, ttl: TTL, creativeId: bid.crid, netRevenue: NET_REVENUE, mediaType: bid.ext.adot.media_type, }; if (bid.adomain) { base.meta = { advertiserDomains: bid.adomain }; } if (isBidANative(bid)) return {...base, native: formatNativeData(bid)}; const size = getSizeFromBid(bid, impressionData); const creative = getCreativeFromBid(bid, impressionData); return { ...base, height: size.height, width: size.width, ad: creative.markup, adUrl: creative.markupUrl, vastXml: isVideo && !isStr(creative.markupUrl) ? creative.markup : null, vastUrl: isVideo && isStr(creative.markupUrl) ? creative.markupUrl : null, renderer: creative.renderer }; } function formatNativeData({adm, price}) { const parsedAdm = tryParse(adm); const {assets, link: {url, clicktrackers}, imptrackers, jstracker} = parsedAdm.native; const placements = NATIVE_PLACEMENTS; const placementIds = NATIVE_ID_MAPPING; return assets.reduce((acc, asset) => { const placementName = placementIds[asset.id]; const content = placementName && asset[placements[placementName].name]; if (!content) return acc; acc[placementName] = content.text || content.value || {url: content.url, width: content.w, height: content.h}; return acc; }, { clickUrl: url, clickTrackers: clicktrackers, impressionTrackers: imptrackers && imptrackers.map(impTracker => replaceAuctionPrice(impTracker, price)), javascriptTrackers: jstracker && [jstracker] }); } function getSizeFromBid(bid, impressionData) { if (isNumber(bid.w) && isNumber(bid.h)) { return { width: bid.w, height: bid.h }; } if (isImpressionAVideo(impressionData.openRTB)) { const { video } = impressionData.openRTB; if (isNumber(video.w) && isNumber(video.h)) { return { width: video.w, height: video.h }; } } return { width: null, height: null }; } function getCreativeFromBid(bid, impressionData) { const shouldUseAdMarkup = !!bid.adm; const price = bid.price; return { markup: shouldUseAdMarkup ? replaceAuctionPrice(bid.adm, price) : null, markupUrl: !shouldUseAdMarkup ? replaceAuctionPrice(bid.nurl, price) : null, renderer: getRendererFromBid(bid, impressionData) }; } function getRendererFromBid(bid, impressionData) { const isOutstreamImpression = isBidAVideo(bid) && isImpressionAVideo(impressionData.openRTB) && isOutstream(impressionData.openRTB.video); return isOutstreamImpression ? buildOutstreamRenderer(impressionData) : null; } function buildOutstreamRenderer(impressionData) { const renderer = Renderer.install({ url: OUTSTREAM_VIDEO_PLAYER_URL, loaded: false, adUnitCode: impressionData.internal.adUnitCode }); renderer.setRender((ad) => { ad.renderer.push(() => { const container = impressionData.internal.container ? document.querySelector(impressionData.internal.container) : document.getElementById(impressionData.internal.adUnitCode); const player = new window.VASTPlayer(container); player.on('ready', () => { player.adVolume = 0; player.startAd(); }); try { isStr(ad.adUrl) ? player.load(ad.adUrl) : player.loadXml(ad.ad) } catch (err) { logError(err); } }); }); return renderer; } function tryParse(data) { try { return JSON.parse(data); } catch (err) { logError(err); return null; } } const adotBidderSpec = { code: 'adot', supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid(adUnit) { const allowedBidderCodes = [this.code]; return isPlainObject(adUnit) && allowedBidderCodes.indexOf(adUnit.bidder) !== -1 && isStr(adUnit.adUnitCode) && isStr(adUnit.bidderRequestId) && isStr(adUnit.bidId) && validateMediaTypes(adUnit.mediaTypes, this.supportedMediaTypes) && validateParameters(adUnit.params, adUnit); }, buildRequests(adUnits, adUnitContext) { if (!adUnits) return null; return groupBy(adUnits, 'bidderRequestId').map(group => { const bidRequestId = group.id; const adUnits = groupBy(group.values, 'bidId').map((group) => { const length = group.values.length; return length > 0 && group.values[length - 1] }); return createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) }); }, interpretResponse(serverResponse, serverRequest) { if (!validateServerRequest(serverRequest)) return []; if (!validateServerResponse(serverResponse)) return []; const bidResponse = serverResponse.body; return bidResponse.seatbid .filter(seatBid => isPlainObject(seatBid) && isArray(seatBid.bid)) .reduce((acc, seatBid) => acc.concat(seatBidsToAds(seatBid, bidResponse, serverRequest)), []); } }; registerBidder(adotBidderSpec); export {adotBidderSpec as spec};