UNPKG

mk9-prebid

Version:

Header Bidding Management Library

486 lines (443 loc) 16 kB
import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import * as utils from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; /// /////////// CONSTANTS ////////////// const ADAPTER_CODE = 'pubmatic'; const SEND_TIMEOUT = 2000; const END_POINT_HOST = 'https://t.pubmatic.com/'; const END_POINT_BID_LOGGER = END_POINT_HOST + 'wl?'; const END_POINT_WIN_BID_LOGGER = END_POINT_HOST + 'wt?'; const LOG_PRE_FIX = 'PubMatic-Analytics: '; const cache = { auctions: {} }; const SUCCESS = 'success'; const NO_BID = 'no-bid'; const ERROR = 'error'; const REQUEST_ERROR = 'request-error'; const TIMEOUT_ERROR = 'timeout-error'; const EMPTY_STRING = ''; const MEDIA_TYPE_BANNER = 'banner'; const CURRENCY_USD = 'USD'; const BID_PRECISION = 2; // todo: input profileId and profileVersionId ; defaults to zero or one const DEFAULT_PUBLISHER_ID = 0; const DEFAULT_PROFILE_ID = 0; const DEFAULT_PROFILE_VERSION_ID = 0; const enc = window.encodeURIComponent; /// /////////// VARIABLES ////////////// let publisherId = DEFAULT_PUBLISHER_ID; // int: mandatory let profileId = DEFAULT_PROFILE_ID; // int: optional let profileVersionId = DEFAULT_PROFILE_VERSION_ID; // int: optional let s2sBidders = []; /// /////////// HELPER FUNCTIONS ////////////// function sizeToDimensions(size) { return { width: size.w || size[0], height: size.h || size[1] }; } function validMediaType(type) { return ({'banner': 1, 'native': 1, 'video': 1}).hasOwnProperty(type); } function formatSource(src) { if (typeof src === 'undefined') { src = 'client'; } else if (src === 's2s') { src = 'server'; } return src.toLowerCase(); } function setMediaTypes(types, bid) { if (bid.mediaType && validMediaType(bid.mediaType)) { return [bid.mediaType]; } if (Array.isArray(types)) { return types.filter(validMediaType); } if (typeof types === 'object') { if (!bid.sizes) { bid.dimensions = []; utils._each(types, (type) => bid.dimensions = bid.dimensions.concat( type.sizes.map(sizeToDimensions) ) ); } return Object.keys(types).filter(validMediaType); } return [MEDIA_TYPE_BANNER]; } function copyRequiredBidDetails(bid) { return utils.pick(bid, [ 'bidder', 'bidId', 'status', () => NO_BID, // default a bid to NO_BID until response is recieved or bid is timed out 'finalSource as source', 'params', 'adUnit', () => utils.pick(bid, [ 'adUnitCode', 'transactionId', 'sizes as dimensions', sizes => sizes.map(sizeToDimensions), 'mediaTypes', (types) => setMediaTypes(types, bid) ]) ]); } function setBidStatus(bid, args) { switch (args.getStatusCode()) { case CONSTANTS.STATUS.GOOD: bid.status = SUCCESS; delete bid.error; // it's possible for this to be set by a previous timeout break; case CONSTANTS.STATUS.NO_BID: bid.status = NO_BID; delete bid.error; break; default: bid.status = ERROR; bid.error = { code: REQUEST_ERROR }; } } function parseBidResponse(bid) { return utils.pick(bid, [ 'bidPriceUSD', () => { // todo: check whether currency cases are handled here if (typeof bid.currency === 'string' && bid.currency.toUpperCase() === CURRENCY_USD) { return window.parseFloat(Number(bid.cpm).toFixed(BID_PRECISION)); } // use currency conversion function if present if (typeof bid.getCpmInNewCurrency === 'function') { return window.parseFloat(Number(bid.getCpmInNewCurrency(CURRENCY_USD)).toFixed(BID_PRECISION)); } utils.logWarn(LOG_PRE_FIX + 'Could not determine the Net cpm in USD for the bid thus using bid.cpm', bid); return bid.cpm }, 'bidGrossCpmUSD', () => { if (typeof bid.originalCurrency === 'string' && bid.originalCurrency.toUpperCase() === CURRENCY_USD) { return window.parseFloat(Number(bid.originalCpm).toFixed(BID_PRECISION)); } // use currency conversion function if present if (typeof getGlobal().convertCurrency === 'function') { return window.parseFloat(Number(getGlobal().convertCurrency(bid.originalCpm, bid.originalCurrency, CURRENCY_USD)).toFixed(BID_PRECISION)); } utils.logWarn(LOG_PRE_FIX + 'Could not determine the Gross cpm in USD for the bid, thus using bid.originalCpm', bid); return bid.originalCpm }, 'dealId', 'currency', 'cpm', () => window.parseFloat(Number(bid.cpm).toFixed(BID_PRECISION)), 'originalCpm', () => window.parseFloat(Number(bid.originalCpm).toFixed(BID_PRECISION)), 'originalCurrency', 'dealChannel', 'meta', 'status', 'error', 'bidId', 'mediaType', 'params', 'mi', 'regexPattern', () => bid.regexPattern || undefined, 'partnerImpId', // partner impression ID 'dimensions', () => utils.pick(bid, [ 'width', 'height' ]) ]); } function getDomainFromUrl(url) { let a = window.document.createElement('a'); a.href = url; return a.hostname; } function getDevicePlatform() { var deviceType = 3; try { var ua = navigator.userAgent; if (ua && utils.isStr(ua) && ua.trim() != '') { ua = ua.toLowerCase().trim(); var isMobileRegExp = new RegExp('(mobi|tablet|ios).*'); if (ua.match(isMobileRegExp)) { deviceType = 2; } else { deviceType = 1; } } } catch (ex) {} return deviceType; } function getValueForKgpv(bid, adUnitId) { if (bid.params.regexPattern) { return bid.params.regexPattern; } else if (bid.bidResponse && bid.bidResponse.regexPattern) { return bid.bidResponse.regexPattern; } else if (bid.params.kgpv) { return bid.params.kgpv; } else { return adUnitId; } } function getAdapterNameForAlias(aliasName) { return adapterManager.aliasRegistry[aliasName] || aliasName; } function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { highestBid = (highestBid && highestBid.length > 0) ? highestBid[0] : null; return Object.keys(adUnit.bids).reduce(function(partnerBids, bidId) { let bid = adUnit.bids[bidId]; partnerBids.push({ 'pn': getAdapterNameForAlias(bid.bidder), 'bc': bid.bidder, 'bidid': bid.bidId, 'db': bid.bidResponse ? 0 : 1, 'kgpv': getValueForKgpv(bid, adUnitId), 'kgpsv': bid.params.kgpv ? bid.params.kgpv : adUnitId, 'psz': bid.bidResponse ? (bid.bidResponse.dimensions.width + 'x' + bid.bidResponse.dimensions.height) : '0x0', 'eg': bid.bidResponse ? bid.bidResponse.bidGrossCpmUSD : 0, 'en': bid.bidResponse ? bid.bidResponse.bidPriceUSD : 0, 'di': bid.bidResponse ? (bid.bidResponse.dealId || EMPTY_STRING) : EMPTY_STRING, 'dc': bid.bidResponse ? (bid.bidResponse.dealChannel || EMPTY_STRING) : EMPTY_STRING, 'l1': bid.bidResponse ? bid.clientLatencyTimeMs : 0, 'l2': 0, 'ss': (s2sBidders.indexOf(bid.bidder) > -1) ? 1 : 0, 't': (bid.status == ERROR && bid.error.code == TIMEOUT_ERROR) ? 1 : 0, 'wb': (highestBid && highestBid.requestId === bid.bidId ? 1 : 0), 'mi': bid.bidResponse ? (bid.bidResponse.mi || undefined) : undefined, 'af': bid.bidResponse ? (bid.bidResponse.mediaType || undefined) : undefined, 'ocpm': bid.bidResponse ? (bid.bidResponse.originalCpm || 0) : 0, 'ocry': bid.bidResponse ? (bid.bidResponse.originalCurrency || CURRENCY_USD) : CURRENCY_USD, 'piid': bid.bidResponse ? (bid.bidResponse.partnerImpId || EMPTY_STRING) : EMPTY_STRING }); return partnerBids; }, []) } function executeBidsLoggerCall(e, highestCpmBids) { let auctionId = e.auctionId; let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; let auctionCache = cache.auctions[auctionId]; let outputObj = { s: [] }; let pixelURL = END_POINT_BID_LOGGER; if (!auctionCache) { return; } if (auctionCache.sent) { return; } pixelURL += 'pubid=' + publisherId; outputObj['pubid'] = '' + publisherId; outputObj['iid'] = '' + auctionId; outputObj['to'] = '' + auctionCache.timeout; outputObj['purl'] = referrer; outputObj['orig'] = getDomainFromUrl(referrer); outputObj['tst'] = Math.round((new window.Date()).getTime() / 1000); outputObj['pid'] = '' + profileId; outputObj['pdvid'] = '' + profileVersionId; outputObj['dvc'] = {'plt': getDevicePlatform()}; outputObj['tgid'] = (function() { var testGroupId = parseInt(config.getConfig('testGroupId') || 0); if (testGroupId <= 15 && testGroupId >= 0) { return testGroupId; } return 0; })(); outputObj.s = Object.keys(auctionCache.adUnitCodes).reduce(function(slotsArray, adUnitId) { let adUnit = auctionCache.adUnitCodes[adUnitId]; let slotObject = { 'sn': adUnitId, 'sz': adUnit.dimensions.map(e => e[0] + 'x' + e[1]), 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestCpmBids.filter(bid => bid.adUnitCode === adUnitId)) }; slotsArray.push(slotObject); return slotsArray; }, []); auctionCache.sent = true; ajax( pixelURL, null, 'json=' + enc(JSON.stringify(outputObj)), { contentType: 'application/x-www-form-urlencoded', withCredentials: true, method: 'POST' } ); } function executeBidWonLoggerCall(auctionId, adUnitId) { const winningBidId = cache.auctions[auctionId].adUnitCodes[adUnitId].bidWon; const winningBid = cache.auctions[auctionId].adUnitCodes[adUnitId].bids[winningBidId]; const adapterName = getAdapterNameForAlias(winningBid.bidder); let pixelURL = END_POINT_WIN_BID_LOGGER; pixelURL += 'pubid=' + publisherId; pixelURL += '&purl=' + enc(config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''); pixelURL += '&tst=' + Math.round((new window.Date()).getTime() / 1000); pixelURL += '&iid=' + enc(auctionId); pixelURL += '&bidid=' + enc(winningBidId); pixelURL += '&pid=' + enc(profileId); pixelURL += '&pdvid=' + enc(profileVersionId); pixelURL += '&slot=' + enc(adUnitId); pixelURL += '&pn=' + enc(adapterName); pixelURL += '&bc=' + enc(winningBid.bidder); pixelURL += '&en=' + enc(winningBid.bidResponse.bidPriceUSD); pixelURL += '&eg=' + enc(winningBid.bidResponse.bidGrossCpmUSD); pixelURL += '&kgpv=' + enc(getValueForKgpv(winningBid, adUnitId)); pixelURL += '&piid=' + enc(winningBid.bidResponse.partnerImpId || EMPTY_STRING); ajax( pixelURL, null, null, { contentType: 'application/x-www-form-urlencoded', withCredentials: true, method: 'GET' } ); } /// /////////// ADAPTER EVENT HANDLER FUNCTIONS ////////////// function auctionInitHandler(args) { s2sBidders = (function() { let s2sConf = config.getConfig('s2sConfig'); return (s2sConf && utils.isArray(s2sConf.bidders)) ? s2sConf.bidders : []; }()); let cacheEntry = utils.pick(args, [ 'timestamp', 'timeout', 'bidderDonePendingCount', () => args.bidderRequests.length, ]); cacheEntry.adUnitCodes = {}; cacheEntry.referer = args.bidderRequests[0].refererInfo.referer; cache.auctions[args.auctionId] = cacheEntry; } function bidRequestedHandler(args) { args.bids.forEach(function(bid) { if (!cache.auctions[args.auctionId].adUnitCodes.hasOwnProperty(bid.adUnitCode)) { cache.auctions[args.auctionId].adUnitCodes[bid.adUnitCode] = { bids: {}, bidWon: false, dimensions: bid.sizes }; } cache.auctions[args.auctionId].adUnitCodes[bid.adUnitCode].bids[bid.bidId] = copyRequiredBidDetails(bid); }) } function bidResponseHandler(args) { let bid = cache.auctions[args.auctionId].adUnitCodes[args.adUnitCode].bids[args.requestId]; if (!bid) { utils.logError(LOG_PRE_FIX + 'Could not find associated bid request for bid response with requestId: ', args.requestId); return; } bid.source = formatSource(bid.source || args.source); setBidStatus(bid, args); bid.clientLatencyTimeMs = Date.now() - cache.auctions[args.auctionId].timestamp; bid.bidResponse = parseBidResponse(args); } function bidderDoneHandler(args) { cache.auctions[args.auctionId].bidderDonePendingCount--; args.bids.forEach(bid => { let cachedBid = cache.auctions[bid.auctionId].adUnitCodes[bid.adUnitCode].bids[bid.bidId || bid.requestId]; if (typeof bid.serverResponseTimeMs !== 'undefined') { cachedBid.serverLatencyTimeMs = bid.serverResponseTimeMs; } if (!cachedBid.status) { cachedBid.status = NO_BID; } if (!cachedBid.clientLatencyTimeMs) { cachedBid.clientLatencyTimeMs = Date.now() - cache.auctions[bid.auctionId].timestamp; } }); } function bidWonHandler(args) { let auctionCache = cache.auctions[args.auctionId]; auctionCache.adUnitCodes[args.adUnitCode].bidWon = args.requestId; executeBidWonLoggerCall(args.auctionId, args.adUnitCode); } function auctionEndHandler(args) { // if for the given auction bidderDonePendingCount == 0 then execute logger call sooners let highestCpmBids = getGlobal().getHighestCpmBids() || []; setTimeout(() => { executeBidsLoggerCall.call(this, args, highestCpmBids); }, (cache.auctions[args.auctionId].bidderDonePendingCount === 0 ? 500 : SEND_TIMEOUT)); } function bidTimeoutHandler(args) { // db = 1 and t = 1 means bidder did NOT respond with a bid but we got a timeout notification // db = 0 and t = 1 means bidder did respond with a bid but post timeout args.forEach(badBid => { let auctionCache = cache.auctions[badBid.auctionId]; let bid = auctionCache.adUnitCodes[badBid.adUnitCode].bids[ badBid.bidId || badBid.requestId ]; if (bid) { bid.status = ERROR; bid.error = { code: TIMEOUT_ERROR }; } else { utils.logWarn(LOG_PRE_FIX + 'bid not found'); } }); } /// /////////// ADAPTER DEFINITION ////////////// let baseAdapter = adapter({analyticsType: 'endpoint'}); let pubmaticAdapter = Object.assign({}, baseAdapter, { enableAnalytics(conf = {}) { let error = false; if (typeof conf.options === 'object') { if (conf.options.publisherId) { publisherId = Number(conf.options.publisherId); } profileId = Number(conf.options.profileId) || DEFAULT_PROFILE_ID; profileVersionId = Number(conf.options.profileVersionId) || DEFAULT_PROFILE_VERSION_ID; } else { utils.logError(LOG_PRE_FIX + 'Config not found.'); error = true; } if (!publisherId) { utils.logError(LOG_PRE_FIX + 'Missing publisherId(Number).'); error = true; } if (error) { utils.logError(LOG_PRE_FIX + 'Not collecting data due to error(s).'); } else { baseAdapter.enableAnalytics.call(this, conf); } }, disableAnalytics() { publisherId = DEFAULT_PUBLISHER_ID; profileId = DEFAULT_PROFILE_ID; profileVersionId = DEFAULT_PROFILE_VERSION_ID; s2sBidders = []; baseAdapter.disableAnalytics.apply(this, arguments); }, track({eventType, args}) { switch (eventType) { case CONSTANTS.EVENTS.AUCTION_INIT: auctionInitHandler(args); break; case CONSTANTS.EVENTS.BID_REQUESTED: bidRequestedHandler(args); break; case CONSTANTS.EVENTS.BID_RESPONSE: bidResponseHandler(args); break; case CONSTANTS.EVENTS.BIDDER_DONE: bidderDoneHandler(args); break; case CONSTANTS.EVENTS.BID_WON: bidWonHandler(args); break; case CONSTANTS.EVENTS.AUCTION_END: auctionEndHandler(args); break; case CONSTANTS.EVENTS.BID_TIMEOUT: bidTimeoutHandler(args); break; } } }); /// /////////// ADAPTER REGISTRATION ////////////// adapterManager.registerAnalyticsAdapter({ adapter: pubmaticAdapter, code: ADAPTER_CODE }); export default pubmaticAdapter;