mk9-prebid
Version:
Header Bidding Management Library
535 lines (486 loc) • 15.4 kB
JavaScript
import adapter from '../src/AnalyticsAdapter.js';
import CONSTANTS from '../src/constants.json';
import adapterManager from '../src/adapterManager.js';
import * as utils from '../src/utils.js';
import {ajax} from '../src/ajax.js';
import {getGlobal} from '../src/prebidGlobal.js';
import { config } from '../src/config.js';
const DEFAULT_PROTOCOL = 'https';
const DEFAULT_HOST = 'pa.deployads.com';
const DEFAULT_URL = `${DEFAULT_PROTOCOL}://${DEFAULT_HOST}/pae`;
const ANALYTICS_TYPE = 'endpoint';
const UTM_STORE_KEY = 'sortable_utm';
export const DEFAULT_PBID_TIMEOUT = 1000;
export const TIMEOUT_FOR_REGISTRY = 250;
const settings = {};
const {
EVENTS: {
AUCTION_INIT,
AUCTION_END,
BID_REQUESTED,
BID_ADJUSTMENT,
BID_WON,
BID_TIMEOUT,
}
} = CONSTANTS;
const minsToMillis = mins => mins * 60 * 1000;
const UTM_TTL = minsToMillis(30);
const SORTABLE_EVENTS = {
BID_WON: 'pbrw',
BID_TIMEOUT: 'pbto',
ERROR: 'pber',
PB_BID: 'pbid'
};
const UTM_PARAMS = [
'utm_campaign',
'utm_source',
'utm_medium',
'utm_content',
'utm_term'
];
const EVENT_KEYS_SHORT_NAMES = {
'auctionId': 'ai',
'adUnitCode': 'ac',
'adId': 'adi',
'bidderAlias': 'bs',
'bidFactor': 'bif',
'bidId': 'bid',
'bidRequestCount': 'brc',
'bidderRequestId': 'brid',
'bidRequestedSizes': 'rs',
'bidTopCpm': 'btcp',
'bidTopCpmCurrency': 'btcc',
'bidTopIsNetRevenue': 'btin',
'bidTopFactor': 'btif',
'bidTopSrc': 'btsrc',
'cpm': 'c',
'currency': 'cc',
'dealId': 'did',
'isNetRevenue': 'inr',
'isTop': 'it',
'isWinner': 'iw',
'isTimeout': 'ito',
'mediaType': 'mt',
'reachedTop': 'rtp',
'numIframes': 'nif',
'size': 'siz',
'start': 'st',
'tagId': 'tgid',
'transactionId': 'trid',
'ttl': 'ttl',
'ttr': 'ttr',
'url': 'u',
'utm_campaign': 'uc',
'utm_source': 'us',
'utm_medium': 'um',
'utm_content': 'un',
'utm_term': 'ut'
};
const auctionCache = {};
let bidderFactors = null;
let timeoutId = null;
let eventsToBeSent = [];
function getStorage() {
try {
return window['sessionStorage'];
} catch (e) {
return null;
}
}
function putParams(k, v) {
try {
const storage = getStorage();
if (!storage) {
return false;
}
if (v === null) {
storage.removeItem(k);
} else {
storage.setItem(k, JSON.stringify(v));
}
return true;
} catch (e) {
return false;
}
}
function getParams(k) {
try {
let storage = getStorage();
if (!storage) {
return null;
}
let value = storage.getItem(k);
return value === null ? null : JSON.parse(value);
} catch (e) {
return null;
}
}
function storeParams(key, paramsToSave) {
if (!settings.disableSessionTracking) {
for (let property in paramsToSave) {
if (paramsToSave.hasOwnProperty(property)) {
putParams(key, paramsToSave);
break;
}
}
}
}
function getSiteKey(options) {
const sortableConfig = config.getConfig('sortable') || {};
const globalSiteId = sortableConfig.siteId;
return globalSiteId || options.siteId;
}
function generateRandomId() {
let s = (+new Date()).toString(36);
for (let i = 0; i < 6; ++i) { s += (Math.random() * 36 | 0).toString(36); }
return s;
}
function getSessionParams() {
const stillValid = paramsFromStorage => (paramsFromStorage.created) < (+new Date() + UTM_TTL);
let sessionParams = null;
if (!settings.disableSessionTracking) {
const paramsFromStorage = getParams(UTM_STORE_KEY);
sessionParams = paramsFromStorage && stillValid(paramsFromStorage) ? paramsFromStorage : null;
}
sessionParams = sessionParams || {'created': +new Date(), 'sessionId': generateRandomId()};
const urlParams = UTM_PARAMS.map(utils.getParameterByName);
if (UTM_PARAMS.every(key => !sessionParams[key])) {
UTM_PARAMS.forEach((v, i) => sessionParams[v] = urlParams[i] || sessionParams[v]);
sessionParams.created = +new Date();
storeParams(UTM_STORE_KEY, sessionParams);
}
return sessionParams;
}
function getPrebidVersion() {
return getGlobal().version;
}
function getFactor(bidder) {
if (bidder && bidder.bidCpmAdjustment) {
return bidder.bidCpmAdjustment(1.0);
} else {
return null;
}
}
function getBiddersFactors() {
const pb = getGlobal();
const result = {};
if (pb && pb.bidderSettings) {
Object.keys(pb.bidderSettings).forEach(bidderKey => {
const bidder = pb.bidderSettings[bidderKey];
const factor = getFactor(bidder);
if (factor !== null) {
result[bidderKey] = factor;
}
});
}
return result;
}
function getBaseEvent(auctionId, adUnitCode, bidderCode) {
const event = {};
event.s = settings.key;
event.ai = auctionId;
event.ac = adUnitCode;
event.bs = bidderCode;
return event;
}
function getBidBaseEvent(auctionId, adUnitCode, bidderCode) {
const sessionParams = getSessionParams();
const prebidVersion = getPrebidVersion();
const event = getBaseEvent(auctionId, adUnitCode, bidderCode);
event.sid = sessionParams.sessionId;
event.pv = settings.pageviewId;
event.to = auctionCache[auctionId].timeout;
event.pbv = prebidVersion;
UTM_PARAMS.filter(k => sessionParams[k]).forEach(k => event[EVENT_KEYS_SHORT_NAMES[k]] = sessionParams[k]);
return event;
}
function createPBBidEvent(bid) {
const event = getBidBaseEvent(bid.auctionId, bid.adUnitCode, bid.bidderAlias);
Object.keys(bid).forEach(k => {
const shortName = EVENT_KEYS_SHORT_NAMES[k];
if (shortName) {
event[shortName] = bid[k];
}
});
event._type = SORTABLE_EVENTS.PB_BID;
return event;
}
function getBidFactor(bidderAlias) {
if (!bidderFactors) {
bidderFactors = getBiddersFactors();
}
const factor = bidderFactors[bidderAlias];
return typeof factor !== 'undefined' ? factor : 1.0;
}
function createPrebidBidWonEvent({auctionId, adUnitCode, bidderAlias, cpm, currency, isNetRevenue}) {
const bidFactor = getBidFactor(bidderAlias);
const event = getBaseEvent(auctionId, adUnitCode, bidderAlias);
event.bif = bidFactor;
bidderFactors = null;
event.c = cpm;
event.cc = currency;
event.inr = isNetRevenue;
event._type = SORTABLE_EVENTS.BID_WON;
return event;
}
function createPrebidTimeoutEvent({auctionId, adUnitCode, bidderAlias}) {
const event = getBaseEvent(auctionId, adUnitCode, bidderAlias);
event._type = SORTABLE_EVENTS.BID_TIMEOUT;
return event;
}
function getDistinct(arr) {
return arr.filter((v, i, a) => a.indexOf(v) === i);
}
function groupBy(list, keyGetterFn) {
const map = {};
list.forEach(item => {
const key = keyGetterFn(item);
map[key] = map[key] ? map[key].concat(item) : [item];
});
return map;
}
function mergeAndCompressEventsByType(events, type) {
if (!events.length) {
return {};
}
const allKeys = getDistinct(events.map(ev => Object.keys(ev)).reduce((prev, curr) => prev.concat(curr), []));
const eventsAsMap = {};
allKeys.forEach(k => {
events.forEach(ev => eventsAsMap[k] = eventsAsMap[k] ? eventsAsMap[k].concat(ev[k]) : [ev[k]]);
});
const allSame = arr => arr.every(el => arr[0] === el);
Object.keys(eventsAsMap)
.forEach(k => eventsAsMap[k] = (eventsAsMap[k].length && allSame(eventsAsMap[k])) ? eventsAsMap[k][0] : eventsAsMap[k]);
eventsAsMap._count = events.length;
const result = {};
result[type] = eventsAsMap;
return result;
}
function mergeAndCompressEvents(events) {
const types = getDistinct(events.map(e => e._type));
const groupedEvents = groupBy(events, e => e._type);
const results = types.map(t => groupedEvents[t])
.map(events => mergeAndCompressEventsByType(events, events[0]._type));
return results.reduce((prev, eventMap) => {
const key = Object.keys(eventMap)[0];
prev[key] = eventMap[key];
return prev;
}, {});
}
function registerEvents(events) {
eventsToBeSent = eventsToBeSent.concat(events);
if (!timeoutId) {
timeoutId = setTimeout(() => {
const _eventsToBeSent = eventsToBeSent.slice();
eventsToBeSent = [];
sendEvents(_eventsToBeSent);
timeoutId = null;
}, TIMEOUT_FOR_REGISTRY);
}
}
function sendEvents(events) {
const url = settings.url;
const mergedEvents = mergeAndCompressEvents(events);
const options = {
'contentType': 'text/plain',
'method': 'POST',
'withCredentials': true
};
const onSend = () => utils.logInfo('Sortable Analytics data sent');
ajax(url, onSend, JSON.stringify(mergedEvents), options);
}
// converts [[300, 250], [728, 90]] to '300x250,728x90'
function sizesToString(sizes) {
return sizes.map(s => s.join('x')).join(',');
}
function dimsToSizeString(width, height) {
return `${width}x${height}`;
}
function handleBidRequested(event) {
const refererInfo = event.refererInfo;
const url = refererInfo.referer;
const reachedTop = refererInfo.reachedTop;
const numIframes = refererInfo.numIframes;
event.bids.forEach(bid => {
const auctionId = bid.auctionId;
const adUnitCode = bid.adUnitCode;
const tagId = bid.bidder === 'sortable' ? bid.params.tagId : '';
if (!auctionCache[auctionId].adUnits[adUnitCode]) {
auctionCache[auctionId].adUnits[adUnitCode] = {bids: {}};
}
const adUnit = auctionCache[auctionId].adUnits[adUnitCode];
const bids = adUnit.bids;
const newBid = {
adUnitCode: bid.adUnitCode,
auctionId: event.auctionId,
bidderAlias: bid.bidder,
bidId: bid.bidId,
bidderRequestId: bid.bidderRequestId,
bidRequestCount: bid.bidRequestsCount,
bidRequestedSizes: sizesToString(bid.sizes),
currency: bid.currency,
cpm: 0.0,
isTimeout: false,
isTop: false,
isWinner: false,
numIframes: numIframes,
start: event.start,
tagId: tagId,
transactionId: bid.transactionId,
reachedTop: reachedTop,
url: encodeURI(url)
};
bids[newBid.bidderAlias] = newBid;
});
}
function handleBidAdjustment(event) {
const auctionId = event.auctionId;
const adUnitCode = event.adUnitCode;
const adUnit = auctionCache[auctionId].adUnits[adUnitCode];
const bid = adUnit.bids[event.bidderCode];
const bidFactor = getBidFactor(event.bidderCode);
bid.adId = event.adId;
bid.adUnitCode = event.adUnitCode;
bid.auctionId = event.auctionId;
bid.bidderAlias = event.bidderCode;
bid.bidFactor = bidFactor;
bid.cpm = event.cpm;
bid.currency = event.currency;
bid.dealId = event.dealId;
bid.isNetRevenue = event.netRevenue;
bid.mediaType = event.mediaType;
bid.responseTimestamp = event.responseTimestamp;
bid.size = dimsToSizeString(event.width, event.height);
bid.ttl = event.ttl;
bid.ttr = event.timeToRespond;
}
function handleBidWon(event) {
const auctionId = event.auctionId;
const auction = auctionCache[auctionId];
if (auction) {
const adUnitCode = event.adUnitCode;
const adUnit = auction.adUnits[adUnitCode];
Object.keys(adUnit.bids).forEach(bidderCode => {
const bidFromUnit = adUnit.bids[bidderCode];
bidFromUnit.isWinner = event.bidderCode === bidderCode;
});
} else {
const ev = createPrebidBidWonEvent({
adUnitCode: event.adUnitCode,
auctionId: event.auctionId,
bidderAlias: event.bidderCode,
currency: event.currency,
cpm: event.cpm,
isNetRevenue: event.netRevenue,
});
registerEvents([ev]);
}
}
function handleBidTimeout(event) {
event.forEach(timeout => {
const auctionId = timeout.auctionId;
const adUnitCode = timeout.adUnitCode;
const bidderAlias = timeout.bidder;
const auction = auctionCache[auctionId];
if (auction) {
const adUnit = auction.adUnits[adUnitCode];
const bid = adUnit.bids[bidderAlias];
bid.isTimeout = true;
} else {
const prebidTimeoutEvent = createPrebidTimeoutEvent({auctionId, adUnitCode, bidderAlias});
registerEvents([prebidTimeoutEvent]);
}
});
}
function handleAuctionInit(event) {
const auctionId = event.auctionId;
const timeout = event.timeout;
auctionCache[auctionId] = {timeout: timeout, auctionId: auctionId, adUnits: {}};
}
function handleAuctionEnd(event) {
const auction = auctionCache[event.auctionId];
const adUnits = auction.adUnits;
setTimeout(() => {
const events = Object.keys(adUnits).map(adUnitCode => {
const bidderKeys = Object.keys(auction.adUnits[adUnitCode].bids);
const bids = bidderKeys.map(bidderCode => auction.adUnits[adUnitCode].bids[bidderCode]);
const highestBid = bids.length ? bids.reduce(utils.getOldestHighestCpmBid) : null;
return bidderKeys.map(bidderCode => {
const bid = auction.adUnits[adUnitCode].bids[bidderCode];
if (highestBid && highestBid.cpm) {
bid.isTop = highestBid.bidderAlias === bid.bidderAlias;
bid.bidTopFactor = getBidFactor(highestBid.bidderAlias);
bid.bidTopCpm = highestBid.cpm;
bid.bidTopCpmCurrency = highestBid.currency;
bid.bidTopIsNetRevenue = highestBid.isNetRevenue;
bid.bidTopSrc = highestBid.bidderAlias;
}
return createPBBidEvent(bid);
});
}).reduce((prev, curr) => prev.concat(curr), []);
bidderFactors = null;
sendEvents(events);
delete auctionCache[event.auctionId];
}, settings.timeoutForPbid);
}
function handleError(eventType, event, e) {
const ev = {};
ev.s = settings.key;
ev.ti = eventType;
ev.args = JSON.stringify(event);
ev.msg = e.message;
ev._type = SORTABLE_EVENTS.ERROR;
registerEvents([ev]);
}
const sortableAnalyticsAdapter = Object.assign(adapter({url: DEFAULT_URL, ANALYTICS_TYPE}), {
track({eventType, args}) {
try {
switch (eventType) {
case AUCTION_INIT:
handleAuctionInit(args);
break;
case AUCTION_END:
handleAuctionEnd(args);
break;
case BID_REQUESTED:
handleBidRequested(args);
break;
case BID_ADJUSTMENT:
handleBidAdjustment(args);
break;
case BID_WON:
handleBidWon(args);
break;
case BID_TIMEOUT:
handleBidTimeout(args);
break;
}
} catch (e) {
handleError(eventType, args, e);
}
}
});
sortableAnalyticsAdapter.originEnableAnalytics = sortableAnalyticsAdapter.enableAnalytics;
sortableAnalyticsAdapter.enableAnalytics = function (setupConfig) {
if (this.initConfig(setupConfig)) {
utils.logInfo('Sortable Analytics adapter enabled');
sortableAnalyticsAdapter.originEnableAnalytics(setupConfig);
}
};
sortableAnalyticsAdapter.initConfig = function (setupConfig) {
settings.disableSessionTracking = setupConfig.disableSessionTracking === undefined ? false : setupConfig.disableSessionTracking;
settings.key = getSiteKey(setupConfig.options);
settings.protocol = setupConfig.options.protocol || DEFAULT_PROTOCOL;
settings.url = `${settings.protocol}://${setupConfig.options.eventHost || DEFAULT_HOST}/pae/${settings.key}`;
settings.pageviewId = generateRandomId();
settings.timeoutForPbid = setupConfig.timeoutForPbid ? Math.max(setupConfig.timeoutForPbid, 0) : DEFAULT_PBID_TIMEOUT;
return !!settings.key;
};
sortableAnalyticsAdapter.getOptions = function () {
return settings;
};
adapterManager.registerAnalyticsAdapter({
adapter: sortableAnalyticsAdapter,
code: 'sortable'
});
export default sortableAnalyticsAdapter;