diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js new file mode 100644 index 00000000000..9cf09209965 --- /dev/null +++ b/modules/medianetAnalyticsAdapter.js @@ -0,0 +1,622 @@ +import adapter from '../src/AnalyticsAdapter'; +import adapterManager from '../src/adapterManager'; +import CONSTANTS from '../src/constants.json'; +import * as utils from '../src/utils'; +import { ajax } from '../src/ajax'; +import { getRefererInfo } from '../src/refererDetection'; +import * as url from '../src/url'; +import { getPriceGranularity, AUCTION_IN_PROGRESS, AUCTION_COMPLETED } from '../src/auction' + +const analyticsType = 'endpoint'; +const ENDPOINT = 'https://pb-logs.media.net/log?logid=kfk&evtid=prebid_analytics_events_client'; +const CONFIG_URL = 'https://prebid.media.net/rtb/prebid/analytics/config'; +const EVENT_PIXEL_URL = 'https://qsearch-a.akamaihd.net/log'; +const DEFAULT_LOGGING_PERCENT = 50; +const PRICE_GRANULARITY = { + 'auto': 'pbAg', + 'custom': 'pbCg', + 'dense': 'pbDg', + 'low': 'pbLg', + 'medium': 'pbMg', + 'high': 'pbHg', +}; + +const MEDIANET_BIDDER_CODE = 'medianet'; +// eslint-disable-next-line no-undef +const PREBID_VERSION = $$PREBID_GLOBAL$$.version; +const ERROR_CONFIG_JSON_PARSE = 'analytics_config_parse_fail'; +const ERROR_CONFIG_FETCH = 'analytics_config_ajax_fail'; +const BID_SUCCESS = 1; +const BID_NOBID = 2; +const BID_TIMEOUT = 3; +const DUMMY_BIDDER = '-2'; + +const CONFIG_PENDING = 0; +const CONFIG_PASS = 1; +const CONFIG_ERROR = 3; + +const VALID_URL_KEY = ['canonical_url', 'og_url', 'twitter_url']; +const DEFAULT_URL_KEY = 'page'; + +let auctions = {}; +let config; +let pageDetails; +let logsQueue = []; + +class ErrorLogger { + constructor(event, additionalData) { + this.event = event; + this.logid = 'kfk'; + this.evtid = 'projectevents'; + this.project = 'prebidanalytics'; + this.dn = pageDetails.domain || ''; + this.requrl = pageDetails.requrl || ''; + this.event = this.event; + this.pbversion = PREBID_VERSION; + this.cid = config.cid || ''; + this.rd = additionalData; + } + + send() { + let url = EVENT_PIXEL_URL + '?' + formatQS(this); + utils.triggerPixel(url); + } +} + +class Configure { + constructor(cid) { + this.cid = cid; + this.pubLper = -1; + this.ajaxState = CONFIG_PENDING; + this.loggingPercent = DEFAULT_LOGGING_PERCENT; + this.urlToConsume = DEFAULT_URL_KEY; + this.debug = false; + this.gdprConsent = undefined; + this.uspConsent = undefined; + } + + set publisherLper(plper) { + this.pubLper = plper; + } + + getLoggingData() { + return { + cid: this.cid, + lper: Math.round(100 / this.loggingPercent), + plper: this.pubLper, + gdpr: this.gdprConsent, + ccpa: this.uspConsent, + ajx: this.ajaxState, + pbv: PREBID_VERSION, + flt: 1, + } + } + + _configURL() { + return CONFIG_URL + '?cid=' + encodeURIComponent(this.cid) + '&dn=' + encodeURIComponent(pageDetails.domain); + } + + _parseResponse(response) { + try { + response = JSON.parse(response); + if (isNaN(response.percentage)) { + throw new Error('not a number'); + } + this.loggingPercent = response.percentage; + this.urlToConsume = VALID_URL_KEY.includes(response.urlKey) ? response.urlKey : this.urlToConsume; + this.ajaxState = CONFIG_PASS; + } catch (e) { + this.ajaxState = CONFIG_ERROR; + /* eslint no-new: "error" */ + new ErrorLogger(ERROR_CONFIG_JSON_PARSE, e).send(); + } + } + + _errorFetch() { + this.ajaxState = CONFIG_ERROR; + /* eslint no-new: "error" */ + new ErrorLogger(ERROR_CONFIG_FETCH).send(); + } + + init() { + // Forces Logging % to 100% + let urlObj = url.parse(pageDetails.page); + if (utils.deepAccess(urlObj, 'search.medianet_test') || urlObj.hostname === 'localhost') { + this.loggingPercent = 100; + this.ajaxState = CONFIG_PASS; + this.debug = true; + return; + } + ajax( + this._configURL(), + { + success: this._parseResponse.bind(this), + error: this._errorFetch.bind(this) + } + ); + } +} + +class PageDetail { + constructor () { + const canonicalUrl = this._getUrlFromSelector('link[rel="canonical"]', 'href'); + const ogUrl = this._getUrlFromSelector('meta[property="og:url"]', 'content'); + const twitterUrl = this._getUrlFromSelector('meta[name="twitter:url"]', 'content'); + const refererInfo = getRefererInfo(); + + this.domain = url.parse(refererInfo.referer).host; + this.page = refererInfo.referer; + this.is_top = refererInfo.reachedTop; + this.referrer = this._getTopWindowReferrer(); + this.canonical_url = canonicalUrl; + this.og_url = ogUrl; + this.twitter_url = twitterUrl; + this.screen = this._getWindowSize() + } + + _getTopWindowReferrer() { + try { + return window.top.document.referrer; + } catch (e) { + return document.referrer; + } + } + + _getWindowSize() { + let w = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth || -1; + let h = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || -1; + return `${w}x${h}`; + } + + _getAttributeFromSelector(selector, attribute) { + try { + let doc = utils.getWindowTop().document; + let element = doc.querySelector(selector); + if (element !== null && element[attribute]) { + return element[attribute]; + } + } catch (e) {} + } + + _getAbsoluteUrl(url) { + let aTag = utils.getWindowTop().document.createElement('a'); + aTag.href = url; + + return aTag.href; + } + + _getUrlFromSelector(selector, attribute) { + let attr = this._getAttributeFromSelector(selector, attribute); + return attr && this._getAbsoluteUrl(attr); + } + + getLoggingData() { + return { + requrl: this[config.urlToConsume] || this.page, + dn: this.domain, + ref: this.referrer, + screen: this.screen + } + } +} + +class AdSlot { + constructor(mediaTypes, bannerSizes, tmax, supplyAdCode, adext) { + this.mediaTypes = mediaTypes; + this.bannerSizes = bannerSizes; + this.tmax = tmax; + this.supplyAdCode = supplyAdCode; + this.adext = adext; + this.logged = false; + this.targeting = undefined; + } + + getLoggingData() { + return Object.assign({ + supcrid: this.supplyAdCode, + mediaTypes: this.mediaTypes && this.mediaTypes.join('|'), + szs: this.bannerSizes.join('|'), + tmax: this.tmax, + targ: JSON.stringify(this.targeting) + }, + this.adext && {'adext': JSON.stringify(this.adext)}, + ); + } +} + +class Bid { + constructor(bidId, bidder, src, start, supplyAdCode) { + this.bidId = bidId; + this.bidder = bidder; + this.src = src; + this.start = start; + this.supplyAdCode = supplyAdCode; + this.iwb = 0; + this.winner = 0; + this.status = bidder === DUMMY_BIDDER ? BID_SUCCESS : BID_TIMEOUT; + this.ext = {}; + this.originalCpm = undefined; + this.cpm = undefined; + this.dfpbd = undefined; + this.width = undefined; + this.height = undefined; + this.mediaType = undefined; + this.timeToRespond = undefined; + this.dealId = undefined; + this.creativeId = undefined; + this.adId = undefined; + this.currency = undefined; + this.crid = undefined; + this.pubcrid = undefined; + this.mpvid = undefined; + } + + get size() { + if (!this.width || !this.height) { + return ''; + } + return this.width + 'x' + this.height; + } + + getLoggingData() { + return { + pvnm: this.bidder, + src: this.src, + ogbdp: this.originalCpm, + bdp: this.cpm, + cbdp: this.dfpbd, + dfpbd: this.dfpbd, + size: this.size, + mtype: this.mediaType, + dId: this.dealId, + winner: this.winner, + curr: this.currency, + rests: this.timeToRespond, + status: this.status, + iwb: this.iwb, + crid: this.crid, + pubcrid: this.pubcrid, + mpvid: this.mpvid, + ext: JSON.stringify(this.ext) + } + } +} + +class Auction { + constructor(acid) { + this.acid = acid; + this.status = AUCTION_IN_PROGRESS; + this.bids = []; + this.adSlots = {}; + this.auctionInitTime = undefined; + this.auctionStartTime = undefined; + this.setTargetingTime = undefined; + this.auctionEndTime = undefined; + this.bidWonTime = undefined; + } + + hasEnded() { + return this.status === AUCTION_COMPLETED; + } + + getLoggingData() { + return { + sts: this.auctionStartTime - this.auctionInitTime, + ets: this.auctionEndTime - this.auctionInitTime, + tts: this.setTargetingTime - this.auctionInitTime, + wts: this.bidWonTime - this.auctionInitTime, + aucstatus: this.status + } + } + + addSlot(supplyAdCode, { mediaTypes, bannerSizes, tmax, adext }) { + if (supplyAdCode && this.adSlots[supplyAdCode] === undefined) { + this.adSlots[supplyAdCode] = new AdSlot(mediaTypes, bannerSizes, tmax, supplyAdCode, adext); + this.addBid( + new Bid('-1', DUMMY_BIDDER, 'client', '-1', supplyAdCode) + ); + } + } + + addBid(bid) { + this.bids.push(bid); + } + + findBid(key, value) { + return this.bids.filter(bid => { + return bid[key] === value + })[0]; + } + + getAdslotBids(adslot) { + return this.bids + .filter((bid) => bid.supplyAdCode === adslot) + .map((bid) => bid.getLoggingData()); + } + + getWinnerAdslotBid(adslot) { + return this.getAdslotBids(adslot).filter((bid) => bid.winner); + } +} + +function auctionInitHandler({auctionId, timestamp}) { + if (auctionId && auctions[auctionId] === undefined) { + auctions[auctionId] = new Auction(auctionId); + auctions[auctionId].auctionInitTime = timestamp; + } +} + +function bidRequestedHandler({ auctionId, auctionStart, bids, start, timeout, uspConsent, gdprConsent }) { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + config.gdprConsent = config.gdprConsent || gdprConsent; + config.uspConsent = config.uspConsent || uspConsent; + + bids.forEach(bid => { + const { adUnitCode, bidder, mediaTypes, sizes, bidId, src } = bid; + if (!auctions[auctionId].adSlots[adUnitCode]) { + auctions[auctionId].auctionStartTime = auctionStart; + auctions[auctionId].addSlot( + adUnitCode, + Object.assign({}, + (mediaTypes instanceof Object) && { mediaTypes: Object.keys(mediaTypes) }, + { bannerSizes: utils.deepAccess(mediaTypes, 'banner.sizes') || sizes || [] }, + { adext: utils.deepAccess(mediaTypes, 'banner.ext') || '' }, + { tmax: timeout } + ) + ); + } + let bidObj = new Bid(bidId, bidder, src, start, adUnitCode); + auctions[auctionId].addBid(bidObj); + if (bidder === MEDIANET_BIDDER_CODE) { + bidObj.crid = utils.deepAccess(bid, 'params.crid'); + bidObj.pubcrid = utils.deepAccess(bid, 'params.crid'); + } + }); +} + +function bidResponseHandler(bid) { + const { width, height, mediaType, cpm, requestId, timeToRespond, auctionId, dealId } = bid; + const {originalCpm, bidderCode, creativeId, adId, currency} = bid; + + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', requestId); + if (!(bidObj instanceof Bid)) { + return; + } + Object.assign( + bidObj, + { cpm, width, height, mediaType, timeToRespond, dealId, creativeId }, + { adId, currency, originalCpm } + ); + let dfpbd = utils.deepAccess(bid, 'adserverTargeting.hb_pb'); + if (!dfpbd) { + let priceGranularity = getPriceGranularity(mediaType, bid); + let priceGranularityKey = PRICE_GRANULARITY[priceGranularity]; + dfpbd = bid[priceGranularityKey] || cpm; + } + bidObj.dfpbd = dfpbd; + bidObj.status = BID_SUCCESS; + + if (bidderCode === MEDIANET_BIDDER_CODE && bid.ext instanceof Object) { + Object.assign( + bidObj, + { 'ext': bid.ext }, + { 'mpvid': bid.ext.pvid }, + bid.ext.crid && { 'crid': bid.ext.crid } + ); + } +} + +function noBidResponseHandler({ auctionId, bidId }) { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + if (auctions[auctionId].hasEnded()) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', bidId); + if (!(bidObj instanceof Bid)) { + return; + } + bidObj.status = BID_NOBID; +} + +function bidTimeoutHandler(timedOutBids) { + timedOutBids.map(({bidId, auctionId}) => { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', bidId); + if (!(bidObj instanceof Bid)) { + return; + } + bidObj.status = BID_TIMEOUT; + }) +} + +function auctionEndHandler({ auctionId, auctionEnd }) { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + auctions[auctionId].status = AUCTION_COMPLETED; + auctions[auctionId].auctionEndTime = auctionEnd; +} + +function setTargetingHandler(params) { + for (const adunit of Object.keys(params)) { + for (const auctionId of Object.keys(auctions)) { + let auctionObj = auctions[auctionId]; + let adunitObj = auctionObj.adSlots[adunit]; + if (!(adunitObj instanceof AdSlot)) { + continue; + } + adunitObj.targeting = params[adunit]; + auctionObj.setTargetingTime = Date.now(); + let targetingObj = Object.keys(params[adunit]).reduce((result, key) => { + if (key.indexOf('hb_adid') !== -1) { + result[key] = params[adunit][key] + } + return result; + }, {}); + let bidAdIds = Object.keys(targetingObj).map(k => targetingObj[k]); + auctionObj.bids.filter((bid) => bidAdIds.indexOf(bid.adId) !== -1).map(function(bid) { + bid.iwb = 1; + }); + sendEvent(auctionId, adunit, false); + } + } +} + +function bidWonHandler(bid) { + const { requestId, auctionId, adUnitCode } = bid; + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', requestId); + if (!(bidObj instanceof Bid)) { + return; + } + auctions[auctionId].bidWonTime = Date.now(); + bidObj.winner = 1; + sendEvent(auctionId, adUnitCode, true); +} + +function isSampled() { + return Math.random() * 100 < parseFloat(config.loggingPercent); +} + +function isValidAuctionAdSlot(acid, adtag) { + return (auctions[acid] instanceof Auction) && (auctions[acid].adSlots[adtag] instanceof AdSlot); +} + +function sendEvent(id, adunit, isBidWonEvent) { + if (!isValidAuctionAdSlot(id, adunit)) { + return; + } + if (isBidWonEvent) { + fireAuctionLog(id, adunit, isBidWonEvent); + } else if (isSampled() && !auctions[id].adSlots[adunit].logged) { + auctions[id].adSlots[adunit].logged = true; + fireAuctionLog(id, adunit, isBidWonEvent); + } +} + +function getCommonLoggingData(acid, adtag) { + let commonParams = Object.assign(pageDetails.getLoggingData(), config.getLoggingData()); + let adunitParams = auctions[acid].adSlots[adtag].getLoggingData(); + let auctionParams = auctions[acid].getLoggingData(); + return Object.assign(commonParams, adunitParams, auctionParams); +} + +function fireAuctionLog(acid, adtag, isBidWonEvent) { + let commonParams = getCommonLoggingData(acid, adtag); + let targeting = utils.deepAccess(commonParams, 'targ'); + + Object.keys(commonParams).forEach((key) => (commonParams[key] == null) && delete commonParams[key]); + delete commonParams.targ; + + let bidParams; + + if (isBidWonEvent) { + bidParams = auctions[acid].getWinnerAdslotBid(adtag); + commonParams.lper = 1; + } else { + bidParams = auctions[acid].getAdslotBids(adtag).map(({winner, ...restParams}) => restParams); + delete commonParams.wts; + } + let mnetPresent = bidParams.filter(b => b.pvnm === MEDIANET_BIDDER_CODE).length > 0; + if (!mnetPresent) { + bidParams = bidParams.map(({mpvid, crid, ext, pubcrid, ...restParams}) => restParams); + } + + let url = formatQS(commonParams) + '&'; + bidParams.forEach(function(bidParams) { + url = url + formatQS(bidParams) + '&'; + }); + url = url + formatQS({targ: targeting}); + firePixel(url); +} + +function formatQS(data) { + return utils._map(data, (value, key) => value === undefined ? key + '=' : key + '=' + encodeURIComponent(value)).join('&'); +} + +function firePixel(qs) { + logsQueue.push(ENDPOINT + '&' + qs); + utils.triggerPixel(ENDPOINT + '&' + qs); +} + +let medianetAnalytics = Object.assign(adapter({URL, analyticsType}), { + getlogsQueue() { + return logsQueue; + }, + clearlogsQueue() { + logsQueue = []; + auctions = {}; + }, + track({ eventType, args }) { + if (config.debug) { + utils.logInfo(eventType, args); + } + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: { + auctionInitHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_REQUESTED: { + bidRequestedHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_RESPONSE: { + bidResponseHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_TIMEOUT: { + bidTimeoutHandler(args); + break; + } + case CONSTANTS.EVENTS.NO_BID: { + noBidResponseHandler(args); + break; + } + case CONSTANTS.EVENTS.AUCTION_END: { + auctionEndHandler(args); + break; + } + case CONSTANTS.EVENTS.SET_TARGETING : { + setTargetingHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_WON: { + bidWonHandler(args); + break; + } + } + }}); + +medianetAnalytics.originEnableAnalytics = medianetAnalytics.enableAnalytics; + +medianetAnalytics.enableAnalytics = function (configuration) { + if (!configuration || !configuration.options || !configuration.options.cid) { + utils.logError('Media.net Analytics adapter: cid is required.'); + return; + } + pageDetails = new PageDetail(); + + config = new Configure(configuration.options.cid); + config.publisherLper = configuration.options.sampling || ''; + config.init(); + configuration.options.sampling = 1; + medianetAnalytics.originEnableAnalytics(configuration); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: medianetAnalytics, + code: 'medianetAnalytics' +}); + +export default medianetAnalytics; diff --git a/modules/medianetAnalyticsAdapter.md b/modules/medianetAnalyticsAdapter.md new file mode 100644 index 00000000000..60ab6c0dd6b --- /dev/null +++ b/modules/medianetAnalyticsAdapter.md @@ -0,0 +1,23 @@ +# Overview +Module Name: media.net Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: prebid-support@media.net + +# Description + +Analytics adapter for Media.net +https://media.net/ + +# Test Parameters + +``` +{ + provider: 'medianetAnalytics', + options : { + cid: '8CUX0H51C' + } +} + +``` \ No newline at end of file diff --git a/test/spec/modules/medianetAnalyticsAdapter_spec.js b/test/spec/modules/medianetAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..dcec1050652 --- /dev/null +++ b/test/spec/modules/medianetAnalyticsAdapter_spec.js @@ -0,0 +1,185 @@ +import { expect } from 'chai'; +import medianetAnalytics from 'modules/medianetAnalyticsAdapter.js'; +import * as utils from 'src/utils.js'; +import CONSTANTS from 'src/constants.json'; +import events from 'src/events.js'; + +const { + EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, NO_BID, BID_TIMEOUT, AUCTION_END, SET_TARGETING, BID_WON } +} = CONSTANTS; + +const MOCK = { + AUCTION_INIT: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739}, + BID_REQUESTED: {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, + BID_RESPONSE: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, + AUCTION_END: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'auctionEnd': 1584563605739}, + SET_TARGETING: {'div-gpt-ad-1460505748561-0': {'prebid_test': '1', 'hb_format': 'banner', 'hb_source': 'client', 'hb_size': '300x250', 'hb_pb': '2.00', 'hb_adid': '3e6e4bce5c8fb3', 'hb_bidder': 'medianet', 'hb_format_medianet': 'banner', 'hb_source_medianet': 'client', 'hb_size_medianet': '300x250', 'hb_pb_medianet': '2.00', 'hb_adid_medianet': '3e6e4bce5c8fb3', 'hb_bidder_medianet': 'medianet'}}, + BID_WON: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'statusMessage': 'Bid available', 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, + NO_BID: {'bidder': 'medianet', 'params': {'cid': 'test123', 'crid': '451466393', 'site': {}}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '303fa0c6-682f-4aea-8e4a-dc68f0d5c7d5', 'sizes': [[300, 250], [300, 600]], 'bidId': '28248b0e6aece2', 'bidderRequestId': '13fccf3809fe43', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}, + BID_TIMEOUT: [{'bidId': '28248b0e6aece2', 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'params': [{'cid': 'test123', 'crid': '451466393', 'site': {}}, {'cid': '8CUX0H51P', 'crid': '451466393', 'site': {}}], 'timeout': 6}] +} + +function performStandardAuctionWithWinner() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON); +} + +function performStandardAuctionWithNoBid() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(NO_BID, MOCK.NO_BID); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); +} + +function performStandardAuctionWithTimeout() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); +} + +function getQueryData(url) { + const queryArgs = url.split('?')[1].split('&'); + return queryArgs.reduce((data, arg) => { + const [key, val] = arg.split('='); + if (data[key] !== undefined) { + if (!Array.isArray(data[key])) { + data[key] = [data[key]]; + } + data[key].push(val); + } else { + data[key] = val; + } + return data; + }, {}); +} + +describe('Media.net Analytics Adapter', function() { + let sandbox; + let CUSTOMER_ID = 'test123'; + let VALID_CONFIGURATION = { + options: { + cid: CUSTOMER_ID + } + } + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('Configuration', function() { + it('should log error if publisher id is not passed', function() { + sandbox.stub(utils, 'logError'); + + medianetAnalytics.enableAnalytics(); + expect( + utils.logError.calledWith( + 'Media.net Analytics adapter: cid is required.' + ) + ).to.be.true; + }); + + it('should not log error if valid config is passed', function() { + sandbox.stub(utils, 'logError'); + + medianetAnalytics.enableAnalytics(VALID_CONFIGURATION); + expect(utils.logError.called).to.equal(false); + medianetAnalytics.disableAnalytics(); + }); + }); + + describe('Events', function() { + beforeEach(function () { + medianetAnalytics.enableAnalytics({ + options: { + cid: 'test123' + } + }); + }); + afterEach(function () { + medianetAnalytics.disableAnalytics(); + }); + + it('should not log if only Auction Init', function() { + medianetAnalytics.clearlogsQueue(); + medianetAnalytics.track({ AUCTION_INIT }) + expect(medianetAnalytics.getlogsQueue().length).to.equal(0); + }); + + it('should have winner log in standard auction', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithWinner(); + let winnerLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter((log) => log.winner); + medianetAnalytics.clearlogsQueue(); + + expect(winnerLog.length).to.equal(1); + }); + + it('should have correct values in winner log', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithWinner(); + let winnerLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter((log) => log.winner); + medianetAnalytics.clearlogsQueue(); + + expect(winnerLog[0]).to.include({ + winner: '1', + pvnm: 'medianet', + curr: 'USD', + src: 'client', + size: '300x250', + mtype: 'banner', + cid: 'test123', + lper: '1', + ogbdp: '1.1495', + flt: '1', + supcrid: 'div-gpt-ad-1460505748561-0', + mpvid: '123' + }); + }); + + it('should have no bid status', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithNoBid(); + let noBidLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); + noBidLog = noBidLog[0]; + + medianetAnalytics.clearlogsQueue(); + expect(noBidLog.pvnm).to.have.ordered.members(['-2', 'medianet']); + expect(noBidLog.iwb).to.have.ordered.members(['0', '0']); + expect(noBidLog.status).to.have.ordered.members(['1', '2']); + expect(noBidLog.src).to.have.ordered.members(['client', 'client']); + expect(noBidLog.curr).to.have.ordered.members(['', '']); + expect(noBidLog.mtype).to.have.ordered.members(['', '']); + expect(noBidLog.ogbdp).to.have.ordered.members(['', '']); + expect(noBidLog.mpvid).to.have.ordered.members(['', '']); + expect(noBidLog.crid).to.have.ordered.members(['', '451466393']); + }); + + it('should have timeout status', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithTimeout(); + let timeoutLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); + timeoutLog = timeoutLog[0]; + + medianetAnalytics.clearlogsQueue(); + expect(timeoutLog.pvnm).to.have.ordered.members(['-2', 'medianet']); + expect(timeoutLog.iwb).to.have.ordered.members(['0', '0']); + expect(timeoutLog.status).to.have.ordered.members(['1', '3']); + expect(timeoutLog.src).to.have.ordered.members(['client', 'client']); + expect(timeoutLog.curr).to.have.ordered.members(['', '']); + expect(timeoutLog.mtype).to.have.ordered.members(['', '']); + expect(timeoutLog.ogbdp).to.have.ordered.members(['', '']); + expect(timeoutLog.mpvid).to.have.ordered.members(['', '']); + expect(timeoutLog.crid).to.have.ordered.members(['', '451466393']); + }); + }); +});