mk9-prebid
Version:
Header Bidding Management Library
754 lines (662 loc) • 21.7 kB
JavaScript
import adapter from '../src/AnalyticsAdapter.js';
import CONSTANTS from '../src/constants.json';
import adapterManager from '../src/adapterManager.js';
import { ajax } from '../src/ajax.js';
import find from 'core-js-pure/features/array/find.js';
import includes from 'core-js-pure/features/array/includes.js';
const utils = require('../src/utils.js');
export const AUCTION_STATES = {
INIT: 'initialized', // auction has initialized
ENDED: 'ended', // all auction requests have been accounted for
COMPLETED: 'completed' // all slots have rendered
};
const ADAPTER_VERSION = '0.1';
const SCHEMA_VERSION = '0.1';
const AUCTION_END_WAIT_TIME = 1000;
const URL_PARAM = '';
const ANALYTICS_TYPE = 'endpoint';
const ENDPOINT = 'https://prebid.openx.net/ox/analytics/';
// Event Types
const {
EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, AUCTION_END, BID_WON }
} = CONSTANTS;
const SLOT_LOADED = 'slotOnload';
const UTM_TAGS = [
'utm_campaign',
'utm_source',
'utm_medium',
'utm_term',
'utm_content'
];
const UTM_TO_CAMPAIGN_PROPERTIES = {
'utm_campaign': 'name',
'utm_source': 'source',
'utm_medium': 'medium',
'utm_term': 'term',
'utm_content': 'content'
};
/**
* @typedef {Object} OxAnalyticsConfig
* @property {string} orgId
* @property {string} publisherPlatformId
* @property {number} publisherAccountId
* @property {string} configId
* @property {string} optimizerConfig
* @property {number} sampling
* @property {Object} campaign
* @property {number} payloadWaitTime
* @property {number} payloadWaitTimePadding
* @property {Array<string>} adUnits
*/
/**
* @type {OxAnalyticsConfig}
*/
const DEFAULT_ANALYTICS_CONFIG = {
orgId: void (0),
publisherPlatformId: void (0),
publisherAccountId: void (0),
sampling: 0.05, // default sampling rate of 5%
testCode: 'default',
campaign: {},
adUnits: [],
payloadWaitTime: AUCTION_END_WAIT_TIME,
payloadWaitTimePadding: 2000
};
// Initialization
/**
* @type {OxAnalyticsConfig}
*/
let analyticsConfig;
let auctionMap = {};
let auctionOrder = 1; // tracks the number of auctions ran on the page
let googletag = window.googletag || {};
googletag.cmd = googletag.cmd || [];
let openxAdapter = Object.assign(adapter({ urlParam: URL_PARAM, analyticsType: ANALYTICS_TYPE }));
openxAdapter.originEnableAnalytics = openxAdapter.enableAnalytics;
openxAdapter.enableAnalytics = function(adapterConfig = {options: {}}) {
if (isValidConfig(adapterConfig)) {
analyticsConfig = {...DEFAULT_ANALYTICS_CONFIG, ...adapterConfig.options};
// campaign properties defined by config will override utm query parameters
analyticsConfig.campaign = {...buildCampaignFromUtmCodes(), ...analyticsConfig.campaign};
utils.logInfo('OpenX Analytics enabled with config', analyticsConfig);
// override track method with v2 handlers
openxAdapter.track = prebidAnalyticsEventHandler;
googletag.cmd.push(function () {
let pubads = googletag.pubads();
if (pubads.addEventListener) {
pubads.addEventListener(SLOT_LOADED, args => {
openxAdapter.track({eventType: SLOT_LOADED, args});
utils.logInfo('OX: SlotOnLoad event triggered');
});
}
});
openxAdapter.originEnableAnalytics(adapterConfig);
}
};
adapterManager.registerAnalyticsAdapter({
adapter: openxAdapter,
code: 'openx'
});
export default openxAdapter;
/**
* Test Helper Functions
*/
// reset the cache for unit tests
openxAdapter.reset = function() {
auctionMap = {};
auctionOrder = 1;
};
/**
* Private Functions
*/
function isValidConfig({options: analyticsOptions}) {
let hasOrgId = analyticsOptions && analyticsOptions.orgId !== void (0);
const fieldValidations = [
// tuple of property, type, required
['orgId', 'string', hasOrgId],
['publisherPlatformId', 'string', !hasOrgId],
['publisherAccountId', 'number', !hasOrgId],
['configId', 'string', false],
['optimizerConfig', 'string', false],
['sampling', 'number', false],
['adIdKey', 'string', false],
['payloadWaitTime', 'number', false],
['payloadWaitTimePadding', 'number', false],
];
let failedValidation = find(fieldValidations, ([property, type, required]) => {
// if required, the property has to exist
// if property exists, type check value
return (required && !analyticsOptions.hasOwnProperty(property)) ||
/* eslint-disable valid-typeof */
(analyticsOptions.hasOwnProperty(property) && typeof analyticsOptions[property] !== type);
});
if (failedValidation) {
let [property, type, required] = failedValidation;
if (required) {
utils.logError(`OpenXAnalyticsAdapter: Expected '${property}' to exist and of type '${type}'`);
} else {
utils.logError(`OpenXAnalyticsAdapter: Expected '${property}' to be type '${type}'`);
}
}
return !failedValidation;
}
function buildCampaignFromUtmCodes() {
const location = utils.getWindowLocation();
const queryParams = utils.parseQS(location && location.search);
let campaign = {};
UTM_TAGS.forEach(function(utmKey) {
let utmValue = queryParams[utmKey];
if (utmValue) {
let key = UTM_TO_CAMPAIGN_PROPERTIES[utmKey];
campaign[key] = decodeURIComponent(utmValue);
}
});
return campaign;
}
function detectMob() {
if (
navigator.userAgent.match(/Android/i) ||
navigator.userAgent.match(/webOS/i) ||
navigator.userAgent.match(/iPhone/i) ||
navigator.userAgent.match(/iPad/i) ||
navigator.userAgent.match(/iPod/i) ||
navigator.userAgent.match(/BlackBerry/i) ||
navigator.userAgent.match(/Windows Phone/i)
) {
return true;
} else {
return false;
}
}
function detectOS() {
if (navigator.userAgent.indexOf('Android') != -1) return 'Android';
if (navigator.userAgent.indexOf('like Mac') != -1) return 'iOS';
if (navigator.userAgent.indexOf('Win') != -1) return 'Windows';
if (navigator.userAgent.indexOf('Mac') != -1) return 'Macintosh';
if (navigator.userAgent.indexOf('Linux') != -1) return 'Linux';
if (navigator.appVersion.indexOf('X11') != -1) return 'Unix';
return 'Others';
}
function detectBrowser() {
var isChrome =
/Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
var isCriOS = navigator.userAgent.match('CriOS');
var isSafari =
/Safari/.test(navigator.userAgent) &&
/Apple Computer/.test(navigator.vendor);
var isFirefox = /Firefox/.test(navigator.userAgent);
var isIE =
/Trident/.test(navigator.userAgent) || /MSIE/.test(navigator.userAgent);
var isEdge = /Edge/.test(navigator.userAgent);
if (isIE) return 'Internet Explorer';
if (isEdge) return 'Microsoft Edge';
if (isCriOS) return 'Chrome';
if (isSafari) return 'Safari';
if (isFirefox) return 'Firefox';
if (isChrome) return 'Chrome';
return 'Others';
}
function prebidAnalyticsEventHandler({eventType, args}) {
utils.logMessage(eventType, Object.assign({}, args));
switch (eventType) {
case AUCTION_INIT:
onAuctionInit(args);
break;
case BID_REQUESTED:
onBidRequested(args);
break;
case BID_RESPONSE:
onBidResponse(args);
break;
case BID_TIMEOUT:
onBidTimeout(args);
break;
case AUCTION_END:
onAuctionEnd(args);
break;
case BID_WON:
onBidWon(args);
break;
case SLOT_LOADED:
onSlotLoadedV2(args);
break;
}
}
/**
* @typedef {Object} PbAuction
* @property {string} auctionId - Auction ID of the request this bid responded to
* @property {number} timestamp //: 1586675964364
* @property {number} auctionEnd - timestamp of when auction ended //: 1586675964364
* @property {string} auctionStatus //: "inProgress"
* @property {Array<Adunit>} adUnits //: [{…}]
* @property {string} adUnitCodes //: ["video1"]
* @property {string} labels //: undefined
* @property {Array<BidRequest>} bidderRequests //: (2) [{…}, {…}]
* @property {Array<BidRequest>} noBids //: []
* @property {Array<BidResponse>} bidsReceived //: []
* @property {Array<BidResponse>} winningBids //: []
* @property {number} timeout //: 3000
* @property {Object} config //: {publisherPlatformId: "a3aece0c-9e80-4316-8deb-faf804779bd1", publisherAccountId: 537143056, sampling: 1}/*
*/
function onAuctionInit({auctionId, timestamp: startTime, timeout, adUnitCodes}) {
auctionMap[auctionId] = {
id: auctionId,
startTime,
endTime: void (0),
timeout,
auctionOrder,
userIds: [],
adUnitCodesCount: adUnitCodes.length,
adunitCodesRenderedCount: 0,
state: AUCTION_STATES.INIT,
auctionSendDelayTimer: void (0),
};
// setup adunit properties in map
auctionMap[auctionId].adUnitCodeToAdUnitMap = adUnitCodes.reduce((obj, adunitCode) => {
obj[adunitCode] = {
code: adunitCode,
adPosition: void (0),
bidRequestsMap: {}
};
return obj;
}, {});
auctionOrder++;
}
/**
* @typedef {Object} PbBidRequest
* @property {string} auctionId - Auction ID of the request this bid responded to
* @property {number} auctionStart //: 1586675964364
* @property {Object} refererInfo
* @property {PbBidderRequest} bids
* @property {number} start - Start timestamp of the bidder request
*
*/
/**
* @typedef {Object} PbBidderRequest
* @property {string} adUnitCode - Name of div or google adunit path
* @property {string} bidder - Bame of bidder
* @property {string} bidId - Identifies the bid request
* @property {Object} mediaTypes
* @property {Object} params
* @property {string} src
* @property {Object} userId - Map of userId module to module object
*/
/**
* Tracks the bid request
* @param {PbBidRequest} bidRequest
*/
function onBidRequested(bidRequest) {
const {auctionId, bids: bidderRequests, start, timeout} = bidRequest;
const auction = auctionMap[auctionId];
const adUnitCodeToAdUnitMap = auction.adUnitCodeToAdUnitMap;
bidderRequests.forEach(bidderRequest => {
const { adUnitCode, bidder, bidId: requestId, mediaTypes, params, src, userId } = bidderRequest;
auction.userIds.push(userId);
adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId] = {
bidder,
params,
mediaTypes,
source: src,
startTime: start,
timedOut: false,
timeLimit: timeout,
bids: {}
};
});
}
/**
*
* @param {BidResponse} bidResponse
*/
function onBidResponse(bidResponse) {
let {
auctionId,
adUnitCode,
requestId,
cpm,
creativeId,
requestTimestamp,
responseTimestamp,
ts,
mediaType,
dealId,
ttl,
netRevenue,
currency,
originalCpm,
originalCurrency,
width,
height,
timeToRespond: latency,
adId,
meta
} = bidResponse;
auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId].bids[adId] = {
cpm,
creativeId,
requestTimestamp,
responseTimestamp,
ts,
adId,
meta,
mediaType,
dealId,
ttl,
netRevenue,
currency,
originalCpm,
originalCurrency,
width,
height,
latency,
winner: false,
rendered: false,
renderTime: 0,
};
}
function onBidTimeout(args) {
utils._each(args, ({auctionId, adUnitCode, bidId: requestId}) => {
let timedOutRequest = utils.deepAccess(auctionMap,
`${auctionId}.adUnitCodeToAdUnitMap.${adUnitCode}.bidRequestsMap.${requestId}`);
if (timedOutRequest) {
timedOutRequest.timedOut = true;
}
});
}
/**
*
* @param {PbAuction} endedAuction
*/
function onAuctionEnd(endedAuction) {
let auction = auctionMap[endedAuction.auctionId];
if (!auction) {
return;
}
clearAuctionTimer(auction);
auction.endTime = endedAuction.auctionEnd;
auction.state = AUCTION_STATES.ENDED;
delayedSend(auction);
}
/**
*
* @param {BidResponse} bidResponse
*/
function onBidWon(bidResponse) {
const { auctionId, adUnitCode, requestId, adId } = bidResponse;
let winningBid = utils.deepAccess(auctionMap,
`${auctionId}.adUnitCodeToAdUnitMap.${adUnitCode}.bidRequestsMap.${requestId}.bids.${adId}`);
if (winningBid) {
winningBid.winner = true;
const auction = auctionMap[auctionId];
if (auction.sent) {
const endpoint = (analyticsConfig.endpoint || ENDPOINT) + 'event';
const bidder = auction.adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId].bidder;
ajax(`${endpoint}?t=win&b=${adId}&a=${analyticsConfig.orgId}&bidder=${bidder}&ts=${auction.startTime}`,
() => {
utils.logInfo(`Openx Analytics - Sending complete impression event for ${adId} at ${Date.now()}`)
});
} else {
utils.logInfo(`Openx Analytics - impression event for ${adId} will be sent with auction data`)
}
}
}
/**
*
* @param {GoogleTagSlot} slot
* @param {string} serviceName
*/
function onSlotLoadedV2({ slot }) {
const renderTime = Date.now();
const elementId = slot.getSlotElementId();
const bidId = slot.getTargeting('hb_adid')[0];
let [auction, adUnit, bid] = getPathToBidResponseByBidId(bidId);
if (!auction) {
// attempt to get auction by adUnitCode
auction = getAuctionByGoogleTagSLot(slot);
if (!auction) {
return; // slot is not participating in an active prebid auction
}
}
clearAuctionTimer(auction);
// track that an adunit code has completed within an auction
auction.adunitCodesRenderedCount++;
// mark adunit as rendered
if (bid) {
let {x, y} = getPageOffset();
bid.rendered = true;
bid.renderTime = renderTime;
adUnit.adPosition = isAtf(elementId, x, y) ? 'ATF' : 'BTF';
}
if (auction.adunitCodesRenderedCount === auction.adUnitCodesCount) {
auction.state = AUCTION_STATES.COMPLETED;
}
// prepare to send regardless if auction is complete or not as a failsafe in case not all events are tracked
// add additional padding when not all slots are rendered
delayedSend(auction);
}
function isAtf(elementId, scrollLeft = 0, scrollTop = 0) {
let elem = document.querySelector('#' + elementId);
let isAtf = false;
if (elem) {
let bounding = elem.getBoundingClientRect();
if (bounding) {
let windowWidth = (window.innerWidth || document.documentElement.clientWidth);
let windowHeight = (window.innerHeight || document.documentElement.clientHeight);
// intersection coordinates
let left = Math.max(0, bounding.left + scrollLeft);
let right = Math.min(windowWidth, bounding.right + scrollLeft);
let top = Math.max(0, bounding.top + scrollTop);
let bottom = Math.min(windowHeight, bounding.bottom + scrollTop);
let intersectionWidth = right - left;
let intersectionHeight = bottom - top;
let intersectionArea = (intersectionHeight > 0 && intersectionWidth > 0) ? (intersectionHeight * intersectionWidth) : 0;
let adSlotArea = (bounding.right - bounding.left) * (bounding.bottom - bounding.top);
if (adSlotArea > 0) {
// Atleast 50% of intersection in window
isAtf = intersectionArea * 2 >= adSlotArea;
}
}
} else {
utils.logWarn('OX: DOM element not for id ' + elementId);
}
return isAtf;
}
// backwards compatible pageOffset from https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollX
function getPageOffset() {
var x = (window.pageXOffset !== undefined)
? window.pageXOffset
: (document.documentElement || document.body.parentNode || document.body).scrollLeft;
var y = (window.pageYOffset !== undefined)
? window.pageYOffset
: (document.documentElement || document.body.parentNode || document.body).scrollTop;
return {x, y};
}
function delayedSend(auction) {
if (auction.sent) {
return;
}
const delayTime = auction.adunitCodesRenderedCount === auction.adUnitCodesCount
? analyticsConfig.payloadWaitTime
: analyticsConfig.payloadWaitTime + analyticsConfig.payloadWaitTimePadding;
auction.auctionSendDelayTimer = setTimeout(() => {
auction.sent = true; // any BidWon emitted after this will be recorded separately
let payload = JSON.stringify([buildAuctionPayload(auction)]);
ajax(analyticsConfig.endpoint || ENDPOINT, () => {
utils.logInfo(`OpenX Analytics - Sending complete auction at ${Date.now()}`);
}, payload, { contentType: 'application/json' });
}, delayTime);
}
function clearAuctionTimer(auction) {
// reset the delay timer to send the auction data
if (auction.auctionSendDelayTimer) {
clearTimeout(auction.auctionSendDelayTimer);
auction.auctionSendDelayTimer = void (0);
}
}
/**
* Returns the path to a bid (auction, adunit, bidRequest, and bid) based on a bidId
* @param {string} bidId
* @returns {Array<*>}
*/
function getPathToBidResponseByBidId(bidId) {
let auction;
let adUnit;
let bidResponse;
if (!bidId) {
return [];
}
utils._each(auctionMap, currentAuction => {
// skip completed auctions
if (currentAuction.state === AUCTION_STATES.COMPLETED) {
return;
}
utils._each(currentAuction.adUnitCodeToAdUnitMap, (currentAdunit) => {
utils._each(currentAdunit.bidRequestsMap, currentBiddRequest => {
utils._each(currentBiddRequest.bids, (currentBidResponse, bidResponseId) => {
if (bidId === bidResponseId) {
auction = currentAuction;
adUnit = currentAdunit;
bidResponse = currentBidResponse;
}
});
});
});
});
return [auction, adUnit, bidResponse];
}
function getAuctionByGoogleTagSLot(slot) {
let slotAdunitCodes = [slot.getSlotElementId(), slot.getAdUnitPath()];
let slotAuction;
utils._each(auctionMap, auction => {
if (auction.state === AUCTION_STATES.COMPLETED) {
return;
}
utils._each(auction.adUnitCodeToAdUnitMap, (bidderRequestIdMap, adUnitCode) => {
if (includes(slotAdunitCodes, adUnitCode)) {
slotAuction = auction;
}
});
});
return slotAuction;
}
function buildAuctionPayload(auction) {
let {startTime, endTime, state, timeout, auctionOrder, userIds, adUnitCodeToAdUnitMap, id} = auction;
const auctionId = id;
let {orgId, publisherPlatformId, publisherAccountId, campaign, testCode, configId, optimizerConfig} = analyticsConfig;
return {
auctionId,
adapterVersion: ADAPTER_VERSION,
schemaVersion: SCHEMA_VERSION,
orgId,
publisherPlatformId,
publisherAccountId,
configId,
optimizerConfig,
campaign,
state,
startTime,
endTime,
timeLimit: timeout,
auctionOrder,
deviceType: detectMob() ? 'Mobile' : 'Desktop',
deviceOSType: detectOS(),
browser: detectBrowser(),
testCode: testCode,
// return an array of module name that have user data
userIdProviders: buildUserIdProviders(userIds),
adUnits: buildAdUnitsPayload(adUnitCodeToAdUnitMap),
};
function buildAdUnitsPayload(adUnitCodeToAdUnitMap) {
return utils._map(adUnitCodeToAdUnitMap, (adUnit) => {
let {code, adPosition} = adUnit;
return {
code,
adPosition,
bidRequests: buildBidRequestPayload(adUnit.bidRequestsMap)
};
function buildBidRequestPayload(bidRequestsMap) {
return utils._map(bidRequestsMap, (bidRequest) => {
let {bidder, source, bids, mediaTypes, timeLimit, timedOut} = bidRequest;
return {
bidder,
source,
hasBidderResponded: Object.keys(bids).length > 0,
availableAdSizes: getMediaTypeSizes(mediaTypes),
availableMediaTypes: getMediaTypes(mediaTypes),
timeLimit,
timedOut,
bidResponses: utils._map(bidRequest.bids, (bidderBidResponse) => {
let {
adId,
cpm,
creativeId,
ts,
meta,
mediaType,
dealId,
ttl,
netRevenue,
currency,
width,
height,
latency,
winner,
rendered,
renderTime
} = bidderBidResponse;
return {
bidId: adId,
microCpm: cpm * 1000000,
netRevenue,
currency,
mediaType,
height,
width,
size: `${width}x${height}`,
dealId,
latency,
ttl,
winner,
creativeId,
ts,
rendered,
renderTime,
meta
}
})
}
});
}
});
}
function buildUserIdProviders(userIds) {
return utils._map(userIds, (userId) => {
return utils._map(userId, (id, module) => {
return hasUserData(module, id) ? module : false
}).filter(module => module);
}).reduce(utils.flatten, []).filter(utils.uniques).sort();
}
function hasUserData(module, idOrIdObject) {
let normalizedId;
switch (module) {
case 'digitrustid':
normalizedId = utils.deepAccess(idOrIdObject, 'data.id');
break;
case 'lipb':
normalizedId = idOrIdObject.lipbid;
break;
default:
normalizedId = idOrIdObject;
}
return !utils.isEmpty(normalizedId);
}
function getMediaTypeSizes(mediaTypes) {
return utils._map(mediaTypes, (mediaTypeConfig, mediaType) => {
return utils.parseSizesInput(mediaTypeConfig.sizes)
.map(size => `${mediaType}_${size}`);
}).reduce(utils.flatten, []);
}
function getMediaTypes(mediaTypes) {
return utils._map(mediaTypes, (mediaTypeConfig, mediaType) => mediaType);
}
}