diff --git a/modules/openxAnalyticsAdapter.js b/modules/openxAnalyticsAdapter.js index 7addfe68bc6..f0c76f1f584 100644 --- a/modules/openxAnalyticsAdapter.js +++ b/modules/openxAnalyticsAdapter.js @@ -1,260 +1,1283 @@ +import includes from 'core-js/library/fn/array/includes.js'; import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import { config } from '../src/config.js'; + +//* ******* V2 Code import { ajax } from '../src/ajax.js'; -import * as utils from '../src/utils.js'; +import {getWindowLocation, parseQS} from '../src/utils'; -const { - EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, BID_WON } -} = CONSTANTS; +// temp dependency on zlib to minimize payload +const zlib = require('zlib'); // eslint-disable-line + +const utils = require('../src/utils.js'); + +const urlParam = ''; +const analyticsType = 'endpoint'; +const ADAPTER_VERSION = '0.1'; +const SCHEMA_VERSION = '0.1'; + +const MAX_RETRIES = 2; +const MAX_TIMEOUT = 10000; +const AUCTION_END_WAIT_TIME = 1000; +const DEFAULT_SLOT_LOAD_BUFFER_TIME = 100; + +const auctionInitConst = CONSTANTS.EVENTS.AUCTION_INIT; +const auctionEndConst = CONSTANTS.EVENTS.AUCTION_END; +const bidWonConst = CONSTANTS.EVENTS.BID_WON; +const bidRequestConst = CONSTANTS.EVENTS.BID_REQUESTED; +const bidAdjustmentConst = CONSTANTS.EVENTS.BID_ADJUSTMENT; +const bidResponseConst = CONSTANTS.EVENTS.BID_RESPONSE; +const bidTimeoutConst = CONSTANTS.EVENTS.BID_TIMEOUT; const SLOT_LOADED = 'slotOnload'; -const ENDPOINT = 'https://ads.openx.net/w/1.0/pban'; +/** + * @typedef {Object} AnalyticsConfig + * @property {string} publisherPlatformId + * @property {number} publisherAccountId + * @property {number} sampling + * @property {boolean} enableV2 + * @property {boolean} testPipeline + * @property {Object} campaign + * @property {string} adIdKey + * @property {number} payloadWaitTime + * @property {number} payloadWaitTimePadding + * @property {Array} adUnits + */ -let initOptions; +/** + * @type {AnalyticsConfig} + */ +const DEFAULT_ANALYTICS_CONFIG = { + publisherPlatformId: void (0), + publisherAccountId: void (0), + sampling: 0.05, // default sampling rate of 5% + testCode: 'default', + enableV2: false, + testPipeline: false, + adIdKey: 'hb_adid', + campaign: {}, + adUnits: [], + payloadWaitTime: AUCTION_END_WAIT_TIME, + payloadWaitTimePadding: 2000 +}; -let auctionMap = {}; +let googletag = window.googletag || {}; +googletag.cmd = googletag.cmd || []; -function onAuctionInit({ auctionId }) { - auctionMap[auctionId] = { - adUnitMap: {} - }; -} +/** + * @type {AnalyticsConfig} + */ +let analyticsConfig; -function onBidRequested({ auctionId, auctionStart, bids, start }) { - const adUnitMap = auctionMap[auctionId]['adUnitMap']; +let eventStack = {}; +let loadedAdSlots = {}; - bids.forEach(bid => { - const { adUnitCode, bidId, bidder, params, transactionId } = bid; +let localStoragePrefix = 'openx_analytics_'; +let utmTags = [ + 'utm_campaign', + 'utm_source', + 'utm_medium', + 'utm_term', + 'utm_content' +]; - adUnitMap[adUnitCode] = adUnitMap[adUnitCode] || { - auctionId, - auctionStart, - transactionId, - bidMap: {} - }; +const UTM_TO_CAMPAIGN_PROPERTIES = { + 'utm_campaign': 'name', + 'utm_source': 'source', + 'utm_medium': 'medium', + 'utm_term': 'term', + 'utm_content': 'content' +}; +let utmTimeoutKey = 'utm_timeout'; +let utmTimeout = 60 * 60 * 1000; +let sessionTimeout = 60 * 60 * 1000; +let sessionIdStorageKey = 'session_id'; +let sessionTimeoutKey = 'session_timeout'; - adUnitMap[adUnitCode]['bidMap'][bidId] = { - bidder, - params, - requestTimestamp: start - }; - }); +function getParameterByName(param) { + let vars = {}; + window.location.href + .replace(location.hash, '') + .replace(/[?&]+([^=&]+)=?([^&]*)?/gi, function(m, key, value) { + vars[key] = value !== undefined ? value : ''; + }); + + return vars[param] ? vars[param] : ''; } -function onBidResponse({ - auctionId, - adUnitCode, - requestId: bidId, - cpm, - creativeId, - responseTimestamp, - ts, - adId -}) { - const adUnit = auctionMap[auctionId]['adUnitMap'][adUnitCode]; - const bid = adUnit['bidMap'][bidId]; - bid.cpm = cpm; - bid.creativeId = creativeId; - bid.responseTimestamp = responseTimestamp; - bid.ts = ts; - bid.adId = adId; +function buildSessionIdLocalStorageKey() { + return localStoragePrefix.concat(sessionIdStorageKey); } -function onBidTimeout(args) { - utils - ._map(args, value => value) - .forEach(({ auctionId, adUnitCode, bidId }) => { - const bid = - auctionMap[auctionId]['adUnitMap'][adUnitCode]['bidMap'][bidId]; - bid.timedOut = true; - }); +function buildSessionIdTimeoutLocalStorageKey() { + return localStoragePrefix.concat(sessionTimeoutKey); } -function onBidWon({ auctionId, adUnitCode, requestId: bidId }) { - const adUnit = auctionMap[auctionId]['adUnitMap'][adUnitCode]; - const bid = adUnit['bidMap'][bidId]; - bid.won = true; +function updateSessionId() { + if (isSessionIdTimeoutExpired()) { + let newSessionId = utils.generateUUID(); + localStorage.setItem(buildSessionIdLocalStorageKey(), newSessionId); + } + analyticsConfig.sessionId = getSessionId(); + updateSessionIdTimeout(); } -function onSlotLoaded({ slot }) { - const targeting = slot.getTargetingKeys().reduce((targeting, key) => { - targeting[key] = slot.getTargeting(key); - return targeting; - }, {}); - utils.logMessage( - 'GPT slot is loaded. Current targeting set on slot:', - targeting +function updateSessionIdTimeout() { + localStorage.setItem(buildSessionIdTimeoutLocalStorageKey(), Date.now()); +} + +function isSessionIdTimeoutExpired() { + let cpmSessionTimestamp = localStorage.getItem( + buildSessionIdTimeoutLocalStorageKey() ); + return Date.now() - cpmSessionTimestamp > sessionTimeout; +} - const adId = slot.getTargeting('hb_adid')[0]; - if (!adId) { - return; +function getSessionId() { + return localStorage.getItem(buildSessionIdLocalStorageKey()) + ? localStorage.getItem(buildSessionIdLocalStorageKey()) + : ''; +} + +function updateUtmTimeout() { + localStorage.setItem(buildUtmLocalStorageTimeoutKey(), Date.now()); +} + +function isUtmTimeoutExpired() { + let utmTimestamp = localStorage.getItem(buildUtmLocalStorageTimeoutKey()); + return Date.now() - utmTimestamp > utmTimeout; +} + +function buildUtmLocalStorageTimeoutKey() { + return localStoragePrefix.concat(utmTimeoutKey); +} + +function buildUtmLocalStorageKey(utmMarkKey) { + return localStoragePrefix.concat(utmMarkKey); +} + +function getPublisherPlatformId() { + if (analyticsConfig.publisherPlatformId !== undefined) { + if (typeof analyticsConfig.publisherPlatformId === 'string') { + if (analyticsConfig.publisherPlatformId !== '') { + return analyticsConfig.publisherPlatformId; + } else { + utils.logError('OX: Invalid PublisherPlatformId'); + return null; + } + } else { + utils.logError('OX: Invalid datatype for PublisherPlatformId'); + return null; + } + } else { + utils.logError('OX: PublisherPlatformId not defined'); + return null; } +} - const adUnit = getAdUnitByAdId(adId); - if (!adUnit) { - return; +function getPublisherAccountId() { + if (analyticsConfig.publisherAccountId !== undefined) { + if (typeof analyticsConfig.publisherAccountId === 'number') { + if (analyticsConfig.publisherAccountId > -1) { + return analyticsConfig.publisherAccountId; + } else { + utils.logError('OX: Invalid PublisherAccountId'); + return null; + } + } else { + utils.logError('OX: Invalid datatype for PublisherAccountId'); + return null; + } + } else { + utils.logError('OX: PublisherAccountId not defined'); + return null; } +} - const adUnitData = getAdUnitData(adUnit); - const performanceData = getPerformanceData(adUnit.auctionStart); - const commonFields = { - 'hb.asiid': slot.getAdUnitPath(), - 'hb.cur': config.getConfig('currency.adServerCurrency'), - 'hb.pubid': initOptions.publisherId - }; +function getTestCode() { + if (analyticsConfig.testCode !== undefined) { + if (typeof analyticsConfig.testCode === 'string') { + if (analyticsConfig.testCode !== '') { + return analyticsConfig.testCode; + } else { + utils.logError('OX: Invalid testCode. testCode can\'t be empty'); + return null; + } + } else { + utils.logError('OX: Invalid datatype for testCode'); + return null; + } + } else { + utils.logInfo('OX: testCode not defined'); + return 'default'; + } +} + +function checkInitOptions() { + let publisherPlatformId = getPublisherPlatformId(); + let publisherAccountId = getPublisherAccountId(); + let testCode = getTestCode(); + if (publisherPlatformId && publisherAccountId && testCode) { + return true; + } + return false; +} - const data = Object.assign({}, adUnitData, performanceData, commonFields); - sendEvent(data); +function checkAdUnitConfig() { + if (typeof analyticsConfig.adUnits === 'undefined') { + return false; + } + return analyticsConfig.adUnits.length > 0; } -function getAdUnitByAdId(adId) { - let result; +function buildEventStack(auctionId) { + eventStack[auctionId].options = analyticsConfig; + utils.logInfo('OX: Options Initialized', eventStack); +} - utils._map(auctionMap, value => value).forEach(auction => { - utils._map(auction.adUnitMap, value => value).forEach(adUnit => { - utils._map(adUnit.bidMap, value => value).forEach(bid => { - if (adId === bid.adId) { - result = adUnit; - } - }) +function filterBidsByAdUnit(bids) { + var filteredBids = []; + bids.forEach(function(bid) { + if (includes(analyticsConfig.adUnits, bid.placementCode)) { + filteredBids.push(bid); + } + }); + return filteredBids; +} + +function isValidEvent(eventType, adUnitCode) { + if (checkAdUnitConfig()) { + let validationEvents = [bidAdjustmentConst, bidResponseConst, bidWonConst, bidTimeoutConst]; + if ( + !includes(analyticsConfig.adUnits, adUnitCode) && + includes(validationEvents, eventType) + ) { + return false; + } + } + return true; +} + +function isValidEventStack(auctionId) { + utils.logInfo('OX: Validating eventStack for', auctionId) + if (eventStack[auctionId].events.length > 0) { + return eventStack[auctionId].events.some(function(event) { + // utils.logInfo('OX: EventType of event ', event.eventType) + return ( + bidRequestConst === event.eventType || bidResponseConst === event.eventType || bidAdjustmentConst === event.eventType || auctionEndConst === event.eventType || bidTimeoutConst === event.eventType + ); }); + } + return false; +} + +function removeads(info) { + if (info && info.bidsReceived) { + let newInfo = JSON.parse(JSON.stringify(info)); + let bidsReceivedArray = newInfo.bidsReceived; + for (var index = 0; index < bidsReceivedArray.length; index++) { + if (bidsReceivedArray[index].ad !== undefined) { + bidsReceivedArray[index].ad = ''; + } + } + newInfo.bidsReceived = bidsReceivedArray; + return newInfo; + } else { + return info; + } +} + +function getAuctionIdByAdId(adId) { + let auctionId, adUnitCode; + utils._each(eventStack, function(auctionInfo) { + if (auctionInfo && auctionInfo.events) { + auctionInfo.events.forEach(function(eventsInfo) { + if (eventsInfo.eventType === bidWonConst) { + if (eventsInfo.args && eventsInfo.args.adId && eventsInfo.args.adId === adId) { + auctionId = eventsInfo.args.auctionId; + adUnitCode = eventsInfo.args.adUnitCode; + } + } + }); + } }); + return { + auctionId: auctionId, + adUnitCode: adUnitCode + }; +} - return result; +function getAllAdUnitCodesByAuctionId(auctionId) { + let adUnitCodes; + if (eventStack[auctionId] && eventStack[auctionId].events) { + eventStack[auctionId].events.forEach(function(eventsInfo) { + if (eventsInfo.eventType === 'auctionEnd') { + adUnitCodes = eventsInfo.args.adUnitCodes; + } + }) + } + return adUnitCodes; } -function getAdUnitData(adUnit) { - const bids = utils._map(adUnit.bidMap, value => value); - const bidders = bids.map(bid => bid.bidder); - const requestTimes = bids.map( - bid => bid.requestTimestamp && bid.requestTimestamp - adUnit.auctionStart - ); - const responseTimes = bids.map( - bid => bid.responseTimestamp && bid.responseTimestamp - adUnit.auctionStart - ); - const bidValues = bids.map(bid => bid.cpm || 0); - const timeouts = bids.map(bid => !!bid.timedOut); - const creativeIds = bids.map(bid => bid.creativeId); - const winningBid = bids.filter(bid => bid.won)[0]; - const winningExchangeIndex = bids.indexOf(winningBid); - const openxBid = bids.filter(bid => bid.bidder === 'openx')[0]; +function getAuctionIdByAdUnitCode(adUnitCode) { + let auctionId; + utils._map(eventStack, value => value).forEach(function(auctionInfo) { + if (auctionId === undefined) { + if (auctionInfo && auctionInfo.events) { + auctionInfo.events.forEach(function(eventsInfo) { + if (eventsInfo.eventType === auctionEndConst) { + if (eventsInfo.args && eventsInfo.args.adUnitCodes) { + if (eventsInfo.args.adUnitCodes.includes(adUnitCode)) { + auctionId = eventsInfo.args.auctionId; + } + } + } + }) + } + } + }); + return auctionId; +} + +function onSlotLoaded({ slot }) { + const adId = slot.getTargeting('hb_adid')[0]; + const slotElementId = slot.getSlotElementId(); + const adUnitPath = slot.getAdUnitPath(); + + // AdId will be present in `eventStack` only if winner is through prebid auction. + // Assuming either `adUnitPath` or `slotElementId` to be adUnitCode because there is no other way - + // to know for which ad unit the slot is rendered + + let auctionId, adUnitCode; + let adUnitInfo = getAuctionIdByAdId(adId); + if (adUnitInfo && adUnitInfo.auctionId && adUnitInfo.adUnitCode) { + auctionId = adUnitInfo.auctionId; + adUnitCode = adUnitInfo.adUnitCode; + } else { + adUnitCode = slotElementId; + auctionId = getAuctionIdByAdUnitCode(adUnitCode); + if (!auctionId) { + adUnitCode = adUnitPath; + auctionId = getAuctionIdByAdUnitCode(adUnitPath); + } + } + + let allSlotsLoaded = false; + if (auctionId) { + let adPosition = getAdPositionByElementId(slotElementId); + updateLoadedAdSlotsInfo(auctionId, adUnitCode, adPosition); + let loadedAdUnitCodes = getLoadedAdUnitCodes(auctionId); + let allAdUnitCodes = getAllAdUnitCodesByAuctionId(auctionId); + if (loadedAdUnitCodes.length === allAdUnitCodes.length) { + allSlotsLoaded = true; + } + } + + if (auctionId && eventStack[auctionId] && allSlotsLoaded) { + setTimeout(function() { + if (eventStack[auctionId]) { + send(SLOT_LOADED, eventStack, auctionId); + eventStack[auctionId] = null; + } + delete loadedAdSlots[auctionId]; + }, analyticsConfig.payloadWaitTime); + } +} + +let openxAdapter = Object.assign(adapter({ urlParam, analyticsType })); + +openxAdapter.originEnableAnalytics = openxAdapter.enableAnalytics; + +openxAdapter.enableAnalytics = function(adapterConfig = {options: {}}) { + // Backwards compatibility for external documentation + if (adapterConfig.options.slotLoadWaitTime) { + adapterConfig.options.payloadWaitTime = adapterConfig.options.slotLoadWaitTime; + } + + 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); + + if (analyticsConfig.testPipeline) { + openxAdapter.track = (args) => { + prebidAnalyticsEventHandlerV1(args); + prebidAnalyticsEventHandlerV2(args); + }; + + googletag.cmd.push(function() { + googletag.pubads().addEventListener(SLOT_LOADED, args => { + utils.logInfo('OX: SlotOnLoad event triggered'); + onSlotLoaded(args); + onSlotLoadedV2(args); + }); + }); + } else if (analyticsConfig.enableV2) { + // override track method with v2 handlers + openxAdapter.track = prebidAnalyticsEventHandlerV2; + + googletag.cmd.push(function() { + googletag.pubads().addEventListener(SLOT_LOADED, args => { + openxAdapter.track({ eventType: SLOT_LOADED, args }); + utils.logInfo('OX: SlotOnLoad event triggered'); + }); + }); + } else { + openxAdapter.track = prebidAnalyticsEventHandlerV1; + googletag.cmd.push(function() { + googletag.pubads().addEventListener(SLOT_LOADED, function(args) { + utils.logInfo('OX: SlotOnLoad event triggered'); + onSlotLoaded(args); + }); + }); + } + + openxAdapter.originEnableAnalytics(adapterConfig); + } + + function isValidConfig({options: analyticsOptions}) { + const fieldValidations = [ + // tuple of property, type, required + ['publisherPlatformId', 'string', true], + ['publisherAccountId', 'number', true], + ['sampling', 'number', false], + ['enableV2', 'boolean', false], + ['testPipeline', 'boolean', false], + ['adIdKey', 'string', false], + ['payloadWaitTime', 'number', false], + ['payloadWaitTimePadding', 'number', false], + ]; + + let failedValidation = fieldValidations.find(([property, type, required]) => { + // if required, the property has to exist + // if property exists, type check value + return (required && !analyticsOptions.hasOwnProperty(property)) || + (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() { + let campaign = {}; + let queryParams = utils.parseQS(utils.getWindowLocation() && utils.getWindowLocation().search); + + utmTags.forEach(function(utmKey) { + let utmValue = queryParams[utmKey]; + if(utmValue){ + let key = UTM_TO_CAMPAIGN_PROPERTIES[utmKey]; + campaign[key] = utmValue; + } + }); + return campaign; +} + +function buildPayload( + data, + eventType, + publisherPlatformId, + publisherAccountId, + auctionId, + testCode, + sourceUrl, + campaign +) { return { - 'hb.ct': adUnit.auctionStart, - 'hb.rid': adUnit.auctionId, - 'hb.exn': bidders.join(','), - 'hb.sts': requestTimes.join(','), - 'hb.ets': responseTimes.join(','), - 'hb.bv': bidValues.join(','), - 'hb.to': timeouts.join(','), - 'hb.crid': creativeIds.join(','), - 'hb.we': winningExchangeIndex, - 'hb.g1': winningExchangeIndex === -1, - dddid: adUnit.transactionId, - ts: openxBid && openxBid.ts, - auid: openxBid && openxBid.params && openxBid.params.unit + adapterVersion: ADAPTER_VERSION, + schemaVersion: SCHEMA_VERSION, + data: data, + eventType: eventType, + publisherPlatformId: publisherPlatformId, + publisherAccountId: publisherAccountId, + auctionId: auctionId, + testCode: testCode, + sourceUrl: sourceUrl, + campaign + }; +} + +function apiCall(url, MAX_RETRIES, payload) { + let xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (xhr.readyState !== 4) return; + if (xhr.status >= 200 && xhr.status < 300) { + utils.logInfo('OX: Data sent for event:', payload.eventType); + } else { + if (MAX_RETRIES == 0) { + utils.logError('OX: Retries Exhausted, Data could not be Sent!!'); + return; + } + utils.logInfo('OX: Retrying ...', MAX_RETRIES); + url = getRandomUrl(url); + apiCall(url, MAX_RETRIES - 1, payload); + } }; + xhr.open('POST', url, true); + xhr.setRequestHeader('Content-Type', 'application/gzip'); + if (payload.publisherPlatformId) { + xhr.setRequestHeader('PublisherPlatformId', payload.publisherPlatformId); + } + if (payload.publisherAccountId) { + xhr.setRequestHeader('PublisherAccountId', payload.publisherAccountId); + } + if (payload.auctionId) { + xhr.setRequestHeader('AuctionId', payload.auctionId); + } + xhr.setRequestHeader('Source-Url', payload.sourceUrl); + xhr.timeout = MAX_TIMEOUT; + xhr.send(payload.data); +} + +function getRandomUrl(failedUrl) { + let urlHead = 'https://'; + let urlTail = '.openx.net/publish/'; + let urlList = [ + 'prebid-analytics', + 'prebid-analytics-2' + ]; + let randomIndex = Math.floor(Math.random() * urlList.length); + let randomUrl = urlHead + urlList[randomIndex] + urlTail; + if (failedUrl) { + if (failedUrl === randomUrl) { + return getRandomUrl(failedUrl); + } + return randomUrl; + } + return randomUrl; +} + +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 send(eventType, eventStack, auctionId) { + var ua = window.navigator.userAgent; + var sourceUrl = window.location.href; + var sourceBrowser = detectBrowser(); + var sourceOs = detectOS(); + // utils.logInfo('OX: AuctionId', auctionId); + pushAdPositionData(auctionId); + var data = eventStack[auctionId]; + var publisherPlatformId = eventStack[auctionId].options.publisherPlatformId; + var publisherAccountId = eventStack[auctionId].options.publisherAccountId; + var testCode = eventStack[auctionId].options.testCode; + data['user_agent'] = ua; + data['source_url'] = sourceUrl; + data['source_browser'] = sourceBrowser; + data['source_os'] = sourceOs; + if (detectMob()) { + data['deviceType'] = 'Mobile'; + } else { + data['deviceType'] = 'Desktop'; + } + if (typeof data === 'object') { + const stringData = JSON.stringify(data); + if (typeof stringData === 'string') { + const compressedData = zlib.gzipSync(stringData); + let urlGenerated = getRandomUrl(null); + let payload = buildPayload( + compressedData, + eventType, + publisherPlatformId, + publisherAccountId, + auctionId, + testCode, + sourceUrl, + analyticsConfig.campaign + ); + apiCall(urlGenerated, MAX_RETRIES, payload); + } else { + utils.logError('OX: Invalid data format'); + delete eventStack[auctionId]; + // utils.logInfo('OX: Deleted Auction Info for auctionId', auctionId); + } + } else { + utils.logError('OX: Invalid data format'); + delete eventStack[auctionId]; + // utils.logInfo('OX: Deleted Auction Info for auctionId', auctionId); + } +} +function pushEvent(eventType, args, auctionId) { + if (eventType === bidRequestConst) { + if (checkAdUnitConfig()) { + args.bids = filterBidsByAdUnit(args.bids); + } + if (args.bids.length > 0) { + eventStack[auctionId].events.push({ eventType: eventType }); + } + } else { + if (isValidEvent(eventType, args.adUnitCode)) { + eventStack[auctionId].events.push({ eventType: eventType, args: args }); + } + } +} + +function updateLoadedAdSlotsInfo(auctionId, adUnitCode, adPosition) { + if (auctionId && adUnitCode) { + if (!loadedAdSlots[auctionId]) { + loadedAdSlots[auctionId] = {}; + } + loadedAdSlots[auctionId][adUnitCode] = {}; + if (adPosition) { + loadedAdSlots[auctionId][adUnitCode] = { adPosition: adPosition }; + } + } else { + utils.logWarn("OX: Couldn't update loadedAdSlots information."); + } +} + +function getLoadedAdUnitCodes(auctionId) { + return (!auctionId || !loadedAdSlots[auctionId] || typeof loadedAdSlots[auctionId] !== 'object') + ? [] : Object.keys(loadedAdSlots[auctionId]); +} + +function pushAdPositionData(auctionId) { + if (auctionId && eventStack?.[auctionId]?.events) { + let adUnitPositionMap = loadedAdSlots[auctionId]; + if (adUnitPositionMap && JSON.stringify(adUnitPositionMap) !== '{}') { + eventStack[auctionId].events.filter(function(event) { + return event.eventType === auctionEndConst; + }).forEach(function (auctionEndEvent) { + if (auctionEndEvent.args && auctionEndEvent.args.adUnits) { + auctionEndEvent.args.adUnits.forEach(function (adUnitInfo) { + if (adUnitPositionMap[adUnitInfo.code] && adUnitPositionMap[adUnitInfo.code]['adPosition']) { + adUnitInfo['adPosition'] = adUnitPositionMap[adUnitInfo.code]['adPosition']; + } else { + adUnitInfo['adPosition'] = ''; + } + }) + } + }); + } + } } -function getPerformanceData(auctionStart) { - let timing; - try { - timing = window.top.performance.timing; - } catch (e) {} +function getAdPositionByElementId(elementId) { + let elem = document.querySelector('#' + elementId); + let adPosition; + 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); + let right = Math.min(windowWidth, bounding.right); + let bottom = Math.min(windowHeight, bounding.bottom); + let top = Math.max(0, bounding.top); + + 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 + adPosition = (intersectionArea * 2 >= adSlotArea) ? 'ATF' : 'BTF'; + } + } + } else { + utils.logWarn('OX: DOM element not for id ' + elementId); + } + return adPosition; +} + +openxAdapter.slotOnLoad = onSlotLoaded; + +adapterManager.registerAnalyticsAdapter({ + adapter: openxAdapter, + code: 'openx' +}); - if (!timing) { +function prebidAnalyticsEventHandlerV1({eventType, args}) { + if (!checkInitOptions()) { + utils.logError('OX: Incorrect adapter options') + send(eventType, {}, null); return; } - const { fetchStart, domContentLoadedEventEnd, loadEventEnd } = timing; - const domContentLoadTime = domContentLoadedEventEnd - fetchStart; - const pageLoadTime = loadEventEnd - fetchStart; - const timeToAuction = auctionStart - fetchStart; - const timeToRender = Date.now() - fetchStart; + let info = Object.assign({}, args); - return { - 'hb.dcl': domContentLoadTime, - 'hb.dl': pageLoadTime, - 'hb.tta': timeToAuction, - 'hb.ttr': timeToRender + if (info && info.ad) { + info.ad = ''; + } + + // on bid timeout events, the info is an array of bids + let auctionId = eventType === CONSTANTS.EVENTS.BID_TIMEOUT + ? info[0].auctionId + : info.auctionId; + + if (eventType === auctionInitConst) { + eventStack[auctionId] = { options: {}, events: [] }; + // utils.logInfo('OX: Event Stack updated after AuctionInit', eventStack); + } else if (eventType === bidWonConst) { + pushEvent(eventType, info, auctionId); + // utils.logInfo('OX: Bid won called for', auctionId); + } else if (eventType === auctionEndConst) { + pushEvent(eventType, removeads(info), auctionId); + // utils.logInfo('OX: Auction end called for', auctionId); + updateSessionId(); + buildEventStack(auctionId); + if (isValidEventStack(auctionId)) { + setTimeout(function() { + // utils.logInfo('OX: Sending data', eventStack); + if (eventStack[auctionId]) { + send( + eventType, + eventStack, + auctionId + ); + eventStack[auctionId] = null; + } + delete loadedAdSlots[auctionId]; + // utils.logInfo('OX: Deleted Auction Info for auctionId', auctionId); + }, analyticsConfig.payloadWaitTime); + } else { + setTimeout(function() { + eventStack[auctionId] = null; + // utils.logInfo('OX: Deleted Auction Info for auctionId', auctionId); + }, analyticsConfig.payloadWaitTime); + } + } else if (eventType === bidTimeoutConst) { + // utils.logInfo('SA: Bid Timedout for', auctionId); + pushEvent(eventType, info, auctionId); + } +} + +//* ******* V2 Code ******* +const { + EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, AUCTION_END, BID_WON } +} = CONSTANTS; + +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 ENDPOINT = 'https://prebid.openx.net/ox/analytics'; +let auctionMap = {}; +let auctionOrder = 1; // tracks the number of auctions ran on the page + +function prebidAnalyticsEventHandlerV2({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} adUnits //: [{…}] + * @property {string} adUnitCodes //: ["video1"] + * @property {string} labels //: undefined + * @property {Array} bidderRequests //: (2) [{…}, {…}] + * @property {Array} noBids //: [] + * @property {Array} bidsReceived //: [] + * @property {Array} winningBids //: [] + * @property {number} timeout //: 3000 + * @property {Object} config //: {publisherPlatformId: "a3aece0c-9e80-4316-8deb-faf804779bd1", publisherAccountId: 537143056, sampling: 1, enableV2: true}/* + */ + +function onAuctionInit({auctionId, timestamp: startTime, timeout, adUnitCodes}) { + auctionMap[auctionId] = { + id: auctionId, + startTime, + endTime: void(0), + timeout, + auctionOrder, + adUnitCodesCount: adUnitCodes.length, + adunitCodesRenderedCount: 0, + state: AUCTION_STATES.INIT, + auctionSendDelayTimer: void (0), }; + + // setup adunit properties in map + auctionMap[auctionId].adUnitCodeToBidderRequestMap = adUnitCodes.reduce((obj, adunitCode) => { + obj[adunitCode] = {}; + return obj; + }, {}); + + auctionOrder++; } -function sendEvent(data) { - utils._map(data, (value, key) => [key, value]).forEach(([key, value]) => { - if ( - value === undefined || - value === null || - (typeof value === 'number' && isNaN(value)) +// TODO: type BidRequest +function onBidRequested(bidRequest) { + const {auctionId, auctionStart, refererInfo, bids: bidderRequests, start} = bidRequest; + const auction = auctionMap[auctionId]; + const adUnitCodeToBidderRequestMap = auction.adUnitCodeToBidderRequestMap; + + bidderRequests.forEach(bidderRequest => { + const { adUnitCode, bidder, bidId: requestId, mediaTypes, params, src, userId } = bidderRequest; + + adUnitCodeToBidderRequestMap[adUnitCode][requestId] = { + bidder, + params, + mediaTypes, + source: src, + userId, + startTime: start, + timedOut: false, + 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].adUnitCodeToBidderRequestMap[adUnitCode][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}) => { + if (auctionMap[auctionId] + && auctionMap[auctionId].adUnitCodeToBidderRequestMap + && auctionMap[auctionId].adUnitCodeToBidderRequestMap[adUnitCode] + && auctionMap[auctionId].adUnitCodeToBidderRequestMap[adUnitCode][requestId] ) { - delete data[key]; + auctionMap[auctionId].adUnitCodeToBidderRequestMap[adUnitCode][requestId].timedOut = true; } }); - ajax(ENDPOINT, null, data, { method: 'GET' }); } +/** + * + * @param {PbAuction} endedAuction + */ +function onAuctionEnd(endedAuction) { + let auction = auctionMap[endedAuction.auctionId]; -let googletag = window.googletag || {}; -googletag.cmd = googletag.cmd || []; -googletag.cmd.push(function() { - googletag.pubads().addEventListener(SLOT_LOADED, args => { - openxAdapter.track({ eventType: SLOT_LOADED, args }); - }); -}); + if (!auction) { + return; + } -const openxAdapter = Object.assign( - adapter({ url: ENDPOINT, analyticsType: 'endpoint' }), - { - track({ 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 BID_WON: - onBidWon(args); - break; - case SLOT_LOADED: - onSlotLoaded(args); - break; + 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; + auctionMap[auctionId].adUnitCodeToBidderRequestMap[adUnitCode][requestId].bids[adId].winner = true; +} + +/** + * + * @param {GoogleTagSlot} slot + * @param {string} serviceName + */ +function onSlotLoadedV2({ slot }) { + const renderTime = Date.now(); + const auction = getAuctionByGoogleTagSLot(slot); + + if (!auction) { + return; // slot is not participating in a prebid auction + } + + clearAuctionTimer(auction); + + // track that an adunit code has completed within an auction + auction.adunitCodesRenderedCount++; + + // mark adunit as rendered + const adId = slot.getTargeting('hb_adid')[0]; + const adUnit = getAdUnitByAuctionAndAdId(auction, adId); + + if (adUnit) { + adUnit.rendered = true; + adUnit.renderTime = renderTime; + } + + 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 delayedSend(auction) { + const delayTime = auction.adunitCodesRenderedCount === auction.adUnitCodesCount + ? analyticsConfig.payloadWaitTime + : analyticsConfig.payloadWaitTime + analyticsConfig.payloadWaitTimePadding; + + auction.auctionSendDelayTimer = setTimeout(() => { + let payload = JSON.stringify([buildAuctionPayload(auction)]); + ajax(ENDPOINT, deleteAuctionMap, payload, { contentType: 'application/json' }); + + function deleteAuctionMap() { + delete auctionMap[auction.id]; + } + }, delayTime); +} + +function clearAuctionTimer(auction) { + // reset the delay timer to send the auction data + if (auction.auctionSendDelayTimer) { + clearTimeout(auction.auctionSendDelayTimer); + auction.auctionSendDelayTimer = void (0); + } +} + +function getAuctionByGoogleTagSLot(slot) { + let slotAdunitCodes = [slot.getSlotElementId(), slot.getAdUnitPath()]; + let slotAuction; + + utils._each(auctionMap, auction => { + utils._each(auction.adUnitCodeToBidderRequestMap, (bidderRequestIdMap, adUnitCode) => { + if (slotAdunitCodes.includes(adUnitCode)) { + slotAuction = auction; } + }); + }); + + return slotAuction; +} + +function buildAuctionPayload(auction) { + let {startTime, endTime, state, timeout, auctionOrder, adUnitCodeToBidderRequestMap} = auction; + let {publisherPlatformId, publisherAccountId, campaign} = analyticsConfig; + + return { + publisherPlatformId, + publisherAccountId, + campaign, + state, + startTime, + endTime, + timeLimit: timeout, + auctionOrder, + deviceType: detectMob() ? 'Mobile' : 'Desktop', + deviceOSType: detectOS(), + browser: detectBrowser(), + testCode: analyticsConfig.testCode, + bidRequests: buildBidRequestsPayload(adUnitCodeToBidderRequestMap), + }; + + function buildBidRequestsPayload(adUnitCodeToBidderRequestMap) { + return utils._map(adUnitCodeToBidderRequestMap, (bidderRequestMap, adUnitCode) => { + return utils._map(bidderRequestMap, (bidderRequest) => { + let {bidder, source, bids, mediaTypes, timedOut, userId} = bidderRequest; + return { + adUnitCode, + bidder, + source, + // return an array of objects containing the module name and id + userIds: utils._map(userId, (id, module) => { + return { + module: module, + id: getUserId(module, id)}; + }) + .filter(({id}) => id), + hasBidderResponded: Object.keys(bids).length > 0, + availableAdSizes: getMediaTypeSizes(mediaTypes), + availableMediaTypes: getMediaTypes(mediaTypes), + timedOut, + bidResponses: utils._map(bidderRequest.bids, (bidderBidResponse) => { + let { + cpm, + creativeId, + responseTimestamp, + ts, + adId, + meta, + mediaType, + dealId, + ttl, + netRevenue, + currency, + originalCpm, + originalCurrency, + width, + height, + latency, + winner, + rendered, + renderTime, + } = bidderBidResponse; + + return { + microCpm: cpm * 1000, + netRevenue, + currency, + mediaType, + height, + width, + size: `${width}x${height}`, + dealId, + latency, + ttl, + winner, + creativeId, + ts, + rendered, + renderTime, + meta, + } + }) + } + }) + }).flat(); + } + + function getUserId(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 normalizedId; } -); -// save the base class function -openxAdapter.originEnableAnalytics = openxAdapter.enableAnalytics; + function getMediaTypeSizes(mediaTypes) { + return utils._map(mediaTypes, (mediaTypeConfig, mediaType) => { + return utils.parseSizesInput(mediaTypeConfig.sizes) + .map(size => `${mediaType}_${size}`); + }).flat(); + } -// override enableAnalytics so we can get access to the config passed in from the page -openxAdapter.enableAnalytics = function(config) { - if (!config || !config.options || !config.options.publisherId) { - utils.logError('OpenX analytics adapter: publisherId is required.'); - return; + function getMediaTypes(mediaTypes) { + return utils._map(mediaTypes, (mediaTypeConfig, mediaType) => mediaType); } - initOptions = config.options; - openxAdapter.originEnableAnalytics(config); // call the base class function -}; +} + +function getAdUnitByAuctionAndAdId(auction, adId) { + let adunit; + + utils._each(auction.adUnitCodeToBidderRequestMap, (bidderRequestIdMap) => { + utils._each(bidderRequestIdMap, bidderRequest => { + utils._each(bidderRequest.bids, (bid, bidId) => { + if (bidId === adId) { + adunit = bid; + } + }); + }); + }); + + return adunit; +} + +export default Object.assign({ + adapter: openxAdapter, + auctionEndWaitTime: AUCTION_END_WAIT_TIME +}); + +/** + * Test Helper Functions + */ // reset the cache for unit tests openxAdapter.reset = function() { + // V1 data + eventStack = {}; + loadedAdSlots = {}; + + // V2 data auctionMap = {}; + auctionOrder = 1; }; -adapterManager.registerAnalyticsAdapter({ - adapter: openxAdapter, - code: 'openx' -}); +/** + * Type Definitions + */ + +/** + * @typedef {Object} BidResponse + * @property {string} auctionId - Auction ID of the request this bid responded to + * @property {string} bidderCode - The bidder code. Used by ad server’s line items to identify bidders + * @property {string} adId - The unique identifier of a bid creative. It’s used by the line item’s creative as in this example. + * @property {number} width - The width of the returned creative size. + * @property {number} height - The height of the returned creative size. + * @property {string} size - The width x height of the returned creative size. + * @property {number} originalCpm - The original bid price from the bidder prior to bid adjustments + * @property {number} cpm - The exact bid price from the bidder + * @property {string} originalCurrency - Original currency of the bid prior to bid adjustments + * @property {string} currency - 3-letter ISO 4217 code defining the currency of the bid. + * @property {Boolean} netRevenue - True if bid is Net, False if Gross + * @property {number} requestTimestamp - The time stamp when the bid request is sent out in milliseconds + * @property {number} responseTimestamp - The time stamp when the bid response is received in milliseconds + * @property {number} timeToRespond - The amount of time for the bidder to respond with the bid + * @property {string} adUnitCode - adUnitCode to get the bid responses for + * @property {number} creativeId - Bidder-specific creative ID + * @property {string} mediaType - One of: banner, native, video banner + * @property {string} [dealId] - (Optional) If the bid is associated with a Deal, this field contains the deal ID. + * @property {Object} adserverTargeting - Contains all the adserver targeting parameters + * @property {string} [ad] - Contains the ad payload for banner ads. + * @property {string} [vastUrl] - URL where the VAST document can be retrieved when ready for display. + * @property {string} [vastImpUrl] - Optional; only usable with vastUrl and requires prebid cache to be enabled. + * An impression tracking URL to serve with video Ad + * @property {string} [vastXml] - XML for VAST document to be cached for later retrieval. + * @property {Object} [native] - Contains native key value pairs. + * @property {string} status - Status of the bid. Possible values: targetingSet, rendered "targetingSet" + * @property {string} statusMessage - The bid’s status message “Bid returned empty or error response” or “Bid available” + * @property {number} ttl - How long (in seconds) this bid is considered valid. See this FAQ entry for more info. 300 + * @property {string} requestId - Used to tie this bid back to the request + * @property {string} mediaType - Specifies the type of media type. One of: banner, video, native + * @property {string} source - Whether this bid response came from a client-side or server side request. One of: client, server. + * @property {string} pbLg - CPM quantized to a granularity: Low (pbLg) + * @property {string} pbMg - CPM quantized to a granularity: Medium (pbMg) + * @property {string} pbHg - CPM quantized to a granularity: High (pbHg) + * @property {string} pbAg - CPM quantized to a granularity: Auto (pbAg) + * @property {string} pbDg - CPM quantized to a granularity: Dense (pbDg) + * @property {BidResponseMeta} [meta] - Object containing metadata about the bid + * }} + */ -export default openxAdapter; +/** + * @typedef {Object} BidResponseMeta + * @property {string} [networkId] Bidder-specific Network/DSP Id + * @property {string} [networkName] - Network/DSP Name. example: "NetworkN" + * @property {string} [agencyId] - Bidder-specific Agency ID. example: 2222 + * @property {string} [agencyName] - Agency Name. example: "Agency, Inc." + * @property {string} [advertiserId] - Bidder-specific Advertiser ID. example: 3333 + * @property {string} [advertiserName] - Advertiser Name. example: "AdvertiserA" + * @property {Array} [advertiserDomains] - Array of Advertiser Domains for the landing page(s). This is an array + * to align with the OpenRTB ‘adomain’ field.. example: ["advertisera.com"] + * @property {string} [brandId] - Bidder-specific Brand ID (some advertisers may have many brands). example: 4444 + * @property {string} [brandName] - Brand Name. example: "BrandB" + * @property {string} [primaryCatId] - Primary IAB category ID. example: "IAB-111" + * @property {Array} [secondaryCatIds] - Array of secondary IAB category IDs. example: ["IAB-222","IAB-333"] + */ diff --git a/test/spec/modules/openxAnalyticsAdapter_spec.js b/test/spec/modules/openxAnalyticsAdapter_spec.js index 805435abf80..d57b3bb7d1e 100644 --- a/test/spec/modules/openxAnalyticsAdapter_spec.js +++ b/test/spec/modules/openxAnalyticsAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import openxAdapter from 'modules/openxAnalyticsAdapter.js'; +import openxAdapterParams, {AUCTION_STATES} from 'modules/openxAnalyticsAdapter.js'; import { config } from 'src/config.js'; import events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; @@ -7,28 +7,69 @@ import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; const { - EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, BID_WON } + EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, BID_WON, AUCTION_END } } = CONSTANTS; - const SLOT_LOADED = 'slotOnload'; +const zlib = require('zlib'); +const openxAdapter = openxAdapterParams.adapter; + describe('openx analytics adapter', function() { - it('should require publisher id', function() { - sinon.spy(utils, 'logError'); + describe('when validating the configuration', function () { + let spy; + beforeEach(function () { + spy = sinon.spy(utils, 'logError'); + }); - openxAdapter.enableAnalytics(); - expect( - utils.logError.calledWith( - 'OpenX analytics adapter: publisherId is required.' - ) - ).to.be.true; + afterEach(function() { + utils.logError.restore(); + }); - utils.logError.restore(); + it('should require publisher id when no configuration is passed', function() { + openxAdapter.enableAnalytics(); + expect(spy.firstCall.args[0]).to.match(/publisherPlatformId/); + expect(spy.firstCall.args[0]).to.match(/to exist/); + }); + + it('should validate types', function() { + openxAdapter.enableAnalytics({ + provider: 'openx', + options: { + publisherPlatformId: 'test platformId', + publisherAccountId: 123, + sampling: 'invalid-float' + } + }); + + expect(spy.firstCall.args[0]).to.match(/sampling/); + expect(spy.firstCall.args[0]).to.match(/type 'number'/); + }); }); describe('sending analytics event', function() { const auctionInit = { auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79' }; + const auctionId = 'add5eb0f-587d-441d-86ec-bbb722c70f79'; + const OPENX_ADID = '33dddbb61d359a'; + const ADUNITCODE1 = 'div-1'; + const AUCTION_END_WAIT_TIME = openxAdapterParams.auctionEndWaitTime; + const SLOT_LOAD_WAIT_TIME = 200; + + let clock; + before(function () { + clock = sinon.useFakeTimers(); + }); + after(function () { + clock.restore(); + }); + + const openxAdUnitInfo = [{'code': 'div-1', + 'mediaTypes': {'banner': {'sizes': [[320, 50]]}}, + 'bids': [{'bidder': 'openx', + 'params': {'unit': '540249866', 'delDomain': 'sademo-d.openx.net'}}], + 'sizes': [[320, 50]], + 'transactionId': 'ac66c3e6-3118-4213-a3ae-8cdbe4f72873'}]; + const bidRequestedOpenX = { auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', auctionStart: 1540944528017, @@ -81,6 +122,8 @@ describe('openx analytics adapter', function() { ts: 'hu1QWo6iD3MHs6NG_AQAcFtyNqsj9y4S0YRbX7Kb06IrGns0BABb' }; + const emptyBidResponses = {}; + const bidTimeoutOpenX = { 0: { adUnitCode: 'div-1', @@ -111,6 +154,39 @@ describe('openx analytics adapter', function() { auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79' }; + const auctionEnd = { + 'auctionId': 'add5eb0f-587d-441d-86ec-bbb722c70f79', + 'timestamp': 1540944528017, + 'auctionEnd': 1540944528117, + 'auctionStatus': 'completed', + 'adUnits': openxAdUnitInfo, + 'adUnitCodes': [ + 'div-1' + ], + 'bidderRequests': [bidRequestedOpenX], + 'noBids': [], + 'bidsReceived': [bidResponseOpenX], + 'winningBids': [], + 'timeout': 300 + }; + + const slotLoadDFPWin = { + slot: { + getAdUnitPath: () => { + return '/90577858/test_ad_unit'; + }, + getSlotElementId: function () { + return 'div-1'; + }, + getTargetingKeys: () => { + return []; + }, + getTargeting: () => { + return []; // sinon.stub().withArgs('hb_adid').returns(highestBid ? [highestBid.adId] : []) + } + } + }; + function simulateAuction(events) { let highestBid; @@ -122,24 +198,8 @@ describe('openx analytics adapter', function() { if (highestBid.cpm < args.cpm) { highestBid = args; } - } - }); - - openxAdapter.track({ - eventType: SLOT_LOADED, - args: { - slot: { - getAdUnitPath: () => { - return '/90577858/test_ad_unit'; - }, - getTargetingKeys: () => { - return []; - }, - getTargeting: sinon - .stub() - .withArgs('hb_adid') - .returns(highestBid ? [highestBid.adId] : []) - } + } else if (eventType === SLOT_LOADED) { + openxAdapter.slotOnLoad(args); } }); } @@ -156,8 +216,13 @@ describe('openx analytics adapter', function() { before(function() { sinon.stub(events, 'getEvents').returns([]); openxAdapter.enableAnalytics({ + provider: 'openx', options: { - publisherId: 'test123' + publisherPlatformId: 'a3aece0c-9e80-4316-8deb-faf804779bd1', + publisherAccountId: 537143056, + sampling: 1.0, + testCode: 'test-code-1', + payloadWaitTime: SLOT_LOAD_WAIT_TIME } }); }); @@ -173,7 +238,7 @@ describe('openx analytics adapter', function() { afterEach(function() {}); - it('should not send request if no bid response', function() { + it.skip('should not send request if no bid response', function() { simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX] @@ -182,7 +247,7 @@ describe('openx analytics adapter', function() { expect(server.requests.length).to.equal(0); }); - it('should send 1 request to the right endpoint', function() { + it.skip('should send 1 request to the right endpoint', function() { simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], @@ -197,7 +262,7 @@ describe('openx analytics adapter', function() { }); describe('hb.ct, hb.rid, dddid, hb.asiid, hb.pubid', function() { - it('should always be in the query string', function() { + it.skip('should always be in the query string', function() { simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], @@ -216,7 +281,7 @@ describe('openx analytics adapter', function() { }); describe('hb.cur', function() { - it('should be in the query string if currency is set', function() { + it.skip('should be in the query string if currency is set', function() { sinon .stub(config, 'getConfig') .withArgs('currency.adServerCurrency') @@ -236,7 +301,7 @@ describe('openx analytics adapter', function() { }); }); - it('should not be in the query string if currency is not set', function() { + it.skip('should not be in the query string if currency is not set', function() { simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], @@ -249,7 +314,7 @@ describe('openx analytics adapter', function() { }); describe('hb.dcl, hb.dl, hb.tta, hb.ttr', function() { - it('should be in the query string if browser supports performance API', function() { + it.skip('should be in the query string if browser supports performance API', function() { const timing = { fetchStart: 1540944528000, domContentLoadedEventEnd: 1540944528010, @@ -279,7 +344,7 @@ describe('openx analytics adapter', function() { }); }); - it('should not be in the query string if browser does not support performance API', function() { + it.skip('should not be in the query string if browser does not support performance API', function() { const originalPerf = window.top.performance; window.top.performance = undefined; @@ -302,7 +367,7 @@ describe('openx analytics adapter', function() { }); describe('ts, auid', function() { - it('OpenX is in auction and has a bid response', function() { + it.skip('OpenX is in auction and has a bid response', function() { simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], @@ -318,7 +383,7 @@ describe('openx analytics adapter', function() { }); }); - it('OpenX is in auction but no bid response', function() { + it.skip('OpenX is in auction but no bid response', function() { simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], @@ -333,7 +398,7 @@ describe('openx analytics adapter', function() { expect(queryData).to.not.have.key('ts'); }); - it('OpenX is not in auction', function() { + it.skip('OpenX is not in auction', function() { simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedCloseX], @@ -346,7 +411,7 @@ describe('openx analytics adapter', function() { }); describe('hb.exn, hb.sts, hb.ets, hb.bv, hb.crid, hb.to', function() { - it('2 bidders in auction', function() { + it.skip('2 bidders in auction', function() { simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], @@ -379,7 +444,7 @@ describe('openx analytics adapter', function() { }); }); - it('OpenX timed out', function() { + it.skip('OpenX timed out', function() { simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], @@ -416,28 +481,913 @@ describe('openx analytics adapter', function() { [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], [BID_RESPONSE, bidResponseOpenX], - [BID_WON, bidWonOpenX] + [AUCTION_END, auctionEnd], + [BID_WON, bidWonOpenX], ]); - const queryData = getQueryData(server.requests[0].url); + // Handle timeouts + clock.tick(AUCTION_END_WAIT_TIME + 10); + + let compressedPayload = server.requests[0].requestBody; + let payloadBuffer = new Buffer(compressedPayload); + let unCompressedPayload = zlib.gunzipSync(payloadBuffer).toString(); + let auctionData = JSON.parse(unCompressedPayload); + + let biddersRequests = []; + let biddersResponded = []; + auctionData.events.forEach(function (event) { + if (event.eventType === AUCTION_END) { + event.args.bidderRequests.forEach(function(bidRequestInfo) { + if (bidRequestInfo.bids.length > 0) { + biddersRequests.push(bidRequestInfo.bids[0].bidder); + } + }); + event.args.bidsReceived.forEach(function(bidsInfo) { + biddersResponded.push(bidsInfo); + }); + } + }); + + expect(biddersRequests.length).to.equal(1); + expect(biddersRequests[0]).to.equal('openx'); + expect(biddersResponded.length).to.equal(1); + expect(biddersResponded[0]).to.include({ + creativeId: 'openx-crid', + cpm: 0.5 + }); + + let bidWonEventInfoList = auctionData.events.filter(function (event) { + return event.eventType === BID_WON && event.args.auctionId === auctionId; + }); + + expect(bidWonEventInfoList.length).to.equal(1); + expect(bidWonEventInfoList[0].args).to.include({ + 'adId': OPENX_ADID, + 'adUnitCode': ADUNITCODE1 + }); + + /* const queryData = getQueryData(server.requests[0].url); expect(queryData).to.include({ 'hb.we': '0', 'hb.g1': 'false' - }); + }); */ }); it('DFP won', function() { simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] + [BID_RESPONSE, bidResponseOpenX], + [AUCTION_END, auctionEnd], + [SLOT_LOADED, slotLoadDFPWin] ]); - const queryData = getQueryData(server.requests[0].url); + // Handle timeouts + clock.tick(SLOT_LOAD_WAIT_TIME + 10); + + let compressedPayload = server.requests[0].requestBody; + let payloadBuffer = new Buffer(compressedPayload); + let unCompressedPayload = zlib.gunzipSync(payloadBuffer).toString(); + let auctionData = JSON.parse(unCompressedPayload); + + let biddersRequests = []; + let biddersResponded = []; + auctionData.events.forEach(function (event) { + if (event.eventType === AUCTION_END) { + event.args.bidderRequests.forEach(function(bidRequestInfo) { + if (bidRequestInfo.bids.length > 0) { + biddersRequests.push(bidRequestInfo.bids[0].bidder); + } + }); + event.args.bidsReceived.forEach(function(bidsInfo) { + biddersResponded.push(bidsInfo); + }); + } + }); + + expect(biddersRequests.length).to.equal(1); + expect(biddersRequests[0]).to.equal('openx'); + expect(biddersResponded.length).to.equal(1); + + let bidWonEventInfoList = auctionData.events.filter(function (event) { + return event.eventType === BID_WON && event.args.auctionId === auctionId; + }); + + expect(bidWonEventInfoList.length).to.equal(0); + + /* const queryData = getQueryData(server.requests[0].url); expect(queryData).to.include({ 'hb.we': '-1', 'hb.g1': 'true' + }); */ + }); + }); + }); + + describe('when version 2 is enabled', function () { + const AD_UNIT_CODE = 'test-div-1'; + const SLOT_LOAD_WAIT_TIME = 10; + + const DEFAULT_V2_ANALYTICS_CONFIG = { + publisherAccountId: 123, + publisherPlatformId: 'test-platform-id', + sample: 1.0, + enableV2: true, + payloadWaitTime: SLOT_LOAD_WAIT_TIME, + payloadWaitTimePadding: SLOT_LOAD_WAIT_TIME + }; + + const auctionInit = { + auctionId: 'test-auction-id', + timestamp: 1586000000000, + timeout: 3000, + adUnitCodes: [AD_UNIT_CODE], + }; + + const bidRequestedOpenX = { + auctionId: 'test-auction-id', + auctionStart: 1586000000000, + bids: [ + { + adUnitCode: AD_UNIT_CODE, + bidId: 'test-openx-request-id', + bidder: 'openx', + params: { unit: 'test-openx-ad-unit-id' }, + userId: { + tdid: 'test-tradedesk-id' + } + } + ], + start: 1586000000010 + }; + + const bidRequestedCloseX = { + auctionId: 'test-auction-id', + auctionStart: 1586000000000, + bids: [ + { + adUnitCode: AD_UNIT_CODE, + bidId: 'test-closex-request-id', + bidder: 'closex', + params: { unit: 'test-closex-ad-unit-id' }, + userId: { + tdid: 'test-tradedesk-id' + } + } + ], + start: 1586000000020 + }; + + const bidResponseOpenX = { + adUnitCode: AD_UNIT_CODE, + cpm: 0.5, + netRevenue: true, + requestId: 'test-openx-request-id', + mediaType: 'banner', + width: 300, + height: 250, + adId: 'test-openx-ad-id', + auctionId: 'test-auction-id', + creativeId: 'openx-crid', + currency: 'USD', + timeToRespond: 100, + responseTimestamp: 1586000000030, + ts: 'test-openx-ts' + }; + + const bidResponseCloseX = { + adUnitCode: AD_UNIT_CODE, + cpm: 0.3, + netRevenue: true, + requestId: 'test-closex-request-id', + mediaType: 'video', + width: 300, + height: 250, + adId: 'test-closex-ad-id', + auctionId: 'test-auction-id', + creativeId: 'closex-crid', + currency: 'USD', + timeToRespond: 200, + dealId: 'test-closex-deal-id', + responseTimestamp: 1586000000040, + ts: 'test-closex-ts' + }; + + const bidTimeoutOpenX = { + 0: { + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id', + bidId: 'test-openx-request-id' + }}; + + const bidTimeoutCloseX = { + 0: { + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id', + bidId: 'test-closex-request-id' + } + }; + + const bidWonOpenX = { + requestId: 'test-openx-request-id', + adId: 'test-openx-ad-id', + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id' + }; + + const auctionEnd = { + auctionId: 'test-auction-id', + timestamp: 1586000000000, + auctionEnd: 1586000000100, + timeout: 3000, + adUnitCodes: [AD_UNIT_CODE], + }; + + const bidWonCloseX = { + requestId: 'test-closex-request-id', + adId: 'test-closex-ad-id', + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id' + }; + + function simulateAuction(events) { + let highestBid; + + events.forEach(event => { + const [eventType, args] = event; + if (eventType === BID_RESPONSE) { + highestBid = highestBid || args; + if (highestBid.cpm < args.cpm) { + highestBid = args; + } + } + + if (eventType === SLOT_LOADED) { + const slotLoaded = { + slot: { + getAdUnitPath: () => { + return '/12345678/test_ad_unit'; + }, + getSlotElementId: () => { + return AD_UNIT_CODE; + }, + getTargeting: (key) => { + if (key === 'hb_adid') { + return highestBid ? [highestBid.adId] : []; + } else { + return []; + } + } + } + }; + openxAdapter.track({ eventType, args: slotLoaded }); + } else { + openxAdapter.track({ eventType, args }); + } + }); + } + + let clock; + + beforeEach(function() { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + events.getEvents.restore(); + clock.restore(); + }); + + describe('when there is an auction', function () { + let auction; + let auction2; + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [SLOT_LOADED] + ]); + + simulateAuction([ + [AUCTION_INIT, {...auctionInit, auctionId: 'second-auction-id'} ], + [SLOT_LOADED] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + auction2 = JSON.parse(server.requests[1].requestBody)[0]; + }); + + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track auction start time', function () { + expect(auction.startTime).to.equal(auctionInit.timestamp); + }); + + it('should track auction time limit', function () { + expect(auction.timeLimit).to.equal(auctionInit.timeout); + }); + + it('should track the \'default\' test code', function () { + expect(auction.testCode).to.equal('default'); + }); + + it('should track auction count', function () { + expect(auction.auctionOrder).to.equal(1); + expect(auction2.auctionOrder).to.equal(2); + }); + }); + + describe('when there is a custom test code', function () { + let auction; + beforeEach(function () { + openxAdapter.enableAnalytics({ + options: { + ...DEFAULT_V2_ANALYTICS_CONFIG, + testCode: 'test-code' + } + }); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [SLOT_LOADED], + ]); + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + }); + + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track the custom test code', function () { + expect(auction.testCode).to.equal('test-code'); + }); + }); + + describe('when there is campaign (utm) data', function () { + let auction; + beforeEach(function () { + + }); + + afterEach(function () { + openxAdapter.reset(); + utils.getWindowLocation.restore(); + openxAdapter.disableAnalytics(); + }); + + it('should track values from query params when they exist', function () { + sinon.stub(utils, 'getWindowLocation').returns({search: '?' + + 'utm_campaign=test-campaign-name&' + + 'utm_source=test-source&' + + 'utm_medium=test-medium&' + }); + + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [SLOT_LOADED], + ]); + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + + expect(auction.campaign.name).to.equal('test-campaign-name'); + expect(auction.campaign.source).to.equal('test-source'); + expect(auction.campaign.medium).to.equal('test-medium'); + expect(auction.campaign.content).to.be.undefined; + expect(auction.campaign.term).to.be.undefined; + }); + + it('should override query params if configuration parameters exist', function () { + sinon.stub(utils, 'getWindowLocation').returns({search: '?' + + 'utm_campaign=test-campaign-name&' + + 'utm_source=test-source&' + + 'utm_medium=test-medium&' + + 'utm_content=test-content&' + + 'utm_term=test-term' + }); + + openxAdapter.enableAnalytics({ + options: { + ...DEFAULT_V2_ANALYTICS_CONFIG, + campaign: { + name: 'test-config-name', + source: 'test-config-source', + medium: 'test-config-medium' + } + } + }); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [SLOT_LOADED], + ]); + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + + expect(auction.campaign.name).to.equal('test-config-name'); + expect(auction.campaign.source).to.equal('test-config-source'); + expect(auction.campaign.medium).to.equal('test-config-medium'); + expect(auction.campaign.content).to.equal('test-content'); + expect(auction.campaign.term).to.equal('test-term'); + }); + }); + + describe('when there are bid requests', function () { + let auction; + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [BID_REQUESTED, bidRequestedCloseX], + [BID_REQUESTED, bidRequestedOpenX], + [SLOT_LOADED], + ]); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; + }); + + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track the bidder', function () { + let openxBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + let closexBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex'); + + expect(openxBidder.bidder).to.equal('openx'); + expect(closexBidder.bidder).to.equal('closex'); + }); + + it('should track the adunit code', function () { + let openxBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + let closexBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex'); + + expect(openxBidder.adUnitCode).to.equal(AD_UNIT_CODE); + expect(closexBidder.adUnitCode).to.equal(AD_UNIT_CODE); + }); + + it('should track the user ids', function () { + let openxBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + let closexBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex'); + + expect(openxBidder.userIds).to.deep.include({module: 'tdid', id: bidRequestedOpenX.bids[0].userId.tdid}); + expect(closexBidder.userIds).to.deep.include({module: 'tdid', id: bidRequestedCloseX.bids[0].userId.tdid}); + }); + + it('should not have responded', function () { + let openxBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + let closexBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex'); + + expect(openxBidder.hasBidderResponded).to.equal(false); + expect(closexBidder.hasBidderResponded).to.equal(false); + }); + }); + + describe('when there are request timeouts', function () { + let auction; + let openxBidRequest; + let closexBidRequest; + + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [BID_REQUESTED, bidRequestedCloseX], + [BID_REQUESTED, bidRequestedOpenX], + [BID_TIMEOUT, bidTimeoutCloseX], + [BID_TIMEOUT, bidTimeoutOpenX], + [AUCTION_END, auctionEnd] + ]); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; + + openxBidRequest = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + closexBidRequest = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex'); + }); + + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track the timeout', function () { + expect(openxBidRequest.timedOut).to.equal(true); + expect(closexBidRequest.timedOut).to.equal(true); + }); + }); + + describe('when there are bid responses', function () { + let auction; + let openxBidResponse; + let closexBidResponse; + + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [BID_REQUESTED, bidRequestedCloseX], + [BID_REQUESTED, bidRequestedOpenX], + [BID_RESPONSE, bidResponseOpenX], + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; + + openxBidResponse = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx').bidResponses[0]; + closexBidResponse = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex').bidResponses[0]; + }); + + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track the cpm in microCPM', function () { + expect(openxBidResponse.microCpm).to.equal(bidResponseOpenX.cpm * 1000); + expect(closexBidResponse.microCpm).to.equal(bidResponseCloseX.cpm * 1000); + }); + + it('should track if the bid is in net revenue', function () { + expect(openxBidResponse.netRevenue).to.equal(bidResponseOpenX.netRevenue); + expect(closexBidResponse.netRevenue).to.equal(bidResponseCloseX.netRevenue); + }); + + it('should track the mediaType', function () { + expect(openxBidResponse.mediaType).to.equal(bidResponseOpenX.mediaType); + expect(closexBidResponse.mediaType).to.equal(bidResponseCloseX.mediaType); + }); + + it('should track the currency', function () { + expect(openxBidResponse.currency).to.equal(bidResponseOpenX.currency); + expect(closexBidResponse.currency).to.equal(bidResponseCloseX.currency); + }); + + it('should track the ad width and height', function () { + expect(openxBidResponse.width).to.equal(bidResponseOpenX.width); + expect(openxBidResponse.height).to.equal(bidResponseOpenX.height); + + expect(closexBidResponse.width).to.equal(bidResponseCloseX.width); + expect(closexBidResponse.height).to.equal(bidResponseCloseX.height); + }); + + it('should track the bid dealId', function () { + expect(openxBidResponse.dealId).to.equal(bidResponseOpenX.dealId); // no deal id defined + expect(closexBidResponse.dealId).to.equal(bidResponseCloseX.dealId); // deal id defined + }); + + it('should track the bid\'s latency', function () { + expect(openxBidResponse.latency).to.equal(bidResponseOpenX.timeToRespond); + expect(closexBidResponse.latency).to.equal(bidResponseCloseX.timeToRespond); + }); + + it('should not have any bid winners', function () { + expect(openxBidResponse.winner).to.equal(false); + expect(closexBidResponse.winner).to.equal(false); + }); + + it('should track the bid currency', function () { + expect(openxBidResponse.currency).to.equal(bidResponseOpenX.currency); + expect(closexBidResponse.currency).to.equal(bidResponseCloseX.currency); + }); + + it('should track the auction end time', function () { + expect(auction.endTime).to.equal(auctionEnd.auctionEnd); + }); + + it('should track that the auction ended', function () { + expect(auction.state).to.equal(AUCTION_STATES.ENDED); + }); + }); + + describe('when there are bidder wins', function () { + const CURRENT_TIME = 1586000000000; + let auction; + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + + // set current time + clock = sinon.useFakeTimers(CURRENT_TIME); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [BID_REQUESTED, bidRequestedOpenX], + [BID_REQUESTED, bidRequestedCloseX], + [BID_RESPONSE, bidResponseOpenX], + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd], + [BID_WON, bidWonOpenX] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; + }); + + afterEach(function () { + clock.restore(); + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track that bidder as the winner', function () { + let openxBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + expect(openxBidder.bidResponses[0]).to.contain({winner: true}); + }); + + it('should track that bidder as the losers', function () { + let closexBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex'); + expect(closexBidder.bidResponses[0]).to.contain({winner: false}); + }); + }); + + describe('when a winning bid renders', function () { + const CURRENT_TIME = 1586000000000; + let auction; + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + + // set current time + clock = sinon.useFakeTimers(CURRENT_TIME); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [BID_REQUESTED, bidRequestedOpenX], + [BID_REQUESTED, bidRequestedCloseX], + [BID_RESPONSE, bidResponseOpenX], + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd], + [BID_WON, bidWonOpenX], + [SLOT_LOADED] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; + }); + + afterEach(function () { + clock.restore(); + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track that winning bid rendered', function () { + let openxBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + expect(openxBidder.bidResponses[0]).to.contain({rendered: true}); + }); + + it('should track that winning bid render time', function () { + let openxBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + expect(openxBidder.bidResponses[0]).to.contain({renderTime: CURRENT_TIME}); + }); + + it('should track that the auction completed', function () { + expect(auction.state).to.equal(AUCTION_STATES.COMPLETED); + }); + }); + }); + + describe('when we\'re testing the pipeline', function () { + const AD_UNIT_CODE = 'test-div-1'; + + const auctionInit = { + auctionId: 'test-auction-id', + timestamp: 1586000000000, + timeout: 3000, + adUnitCodes: [AD_UNIT_CODE], + }; + + const bidRequestedOpenX = { + auctionId: 'test-auction-id', + auctionStart: 1586000000000, + bids: [ + { + adUnitCode: AD_UNIT_CODE, + bidId: 'test-openx-request-id', + bidder: 'openx', + params: { unit: 'test-openx-ad-unit-id' }, + } + ], + start: 1586000000010 + }; + + const bidRequestedCloseX = { + auctionId: 'test-auction-id', + auctionStart: 1586000000000, + bids: [ + { + adUnitCode: AD_UNIT_CODE, + bidId: 'test-closex-request-id', + bidder: 'closex', + params: { unit: 'test-closex-ad-unit-id' }, + } + ], + start: 1586000000020 + }; + + const bidResponseOpenX = { + adUnitCode: AD_UNIT_CODE, + cpm: 0.5, + netRevenue: true, + requestId: 'test-openx-request-id', + mediaType: 'banner', + width: 300, + height: 250, + adId: 'test-openx-ad-id', + auctionId: 'test-auction-id', + creativeId: 'openx-crid', + currency: 'USD', + timeToRespond: 100, + responseTimestamp: 1586000000030, + ts: 'test-openx-ts' + }; + + const bidResponseCloseX = { + adUnitCode: AD_UNIT_CODE, + cpm: 0.3, + netRevenue: true, + requestId: 'test-closex-request-id', + mediaType: 'video', + width: 300, + height: 250, + adId: 'test-closex-ad-id', + auctionId: 'test-auction-id', + creativeId: 'closex-crid', + currency: 'USD', + timeToRespond: 200, + dealId: 'test-closex-deal-id', + responseTimestamp: 1586000000040, + ts: 'test-closex-ts' + }; + + const openxAdUnitInfo = [{'code': 'test-div-1', + 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, + 'bids': [{'bidder': 'openx', + 'params': {'unit': '540249866', 'delDomain': 'sademo-d.openx.net'}}, + {'bidder': 'closex', + 'params': {'unit': '540249866', 'delDomain': 'sademo-d.openx.net'}}], + 'sizes': [[300, 250]], + 'transactionId': 'test-transaction-id'}]; + + const auctionEnd = { + 'auctionId': 'test-auction-id', + 'timestamp': 1586000000000, + 'auctionEnd': 1586000000100, + 'auctionStatus': 'completed', + 'adUnits': openxAdUnitInfo, + 'adUnitCodes': [ + 'test-div-1' + ], + 'bidderRequests': [bidRequestedOpenX, bidRequestedCloseX], + 'noBids': [], + 'bidsReceived': [bidResponseOpenX, bidResponseCloseX], + 'winningBids': [bidResponseOpenX], + 'timeout': 300 + }; + + const bidWonOpenX = { + requestId: 'test-openx-request-id', + adId: 'test-openx-ad-id', + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id' + }; + + let highestBid; + const onSlotLoadEvent = { + eventType: SLOT_LOADED, + args: { + slot: { + getAdUnitPath: () => { + return '/12345678/test_ad_unit'; + }, + getSlotElementId: () => { + return AD_UNIT_CODE; + }, + getTargeting: () => { + return highestBid ? [highestBid.adId] : [] + } + } + } + }; + function simulateAuction(events) { + events.forEach(event => { + const [eventType, args] = event; + openxAdapter.track({eventType, args}); + if (eventType === BID_RESPONSE) { + highestBid = highestBid || args; + if (highestBid.cpm < args.cpm) { + highestBid = args; + } + } + }); + openxAdapter.slotOnLoad(onSlotLoadEvent.args); + openxAdapter.track(onSlotLoadEvent); + } + + const SLOT_LOAD_WAIT_TIME = 10; + let clock; + + beforeEach(function() { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + events.getEvents.restore(); + clock.restore(); + }); + + describe('when are bidder wins', function () { + const CURRENT_TIME = 1586000000000; + let v1Auction; + let v2Auction; + beforeEach(function () { + openxAdapter.enableAnalytics({ + options: { + publisherAccountId: 123123, + publisherPlatformId: 'test-platform-id', + sample: 1.0, + testPipeline: true, + payloadWaitTime: SLOT_LOAD_WAIT_TIME + } }); + + // set current time + clock = sinon.useFakeTimers(CURRENT_TIME); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [BID_REQUESTED, bidRequestedOpenX], + [BID_REQUESTED, bidRequestedCloseX], + [BID_RESPONSE, bidResponseOpenX], + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd], + [BID_WON, bidWonOpenX] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME + 1); + + // ******* + let compressedPayload = server.requests[0].requestBody; + let payloadBuffer = new Buffer(compressedPayload); + let unCompressedPayload = zlib.gunzipSync(payloadBuffer).toString(); + v1Auction = JSON.parse(unCompressedPayload); + + v2Auction = JSON.parse(server.requests[1].requestBody)[0]; + }); + + afterEach(function () { + clock.restore(); + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should send out both payloads', function() { + expect(server.requests.length).to.equal(2); + }); + + it('should track the bidder as the winner in both requests', function () { + let biddersRequests = []; + let biddersResponded = []; + v1Auction.events.forEach(function (event) { + if (event.eventType === AUCTION_END) { + event.args.bidderRequests.forEach(function(bidRequestInfo) { + if (bidRequestInfo.bids.length > 0) { + biddersRequests.push(bidRequestInfo.bids[0].bidder); + } + }); + event.args.bidsReceived.forEach(function(bidsInfo) { + biddersResponded.push(bidsInfo); + }); + } + }); + + expect(biddersRequests.length).to.equal(2); + expect(biddersRequests[0]).to.equal('openx'); + expect(biddersRequests[1]).to.equal('closex'); + expect(biddersResponded.length).to.equal(2); + + let bidWonEventInfoList = v1Auction.events.filter(function (event) { + return event.eventType === BID_WON && event.args.auctionId === bidWonOpenX.auctionId; + }); + + expect(bidWonEventInfoList.length).to.equal(1); + + let openxBidder = v2Auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + expect(openxBidder.bidResponses[0]).to.contain({winner: true}); }); }); });