From 7a6fc76dae52f599e231b2643ada760f3b9e9130 Mon Sep 17 00:00:00 2001 From: Mikhail Necheporenko Date: Wed, 6 Jun 2018 20:52:25 +0300 Subject: [PATCH 1/2] Refactoring Prebid Analytics by Roxot --- modules/roxotAnalyticsAdapter.js | 568 +++++++++++++----- .../modules/roxotAnalyticsAdapter_spec.js | 470 ++++++++++++++- 2 files changed, 856 insertions(+), 182 deletions(-) diff --git a/modules/roxotAnalyticsAdapter.js b/modules/roxotAnalyticsAdapter.js index 65771ad245d..31239776982 100644 --- a/modules/roxotAnalyticsAdapter.js +++ b/modules/roxotAnalyticsAdapter.js @@ -2,233 +2,365 @@ import adapter from 'src/AnalyticsAdapter'; import CONSTANTS from 'src/constants.json'; import adaptermanager from 'src/adaptermanager'; import includes from 'core-js/library/fn/array/includes'; +import {ajaxBuilder} from 'src/ajax'; const utils = require('src/utils'); +let ajax = ajaxBuilder(0); -const url = '//pa.rxthdr.com/analytic'; +const DEFAULT_EVENT_URL = 'pa.rxthdr.com/v3'; +const DEFAULT_SERVER_CONFIG_URL = 'pa.rxthdr.com/v3'; const analyticsType = 'endpoint'; -let auctionInitConst = CONSTANTS.EVENTS.AUCTION_INIT; -let auctionEndConst = CONSTANTS.EVENTS.AUCTION_END; -let bidWonConst = CONSTANTS.EVENTS.BID_WON; -let bidRequestConst = CONSTANTS.EVENTS.BID_REQUESTED; -let bidAdjustmentConst = CONSTANTS.EVENTS.BID_ADJUSTMENT; -let bidResponseConst = CONSTANTS.EVENTS.BID_RESPONSE; +const { + EVENTS: { + AUCTION_INIT, + AUCTION_END, + BID_REQUESTED, + BID_ADJUSTMENT, + BIDDER_DONE, + BID_WON + } +} = CONSTANTS; -let initOptions = { publisherIds: [], utmTagData: [], adUnits: [] }; -let bidWon = {options: {}, events: []}; -let eventStack = {options: {}, events: []}; +const AUCTION_STATUS = { + 'RUNNING': 'running', + 'FINISHED': 'finished' +}; +const BIDDER_STATUS = { + 'REQUESTED': 'requested', + 'BID': 'bid', + 'NO_BID': 'noBid', + 'TIMEOUT': 'timeout' +}; +const ROXOT_EVENTS = { + 'AUCTION': 'a', + 'IMPRESSION': 'i', + 'BID_AFTER_TIMEOUT': 'bat' +}; -let auctionStatus = 'not_started'; +let initOptions = {}; let localStoragePrefix = 'roxot_analytics_'; + let utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; -let utmTimeoutKey = 'utm_timeout'; -let utmTimeout = 60 * 60 * 1000; -let sessionTimeout = 60 * 60 * 1000; -let sessionIdStorageKey = 'session_id'; -let sessionTimeoutKey = 'session_timeout'; - -function getParameterByName(param) { - let vars = {}; - window.location.href.replace(location.hash, '').replace( - /[?&]+([^=&]+)=?([^&]*)?/gi, - function(m, key, value) { - vars[key] = value !== undefined ? value : ''; - } - ); +let utmTtlKey = 'utm_ttl'; +let utmTtl = 60 * 60 * 1000; - return vars[param] ? vars[param] : ''; -} +let isNewKey = 'is_new_flag'; +let isNewTtl = 60 * 60 * 1000; -function buildSessionIdLocalStorageKey() { - return localStoragePrefix.concat(sessionIdStorageKey); -} +let auctionCache = {}; +let auctionTtl = 60 * 60 * 1000; -function buildSessionIdTimeoutLocalStorageKey() { - return localStoragePrefix.concat(sessionTimeoutKey); -} +let sendEventCache = []; +let sendEventTimeoutId = null; +let sendEventTimeoutTime = 1000; -function updateSessionId() { - if (isSessionIdTimeoutExpired()) { - let newSessionId = utils.generateUUID(); - localStorage.setItem(buildSessionIdLocalStorageKey(), newSessionId); +function detectDevice() { + if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { + return 'tablet'; + } + if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { + return 'mobile'; } - initOptions.sessionId = getSessionId(); - updateSessionIdTimeout(); + return 'desktop'; +} + +function checkIsNewFlag() { + let key = buildLocalStorageKey(isNewKey); + let lastUpdate = Number(localStorage.getItem(key)); + localStorage.setItem(key, Date.now()); + return Date.now() - lastUpdate > isNewTtl; } -function updateSessionIdTimeout() { - localStorage.setItem(buildSessionIdTimeoutLocalStorageKey(), Date.now()); +function updateUtmTimeout() { + localStorage.setItem(buildLocalStorageKey(utmTtlKey), Date.now()); } -function isSessionIdTimeoutExpired() { - let cpmSessionTimestamp = localStorage.getItem(buildSessionIdTimeoutLocalStorageKey()); - return Date.now() - cpmSessionTimestamp > sessionTimeout; +function isUtmTimeoutExpired() { + let utmTimestamp = localStorage.getItem(buildLocalStorageKey(utmTtlKey)); + return (Date.now() - utmTimestamp) > utmTtl; } -function getSessionId() { - return localStorage.getItem(buildSessionIdLocalStorageKey()) ? localStorage.getItem(buildSessionIdLocalStorageKey()) : ''; +function buildLocalStorageKey(key) { + return localStoragePrefix.concat(key); } -function updateUtmTimeout() { - localStorage.setItem(buildUtmLocalStorageTimeoutKey(), Date.now()); +function isSupportedAdUnit(adUnit) { + if (!initOptions.adUnits.length) { + return true; + } + + return includes(initOptions.adUnits, adUnit); } -function isUtmTimeoutExpired() { - let utmTimestamp = localStorage.getItem(buildUtmLocalStorageTimeoutKey()); - return (Date.now() - utmTimestamp) > utmTimeout; +function deleteOldAuctions() { + for (let auctionId in auctionCache) { + let auction = auctionCache[auctionId]; + if (Date.now() - auction.start > auctionTtl) { + delete auctionCache[auctionId]; + } + } } -function buildUtmLocalStorageTimeoutKey() { - return localStoragePrefix.concat(utmTimeoutKey); +function buildAuctionEntity(args) { + return { + 'id': args.auctionId, + 'start': args.timestamp, + 'timeout': args.timeout, + 'adUnits': {} + }; } -function buildUtmLocalStorageKey(utmMarkKey) { - return localStoragePrefix.concat(utmMarkKey); +function extractAdUnitCode(args) { + return args.adUnitCode.toLowerCase(); } -function checkOptions() { - if (typeof initOptions.publisherIds === 'undefined') { - return false; - } +function extractBidder(args) { + return args.bidder.toLowerCase(); +} - return initOptions.publisherIds.length > 0; +function buildAdUnitAuctionEntity(auction, bidRequest) { + return { + 'adUnit': extractAdUnitCode(bidRequest), + 'start': auction.start, + 'timeout': auction.timeout, + 'finish': 0, + 'status': AUCTION_STATUS.RUNNING, + 'bidders': {} + }; } -function checkAdUnitConfig() { - if (typeof initOptions.adUnits === 'undefined') { - return false; - } +function buildBidderRequest(auction, bidRequest) { + return { + 'bidder': extractBidder(bidRequest), + 'isAfterTimeout': auction.status === AUCTION_STATUS.FINISHED ? 1 : 0, + 'start': bidRequest.startTime || Date.now(), + 'finish': 0, + 'status': BIDDER_STATUS.REQUESTED, + 'cpm': -1, + 'size': { + 'width': 0, + 'height': 0 + }, + 'mediaType': '-', + 'source': bidRequest.source || 'client' + }; +} - return initOptions.adUnits.length > 0; +function buildBidAfterTimeout(adUnitAuction, args) { + return { + 'auction': utils.deepClone(adUnitAuction), + 'adUnit': extractAdUnitCode(args), + 'bidder': extractBidder(args), + 'cpm': args.cpm, + 'size': { + 'width': args.width || 0, + 'height': args.height || 0 + }, + 'mediaType': args.mediaType || '-', + 'start': args.requestTimestamp, + 'finish': args.responseTimestamp, + }; } -function buildBidWon(eventType, args) { - bidWon.options = initOptions; - if (checkAdUnitConfig()) { - if (includes(initOptions.adUnits, args.adUnitCode)) { - bidWon.events = [{ args: args, eventType: eventType }]; - } - } else { - bidWon.events = [{ args: args, eventType: eventType }]; - } +function buildImpression(adUnitAuction, args) { + return { + 'isNew': checkIsNewFlag() ? 1 : 0, + 'auction': utils.deepClone(adUnitAuction), + 'adUnit': extractAdUnitCode(args), + 'bidder': extractBidder(args), + 'cpm': args.cpm, + 'size': { + 'width': args.width, + 'height': args.height + }, + 'mediaType': args.mediaType, + 'source': args.source || 'client' + }; } -function buildEventStack() { - eventStack.options = initOptions; +function handleAuctionInit(args) { + auctionCache[args.auctionId] = buildAuctionEntity(args); + deleteOldAuctions(); } -function filterBidsByAdUnit(bids) { - var filteredBids = []; - bids.forEach(function (bid) { - if (includes(initOptions.adUnits, bid.placementCode)) { - filteredBids.push(bid); +function handleBidRequested(args) { + let auction = auctionCache[args.auctionId]; + args.bids.forEach(function (bidRequest) { + let adUnitCode = extractAdUnitCode(bidRequest); + let bidder = extractBidder(bidRequest); + if (!isSupportedAdUnit(adUnitCode)) { + return; } + auction['adUnits'][adUnitCode] = auction['adUnits'][adUnitCode] || buildAdUnitAuctionEntity(auction, bidRequest); + let adUnitAuction = auction['adUnits'][adUnitCode]; + adUnitAuction['bidders'][bidder] = adUnitAuction['bidders'][bidder] || buildBidderRequest(auction, bidRequest); }); - return filteredBids; } -function isValidEvent(eventType, adUnitCode) { - if (checkAdUnitConfig()) { - let validationEvents = [bidAdjustmentConst, bidResponseConst, bidWonConst]; - if (!includes(initOptions.adUnits, adUnitCode) && includes(validationEvents, eventType)) { - return false; - } +function handleBidAdjustment(args) { + let adUnitCode = extractAdUnitCode(args); + let bidder = extractBidder(args); + if (!isSupportedAdUnit(adUnitCode)) { + return; } - return true; -} -function isValidEventStack() { - if (eventStack.events.length > 0) { - return eventStack.events.some(function(event) { - return bidRequestConst === event.eventType || bidWonConst === event.eventType; - }); + let adUnitAuction = auctionCache[args.auctionId]['adUnits'][adUnitCode]; + if (adUnitAuction.status === AUCTION_STATUS.FINISHED) { + handleBidAfterTimeout(adUnitAuction, args); + return; } - return false; -} -function isValidBidWon() { - return bidWon.events.length > 0; + let bidderRequest = adUnitAuction['bidders'][bidder]; + if (bidderRequest.cpm < args.cpm) { + bidderRequest.cpm = args.cpm; + bidderRequest.finish = args.responseTimestamp; + bidderRequest.status = args.cpm === 0 ? BIDDER_STATUS.NO_BID : BIDDER_STATUS.BID; + bidderRequest.size.width = args.width || 0; + bidderRequest.size.height = args.height || 0; + bidderRequest.mediaType = args.mediaType || '-'; + bidderRequest.source = args.source || 'client'; + } } -function flushEventStack() { - eventStack.events = []; +function handleBidAfterTimeout(adUnitAuction, args) { + let bidder = extractBidder(args); + let bidderRequest = adUnitAuction['bidders'][bidder]; + let bidAfterTimeout = buildBidAfterTimeout(adUnitAuction, args); + + if (bidAfterTimeout.cpm > bidderRequest.cpm) { + bidderRequest.cpm = bidAfterTimeout.cpm; + bidderRequest.isAfterTimeout = 1; + bidderRequest.finish = bidAfterTimeout.finish; + bidderRequest.size = bidAfterTimeout.size; + bidderRequest.mediaType = bidAfterTimeout.mediaType; + bidderRequest.status = bidAfterTimeout.cpm === 0 ? BIDDER_STATUS.NO_BID : BIDDER_STATUS.BID; + } + + registerEvent(ROXOT_EVENTS.BID_AFTER_TIMEOUT, 'Bid After Timeout', bidAfterTimeout); } -function flushBidWon() { - bidWon.events = []; +function handleBidderDone(args) { + let auction = auctionCache[args.auctionId]; + + args.bids.forEach(function (bidDone) { + let adUnitCode = extractAdUnitCode(bidDone); + let bidder = extractBidder(bidDone); + if (!isSupportedAdUnit(adUnitCode)) { + return; + } + + let adUnitAuction = auction['adUnits'][adUnitCode]; + if (adUnitAuction.status === AUCTION_STATUS.FINISHED) { + return; + } + let bidderRequest = adUnitAuction['bidders'][bidder]; + if (bidderRequest.status !== BIDDER_STATUS.REQUESTED) { + return; + } + + bidderRequest.finish = Date.now(); + bidderRequest.status = BIDDER_STATUS.NO_BID; + bidderRequest.cpm = 0; + }); } -let roxotAdapter = Object.assign(adapter({url, analyticsType}), - { - track({eventType, args}) { - if (!checkOptions()) { - return; +function handleAuctionEnd(args) { + let auction = auctionCache[args.auctionId]; + if (!Object.keys(auction.adUnits).length) { + delete auctionCache[args.auctionId]; + } + + let finish = Date.now(); + auction.finish = finish; + for (let adUnit in auction.adUnits) { + let adUnitAuction = auction.adUnits[adUnit]; + adUnitAuction.finish = finish; + adUnitAuction.status = AUCTION_STATUS.FINISHED; + + for (let bidder in adUnitAuction.bidders) { + let bidderRequest = adUnitAuction.bidders[bidder]; + if (bidderRequest.status !== BIDDER_STATUS.REQUESTED) { + continue; } - let info = Object.assign({}, args); + bidderRequest.status = BIDDER_STATUS.TIMEOUT; + } + } - if (info && info.ad) { - info.ad = ''; - } + registerEvent(ROXOT_EVENTS.AUCTION, 'Auction', auction); +} - if (eventType === auctionInitConst) { - auctionStatus = 'started'; - flushEventStack(); - } +function handleBidWon(args) { + let adUnitCode = extractAdUnitCode(args); + if (!isSupportedAdUnit(adUnitCode)) { + return; + } + let adUnitAuction = auctionCache[args.auctionId]['adUnits'][adUnitCode]; + let impression = buildImpression(adUnitAuction, args); + registerEvent(ROXOT_EVENTS.IMPRESSION, 'Bid won', impression); +} - if (eventType === bidWonConst && auctionStatus === 'not_started') { - updateSessionId(); - buildBidWon(eventType, info); - if (isValidBidWon()) { - send(eventType, bidWon, 'bidWon'); - } - flushBidWon(); - return; - } +function handleOtherEvents(eventType, args) { + registerEvent(eventType, eventType, args); +} - if (eventType === auctionEndConst) { - updateSessionId(); - buildEventStack(eventType); - if (isValidEventStack()) { - send(eventType, eventStack, 'eventStack'); - } - flushEventStack(); - auctionStatus = 'not_started'; - } else { - pushEvent(eventType, info); - } - }, +let roxotAdapter = Object.assign(adapter({url: DEFAULT_EVENT_URL, analyticsType}), { + track({eventType, args}) { + switch (eventType) { + case AUCTION_INIT: + handleAuctionInit(args); + break; + case BID_REQUESTED: + handleBidRequested(args); + break; + case BID_ADJUSTMENT: + handleBidAdjustment(args); + break; + case BIDDER_DONE: + handleBidderDone(args); + break; + case AUCTION_END: + handleAuctionEnd(args); + break; + case BID_WON: + handleBidWon(args); + break; + default: + handleOtherEvents(eventType, args); + break; + } + }, - }); +}); roxotAdapter.originEnableAnalytics = roxotAdapter.enableAnalytics; roxotAdapter.enableAnalytics = function (config) { - initOptions = config.options; - initOptions.utmTagData = this.buildUtmTagData(); - utils.logInfo('Roxot Analytics enabled with config', initOptions); - roxotAdapter.originEnableAnalytics(config); + if (this.initConfig(config)) { + logInfo('Analytics adapter enabled', initOptions); + roxotAdapter.originEnableAnalytics(config); + } }; roxotAdapter.buildUtmTagData = function () { let utmTagData = {}; let utmTagsDetected = false; - utmTags.forEach(function(utmTagKey) { - let utmTagValue = getParameterByName(utmTagKey); + utmTags.forEach(function (utmTagKey) { + let utmTagValue = utils.getParameterByName(utmTagKey); if (utmTagValue !== '') { utmTagsDetected = true; } utmTagData[utmTagKey] = utmTagValue; }); - utmTags.forEach(function(utmTagKey) { + utmTags.forEach(function (utmTagKey) { if (utmTagsDetected) { - localStorage.setItem(buildUtmLocalStorageKey(utmTagKey), utmTagData[utmTagKey]); + localStorage.setItem(buildLocalStorageKey(utmTagKey), utmTagData[utmTagKey]); updateUtmTimeout(); } else { if (!isUtmTimeoutExpired()) { - utmTagData[utmTagKey] = localStorage.getItem(buildUtmLocalStorageKey(utmTagKey)) ? localStorage.getItem(buildUtmLocalStorageKey(utmTagKey)) : ''; + utmTagData[utmTagKey] = localStorage.getItem(buildLocalStorageKey(utmTagKey)) ? localStorage.getItem(buildLocalStorageKey(utmTagKey)) : ''; updateUtmTimeout(); } } @@ -236,33 +368,135 @@ roxotAdapter.buildUtmTagData = function () { return utmTagData; }; -function send(eventType, data, sendDataType) { - let fullUrl = url + '?publisherIds[]=' + initOptions.publisherIds.join('&publisherIds[]=') + '&host=' + window.location.hostname; - let xhr = new XMLHttpRequest(); - xhr.open('POST', fullUrl, true); - xhr.setRequestHeader('Content-Type', 'text/plain'); - xhr.withCredentials = true; - xhr.onreadystatechange = function(result) { - if (this.readyState != 4) return; +roxotAdapter.initConfig = function (config) { + let isCorrectConfig = true; + initOptions = {}; + initOptions.options = utils.deepClone(config.options); - utils.logInfo('Event ' + eventType + ' sent ' + sendDataType + ' to roxot prebid analytic with result' + result); + initOptions.publisherId = initOptions.options.publisherId || (initOptions.options.publisherIds[0]) || null; + if (!initOptions.publisherId) { + logError('"options.publisherId" is empty'); + isCorrectConfig = false; } - xhr.send(JSON.stringify(data)); + + initOptions.adUnits = initOptions.options.adUnits || []; + initOptions.adUnits = initOptions.adUnits.map(value => value.toLowerCase()); + initOptions.server = initOptions.options.server || DEFAULT_EVENT_URL; + initOptions.configServer = initOptions.options.configServer || (initOptions.options.server || DEFAULT_SERVER_CONFIG_URL); + initOptions.utmTagData = this.buildUtmTagData(); + initOptions.host = initOptions.options.host || window.location.hostname; + initOptions.device = detectDevice(); + + loadServerConfig(); + return isCorrectConfig; +}; + +roxotAdapter.getOptions = function () { + return initOptions; }; -function pushEvent(eventType, args) { - if (eventType === bidRequestConst) { - if (checkAdUnitConfig()) { - args.bids = filterBidsByAdUnit(args.bids); +function registerEvent(eventType, eventName, data) { + let eventData = { + eventType: eventType, + eventName: eventName, + data: data + }; + + sendEventCache.push(eventData); + + logInfo('Register event', eventData); + + (typeof initOptions.serverConfig === 'undefined') ? checkEventAfterTimeout() : checkSendEvent(); +} + +function checkSendEvent() { + if (sendEventTimeoutId) { + clearTimeout(sendEventTimeoutId); + sendEventTimeoutId = null; + } + + if (typeof initOptions.serverConfig === 'undefined') { + checkEventAfterTimeout(); + return; + } + + while (sendEventCache.length) { + let event = sendEventCache.shift(); + let isNeedSend = initOptions.serverConfig[event.eventType] || 0; + if (Number(isNeedSend) === 0) { + logInfo('Skip event ' + event.eventName, event); + continue; } - if (args.bids.length > 0) { - eventStack.events.push({ eventType: eventType, args: args }); + sendEvent(event.eventType, event.eventName, event.data); + } +} + +function checkEventAfterTimeout() { + if (sendEventTimeoutId) { + return; + } + + sendEventTimeoutId = setTimeout(checkSendEvent, sendEventTimeoutTime); +} + +function sendEvent(eventType, eventName, data) { + let url = '//' + initOptions.server + '/' + eventType + '?publisherId=' + initOptions.publisherId + '&host=' + initOptions.host; + let eventData = { + 'event': eventType, + 'eventName': eventName, + 'options': initOptions, + 'data': data + }; + + ajax( + url, + function () { + logInfo(eventName + ' sent', eventData); + }, + JSON.stringify(eventData), + { + contentType: 'text/plain', + method: 'POST', + withCredentials: true } - } else { - if (isValidEvent(eventType, args.adUnitCode)) { - eventStack.events.push({ eventType: eventType, args: args }); + ); +} + +function loadServerConfig() { + let url = '//' + initOptions.configServer + '/c' + '?publisherId=' + initOptions.publisherId + '&host=' + initOptions.host; + ajax( + url, + { + 'success': function (data) { + initOptions.serverConfig = JSON.parse(data); + }, + 'error': function () { + initOptions.serverConfig = {}; + initOptions.serverConfig[ROXOT_EVENTS.AUCTION] = 1; + initOptions.serverConfig[ROXOT_EVENTS.IMPRESSION] = 1; + initOptions.serverConfig[ROXOT_EVENTS.BID_AFTER_TIMEOUT] = 1; + initOptions.serverConfig['isError'] = 1; + } + }, + null, + { + contentType: 'text/json', + method: 'GET', + withCredentials: true } - } + ); +} + +function logInfo(message, meta) { + utils.logInfo(buildLogMessage(message), meta); +} + +function logError(message) { + utils.logError(buildLogMessage(message)); +} + +function buildLogMessage(message) { + return 'Roxot Prebid Analytics: ' + message; } adaptermanager.registerAnalyticsAdapter({ diff --git a/test/spec/modules/roxotAnalyticsAdapter_spec.js b/test/spec/modules/roxotAnalyticsAdapter_spec.js index f2db77892f6..d710a9c5d6e 100644 --- a/test/spec/modules/roxotAnalyticsAdapter_spec.js +++ b/test/spec/modules/roxotAnalyticsAdapter_spec.js @@ -1,30 +1,269 @@ import roxotAnalytic from 'modules/roxotAnalyticsAdapter'; -import { expect } from 'chai'; +import {expect} from 'chai'; + let events = require('src/events'); let adaptermanager = require('src/adaptermanager'); let constants = require('src/constants.json'); describe('Roxot Prebid Analytic', function () { let xhr; + let requests; + + let roxotConfigServerUrl = 'config-server'; + let roxotEventServerUrl = 'event-server'; + let publisherId = 'test_roxot_prebid_analytics_publisher_id'; + + let auctionId = '0ea14159-2058-4b87-a966-9d7652176a56'; + let timeout = 3000; + let auctionStartTimestamp = Date.now(); + let bidder = 'rubicon'; + + let bidAdUnit = 'div_with_bid'; + let noBidAdUnit = 'div_no_bid'; + let bidAfterTimeoutAdUnit = 'div_after_timeout'; + + let auctionInit = { + timestamp: auctionStartTimestamp, + auctionId: auctionId, + timeout: timeout + }; + + let bidRequested = { + auctionId: auctionId, + auctionStart: auctionStartTimestamp, + bidderCode: bidder, + bidderRequestId: '10340af0c7dc72', + bids: [ + { + adUnitCode: bidAdUnit, + auctionId: auctionId, + bidId: '298bf14ecbafb', + bidder: bidder, + bidderRequestId: '10340af0c7dc72', + sizes: [[300, 250]], + startTime: auctionStartTimestamp + 50, + transactionId: '7aafa3ee-a80a-46d7-a4a0-cbcba463d97a' + }, + { + adUnitCode: bidAfterTimeoutAdUnit, + auctionId: auctionId, + bidId: '36c6375e2dceba', + bidder: bidder, + bidderRequestId: '10340af0c7dc72', + sizes: [[300, 250]], + startTime: auctionStartTimestamp + 70, + transactionId: 'cf627df3-5828-4d3e-9dd0-c1733d328142' + }, + { + adUnitCode: noBidAdUnit, + auctionId: auctionId, + bidId: '36c6375e2dce21', + bidder: bidder, + bidderRequestId: '10340af0c7dc72', + sizes: [[300, 250]], + startTime: auctionStartTimestamp + 90, + transactionId: 'cf627df3-5828-4d3e-9dd0-c1737aafa3ee' + } + ], + doneCbCallCount: 1, + start: auctionStartTimestamp, + timeout: timeout + }; + + let bidAdjustmentWithBid = { + ad: 'html', + adId: '298bf14ecbafb', + adUnitCode: bidAdUnit, + auctionId: auctionId, + bidder: bidder, + bidderCode: bidder, + cpm: 1.01, + creativeId: '2249:92806132', + currency: 'USD', + height: 250, + mediaType: 'banner', + requestId: '298bf14ecbafb', + requestTimestamp: auctionStartTimestamp + 50, + responseTimestamp: auctionStartTimestamp + 50 + 421, + size: '300x250', + source: 'client', + status: 'rendered', + statusMessage: 'Bid available', + timeToRespond: 421, + ttl: 300, + width: 300 + }; + + let bidAdjustmentAfterTimeout = { + ad: 'html', + adId: '36c6375e2dceba', + adUnitCode: bidAfterTimeoutAdUnit, + auctionId: auctionId, + bidder: bidder, + bidderCode: bidder, + cpm: 2.02, + creativeId: '2249:92806132', + currency: 'USD', + height: 250, + mediaType: 'banner', + requestId: '36c6375e2dceba', + requestTimestamp: auctionStartTimestamp + 70, + responseTimestamp: auctionStartTimestamp + 70 + 6141, + size: '300x250', + source: 'client', + status: 'rendered', + statusMessage: 'Bid available', + timeToRespond: 6141, + ttl: 300, + width: 300 + }; + + let bidAdjustmentNoBid = { + ad: 'html', + adId: '36c6375e2dce21', + adUnitCode: noBidAdUnit, + auctionId: auctionId, + bidder: bidder, + bidderCode: bidder, + cpm: 0, + creativeId: '2249:92806132', + currency: 'USD', + height: 0, + mediaType: 'banner', + requestId: '36c6375e2dce21', + requestTimestamp: auctionStartTimestamp + 90, + responseTimestamp: auctionStartTimestamp + 90 + 215, + size: '300x250', + source: 'client', + status: 'rendered', + statusMessage: 'Bid available', + timeToRespond: 215, + ttl: 300, + width: 0 + }; + + let auctionEnd = { + auctionId: auctionId + }; + + let bidTimeout = [ + { + adUnitCode: bidAfterTimeoutAdUnit, + auctionId: auctionId, + bidId: '389444beed7361', + bidder: bidder, + timeout: timeout + } + ]; + + let bidResponseWithBid = bidAdjustmentWithBid; + let bidResponseAfterTimeout = bidAdjustmentAfterTimeout; + let bidResponseNoBid = bidAdjustmentNoBid; + let bidderDone = bidRequested; + let bidWon = bidAdjustmentWithBid; + before(() => { xhr = sinon.useFakeXMLHttpRequest(); - }) + xhr.onCreate = request => requests.push(request); + }); after(() => { - roxotAnalytic.disableAnalytics(); xhr.restore(); }); - describe('enableAnalytics', function () { + describe('correct build and send events', function () { beforeEach(() => { - sinon.spy(roxotAnalytic, 'track'); + requests = []; sinon.stub(events, 'getEvents').returns([]); }); + afterEach(() => { + roxotAnalytic.disableAnalytics(); + events.getEvents.restore(); + }); + it('should send prepared events to backend', function () { + adaptermanager.registerAnalyticsAdapter({ + code: 'roxot', + adapter: roxotAnalytic + }); + + adaptermanager.enableAnalytics({ + provider: 'roxot', + options: { + publisherId: publisherId, + configServer: roxotConfigServerUrl, + server: roxotEventServerUrl + } + }); + + expect(requests.length).to.equal(1); + expect(requests[0].url).to.equal('//' + roxotConfigServerUrl + '/c?publisherId=' + publisherId + '&host=localhost'); + requests[0].respond(200, {'Content-Type': 'application/json'}, '{"a": 1, "i": 1, "bat": 1}'); + + events.emit(constants.EVENTS.AUCTION_INIT, auctionInit); + events.emit(constants.EVENTS.BID_REQUESTED, bidRequested); + events.emit(constants.EVENTS.BID_ADJUSTMENT, bidAdjustmentWithBid); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponseWithBid); + events.emit(constants.EVENTS.BID_ADJUSTMENT, bidAdjustmentNoBid); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponseNoBid); + events.emit(constants.EVENTS.BID_TIMEOUT, bidTimeout); + events.emit(constants.EVENTS.AUCTION_END, auctionEnd); + events.emit(constants.EVENTS.BID_ADJUSTMENT, bidAdjustmentAfterTimeout); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponseAfterTimeout); + events.emit(constants.EVENTS.BIDDER_DONE, bidderDone); + events.emit(constants.EVENTS.BID_WON, bidWon); + + expect(requests.length).to.equal(4); + + expect(requests[1].url).to.equal('//' + roxotEventServerUrl + '/a?publisherId=' + publisherId + '&host=localhost'); + expect(requests[2].url).to.equal('//' + roxotEventServerUrl + '/bat?publisherId=' + publisherId + '&host=localhost'); + expect(requests[3].url).to.equal('//' + roxotEventServerUrl + '/i?publisherId=' + publisherId + '&host=localhost'); + + let auction = JSON.parse(requests[1].requestBody); + expect(auction).to.include.all.keys('event', 'eventName', 'options', 'data'); + expect(auction.event).to.equal('a'); + + expect(auction.data).to.include.all.keys('id', 'start', 'finish', 'timeout', 'adUnits'); + expect(auction.data.id).to.equal(auctionId); + expect(auction.data.timeout).to.equal(timeout); + + expect(auction.data.adUnits).to.include.all.keys(bidAdUnit, bidAfterTimeoutAdUnit, noBidAdUnit); + expect(auction.data.adUnits[bidAdUnit].bidders).to.have.property(bidder); + expect(auction.data.adUnits[bidAfterTimeoutAdUnit].bidders).to.have.property(bidder); + expect(auction.data.adUnits[noBidAdUnit].bidders).to.have.property(bidder); + + expect(auction.data.adUnits[bidAdUnit].bidders[bidder].status).to.equal('bid'); + expect(auction.data.adUnits[bidAfterTimeoutAdUnit].bidders[bidder].status).to.equal('timeout'); + expect(auction.data.adUnits[noBidAdUnit].bidders[bidder].status).to.equal('noBid'); + + let bidAfterTimeout = JSON.parse(requests[2].requestBody); + expect(bidAfterTimeout).to.include.all.keys('event', 'eventName', 'options', 'data'); + expect(bidAfterTimeout.event).to.equal('bat'); + + expect(bidAfterTimeout.data).to.include.all.keys('start', 'finish', 'mediaType', 'adUnit', 'bidder', 'cpm', 'size', 'auction'); + expect(bidAfterTimeout.data.adUnit).to.equal(bidAfterTimeoutAdUnit); + expect(bidAfterTimeout.data.bidder).to.equal(bidder); + expect(bidAfterTimeout.data.cpm).to.equal(bidAdjustmentAfterTimeout.cpm); + + let impression = JSON.parse(requests[3].requestBody); + expect(impression).to.include.all.keys('event', 'eventName', 'options', 'data'); + expect(impression.event).to.equal('i'); + + expect(impression.data).to.include.all.keys('mediaType', 'adUnit', 'bidder', 'cpm', 'size', 'auction', 'isNew'); + expect(impression.data.adUnit).to.equal(bidAdUnit); + expect(impression.data.bidder).to.equal(bidder); + expect(impression.data.cpm).to.equal(bidAdjustmentWithBid.cpm); + }); + }); + describe('support ad unit filter', function () { + beforeEach(() => { + requests = []; + sinon.stub(events, 'getEvents').returns([]); + }); afterEach(() => { - roxotAnalytic.track.restore(); + roxotAnalytic.disableAnalytics(); events.getEvents.restore(); }); - it('should catch all events', function () { + it('should not send event for blocked ad unit', function () { adaptermanager.registerAnalyticsAdapter({ code: 'roxot', adapter: roxotAnalytic @@ -33,19 +272,212 @@ describe('Roxot Prebid Analytic', function () { adaptermanager.enableAnalytics({ provider: 'roxot', options: { - publisherIds: ['test_roxot_prebid_analytid_publisher_id'] + publisherId: publisherId, + configServer: roxotConfigServerUrl, + server: roxotEventServerUrl, + adUnits: [noBidAdUnit, bidAfterTimeoutAdUnit] } }); - events.emit(constants.EVENTS.AUCTION_INIT, {}); - events.emit(constants.EVENTS.AUCTION_END, {}); - events.emit(constants.EVENTS.BID_REQUESTED, {}); - events.emit(constants.EVENTS.BID_RESPONSE, {}); - events.emit(constants.EVENTS.BID_WON, {}); + expect(requests.length).to.equal(1); + expect(requests[0].url).to.equal('//' + roxotConfigServerUrl + '/c?publisherId=' + publisherId + '&host=localhost'); + requests[0].respond(200, {'Content-Type': 'application/json'}, '{"a": 1, "i": 1, "bat": 1}'); + + events.emit(constants.EVENTS.AUCTION_INIT, auctionInit); + events.emit(constants.EVENTS.BID_REQUESTED, bidRequested); + events.emit(constants.EVENTS.BID_ADJUSTMENT, bidAdjustmentWithBid); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponseWithBid); + events.emit(constants.EVENTS.BID_ADJUSTMENT, bidAdjustmentNoBid); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponseNoBid); + events.emit(constants.EVENTS.BID_TIMEOUT, bidTimeout); + events.emit(constants.EVENTS.AUCTION_END, auctionEnd); + events.emit(constants.EVENTS.BID_ADJUSTMENT, bidAdjustmentAfterTimeout); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponseAfterTimeout); + events.emit(constants.EVENTS.BIDDER_DONE, bidderDone); + events.emit(constants.EVENTS.BID_WON, bidWon); + + expect(requests.length).to.equal(3); + + expect(requests[1].url).to.equal('//' + roxotEventServerUrl + '/a?publisherId=' + publisherId + '&host=localhost'); + expect(requests[2].url).to.equal('//' + roxotEventServerUrl + '/bat?publisherId=' + publisherId + '&host=localhost'); + + let auction = JSON.parse(requests[1].requestBody); + expect(auction.data.adUnits).to.include.all.keys(noBidAdUnit, bidAfterTimeoutAdUnit); + expect(auction.data.adUnits).to.not.include.all.keys(bidAdUnit); + }); + }); + + describe('should correct parse config', function () { + beforeEach(() => { + requests = []; + sinon.stub(events, 'getEvents').returns([]); + }); + + afterEach(() => { + roxotAnalytic.disableAnalytics(); + events.getEvents.restore(); + }); + + it('correct parse publisher config', function () { + adaptermanager.registerAnalyticsAdapter({ + code: 'roxot', + adapter: roxotAnalytic + }); + + let publisherOptions = { + publisherId: publisherId, + configServer: roxotConfigServerUrl, + server: roxotEventServerUrl, + anything: 'else', + }; + + adaptermanager.enableAnalytics({ + provider: 'roxot', + options: publisherOptions + }); + + expect(roxotAnalytic.getOptions().options).to.deep.equal(publisherOptions); + }); + + it('support deprecated options', function () { + adaptermanager.registerAnalyticsAdapter({ + code: 'roxot', + adapter: roxotAnalytic + }); + + let publisherOptions = { + publisherIds: [publisherId], + }; + + adaptermanager.enableAnalytics({ + provider: 'roxot', + options: publisherOptions + }); + + expect(roxotAnalytic.getOptions().options).to.deep.equal(publisherOptions); + expect(roxotAnalytic.getOptions().publisherId).to.equal(publisherId); + }); + + it('support default end-points', function () { + adaptermanager.registerAnalyticsAdapter({ + code: 'roxot', + adapter: roxotAnalytic + }); + + let publisherOptions = { + publisherId: publisherId, + }; + + adaptermanager.enableAnalytics({ + provider: 'roxot', + options: publisherOptions + }); + + expect(roxotAnalytic.getOptions().configServer).to.equal('pa.rxthdr.com/v3'); + expect(roxotAnalytic.getOptions().server).to.equal('pa.rxthdr.com/v3'); + }); + + it('support custom config end-point', function () { + adaptermanager.registerAnalyticsAdapter({ + code: 'roxot', + adapter: roxotAnalytic + }); + + let publisherOptions = { + publisherId: publisherId, + configServer: roxotConfigServerUrl + }; + + adaptermanager.enableAnalytics({ + provider: 'roxot', + options: publisherOptions + }); + + expect(roxotAnalytic.getOptions().configServer).to.equal(roxotConfigServerUrl); + expect(roxotAnalytic.getOptions().server).to.equal('pa.rxthdr.com/v3'); + }); + + it('support custom config and event end-point', function () { + adaptermanager.registerAnalyticsAdapter({ + code: 'roxot', + adapter: roxotAnalytic + }); + + let publisherOptions = { + publisherId: publisherId, + server: roxotEventServerUrl + }; + + adaptermanager.enableAnalytics({ + provider: 'roxot', + options: publisherOptions + }); + + expect(roxotAnalytic.getOptions().configServer).to.equal(roxotEventServerUrl); + expect(roxotAnalytic.getOptions().server).to.equal(roxotEventServerUrl); + }); + + it('support different config and event end-points', function () { + adaptermanager.registerAnalyticsAdapter({ + code: 'roxot', + adapter: roxotAnalytic + }); + + let publisherOptions = { + publisherId: publisherId, + configServer: roxotConfigServerUrl, + server: roxotEventServerUrl + }; + + adaptermanager.enableAnalytics({ + provider: 'roxot', + options: publisherOptions + }); - sinon.assert.callCount(roxotAnalytic.track, 5); + expect(roxotAnalytic.getOptions().configServer).to.equal(roxotConfigServerUrl); + expect(roxotAnalytic.getOptions().server).to.equal(roxotEventServerUrl); + }); + + it('support adUnit filter', function () { + adaptermanager.registerAnalyticsAdapter({ + code: 'roxot', + adapter: roxotAnalytic + }); + + let publisherOptions = { + publisherId: publisherId, + adUnits: ['div1', 'div2'] + }; + + adaptermanager.enableAnalytics({ + provider: 'roxot', + options: publisherOptions + }); + + expect(roxotAnalytic.getOptions().adUnits).to.deep.equal(['div1', 'div2']); + }); + + it('support fail loading server config', function () { + adaptermanager.registerAnalyticsAdapter({ + code: 'roxot', + adapter: roxotAnalytic + }); + + let publisherOptions = { + publisherId: publisherId + }; + + adaptermanager.enableAnalytics({ + provider: 'roxot', + options: publisherOptions + }); + + requests[0].respond(500); + + expect(roxotAnalytic.getOptions().serverConfig).to.deep.equal({a: 1, i: 1, bat: 1, isError: 1}); }); }); + describe('build utm tag data', () => { beforeEach(() => { localStorage.setItem('roxot_analytics_utm_source', 'utm_source'); @@ -53,7 +485,15 @@ describe('Roxot Prebid Analytic', function () { localStorage.setItem('roxot_analytics_utm_campaign', ''); localStorage.setItem('roxot_analytics_utm_term', ''); localStorage.setItem('roxot_analytics_utm_content', ''); - localStorage.setItem('roxot_analytics_utm_timeout', Date.now()); + localStorage.setItem('roxot_analytics_utm_ttl', Date.now()); + }); + afterEach(() => { + localStorage.removeItem('roxot_analytics_utm_source'); + localStorage.removeItem('roxot_analytics_utm_medium'); + localStorage.removeItem('roxot_analytics_utm_campaign'); + localStorage.removeItem('roxot_analytics_utm_term'); + localStorage.removeItem('roxot_analytics_utm_content'); + localStorage.removeItem('roxot_analytics_utm_ttl'); }); it('should build utm data from local storage', () => { let utmTagData = roxotAnalytic.buildUtmTagData(); From df66ba1b2325c54afcfdb5c210ebb21677640464 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Thu, 7 Jun 2018 11:31:37 -0600 Subject: [PATCH 2/2] remove adaptermanager from analytics adapter tests --- .../modules/roxotAnalyticsAdapter_spec.js | 71 +++---------------- .../modules/rubiconAnalyticsAdapter_spec.js | 1 - 2 files changed, 10 insertions(+), 62 deletions(-) diff --git a/test/spec/modules/roxotAnalyticsAdapter_spec.js b/test/spec/modules/roxotAnalyticsAdapter_spec.js index d710a9c5d6e..9a80d2d0597 100644 --- a/test/spec/modules/roxotAnalyticsAdapter_spec.js +++ b/test/spec/modules/roxotAnalyticsAdapter_spec.js @@ -2,7 +2,6 @@ import roxotAnalytic from 'modules/roxotAnalyticsAdapter'; import {expect} from 'chai'; let events = require('src/events'); -let adaptermanager = require('src/adaptermanager'); let constants = require('src/constants.json'); describe('Roxot Prebid Analytic', function () { @@ -180,12 +179,7 @@ describe('Roxot Prebid Analytic', function () { events.getEvents.restore(); }); it('should send prepared events to backend', function () { - adaptermanager.registerAnalyticsAdapter({ - code: 'roxot', - adapter: roxotAnalytic - }); - - adaptermanager.enableAnalytics({ + roxotAnalytic.enableAnalytics({ provider: 'roxot', options: { publisherId: publisherId, @@ -264,12 +258,7 @@ describe('Roxot Prebid Analytic', function () { events.getEvents.restore(); }); it('should not send event for blocked ad unit', function () { - adaptermanager.registerAnalyticsAdapter({ - code: 'roxot', - adapter: roxotAnalytic - }); - - adaptermanager.enableAnalytics({ + roxotAnalytic.enableAnalytics({ provider: 'roxot', options: { publisherId: publisherId, @@ -319,11 +308,6 @@ describe('Roxot Prebid Analytic', function () { }); it('correct parse publisher config', function () { - adaptermanager.registerAnalyticsAdapter({ - code: 'roxot', - adapter: roxotAnalytic - }); - let publisherOptions = { publisherId: publisherId, configServer: roxotConfigServerUrl, @@ -331,7 +315,7 @@ describe('Roxot Prebid Analytic', function () { anything: 'else', }; - adaptermanager.enableAnalytics({ + roxotAnalytic.enableAnalytics({ provider: 'roxot', options: publisherOptions }); @@ -340,16 +324,11 @@ describe('Roxot Prebid Analytic', function () { }); it('support deprecated options', function () { - adaptermanager.registerAnalyticsAdapter({ - code: 'roxot', - adapter: roxotAnalytic - }); - let publisherOptions = { publisherIds: [publisherId], }; - adaptermanager.enableAnalytics({ + roxotAnalytic.enableAnalytics({ provider: 'roxot', options: publisherOptions }); @@ -359,16 +338,11 @@ describe('Roxot Prebid Analytic', function () { }); it('support default end-points', function () { - adaptermanager.registerAnalyticsAdapter({ - code: 'roxot', - adapter: roxotAnalytic - }); - let publisherOptions = { publisherId: publisherId, }; - adaptermanager.enableAnalytics({ + roxotAnalytic.enableAnalytics({ provider: 'roxot', options: publisherOptions }); @@ -378,17 +352,12 @@ describe('Roxot Prebid Analytic', function () { }); it('support custom config end-point', function () { - adaptermanager.registerAnalyticsAdapter({ - code: 'roxot', - adapter: roxotAnalytic - }); - let publisherOptions = { publisherId: publisherId, configServer: roxotConfigServerUrl }; - adaptermanager.enableAnalytics({ + roxotAnalytic.enableAnalytics({ provider: 'roxot', options: publisherOptions }); @@ -398,17 +367,12 @@ describe('Roxot Prebid Analytic', function () { }); it('support custom config and event end-point', function () { - adaptermanager.registerAnalyticsAdapter({ - code: 'roxot', - adapter: roxotAnalytic - }); - let publisherOptions = { publisherId: publisherId, server: roxotEventServerUrl }; - adaptermanager.enableAnalytics({ + roxotAnalytic.enableAnalytics({ provider: 'roxot', options: publisherOptions }); @@ -418,18 +382,13 @@ describe('Roxot Prebid Analytic', function () { }); it('support different config and event end-points', function () { - adaptermanager.registerAnalyticsAdapter({ - code: 'roxot', - adapter: roxotAnalytic - }); - let publisherOptions = { publisherId: publisherId, configServer: roxotConfigServerUrl, server: roxotEventServerUrl }; - adaptermanager.enableAnalytics({ + roxotAnalytic.enableAnalytics({ provider: 'roxot', options: publisherOptions }); @@ -439,17 +398,12 @@ describe('Roxot Prebid Analytic', function () { }); it('support adUnit filter', function () { - adaptermanager.registerAnalyticsAdapter({ - code: 'roxot', - adapter: roxotAnalytic - }); - let publisherOptions = { publisherId: publisherId, adUnits: ['div1', 'div2'] }; - adaptermanager.enableAnalytics({ + roxotAnalytic.enableAnalytics({ provider: 'roxot', options: publisherOptions }); @@ -458,16 +412,11 @@ describe('Roxot Prebid Analytic', function () { }); it('support fail loading server config', function () { - adaptermanager.registerAnalyticsAdapter({ - code: 'roxot', - adapter: roxotAnalytic - }); - let publisherOptions = { publisherId: publisherId }; - adaptermanager.enableAnalytics({ + roxotAnalytic.enableAnalytics({ provider: 'roxot', options: publisherOptions }); diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 21ed1cfa522..3af82a1fb62 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -1,4 +1,3 @@ -import adaptermanager from 'src/adaptermanager'; import rubiconAnalyticsAdapter, { SEND_TIMEOUT } from 'modules/rubiconAnalyticsAdapter'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config';