From a0657cb7bfa6e0ae0dde41e2a334da2308363ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wo=C5=BAniak?= Date: Mon, 14 May 2018 10:20:27 +0000 Subject: [PATCH 01/25] RVR-1124 Setup initial skeleton analytics adapter that can send something. Approved-by: Alessandro Di Giovanni --- modules/rivrAnalyticsAdapter.js | 188 ++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 modules/rivrAnalyticsAdapter.js diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js new file mode 100644 index 00000000000..37d00938c18 --- /dev/null +++ b/modules/rivrAnalyticsAdapter.js @@ -0,0 +1,188 @@ +import {ajax} from 'src/ajax'; +import adapter from 'src/AnalyticsAdapter'; +import CONSTANTS from 'src/constants.json'; +import adaptermanager from 'src/adaptermanager'; +import { logInfo } from 'src/utils'; + +const analyticsType = 'endpoint'; +const DEFAULT_HOST = 'integrations.rivr.simplaex.net/prebid/auctions'; +const DEFAULT_QUEUE_TIMEOUT = 4000; + +const RIVR_HB_EVENTS = { + AUCTION_INIT: 'auctionInit', + BID_REQUEST: 'bidRequested', + BID_RESPONSE: 'bidResponse', + BID_WON: 'bidWon', + AUCTION_END: 'auctionEnd', + TIMEOUT: 'adapterTimedOut' +}; + +let rivrAnalytics = Object.assign(adapter({analyticsType}), { + track({ eventType, args }) { + if (!rivrAnalytics.context) { + return; + } + logInfo(`ARGUMENTS FOR TYPE: ============= ${eventType}`, args); + let handler = null; + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + if (rivrAnalytics.context.queue) { + rivrAnalytics.context.queue.init(); + } + handler = trackAuctionInit; + break; + case CONSTANTS.EVENTS.BID_REQUESTED: + handler = trackBidRequest; + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + handler = trackBidResponse; + break; + case CONSTANTS.EVENTS.BID_WON: + handler = trackBidWon; + break; + case CONSTANTS.EVENTS.BID_TIMEOUT: + handler = trackBidTimeout; + break; + case CONSTANTS.EVENTS.AUCTION_END: + handler = trackAuctionEnd; + break; + } + if (handler) { + let events = handler(args); + if (rivrAnalytics.context.queue) { + rivrAnalytics.context.queue.push(events); + } + if (eventType === CONSTANTS.EVENTS.AUCTION_END) { + sendAll(); + } + } + } +}); + +function sendAll() { + let events = rivrAnalytics.context.queue.popAll(); + if (events.length !== 0) { + let req = Object.assign({}, {hb_ev: events}); + logInfo('sending request to analytics => ', req); + ajax(`http://${rivrAnalytics.context.host}`, () => { + }, JSON.stringify(req)); + } +} + +function trackAuctionInit() { + rivrAnalytics.context.auctionTimeStart = Date.now(); + const event = createHbEvent(undefined, RIVR_HB_EVENTS.AUCTION_INIT); + return [event]; +} + +function trackBidRequest(args) { + return args.bids.map(bid => + createHbEvent(args.bidderCode, RIVR_HB_EVENTS.BID_REQUEST, bid.adUnitCode)); +} + +function trackBidResponse(args) { + const event = createHbEvent(args.bidderCode, RIVR_HB_EVENTS.BID_RESPONSE, + args.adUnitCode, args.cpm, args.timeToRespond / 1000); + return [event]; +} + +function trackBidWon(args) { + const event = createHbEvent(args.bidderCode, RIVR_HB_EVENTS.BID_WON, args.adUnitCode, args.cpm); + return [event]; +} + +function trackAuctionEnd(args) { + const event = createHbEvent(undefined, RIVR_HB_EVENTS.AUCTION_END, undefined, + undefined, (Date.now() - rivrAnalytics.context.auctionTimeStart) / 1000); + return [event]; +} + +function trackBidTimeout(args) { + return args.map(bidderName => createHbEvent(bidderName, RIVR_HB_EVENTS.TIMEOUT)); +} + +function createHbEvent(adapter, event, tagid = undefined, value = 0, time = 0) { + let ev = { event: event }; + if (adapter) { + ev.adapter = adapter + } + if (tagid) { + ev.tagid = tagid; + } + if (value) { + ev.val = value; + } + if (time) { + ev.time = time; + } + return ev; +} + +/** + * Expiring queue implementation. Fires callback on elapsed timeout since last last update or creation. + * @param callback + * @param ttl + * @constructor + */ +export function ExpiringQueue(callback, ttl) { + let queue = []; + let timeoutId; + + this.push = (event) => { + if (event instanceof Array) { + queue.push.apply(queue, event); + } else { + queue.push(event); + } + reset(); + }; + + this.popAll = () => { + let result = queue; + queue = []; + reset(); + return result; + }; + + /** + * For test/debug purposes only + * @return {Array} + */ + this.peekAll = () => { + return queue; + }; + + this.init = reset; + + function reset() { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => { + if (queue.length) { + callback(); + } + }, ttl); + } +} + +// save the base class function +rivrAnalytics.originEnableAnalytics = rivrAnalytics.enableAnalytics; + +// override enableAnalytics so we can get access to the config passed in from the page +rivrAnalytics.enableAnalytics = (config) => { + rivrAnalytics.context = { + host: config.options.host || DEFAULT_HOST, + pubId: config.options.pubId, + queue: new ExpiringQueue(sendAll, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) + }; + logInfo('Rivr Analytics enabled with config', rivrAnalytics.context); + rivrAnalytics.originEnableAnalytics(config); +}; + +adaptermanager.registerAnalyticsAdapter({ + adapter: rivrAnalytics, + code: 'rivr' +}); + +export default rivrAnalytics From 5116294ab1775ebfe44262f8380b986a2678b215 Mon Sep 17 00:00:00 2001 From: Wozniaxos Date: Mon, 14 May 2018 14:17:24 +0200 Subject: [PATCH 02/25] Formatted auction/events data to fit needed schema. --- modules/rivrAnalyticsAdapter.js | 208 ++++++++++++++++++++++++-------- 1 file changed, 156 insertions(+), 52 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 37d00938c18..7a5dc266646 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -5,18 +5,9 @@ import adaptermanager from 'src/adaptermanager'; import { logInfo } from 'src/utils'; const analyticsType = 'endpoint'; -const DEFAULT_HOST = 'integrations.rivr.simplaex.net/prebid/auctions'; +const DEFAULT_HOST = 'integrations.rivr.simplaex.net/prebid'; const DEFAULT_QUEUE_TIMEOUT = 4000; -const RIVR_HB_EVENTS = { - AUCTION_INIT: 'auctionInit', - BID_REQUEST: 'bidRequested', - BID_RESPONSE: 'bidResponse', - BID_WON: 'bidWon', - AUCTION_END: 'auctionEnd', - TIMEOUT: 'adapterTimedOut' -}; - let rivrAnalytics = Object.assign(adapter({analyticsType}), { track({ eventType, args }) { if (!rivrAnalytics.context) { @@ -26,8 +17,8 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { let handler = null; switch (eventType) { case CONSTANTS.EVENTS.AUCTION_INIT: - if (rivrAnalytics.context.queue) { - rivrAnalytics.context.queue.init(); + if (rivrAnalytics.context.impressionsQueue) { + rivrAnalytics.context.impressionsQueue.init(); } handler = trackAuctionInit; break; @@ -48,83 +39,196 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { break; } if (handler) { - let events = handler(args); - if (rivrAnalytics.context.queue) { - rivrAnalytics.context.queue.push(events); - } - if (eventType === CONSTANTS.EVENTS.AUCTION_END) { - sendAll(); + if (handler == trackBidWon) { + let impressions = handler(args); + if (rivrAnalytics.context.impressionsQueue) { + rivrAnalytics.context.impressionsQueue.push(impressions); + } + } else { + handler(args) } } } }); function sendAll() { - let events = rivrAnalytics.context.queue.popAll(); - if (events.length !== 0) { - let req = Object.assign({}, {hb_ev: events}); - logInfo('sending request to analytics => ', req); - ajax(`http://${rivrAnalytics.context.host}`, () => { - }, JSON.stringify(req)); + let impressions = rivrAnalytics.context.impressionsQueue.popAll(); + let auctionObject = rivrAnalytics.context.auctionObject + let req = Object.assign({}, {Auction: auctionObject}); + auctionObject = createAuctionObject(); + logInfo('sending request to analytics => ', req); + ajax(`http://${rivrAnalytics.context.host}/auctions`, () => { + }, JSON.stringify(req)); + + if (impressions.length !== 0) { + let impressionsReq = Object.assign({}, {impressions}); + logInfo('sending impressions request to analytics => ', impressionsReq); + ajax(`http://${rivrAnalytics.context.host}/impressions`, () => { + }, JSON.stringify(impressionsReq)); } } -function trackAuctionInit() { +function trackAuctionInit(args) { rivrAnalytics.context.auctionTimeStart = Date.now(); - const event = createHbEvent(undefined, RIVR_HB_EVENTS.AUCTION_INIT); - return [event]; + rivrAnalytics.context.auctionObject.id = args.auctionId } function trackBidRequest(args) { - return args.bids.map(bid => - createHbEvent(args.bidderCode, RIVR_HB_EVENTS.BID_REQUEST, bid.adUnitCode)); + setCurrentUserId(args); } function trackBidResponse(args) { - const event = createHbEvent(args.bidderCode, RIVR_HB_EVENTS.BID_RESPONSE, - args.adUnitCode, args.cpm, args.timeToRespond / 1000); - return [event]; + let bidResponse = createBidResponse(args); + rivrAnalytics.context.auctionObject.bidResponses.push(bidResponse) } function trackBidWon(args) { - const event = createHbEvent(args.bidderCode, RIVR_HB_EVENTS.BID_WON, args.adUnitCode, args.cpm); - return [event]; + let auctionObject = rivrAnalytics.context.auctionObject; + let bidResponse = createBidResponse(args) + let impression = createImpression(args) + let imp = createImp(args); + auctionObject.bidResponses.push(bidResponse) + auctionObject.imp.push(imp) + + return [impression]; } function trackAuctionEnd(args) { - const event = createHbEvent(undefined, RIVR_HB_EVENTS.AUCTION_END, undefined, - undefined, (Date.now() - rivrAnalytics.context.auctionTimeStart) / 1000); - return [event]; + rivrAnalytics.context.auctionTimeEnd = Date.now(); } function trackBidTimeout(args) { - return args.map(bidderName => createHbEvent(bidderName, RIVR_HB_EVENTS.TIMEOUT)); + return [args] } -function createHbEvent(adapter, event, tagid = undefined, value = 0, time = 0) { - let ev = { event: event }; - if (adapter) { - ev.adapter = adapter +function setCurrentUserId(bidRequested) { + let user = rivrAnalytics.context.auctionObject.user + if (!user.id) { + rivrAnalytics.context.pubId ? user.id = rivrAnalytics.context.pubId : user.id = bidRequested.bids[0].crumbs.pubcid } - if (tagid) { - ev.tagid = tagid; +} + +function createBidResponse(bidResponseEvent) { + return { + timestamp: bidResponseEvent.responseTimestamp, + status: bidResponseEvent.getStatusCode(), + total_duration: bidResponseEvent.timeToRespond, + bidderId: null, + bidder_name: bidResponseEvent.bidder, + cur: bidResponseEvent.currency, + seatid: [ + { + seat: null, + bid: [ + { + status: bidResponseEvent.getStatusCode(), + clear_price: bidResponseEvent.cpm, + attr: [], + crid: bidResponseEvent.creativeId, + cid: null, + id: null, + adid: bidResponseEvent.adId, + adomain: [], + iurl: null + } + ] + } + ] } - if (value) { - ev.val = value; +} + +function createImpression(bidWonEvent) { + return { + timestamp: bidWonEvent.responseTimestamp, + requestId: bidWonEvent.auctionId, + chargePrice: bidWonEvent.adserverTargeting.hb_pb, + publisherRevenue: bidWonEvent.cpm } - if (time) { - ev.time = time; +} + +function createImp(bidWonEvent) { + return { + tagid: bidWonEvent.adUnitCode, + displaymanager: null, + displaymanagerver: null, + secure: null, + bidfloor: null, + banner: { + w: bidWonEvent.width, + h: bidWonEvent.height, + pos: null, + expandable: [], + api: [] + } } - return ev; } +function createAuctionObject() { + return { + id: null, + timestamp: null, + at: null, + bcat: [], + imp: [], + app: { + id: null, + name: null, + domain: null, + bundle: null, + cat: [], + publisher: { + id: null, + name: null + } + }, + site: { + id: null, + name: null, + domain: null, + cat: [], + publisher: { + id: null, + name: null + } + }, + device: { + geo: { + city: null, + country: null, + region: null, + zip: null, + type: null, + metro: null + }, + connectiontype: null, + devicetype: null, + osv: null, + os: null, + model: null, + make: null, + carrier: null, + ip: null, + didsha1: null, + dpidmd5: null, + ext: { + uid: null + } + }, + user: { + id: null, + yob: null, + gender: null, + }, + bidResponses: [] + } +} /** * Expiring queue implementation. Fires callback on elapsed timeout since last last update or creation. * @param callback * @param ttl * @constructor */ -export function ExpiringQueue(callback, ttl) { +export function ExpiringQueue(callback, ttl, log) { let queue = []; let timeoutId; @@ -143,7 +247,6 @@ export function ExpiringQueue(callback, ttl) { reset(); return result; }; - /** * For test/debug purposes only * @return {Array} @@ -174,7 +277,8 @@ rivrAnalytics.enableAnalytics = (config) => { rivrAnalytics.context = { host: config.options.host || DEFAULT_HOST, pubId: config.options.pubId, - queue: new ExpiringQueue(sendAll, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) + auctionObject: createAuctionObject(), + impressionsQueue: new ExpiringQueue(sendAll, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) }; logInfo('Rivr Analytics enabled with config', rivrAnalytics.context); rivrAnalytics.originEnableAnalytics(config); From 858b208c12656d3a670176aa997cc87e67e42c2d Mon Sep 17 00:00:00 2001 From: Wozniaxos Date: Thu, 17 May 2018 15:40:33 +0200 Subject: [PATCH 03/25] Applied feedback. --- modules/rivrAnalyticsAdapter.js | 56 +++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 7a5dc266646..d8a12c53707 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -66,47 +66,55 @@ function sendAll() { ajax(`http://${rivrAnalytics.context.host}/impressions`, () => { }, JSON.stringify(impressionsReq)); } -} +}; function trackAuctionInit(args) { rivrAnalytics.context.auctionTimeStart = Date.now(); - rivrAnalytics.context.auctionObject.id = args.auctionId -} + rivrAnalytics.context.auctionObject.id = args.auctionId; +}; function trackBidRequest(args) { - setCurrentUserId(args); -} + setCurrentPublisherId(args); +}; function trackBidResponse(args) { let bidResponse = createBidResponse(args); - rivrAnalytics.context.auctionObject.bidResponses.push(bidResponse) -} + rivrAnalytics.context.auctionObject.bidResponses.push(bidResponse); +}; function trackBidWon(args) { let auctionObject = rivrAnalytics.context.auctionObject; - let bidResponse = createBidResponse(args) - let impression = createImpression(args) + let bidResponse = createBidResponse(args); + let impression = createImpression(args); let imp = createImp(args); - auctionObject.bidResponses.push(bidResponse) - auctionObject.imp.push(imp) + auctionObject.bidResponses.push(bidResponse); + auctionObject.imp.push(imp); return [impression]; } function trackAuctionEnd(args) { rivrAnalytics.context.auctionTimeEnd = Date.now(); -} +}; function trackBidTimeout(args) { - return [args] -} + return [args]; +}; -function setCurrentUserId(bidRequested) { - let user = rivrAnalytics.context.auctionObject.user - if (!user.id) { - rivrAnalytics.context.pubId ? user.id = rivrAnalytics.context.pubId : user.id = bidRequested.bids[0].crumbs.pubcid +function setCurrentPublisherId(bidRequested) { + let site = rivrAnalytics.context.auctionObject.site; + let app = rivrAnalytics.context.auctionObject.app; + let pubId = rivrAnalytics.context.pubId; + if (!site.publisher.id || app.publisher.id) { + if (pubId) { + site.publisher.id = pubId; + app.publisher.id = pubId; + } else { + site.publisher.id = bidRequested.bids[0].crumbs.pubcid; + app.publisher.id = bidRequested.bids[0].crumbs.pubcid; + } } -} +}; function createBidResponse(bidResponseEvent) { return { @@ -135,7 +143,7 @@ function createBidResponse(bidResponseEvent) { } ] } -} +}; function createImpression(bidWonEvent) { return { @@ -144,7 +152,7 @@ function createImpression(bidWonEvent) { chargePrice: bidWonEvent.adserverTargeting.hb_pb, publisherRevenue: bidWonEvent.cpm } -} +}; function createImp(bidWonEvent) { return { @@ -161,7 +169,7 @@ function createImp(bidWonEvent) { api: [] } } -} +}; function createAuctionObject() { return { @@ -221,7 +229,7 @@ function createAuctionObject() { }, bidResponses: [] } -} +}; /** * Expiring queue implementation. Fires callback on elapsed timeout since last last update or creation. * @param callback @@ -267,7 +275,7 @@ export function ExpiringQueue(callback, ttl, log) { } }, ttl); } -} +}; // save the base class function rivrAnalytics.originEnableAnalytics = rivrAnalytics.enableAnalytics; From 3b3a2304bc2f304e8bc28f8348f6674d9ec151ed Mon Sep 17 00:00:00 2001 From: Wozniaxos Date: Wed, 16 May 2018 13:35:28 +0200 Subject: [PATCH 04/25] RVR-1135 fetched device data. --- modules/rivrAnalyticsAdapter.js | 37 +++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index d8a12c53707..b6157a3d758 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -20,6 +20,10 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { if (rivrAnalytics.context.impressionsQueue) { rivrAnalytics.context.impressionsQueue.init(); } + if (rivrAnalytics.context.auctionObject) { + rivrAnalytics.context.auctionObject = fulfillAuctionObject(); + fetchLocalization(); + } handler = trackAuctionInit; break; case CONSTANTS.EVENTS.BID_REQUESTED: @@ -55,7 +59,7 @@ function sendAll() { let impressions = rivrAnalytics.context.impressionsQueue.popAll(); let auctionObject = rivrAnalytics.context.auctionObject let req = Object.assign({}, {Auction: auctionObject}); - auctionObject = createAuctionObject(); + auctionObject = fulfillAuctionObject(); logInfo('sending request to analytics => ', req); ajax(`http://${rivrAnalytics.context.host}/auctions`, () => { }, JSON.stringify(req)); @@ -116,6 +120,27 @@ function setCurrentPublisherId(bidRequested) { } }; +function fetchLocalization() { + ajax(`https://ipapi.co/json`, (rawLocalization) => { + let deviceLocation = rivrAnalytics.context.auctionObject.device.geo + let location = JSON.parse(rawLocalization) + deviceLocation.city = location.city; + deviceLocation.country = location.country + deviceLocation.region = location.region + deviceLocation.zip = location.postal + }); +}; + +function getPlatformType() { + if (navigator.userAgent.match(/mobile/i)) { + return 'Mobile'; + } else if (navigator.userAgent.match(/iPad|Android|Touch/i)) { + return 'Tablet'; + } else { + return 'Desktop'; + } +} + function createBidResponse(bidResponseEvent) { return { timestamp: bidResponseEvent.responseTimestamp, @@ -171,7 +196,7 @@ function createImp(bidWonEvent) { } }; -function createAuctionObject() { +function fulfillAuctionObject() { return { id: null, timestamp: null, @@ -192,7 +217,7 @@ function createAuctionObject() { site: { id: null, name: null, - domain: null, + domain: window.location.hostname, cat: [], publisher: { id: null, @@ -208,8 +233,8 @@ function createAuctionObject() { type: null, metro: null }, - connectiontype: null, - devicetype: null, + connectiontype: navigator.connection.effectiveType, + devicetype: getPlatformType(), osv: null, os: null, model: null, @@ -285,7 +310,7 @@ rivrAnalytics.enableAnalytics = (config) => { rivrAnalytics.context = { host: config.options.host || DEFAULT_HOST, pubId: config.options.pubId, - auctionObject: createAuctionObject(), + auctionObject: {}, impressionsQueue: new ExpiringQueue(sendAll, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) }; logInfo('Rivr Analytics enabled with config', rivrAnalytics.context); From 4940f5513ba8d210ffae1fc01916479a849e2600 Mon Sep 17 00:00:00 2001 From: Wozniaxos Date: Thu, 17 May 2018 16:17:01 +0200 Subject: [PATCH 05/25] Applied feedback. --- modules/rivrAnalyticsAdapter.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index b6157a3d758..34eeeed745d 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -57,7 +57,7 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { function sendAll() { let impressions = rivrAnalytics.context.impressionsQueue.popAll(); - let auctionObject = rivrAnalytics.context.auctionObject + let auctionObject = rivrAnalytics.context.auctionObject; let req = Object.assign({}, {Auction: auctionObject}); auctionObject = fulfillAuctionObject(); logInfo('sending request to analytics => ', req); @@ -121,13 +121,10 @@ function setCurrentPublisherId(bidRequested) { }; function fetchLocalization() { - ajax(`https://ipapi.co/json`, (rawLocalization) => { - let deviceLocation = rivrAnalytics.context.auctionObject.device.geo - let location = JSON.parse(rawLocalization) - deviceLocation.city = location.city; - deviceLocation.country = location.country - deviceLocation.region = location.region - deviceLocation.zip = location.postal + navigator.geolocation.getCurrentPosition((position) => { + let deviceLocation = rivrAnalytics.context.auctionObject.device.geo; + deviceLocation.lat = position.coords.latitude; + deviceLocation.long = position.coords.longitude; }); }; From 3a29daaec489d369f324615ea97ee7965dc74649 Mon Sep 17 00:00:00 2001 From: Wozniaxos Date: Fri, 18 May 2018 12:01:49 +0200 Subject: [PATCH 06/25] Added click handler for reporting banners click events. --- modules/rivrAnalyticsAdapter.js | 53 ++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 34eeeed745d..6aa61f296bf 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -2,10 +2,10 @@ import {ajax} from 'src/ajax'; import adapter from 'src/AnalyticsAdapter'; import CONSTANTS from 'src/constants.json'; import adaptermanager from 'src/adaptermanager'; -import { logInfo } from 'src/utils'; +import { logInfo, generateUUID } from 'src/utils'; const analyticsType = 'endpoint'; -const DEFAULT_HOST = 'integrations.rivr.simplaex.net/prebid'; +const DEFAULT_HOST = 'integrations.rivr.simplaex.net'; const DEFAULT_QUEUE_TIMEOUT = 4000; let rivrAnalytics = Object.assign(adapter({analyticsType}), { @@ -89,13 +89,13 @@ function trackBidResponse(args) { function trackBidWon(args) { let auctionObject = rivrAnalytics.context.auctionObject; let bidResponse = createBidResponse(args); - let impression = createImpression(args); - let imp = createImp(args); + let standaloneImpression = createStandaloneImpression(args); + let auctionImpression = createAuctionImpression(args); auctionObject.bidResponses.push(bidResponse); - auctionObject.imp.push(imp); + auctionObject.imp.push(auctionImpression); - return [impression]; -} + return [standaloneImpression]; +}; function trackAuctionEnd(args) { rivrAnalytics.context.auctionTimeEnd = Date.now(); @@ -136,7 +136,7 @@ function getPlatformType() { } else { return 'Desktop'; } -} +}; function createBidResponse(bidResponseEvent) { return { @@ -167,7 +167,7 @@ function createBidResponse(bidResponseEvent) { } }; -function createImpression(bidWonEvent) { +function createStandaloneImpression(bidWonEvent) { return { timestamp: bidWonEvent.responseTimestamp, requestId: bidWonEvent.auctionId, @@ -176,7 +176,7 @@ function createImpression(bidWonEvent) { } }; -function createImp(bidWonEvent) { +function createAuctionImpression(bidWonEvent) { return { tagid: bidWonEvent.adUnitCode, displaymanager: null, @@ -193,6 +193,38 @@ function createImp(bidWonEvent) { } }; +function reportClickEvent(event) { + let link = event.currentTarget.getElementsByTagName('a')[0]; + let clickUrl; + if (link) { + clickUrl = link.getAttribute('href'); + } + let timestamp = new Date().toISOString(); + let requestId = generateUUID(); + let req = { + timestamp, + 'request_id': requestId, + 'click_url': clickUrl + }; + logInfo('Sending click events with parameters: ', req); + ajax(`http://${rivrAnalytics.context.host}/${window.location.href}/clicks`, () => { + }, JSON.stringify(req)); +}; + +function clickHandler(bannersIds) { + setTimeout(function () { + bannersIds.forEach(function (bannerId) { + var doc = document.getElementById(bannerId); + if (doc) { + var iframe = doc.getElementsByTagName('iframe')[0]; + if (iframe) { + iframe.contentDocument.addEventListener('click', reportClickEvent); + } + } + }); + }, 1500); +}; + function fulfillAuctionObject() { return { id: null, @@ -310,6 +342,7 @@ rivrAnalytics.enableAnalytics = (config) => { auctionObject: {}, impressionsQueue: new ExpiringQueue(sendAll, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) }; + clickHandler(config.options.bannersIds); logInfo('Rivr Analytics enabled with config', rivrAnalytics.context); rivrAnalytics.originEnableAnalytics(config); }; From 63d0d2fda967dc87fa2b2364276b53c4a80e4f0e Mon Sep 17 00:00:00 2001 From: Wozniaxos Date: Fri, 18 May 2018 14:06:08 +0200 Subject: [PATCH 07/25] Applied analyzer for reporting displayed impressions. --- modules/rivrAnalyticsAdapter.js | 78 +++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 6aa61f296bf..574c0077c10 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -17,8 +17,8 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { let handler = null; switch (eventType) { case CONSTANTS.EVENTS.AUCTION_INIT: - if (rivrAnalytics.context.impressionsQueue) { - rivrAnalytics.context.impressionsQueue.init(); + if (rivrAnalytics.context.queue) { + rivrAnalytics.context.queue.init(); } if (rivrAnalytics.context.auctionObject) { rivrAnalytics.context.auctionObject = fulfillAuctionObject(); @@ -43,27 +43,22 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { break; } if (handler) { - if (handler == trackBidWon) { - let impressions = handler(args); - if (rivrAnalytics.context.impressionsQueue) { - rivrAnalytics.context.impressionsQueue.push(impressions); - } - } else { - handler(args) - } + handler(args) } } }); -function sendAll() { - let impressions = rivrAnalytics.context.impressionsQueue.popAll(); +function sendAuction() { let auctionObject = rivrAnalytics.context.auctionObject; let req = Object.assign({}, {Auction: auctionObject}); auctionObject = fulfillAuctionObject(); logInfo('sending request to analytics => ', req); - ajax(`http://${rivrAnalytics.context.host}/auctions`, () => { + ajax(`http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientURL}/auctions`, () => { }, JSON.stringify(req)); +}; +function sendImpressions() { + let impressions = rivrAnalytics.context.queue.popAll(); if (impressions.length !== 0) { let impressionsReq = Object.assign({}, {impressions}); logInfo('sending impressions request to analytics => ', impressionsReq); @@ -89,12 +84,9 @@ function trackBidResponse(args) { function trackBidWon(args) { let auctionObject = rivrAnalytics.context.auctionObject; let bidResponse = createBidResponse(args); - let standaloneImpression = createStandaloneImpression(args); let auctionImpression = createAuctionImpression(args); auctionObject.bidResponses.push(bidResponse); auctionObject.imp.push(auctionImpression); - - return [standaloneImpression]; }; function trackAuctionEnd(args) { @@ -167,15 +159,6 @@ function createBidResponse(bidResponseEvent) { } }; -function createStandaloneImpression(bidWonEvent) { - return { - timestamp: bidWonEvent.responseTimestamp, - requestId: bidWonEvent.auctionId, - chargePrice: bidWonEvent.adserverTargeting.hb_pb, - publisherRevenue: bidWonEvent.cpm - } -}; - function createAuctionImpression(bidWonEvent) { return { tagid: bidWonEvent.adUnitCode, @@ -207,16 +190,16 @@ function reportClickEvent(event) { 'click_url': clickUrl }; logInfo('Sending click events with parameters: ', req); - ajax(`http://${rivrAnalytics.context.host}/${window.location.href}/clicks`, () => { + ajax(`http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientURL}/clicks`, () => { }, JSON.stringify(req)); }; function clickHandler(bannersIds) { setTimeout(function () { bannersIds.forEach(function (bannerId) { - var doc = document.getElementById(bannerId); + let doc = document.getElementById(bannerId); if (doc) { - var iframe = doc.getElementsByTagName('iframe')[0]; + let iframe = doc.getElementsByTagName('iframe')[0]; if (iframe) { iframe.contentDocument.addEventListener('click', reportClickEvent); } @@ -225,6 +208,32 @@ function clickHandler(bannersIds) { }, 1500); }; +function displayedImpressionHandler(bannersIds) { + setTimeout(function () { + bannersIds.forEach((bannerId) => { + let doc = document.getElementById(bannerId); + if (doc) { + let iframe = doc.getElementsByTagName('iframe')[0]; + if (iframe) { + let displayedImpressionImg = iframe.contentDocument.getElementsByTagName('img').length > 0; + if (displayedImpressionImg) { + let timestamp = new Date().toISOString(); + let requestId = generateUUID(); + let impression = { + timestamp, + 'request_id': requestId, + }; + if (rivrAnalytics.context.queue) { + rivrAnalytics.context.queue.push(impression); + } + } + } + } + }); + sendImpressions(); + }, 3000); +}; + function fulfillAuctionObject() { return { id: null, @@ -235,7 +244,7 @@ function fulfillAuctionObject() { app: { id: null, name: null, - domain: null, + domain: rivrAnalytics.context.clientURL, bundle: null, cat: [], publisher: { @@ -246,7 +255,7 @@ function fulfillAuctionObject() { site: { id: null, name: null, - domain: window.location.hostname, + domain: rivrAnalytics.context.clientURL, cat: [], publisher: { id: null, @@ -290,7 +299,7 @@ function fulfillAuctionObject() { * @param ttl * @constructor */ -export function ExpiringQueue(callback, ttl, log) { +export function ExpiringQueue(sendImpressions, sendAuction, ttl, log) { let queue = []; let timeoutId; @@ -324,8 +333,9 @@ export function ExpiringQueue(callback, ttl, log) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { + sendAuction(); if (queue.length) { - callback(); + sendImpressions(); } }, ttl); } @@ -340,9 +350,11 @@ rivrAnalytics.enableAnalytics = (config) => { host: config.options.host || DEFAULT_HOST, pubId: config.options.pubId, auctionObject: {}, - impressionsQueue: new ExpiringQueue(sendAll, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) + clientURL: window.location.href, + queue: new ExpiringQueue(sendImpressions, sendAuction, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) }; clickHandler(config.options.bannersIds); + displayedImpressionHandler(config.options.bannersIds); logInfo('Rivr Analytics enabled with config', rivrAnalytics.context); rivrAnalytics.originEnableAnalytics(config); }; From 1ccc72a2ac8e355ab4e3db504c9ce6c2f755f764 Mon Sep 17 00:00:00 2001 From: Wozniaxos Date: Tue, 22 May 2018 21:05:28 +0200 Subject: [PATCH 08/25] Applied feedback. --- modules/rivrAnalyticsAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 574c0077c10..c1cfec7c1c9 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -194,7 +194,7 @@ function reportClickEvent(event) { }, JSON.stringify(req)); }; -function clickHandler(bannersIds) { +function addClickHandlers(bannersIds) { setTimeout(function () { bannersIds.forEach(function (bannerId) { let doc = document.getElementById(bannerId); @@ -353,7 +353,7 @@ rivrAnalytics.enableAnalytics = (config) => { clientURL: window.location.href, queue: new ExpiringQueue(sendImpressions, sendAuction, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) }; - clickHandler(config.options.bannersIds); + addClickHandlers(config.options.bannersIds); displayedImpressionHandler(config.options.bannersIds); logInfo('Rivr Analytics enabled with config', rivrAnalytics.context); rivrAnalytics.originEnableAnalytics(config); From 5e819db67d640d896ced86a40d75ea6fc3388292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wo=C5=BAniak?= Date: Thu, 24 May 2018 08:24:30 +0000 Subject: [PATCH 09/25] Merged in RVR-1214-invoke-handlers-on-rendering (pull request #7) RVR-1214 Invoke handlers on rendering * RVR-1214 Invoked handlers right after ad is displayed. * Applied feedback. Approved-by: Alessandro Di Giovanni --- modules/rivrAnalyticsAdapter.js | 93 ++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 37 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index c1cfec7c1c9..04648e3e3cb 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -194,44 +194,64 @@ function reportClickEvent(event) { }, JSON.stringify(req)); }; -function addClickHandlers(bannersIds) { - setTimeout(function () { - bannersIds.forEach(function (bannerId) { - let doc = document.getElementById(bannerId); - if (doc) { - let iframe = doc.getElementsByTagName('iframe')[0]; - if (iframe) { - iframe.contentDocument.addEventListener('click', reportClickEvent); - } - } - }); - }, 1500); +function addClickHandler(bannerId) { + pinHandlerToHTMLElement(bannerId, dataLoaderForHandler, addClickListener); }; -function displayedImpressionHandler(bannersIds) { - setTimeout(function () { - bannersIds.forEach((bannerId) => { - let doc = document.getElementById(bannerId); - if (doc) { - let iframe = doc.getElementsByTagName('iframe')[0]; - if (iframe) { - let displayedImpressionImg = iframe.contentDocument.getElementsByTagName('img').length > 0; - if (displayedImpressionImg) { - let timestamp = new Date().toISOString(); - let requestId = generateUUID(); - let impression = { - timestamp, - 'request_id': requestId, - }; - if (rivrAnalytics.context.queue) { - rivrAnalytics.context.queue.push(impression); - } - } - } +function addDisplayedImpHandler(bannerId) { + pinHandlerToHTMLElement(bannerId, dataLoaderForHandler, impHandler); +}; + +function pinHandlerToHTMLElement(elementId, dataLoaderForHandler, specializedHandler) { + function waitForElement() { + let element = document.getElementById(elementId); + if (!element) { + window.requestAnimationFrame(waitForElement); + } else { + dataLoaderForHandler(element, specializedHandler); + } + } + waitForElement(); +} + +function dataLoaderForHandler(element, specializedHandler) { + function waitForElement() { + let iframe = element.getElementsByTagName('iframe')[0]; + if (!iframe) { + window.requestAnimationFrame(waitForElement); + } else { + let displayedImpression = iframe.contentDocument.getElementsByTagName('img').length > 0; + if (!displayedImpression) { + window.requestAnimationFrame(waitForElement); + } else { + specializedHandler(iframe); } - }); - sendImpressions(); - }, 3000); + } + } + waitForElement(); +}; + +function addClickListener(iframe) { + iframe.contentDocument.addEventListener('click', reportClickEvent); +} + +function impHandler(iframe) { + let timestamp = new Date().toISOString(); + let requestId = generateUUID(); + let impression = { + timestamp, + 'request_id': requestId, + }; + if (rivrAnalytics.context.queue) { + rivrAnalytics.context.queue.push(impression); + } +} + +function addHandlers(bannersIds) { + bannersIds.forEach((bannerId) => { + addClickHandler(bannerId); + addDisplayedImpHandler(bannerId); + }) }; function fulfillAuctionObject() { @@ -353,8 +373,7 @@ rivrAnalytics.enableAnalytics = (config) => { clientURL: window.location.href, queue: new ExpiringQueue(sendImpressions, sendAuction, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) }; - addClickHandlers(config.options.bannersIds); - displayedImpressionHandler(config.options.bannersIds); + addHandlers(config.options.bannersIds); logInfo('Rivr Analytics enabled with config', rivrAnalytics.context); rivrAnalytics.originEnableAnalytics(config); }; From 80dba26bf94799d2aa25b7ca302a90823b6c06ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wo=C5=BAniak?= Date: Thu, 24 May 2018 09:28:39 +0000 Subject: [PATCH 10/25] Merged in RVR-1192-configuration-global-parameters (pull request #8) RVR-1192 Configuration/Global parameters Approved-by: Alessandro Di Giovanni --- modules/rivrAnalyticsAdapter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 04648e3e3cb..35a14c588bc 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -53,7 +53,7 @@ function sendAuction() { let req = Object.assign({}, {Auction: auctionObject}); auctionObject = fulfillAuctionObject(); logInfo('sending request to analytics => ', req); - ajax(`http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientURL}/auctions`, () => { + ajax(`http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientID}/auctions`, () => { }, JSON.stringify(req)); }; @@ -190,7 +190,7 @@ function reportClickEvent(event) { 'click_url': clickUrl }; logInfo('Sending click events with parameters: ', req); - ajax(`http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientURL}/clicks`, () => { + ajax(`http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientID}/clicks`, () => { }, JSON.stringify(req)); }; @@ -370,7 +370,7 @@ rivrAnalytics.enableAnalytics = (config) => { host: config.options.host || DEFAULT_HOST, pubId: config.options.pubId, auctionObject: {}, - clientURL: window.location.href, + clientID: config.options.clientID, queue: new ExpiringQueue(sendImpressions, sendAuction, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) }; addHandlers(config.options.bannersIds); From 1503b9d7f139af1b2a6caa1aa0a19350802bad6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wo=C5=BAniak?= Date: Fri, 25 May 2018 09:12:04 +0000 Subject: [PATCH 11/25] Merged in RVR-1181-Prebid-js-unit-tests-setup (pull request #6) RVR-1181 Prebid.js Unit tests setup Approved-by: Alessandro Di Giovanni --- modules/rivrAnalyticsAdapter.js | 7 +- .../spec/modules/rivrAnalyticsAdapter_spec.js | 221 ++++++++++++++++++ 2 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 test/spec/modules/rivrAnalyticsAdapter_spec.js diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 35a14c588bc..ea13d233d3e 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -51,7 +51,7 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { function sendAuction() { let auctionObject = rivrAnalytics.context.auctionObject; let req = Object.assign({}, {Auction: auctionObject}); - auctionObject = fulfillAuctionObject(); + rivrAnalytics.context.auctionObject = fulfillAuctionObject(); logInfo('sending request to analytics => ', req); ajax(`http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientID}/auctions`, () => { }, JSON.stringify(req)); @@ -83,9 +83,7 @@ function trackBidResponse(args) { function trackBidWon(args) { let auctionObject = rivrAnalytics.context.auctionObject; - let bidResponse = createBidResponse(args); let auctionImpression = createAuctionImpression(args); - auctionObject.bidResponses.push(bidResponse); auctionObject.imp.push(auctionImpression); }; @@ -255,7 +253,7 @@ function addHandlers(bannersIds) { }; function fulfillAuctionObject() { - return { + let newAuction = { id: null, timestamp: null, at: null, @@ -312,6 +310,7 @@ function fulfillAuctionObject() { }, bidResponses: [] } + return newAuction; }; /** * Expiring queue implementation. Fires callback on elapsed timeout since last last update or creation. diff --git a/test/spec/modules/rivrAnalyticsAdapter_spec.js b/test/spec/modules/rivrAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..b0d3b2fd72f --- /dev/null +++ b/test/spec/modules/rivrAnalyticsAdapter_spec.js @@ -0,0 +1,221 @@ +import analyticsAdapter, {ExpiringQueue} from 'modules/rivrAnalyticsAdapter'; +import {expect} from 'chai'; +import adaptermanager from 'src/adaptermanager'; +import * as ajax from 'src/ajax'; +import CONSTANTS from 'src/constants.json'; + +const events = require('../../../src/events'); + +describe('', () => { + let sandbox; + + before(() => { + sandbox = sinon.sandbox.create(); + }); + + after(() => { + sandbox.restore(); + analyticsAdapter.disableAnalytics(); + }); + + describe('ExpiringQueue', () => { + let timer; + before(() => { + timer = sandbox.useFakeTimers(0); + }); + after(() => { + timer.restore(); + }); + + it('should notify after timeout period', (done) => { + let queue = new ExpiringQueue(() => { + let elements = queue.popAll(); + expect(elements).to.be.eql([1, 2, 3, 4]); + elements = queue.popAll(); + expect(elements).to.have.lengthOf(0); + expect(Date.now()).to.be.equal(200); + done(); + }, () => {}, 100); + + queue.push(1); + setTimeout(() => { + queue.push([2, 3]); + timer.tick(50); + }, 50); + setTimeout(() => { + queue.push([4]); + timer.tick(100); + }, 100); + timer.tick(50); + }); + }); + + const REQUEST = { + bidderCode: 'adapter', + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', + bidderRequestId: '1a6fc81528d0f6', + bids: [{ + bidder: 'adapter', + params: {}, + adUnitCode: 'container-1', + transactionId: 'de90df62-7fd0-4fbc-8787-92d133a7dc06', + sizes: [[300, 250]], + bidId: '208750227436c1', + bidderRequestId: '1a6fc81528d0f6', + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f' + }], + auctionStart: 1509369418387, + timeout: 3000, + start: 1509369418389 + }; + + const RESPONSE = { + bidderCode: 'adapter', + width: 300, + height: 250, + statusMessage: 'Bid available', + getStatusCode: () => 1, + adId: '208750227436c1', + mediaType: 'banner', + cpm: 0.015, + creativeId: 999, + ad: '', + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', + responseTimestamp: 1509369418832, + requestTimestamp: 1509369418389, + bidder: 'adapter', + adUnitCode: 'container-1', + timeToRespond: 443, + currency: 'EU', + size: '300x250' + }; + + describe('Analytics adapter', () => { + let ajaxStub; + let timer; + + before(() => { + ajaxStub = sandbox.stub(ajax, 'ajax'); + timer = sandbox.useFakeTimers(0); + }); + + beforeEach(() => { + sandbox.stub(events, 'getEvents').callsFake(() => { + return [] + }); + }); + + afterEach(() => { + events.getEvents.restore(); + }); + + it('should be configurable', () => { + adaptermanager.registerAnalyticsAdapter({ + code: 'rivr', + adapter: analyticsAdapter + }); + + adaptermanager.enableAnalytics({ + provider: 'rivr', + options: { + pubId: 777, + } + }); + + expect(analyticsAdapter.context).to.have.property('host', 'integrations.rivr.simplaex.net'); + expect(analyticsAdapter.context).to.have.property('pubId', 777); + }); + + it('should handle auction init event', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 1, config: {}, timeout: 3000}); + const auctionId = analyticsAdapter.context.auctionObject.id; + const auctionStart = analyticsAdapter.context.auctionTimeStart; + expect(auctionId).to.be.eql(1); + }); + + it('should handle bid request event', () => { + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST); + const sitePubcid = analyticsAdapter.context.auctionObject.site.publisher.id; + const appPubcid = analyticsAdapter.context.auctionObject.app.publisher.id; + expect(sitePubcid).to.be.eql(777); + expect(sitePubcid).to.be.eql(appPubcid); + }); + + it('should handle bid response event', () => { + events.emit(CONSTANTS.EVENTS.BID_RESPONSE, RESPONSE); + const ev = analyticsAdapter.context.auctionObject.bidResponses; + expect(ev).to.have.length(1); + expect(ev[0]).to.be.eql({ + timestamp: 1509369418832, + status: 1, + 'total_duration': 443, + bidderId: null, + 'bidder_name': 'adapter', + cur: 'EU', + seatid: [ + { + seat: null, + bid: [ + { + status: 1, + 'clear_price': 0.015, + attr: [], + crid: 999, + cid: null, + id: null, + adid: '208750227436c1', + adomain: [], + iurl: null + } + ] + } + ] + }); + }); + + it('should handle auction end event', () => { + timer.tick(447); + events.emit(CONSTANTS.EVENTS.AUCTION_END, RESPONSE); + const endTime = analyticsAdapter.context.auctionTimeEnd; + expect(endTime).to.be.eql(447); + }); + + it('should handle winning bid', () => { + events.emit(CONSTANTS.EVENTS.BID_WON, RESPONSE); + const ev = analyticsAdapter.context.auctionObject.imp; + expect(ev.length).to.be.eql(1); + expect(ev[0]).to.be.eql({ + tagid: 'container-1', + displaymanager: null, + displaymanagerver: null, + secure: null, + bidfloor: null, + banner: { + w: 300, + h: 250, + pos: null, + expandable: [], + api: [] + } + }); + }); + + it('sends request after timeout', () => { + let impressions = analyticsAdapter.context.auctionObject.imp; + let responses = analyticsAdapter.context.auctionObject.bidResponses; + + expect(impressions.length).to.be.eql(1); + expect(responses.length).to.be.eql(1); + expect(ajaxStub.calledOnce).to.be.equal(false); + + timer.tick(4500); + + let impressionss = analyticsAdapter.context.auctionObject.imp; + let responsess = analyticsAdapter.context.auctionObject.bidResponses; + + expect(ajaxStub.calledOnce).to.be.equal(true); + expect(impressionss.length).to.be.eql(0); + expect(responsess.length).to.be.eql(0); + }); + }); +}); From c0bbaf6ef10b33b500efc4aab2984afe226b0120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wo=C5=BAniak?= Date: Fri, 25 May 2018 09:12:42 +0000 Subject: [PATCH 12/25] Merged in RVR-1247-additional-data-to-impression-records (pull request #9) RVR-1247 Additional data to impression records Approved-by: Alessandro Di Giovanni --- modules/rivrAnalyticsAdapter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index ea13d233d3e..3ef6d325ad3 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -236,9 +236,11 @@ function addClickListener(iframe) { function impHandler(iframe) { let timestamp = new Date().toISOString(); let requestId = generateUUID(); + let adContainerId = iframe.parentElement.parentElement.id; let impression = { timestamp, 'request_id': requestId, + 'tag_id': adContainerId }; if (rivrAnalytics.context.queue) { rivrAnalytics.context.queue.push(impression); From 1a2739793616e9e6181d19578c3d5d5431823b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wo=C5=BAniak?= Date: Fri, 25 May 2018 09:13:27 +0000 Subject: [PATCH 13/25] Merged in RVR-1249-add-requestedbids-to-auction (pull request #10) RVR-1249 Add requested bids to auction object request. Approved-by: Alessandro Di Giovanni --- modules/rivrAnalyticsAdapter.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 3ef6d325ad3..b9222bdc0f1 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -74,6 +74,8 @@ function trackAuctionInit(args) { function trackBidRequest(args) { setCurrentPublisherId(args); + let bidRequest = args; + rivrAnalytics.context.auctionObject.bidRequests.push(bidRequest); }; function trackBidResponse(args) { @@ -310,7 +312,8 @@ function fulfillAuctionObject() { yob: null, gender: null, }, - bidResponses: [] + bidResponses: [], + bidRequests: [] } return newAuction; }; From 72ab6856ce090e12393537f642c5b661f938f468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wo=C5=BAniak?= Date: Wed, 30 May 2018 09:42:04 +0000 Subject: [PATCH 14/25] Merged in RVR-1261-fix-tests (pull request #11) RVR-1261 fix tests * RVR-1261 Secured adapter from no containers configuration. And changed fetching URL. * RVR-1261 Added event check for request and changed some names. * Applied feedback. Approved-by: Alessandro Di Giovanni --- modules/rivrAnalyticsAdapter.js | 12 +++-- .../spec/modules/rivrAnalyticsAdapter_spec.js | 44 ++++++++++++++----- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index b9222bdc0f1..575bf996bd3 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -266,7 +266,7 @@ function fulfillAuctionObject() { app: { id: null, name: null, - domain: rivrAnalytics.context.clientURL, + domain: window.location.href, bundle: null, cat: [], publisher: { @@ -277,7 +277,7 @@ function fulfillAuctionObject() { site: { id: null, name: null, - domain: rivrAnalytics.context.clientURL, + domain: window.location.href, cat: [], publisher: { id: null, @@ -293,7 +293,6 @@ function fulfillAuctionObject() { type: null, metro: null }, - connectiontype: navigator.connection.effectiveType, devicetype: getPlatformType(), osv: null, os: null, @@ -377,7 +376,12 @@ rivrAnalytics.enableAnalytics = (config) => { clientID: config.options.clientID, queue: new ExpiringQueue(sendImpressions, sendAuction, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) }; - addHandlers(config.options.bannersIds); + let bannersIds = config.options.bannersIds + if (bannersIds) { + if (bannersIds.length > 0) { + addHandlers(config.options.bannersIds); + } + } logInfo('Rivr Analytics enabled with config', rivrAnalytics.context); rivrAnalytics.originEnableAnalytics(config); }; diff --git a/test/spec/modules/rivrAnalyticsAdapter_spec.js b/test/spec/modules/rivrAnalyticsAdapter_spec.js index b0d3b2fd72f..7c2bb55e155 100644 --- a/test/spec/modules/rivrAnalyticsAdapter_spec.js +++ b/test/spec/modules/rivrAnalyticsAdapter_spec.js @@ -137,15 +137,35 @@ describe('', () => { events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST); const sitePubcid = analyticsAdapter.context.auctionObject.site.publisher.id; const appPubcid = analyticsAdapter.context.auctionObject.app.publisher.id; + const requestEvent = analyticsAdapter.context.auctionObject.bidRequests; expect(sitePubcid).to.be.eql(777); expect(sitePubcid).to.be.eql(appPubcid); + expect(requestEvent).to.have.length(1); + expect(requestEvent[0]).to.be.eql({ + bidderCode: 'adapter', + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', + bidderRequestId: '1a6fc81528d0f6', + bids: [{ + bidder: 'adapter', + params: {}, + adUnitCode: 'container-1', + transactionId: 'de90df62-7fd0-4fbc-8787-92d133a7dc06', + sizes: [[300, 250]], + bidId: '208750227436c1', + bidderRequestId: '1a6fc81528d0f6', + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f' + }], + auctionStart: 1509369418387, + timeout: 3000, + start: 1509369418389 + }); }); it('should handle bid response event', () => { events.emit(CONSTANTS.EVENTS.BID_RESPONSE, RESPONSE); - const ev = analyticsAdapter.context.auctionObject.bidResponses; - expect(ev).to.have.length(1); - expect(ev[0]).to.be.eql({ + const responseEvent = analyticsAdapter.context.auctionObject.bidResponses; + expect(responseEvent).to.have.length(1); + expect(responseEvent[0]).to.be.eql({ timestamp: 1509369418832, status: 1, 'total_duration': 443, @@ -182,9 +202,9 @@ describe('', () => { it('should handle winning bid', () => { events.emit(CONSTANTS.EVENTS.BID_WON, RESPONSE); - const ev = analyticsAdapter.context.auctionObject.imp; - expect(ev.length).to.be.eql(1); - expect(ev[0]).to.be.eql({ + const wonEvent = analyticsAdapter.context.auctionObject.imp; + expect(wonEvent.length).to.be.eql(1); + expect(wonEvent[0]).to.be.eql({ tagid: 'container-1', displaymanager: null, displaymanagerver: null, @@ -203,19 +223,23 @@ describe('', () => { it('sends request after timeout', () => { let impressions = analyticsAdapter.context.auctionObject.imp; let responses = analyticsAdapter.context.auctionObject.bidResponses; + let requests = analyticsAdapter.context.auctionObject.bidRequests; expect(impressions.length).to.be.eql(1); expect(responses.length).to.be.eql(1); + expect(requests.length).to.be.eql(1); expect(ajaxStub.calledOnce).to.be.equal(false); timer.tick(4500); - let impressionss = analyticsAdapter.context.auctionObject.imp; - let responsess = analyticsAdapter.context.auctionObject.bidResponses; + let impressionsAfterSend = analyticsAdapter.context.auctionObject.imp; + let responsesAfterSend = analyticsAdapter.context.auctionObject.bidResponses; + let requestsAfterSend = analyticsAdapter.context.auctionObject.bidRequests; expect(ajaxStub.calledOnce).to.be.equal(true); - expect(impressionss.length).to.be.eql(0); - expect(responsess.length).to.be.eql(0); + expect(impressionsAfterSend.length).to.be.eql(0); + expect(responsesAfterSend.length).to.be.eql(0); + expect(requestsAfterSend.length).to.be.eql(0); }); }); }); From 05197d68113b3381aa9aa02d99ac70c9636111bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wo=C5=BAniak?= Date: Wed, 13 Jun 2018 08:55:11 +0000 Subject: [PATCH 15/25] RVR-1352 analytics adapter bugs Approved-by: Alessandro Di Giovanni --- modules/rivrAnalyticsAdapter.js | 53 +++++++++++- .../spec/modules/rivrAnalyticsAdapter_spec.js | 82 ++++++++++++++++++- 2 files changed, 129 insertions(+), 6 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 575bf996bd3..854ee311aab 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -87,10 +87,12 @@ function trackBidWon(args) { let auctionObject = rivrAnalytics.context.auctionObject; let auctionImpression = createAuctionImpression(args); auctionObject.imp.push(auctionImpression); + assignBidWonStatusToResponse(args); }; function trackAuctionEnd(args) { rivrAnalytics.context.auctionTimeEnd = Date.now(); + createEmptyBidResponses(); }; function trackBidTimeout(args) { @@ -138,12 +140,12 @@ function createBidResponse(bidResponseEvent) { bidderId: null, bidder_name: bidResponseEvent.bidder, cur: bidResponseEvent.currency, - seatid: [ + seatbid: [ { seat: null, bid: [ { - status: bidResponseEvent.getStatusCode(), + status: 2, clear_price: bidResponseEvent.cpm, attr: [], crid: bidResponseEvent.creativeId, @@ -159,6 +161,18 @@ function createBidResponse(bidResponseEvent) { } }; +function createSingleEmptyBidResponse(bidResponse) { + return { + timestamp: bidResponse.start, + total_duration: 'noResponseDuration', + bidderId: null, + bidder_name: bidResponse.bidder, + cur: null, + response: 'noBid', + seatbid: [] + } +}; + function createAuctionImpression(bidWonEvent) { return { tagid: bidWonEvent.adUnitCode, @@ -364,6 +378,41 @@ export function ExpiringQueue(sendImpressions, sendAuction, ttl, log) { } }; +function assignBidWonStatusToResponse(wonBid) { + let wonBidId = wonBid.adId; + rivrAnalytics.context.auctionObject.bidResponses.forEach((response) => { + if (response.seatbid.length > 0) { + let bidObjectResponse = response.seatbid[0].bid[0]; + if (wonBidId === bidObjectResponse.adid) { + bidObjectResponse.status = 1 + } + } + }); +}; + +function createEmptyBidResponses() { + let unRespondedBidRequests = findAllUnrespondedBidRequests(); + unRespondedBidRequests.forEach((bid) => { + let emptyBidResponse = createSingleEmptyBidResponse(bid); + rivrAnalytics.context.auctionObject.bidResponses.push(emptyBidResponse); + }); +}; + +function findAllUnrespondedBidRequests() { + let respondedBidIds = getAllRespondedBidIds(); + let bidRequests = rivrAnalytics.context.auctionObject.bidRequests; + let allNotRespondedBidRequests = bidRequests.reduce((cache, requestBidder) => { + let notRespondedBids = requestBidder.bids.filter((bid) => !respondedBidIds.includes(bid.bidId)); + notRespondedBids.forEach((bid) => bid.start = requestBidder.start); + return cache.concat(notRespondedBids); + }, []); + return allNotRespondedBidRequests; +}; + +function getAllRespondedBidIds() { + return rivrAnalytics.context.auctionObject.bidResponses.map((response) => response.seatbid[0].bid[0].adid); +}; + // save the base class function rivrAnalytics.originEnableAnalytics = rivrAnalytics.enableAnalytics; diff --git a/test/spec/modules/rivrAnalyticsAdapter_spec.js b/test/spec/modules/rivrAnalyticsAdapter_spec.js index 7c2bb55e155..d4bf1c344e8 100644 --- a/test/spec/modules/rivrAnalyticsAdapter_spec.js +++ b/test/spec/modules/rivrAnalyticsAdapter_spec.js @@ -69,6 +69,44 @@ describe('', () => { start: 1509369418389 }; + const REQUEST2 = { + bidderCode: 'adapter', + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', + bidderRequestId: '1a6fc81528d0f6', + bids: [{ + bidder: 'adapter', + params: {}, + adUnitCode: 'container-1', + transactionId: 'de90df62-7fd0-4fbc-8787-92d133a7dc06', + sizes: [[300, 250]], + bidId: 'request2id', + bidderRequestId: '1a6fc81528d0f6', + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f' + }], + auctionStart: 1509369418387, + timeout: 3000, + start: 1509369418389 + }; + + const REQUEST3 = { + bidderCode: 'adapter', + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', + bidderRequestId: '1a6fc81528d0f6', + bids: [{ + bidder: 'adapter', + params: {}, + adUnitCode: 'container-1', + transactionId: 'de90df62-7fd0-4fbc-8787-92d133a7dc06', + sizes: [[300, 250]], + bidId: 'request3id', + bidderRequestId: '1a6fc81528d0f6', + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f' + }], + auctionStart: 1509369418387, + timeout: 3000, + start: 1509369418389 + }; + const RESPONSE = { bidderCode: 'adapter', width: 300, @@ -172,12 +210,12 @@ describe('', () => { bidderId: null, 'bidder_name': 'adapter', cur: 'EU', - seatid: [ + seatbid: [ { seat: null, bid: [ { - status: 1, + status: 2, 'clear_price': 0.015, attr: [], crid: 999, @@ -200,8 +238,17 @@ describe('', () => { expect(endTime).to.be.eql(447); }); + it('should map unresponded requests to empty responded on auction end', () => { + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST2); + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST3); + events.emit(CONSTANTS.EVENTS.AUCTION_END, RESPONSE); + const responses = analyticsAdapter.context.auctionObject.bidResponses; + expect(responses.length).to.be.eql(3); + }) + it('should handle winning bid', () => { events.emit(CONSTANTS.EVENTS.BID_WON, RESPONSE); + const responseWhichIsWonAlso = analyticsAdapter.context.auctionObject.bidResponses[0]; const wonEvent = analyticsAdapter.context.auctionObject.imp; expect(wonEvent.length).to.be.eql(1); expect(wonEvent[0]).to.be.eql({ @@ -218,6 +265,33 @@ describe('', () => { api: [] } }); + + expect(responseWhichIsWonAlso).to.be.eql({ + timestamp: 1509369418832, + status: 1, + 'total_duration': 443, + bidderId: null, + 'bidder_name': 'adapter', + cur: 'EU', + seatbid: [ + { + seat: null, + bid: [ + { + status: 1, + 'clear_price': 0.015, + attr: [], + crid: 999, + cid: null, + id: null, + adid: '208750227436c1', + adomain: [], + iurl: null + } + ] + } + ] + }); }); it('sends request after timeout', () => { @@ -226,8 +300,8 @@ describe('', () => { let requests = analyticsAdapter.context.auctionObject.bidRequests; expect(impressions.length).to.be.eql(1); - expect(responses.length).to.be.eql(1); - expect(requests.length).to.be.eql(1); + expect(responses.length).to.be.eql(3); + expect(requests.length).to.be.eql(3); expect(ajaxStub.calledOnce).to.be.equal(false); timer.tick(4500); From 8692177d9814239ad80d09aeaced1694bfe5bffd Mon Sep 17 00:00:00 2001 From: Wozniaxos Date: Wed, 13 Jun 2018 14:27:17 +0200 Subject: [PATCH 16/25] Fixed bug with geolocation notification. --- modules/rivrAnalyticsAdapter.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 854ee311aab..55552420278 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -115,12 +115,15 @@ function setCurrentPublisherId(bidRequested) { }; function fetchLocalization() { - navigator.geolocation.getCurrentPosition((position) => { - let deviceLocation = rivrAnalytics.context.auctionObject.device.geo; - deviceLocation.lat = position.coords.latitude; - deviceLocation.long = position.coords.longitude; + navigator.permissions.query({ name: 'geolocation' }).then(function (permission) { + if (permission.status === 'granted') { + navigator.geolocation.getCurrentPosition((position) => { + let deviceLocation = rivrAnalytics.context.auctionObject.device.geo; + deviceLocation.lat = position.coords.latitude; + deviceLocation.long = position.coords.longitude; + }); + } }); -}; function getPlatformType() { if (navigator.userAgent.match(/mobile/i)) { From da842702bed6459aa50958237808f0aab54528a5 Mon Sep 17 00:00:00 2001 From: Wozniaxos Date: Wed, 13 Jun 2018 14:35:46 +0200 Subject: [PATCH 17/25] fixed missing bracket. --- modules/rivrAnalyticsAdapter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 55552420278..005e4c25a77 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -115,7 +115,7 @@ function setCurrentPublisherId(bidRequested) { }; function fetchLocalization() { - navigator.permissions.query({ name: 'geolocation' }).then(function (permission) { + navigator.permissions.query({ name: 'geolocation' }).then((permission) => { if (permission.status === 'granted') { navigator.geolocation.getCurrentPosition((position) => { let deviceLocation = rivrAnalytics.context.auctionObject.device.geo; @@ -124,6 +124,7 @@ function fetchLocalization() { }); } }); +} function getPlatformType() { if (navigator.userAgent.match(/mobile/i)) { From 7b7c1d372e3da5e3c1f9c5112896477ad7e32b44 Mon Sep 17 00:00:00 2001 From: Wozniaxos Date: Wed, 13 Jun 2018 14:46:43 +0200 Subject: [PATCH 18/25] one more fix. --- modules/rivrAnalyticsAdapter.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 005e4c25a77..b3a4c0f66ca 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -115,15 +115,17 @@ function setCurrentPublisherId(bidRequested) { }; function fetchLocalization() { - navigator.permissions.query({ name: 'geolocation' }).then((permission) => { - if (permission.status === 'granted') { - navigator.geolocation.getCurrentPosition((position) => { - let deviceLocation = rivrAnalytics.context.auctionObject.device.geo; - deviceLocation.lat = position.coords.latitude; - deviceLocation.long = position.coords.longitude; - }); - } - }); + if (navigator.permissions) { + navigator.permissions.query({ name: 'geolocation' }).then((permission) => { + if (permission.status === 'granted') { + navigator.geolocation.getCurrentPosition((position) => { + let deviceLocation = rivrAnalytics.context.auctionObject.device.geo; + deviceLocation.lat = position.coords.latitude; + deviceLocation.long = position.coords.longitude; + }); + } + }); + } } function getPlatformType() { From ed1eec24d5afbf6508adf8949d9986033c3ea00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wo=C5=BAniak?= Date: Tue, 26 Jun 2018 11:58:38 +0000 Subject: [PATCH 19/25] RVR-1357 Different optimisation responses & tracking into auction event --- modules/rivrAnalyticsAdapter.js | 61 ++++++++++++++++++- .../spec/modules/rivrAnalyticsAdapter_spec.js | 20 ++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index b3a4c0f66ca..708bc957a9c 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -22,6 +22,7 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { } if (rivrAnalytics.context.auctionObject) { rivrAnalytics.context.auctionObject = fulfillAuctionObject(); + saveUnoptimisedParams(); fetchLocalization(); } handler = trackAuctionInit; @@ -332,10 +333,62 @@ function fulfillAuctionObject() { gender: null, }, bidResponses: [], - bidRequests: [] + bidRequests: [], + 'ext.rivr.optimiser': localStorage.getItem('rivr_should_optimise') || 'unoptimised', + modelVersion: localStorage.getItem('rivr_model_version') || null, + 'ext.rivr.originalvalues': [] } return newAuction; }; + +function saveUnoptimisedParams() { + let units = rivrAnalytics.context.adUnits; + if (units) { + if (units.length > 0) { + let allUnits = connectAllUnits(units); + allUnits.forEach((adUnit) => { + adUnit.bids.forEach((bid) => { + let configForAd = fetchConfigForBidder(bid.bidder); + if (configForAd) { + let unOptimisedParamsField = createUnOptimisedParamsField(bid, configForAd) + rivrAnalytics.context.auctionObject['ext.rivr.originalvalues'].push(unOptimisedParamsField); + } + }) + }); + } + } +}; + +function connectAllUnits(units) { + return units.reduce((acc, units) => { + units.forEach((unit) => acc.push(unit)) + return acc + }, []); +} + +function createUnOptimisedParamsField(unit, config) { + let floorPriceLabel = config['floorPriceLabel']; + let currencyLabel = config['currencyLabel']; + let pmpLabel = config['pmpLabel']; + return { + 'ext.rivr.demand_source_original': unit.bidder, + 'ext.rivr.bidfloor_original': unit.params[floorPriceLabel], + 'ext.rivr.currency_original': unit.params[currencyLabel], + 'ext.rivr.pmp_original': unit.params[pmpLabel], + } +} + +function fetchConfigForBidder(bidderName) { + let config = localStorage.getItem('rivr_config_string'); + if (config) { + let parsed = JSON.parse(config); + return parsed.demand.map((bidderConfig) => { + if (bidderName === bidderConfig.partner) { + return bidderConfig + }; + })[0]; + } +} /** * Expiring queue implementation. Fires callback on elapsed timeout since last last update or creation. * @param callback @@ -424,10 +477,16 @@ rivrAnalytics.originEnableAnalytics = rivrAnalytics.enableAnalytics; // override enableAnalytics so we can get access to the config passed in from the page rivrAnalytics.enableAnalytics = (config) => { + let copiedUnits; + if (config.options.adUnits) { + let stringifiedAdUnits = JSON.stringify(config.options.adUnits); + copiedUnits = JSON.parse(stringifiedAdUnits); + } rivrAnalytics.context = { host: config.options.host || DEFAULT_HOST, pubId: config.options.pubId, auctionObject: {}, + adUnits: copiedUnits, clientID: config.options.clientID, queue: new ExpiringQueue(sendImpressions, sendAuction, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) }; diff --git a/test/spec/modules/rivrAnalyticsAdapter_spec.js b/test/spec/modules/rivrAnalyticsAdapter_spec.js index d4bf1c344e8..00bcfbc5357 100644 --- a/test/spec/modules/rivrAnalyticsAdapter_spec.js +++ b/test/spec/modules/rivrAnalyticsAdapter_spec.js @@ -171,6 +171,26 @@ describe('', () => { expect(auctionId).to.be.eql(1); }); + it('should map proper response params on auction init', () => { + localStorage.setItem('rivr_should_optimise', 'optimise') + localStorage.setItem('rivr_model_version', 'some model version'); + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 2, config: {}, timeout: 3000}); + let auctionObject2 = analyticsAdapter.context.auctionObject; + + expect(auctionObject2['ext.rivr.optimiser']).to.be.eql('optimise'); + expect(auctionObject2['modelVersion']).to.be.eql('some model version'); + + localStorage.removeItem('rivr_should_optimise'); + localStorage.removeItem('rivr_model_version'); + + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 3, config: {}, timeout: 3000}); + + let auctionObject3 = analyticsAdapter.context.auctionObject; + + expect(auctionObject3['ext.rivr.optimiser']).to.be.eql('unoptimised'); + expect(auctionObject3['modelVersion']).to.be.eql(null); + }) + it('should handle bid request event', () => { events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST); const sitePubcid = analyticsAdapter.context.auctionObject.site.publisher.id; From 492d0ee629fcc203d11796fdd3ee235854f6fd7d Mon Sep 17 00:00:00 2001 From: adg Date: Thu, 30 Aug 2018 17:50:34 +0200 Subject: [PATCH 20/25] RVR-1852 - Add content type and hardcoded auth headers (cherry picked from commit 4def881) --- modules/rivrAnalyticsAdapter.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 708bc957a9c..91b01d2362f 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -54,8 +54,17 @@ function sendAuction() { let req = Object.assign({}, {Auction: auctionObject}); rivrAnalytics.context.auctionObject = fulfillAuctionObject(); logInfo('sending request to analytics => ', req); - ajax(`http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientID}/auctions`, () => { - }, JSON.stringify(req)); + ajax( + `http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientID}/auctions`, + () => {}, + JSON.stringify(req), + { + customHeaders: { + 'Authorization': 'Basic b3V0ZXJwYXNzaXZlOkQ3OVZ5YXI1eVZXUEVBaHI=', + 'Content-type': 'application/json' + } + } + ); }; function sendImpressions() { @@ -63,8 +72,17 @@ function sendImpressions() { if (impressions.length !== 0) { let impressionsReq = Object.assign({}, {impressions}); logInfo('sending impressions request to analytics => ', impressionsReq); - ajax(`http://${rivrAnalytics.context.host}/impressions`, () => { - }, JSON.stringify(impressionsReq)); + ajax( + `http://${rivrAnalytics.context.host}/impressions`, + () => {}, + JSON.stringify(impressionsReq), + { + customHeaders: { + 'Authorization': 'Basic b3V0ZXJwYXNzaXZlOkQ3OVZ5YXI1eVZXUEVBaHI=', + 'Content-type': 'application/json' + } + } + ); } }; From 727c455cf823be565a9cb354840d96016d82b8f7 Mon Sep 17 00:00:00 2001 From: adg Date: Fri, 31 Aug 2018 14:09:20 +0200 Subject: [PATCH 21/25] RVR-1852 - Change tracker host --- modules/rivrAnalyticsAdapter.js | 2 +- test/spec/modules/rivrAnalyticsAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 91b01d2362f..1d13ffe944d 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -5,7 +5,7 @@ import adaptermanager from 'src/adaptermanager'; import { logInfo, generateUUID } from 'src/utils'; const analyticsType = 'endpoint'; -const DEFAULT_HOST = 'integrations.rivr.simplaex.net'; +const DEFAULT_HOST = 'tracker.rivr.simplaex.com'; const DEFAULT_QUEUE_TIMEOUT = 4000; let rivrAnalytics = Object.assign(adapter({analyticsType}), { diff --git a/test/spec/modules/rivrAnalyticsAdapter_spec.js b/test/spec/modules/rivrAnalyticsAdapter_spec.js index 00bcfbc5357..c52711f6ee9 100644 --- a/test/spec/modules/rivrAnalyticsAdapter_spec.js +++ b/test/spec/modules/rivrAnalyticsAdapter_spec.js @@ -160,7 +160,7 @@ describe('', () => { } }); - expect(analyticsAdapter.context).to.have.property('host', 'integrations.rivr.simplaex.net'); + expect(analyticsAdapter.context).to.have.property('host', 'tracker.rivr.simplaex.com'); expect(analyticsAdapter.context).to.have.property('pubId', 777); }); From 6e6b172a3d57a447c8af42838a97ccb86b0b2a8a Mon Sep 17 00:00:00 2001 From: adg Date: Fri, 31 Aug 2018 14:25:06 +0200 Subject: [PATCH 22/25] RVR-1852 - Override content type instead of adding header --- modules/rivrAnalyticsAdapter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 1d13ffe944d..f3afde6ba95 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -59,9 +59,9 @@ function sendAuction() { () => {}, JSON.stringify(req), { + contentType: 'application/json', customHeaders: { - 'Authorization': 'Basic b3V0ZXJwYXNzaXZlOkQ3OVZ5YXI1eVZXUEVBaHI=', - 'Content-type': 'application/json' + 'Authorization': 'Basic b3V0ZXJwYXNzaXZlOkQ3OVZ5YXI1eVZXUEVBaHI=' } } ); @@ -77,9 +77,9 @@ function sendImpressions() { () => {}, JSON.stringify(impressionsReq), { + contentType: 'application/json', customHeaders: { - 'Authorization': 'Basic b3V0ZXJwYXNzaXZlOkQ3OVZ5YXI1eVZXUEVBaHI=', - 'Content-type': 'application/json' + 'Authorization': 'Basic b3V0ZXJwYXNzaXZlOkQ3OVZ5YXI1eVZXUEVBaHI=' } } ); From d73013554b5487dd08d08da295787c1995890a1e Mon Sep 17 00:00:00 2001 From: Alessandro Di Giovanni Date: Thu, 13 Sep 2018 15:14:40 +0000 Subject: [PATCH 23/25] RVR-1914 Consistent data types in events Also removes undefined and null properties in audience events --- modules/rivrAnalyticsAdapter.js | 24 ++++++++++++++----- .../spec/modules/rivrAnalyticsAdapter_spec.js | 21 +++++++++++++++- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index f3afde6ba95..ca64d4710bf 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -49,7 +49,9 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { } }); -function sendAuction() { +export function sendAuction() { + console.log('Function called: ============= sendAuction'); + removeEmptyProperties(rivrAnalytics.context.auctionObject) let auctionObject = rivrAnalytics.context.auctionObject; let req = Object.assign({}, {Auction: auctionObject}); rivrAnalytics.context.auctionObject = fulfillAuctionObject(); @@ -68,6 +70,7 @@ function sendAuction() { }; function sendImpressions() { + console.log('Function called: ============= sendImpressions'); let impressions = rivrAnalytics.context.queue.popAll(); if (impressions.length !== 0) { let impressionsReq = Object.assign({}, {impressions}); @@ -111,7 +114,7 @@ function trackBidWon(args) { function trackAuctionEnd(args) { rivrAnalytics.context.auctionTimeEnd = Date.now(); - createEmptyBidResponses(); + fillBidResponsesOfUnrespondedBidRequests(); }; function trackBidTimeout(args) { @@ -189,7 +192,7 @@ function createBidResponse(bidResponseEvent) { function createSingleEmptyBidResponse(bidResponse) { return { timestamp: bidResponse.start, - total_duration: 'noResponseDuration', + total_duration: null, bidderId: null, bidder_name: bidResponse.bidder, cur: null, @@ -443,10 +446,12 @@ export function ExpiringQueue(sendImpressions, sendAuction, ttl, log) { this.init = reset; function reset() { + console.log('Function called: ============= reset'); if (timeoutId) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { + console.log('Function called: ============= reset -> timeout expired'); sendAuction(); if (queue.length) { sendImpressions(); @@ -467,15 +472,15 @@ function assignBidWonStatusToResponse(wonBid) { }); }; -function createEmptyBidResponses() { - let unRespondedBidRequests = findAllUnrespondedBidRequests(); +function fillBidResponsesOfUnrespondedBidRequests() { + let unRespondedBidRequests = getAllUnrespondedBidRequests(); unRespondedBidRequests.forEach((bid) => { let emptyBidResponse = createSingleEmptyBidResponse(bid); rivrAnalytics.context.auctionObject.bidResponses.push(emptyBidResponse); }); }; -function findAllUnrespondedBidRequests() { +function getAllUnrespondedBidRequests() { let respondedBidIds = getAllRespondedBidIds(); let bidRequests = rivrAnalytics.context.auctionObject.bidRequests; let allNotRespondedBidRequests = bidRequests.reduce((cache, requestBidder) => { @@ -490,6 +495,13 @@ function getAllRespondedBidIds() { return rivrAnalytics.context.auctionObject.bidResponses.map((response) => response.seatbid[0].bid[0].adid); }; +function removeEmptyProperties(obj) { + Object.keys(obj).forEach(function(key) { + if (obj[key] && typeof obj[key] === 'object') removeEmptyProperties(obj[key]) + else if (obj[key] == null) delete obj[key] + }); +}; + // save the base class function rivrAnalytics.originEnableAnalytics = rivrAnalytics.enableAnalytics; diff --git a/test/spec/modules/rivrAnalyticsAdapter_spec.js b/test/spec/modules/rivrAnalyticsAdapter_spec.js index c52711f6ee9..0a462563d02 100644 --- a/test/spec/modules/rivrAnalyticsAdapter_spec.js +++ b/test/spec/modules/rivrAnalyticsAdapter_spec.js @@ -1,4 +1,4 @@ -import analyticsAdapter, {ExpiringQueue} from 'modules/rivrAnalyticsAdapter'; +import analyticsAdapter, {ExpiringQueue, sendAuction} from 'modules/rivrAnalyticsAdapter'; import {expect} from 'chai'; import adaptermanager from 'src/adaptermanager'; import * as ajax from 'src/ajax'; @@ -264,6 +264,7 @@ describe('', () => { events.emit(CONSTANTS.EVENTS.AUCTION_END, RESPONSE); const responses = analyticsAdapter.context.auctionObject.bidResponses; expect(responses.length).to.be.eql(3); + expect(responses[2].total_duration).to.be.eql(null); }) it('should handle winning bid', () => { @@ -335,5 +336,23 @@ describe('', () => { expect(responsesAfterSend.length).to.be.eql(0); expect(requestsAfterSend.length).to.be.eql(0); }); + + describe('sendAuction', () => { + it('clears empty payload properties', () => { + analyticsAdapter.context.auctionObject.nullProperty = null; + analyticsAdapter.context.auctionObject.notNullProperty = 'aValue'; + + sendAuction(); + + // sendAuction is called automatically. This is the reason why we are testing the second call here. + // Understand how to avoid it and isolate the test. + expect(ajaxStub.getCall(1).args[0]).to.match(/http:\/\/tracker.rivr.simplaex.com\/(\w+)\/auctions/); + + const payload = JSON.parse(ajaxStub.getCall(1).args[2]); + + expect(payload.Auction.notNullProperty).to.be.equal('aValue'); + expect(payload.nullProperty).to.be.equal(undefined); + }); + }); }); }); From b58312c034587645e0cdf6be5bd24032136cb809 Mon Sep 17 00:00:00 2001 From: Alessandro Di Giovanni Date: Mon, 17 Sep 2018 14:08:56 +0000 Subject: [PATCH 24/25] Merged in RVR-1883-Add-Basic-Access-Authentication (pull request #17) RVR-1883 Add Basic Access Authentication * RVR-1914 - Rename functions * RVR-1914 - Set default total_duration to null in bid response * RVR-1883 - Use RIVR_CLIENT_AUTH_TOKEN global variable for Auth token * RVR-1883 - Restore stub after every test not just at the end * RVR-1883 - Remove commented code --- modules/rivrAnalyticsAdapter.js | 60 ++++++++------- .../spec/modules/rivrAnalyticsAdapter_spec.js | 77 ++++++++++++------- 2 files changed, 83 insertions(+), 54 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index ca64d4710bf..38e857f472a 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -51,44 +51,49 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { export function sendAuction() { console.log('Function called: ============= sendAuction'); - removeEmptyProperties(rivrAnalytics.context.auctionObject) - let auctionObject = rivrAnalytics.context.auctionObject; - let req = Object.assign({}, {Auction: auctionObject}); - rivrAnalytics.context.auctionObject = fulfillAuctionObject(); - logInfo('sending request to analytics => ', req); - ajax( - `http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientID}/auctions`, - () => {}, - JSON.stringify(req), - { - contentType: 'application/json', - customHeaders: { - 'Authorization': 'Basic b3V0ZXJwYXNzaXZlOkQ3OVZ5YXI1eVZXUEVBaHI=' - } - } - ); -}; - -function sendImpressions() { - console.log('Function called: ============= sendImpressions'); - let impressions = rivrAnalytics.context.queue.popAll(); - if (impressions.length !== 0) { - let impressionsReq = Object.assign({}, {impressions}); - logInfo('sending impressions request to analytics => ', impressionsReq); + console.log('Function called: ============= sendAuction rivrAnalytics.context.authToken', rivrAnalytics.context.authToken); + if (rivrAnalytics.context.authToken) { + removeEmptyProperties(rivrAnalytics.context.auctionObject) + let auctionObject = rivrAnalytics.context.auctionObject; + let req = Object.assign({}, {Auction: auctionObject}); + rivrAnalytics.context.auctionObject = fulfillAuctionObject(); + logInfo('sending request to analytics => ', req); ajax( - `http://${rivrAnalytics.context.host}/impressions`, + `http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientID}/auctions`, () => {}, - JSON.stringify(impressionsReq), + JSON.stringify(req), { contentType: 'application/json', customHeaders: { - 'Authorization': 'Basic b3V0ZXJwYXNzaXZlOkQ3OVZ5YXI1eVZXUEVBaHI=' + 'Authorization': 'Basic ' + rivrAnalytics.context.authToken } } ); } }; +function sendImpressions() { + console.log('Function called: ============= sendImpressions'); + if (rivrAnalytics.context.authToken) { + let impressions = rivrAnalytics.context.queue.popAll(); + if (impressions.length !== 0) { + let impressionsReq = Object.assign({}, {impressions}); + logInfo('sending impressions request to analytics => ', impressionsReq); + ajax( + `http://${rivrAnalytics.context.host}/impressions`, + () => {}, + JSON.stringify(impressionsReq), + { + contentType: 'application/json', + customHeaders: { + 'Authorization': 'Basic ' + rivrAnalytics.context.authToken + } + } + ); + } + } +}; + function trackAuctionInit(args) { rivrAnalytics.context.auctionTimeStart = Date.now(); rivrAnalytics.context.auctionObject.id = args.auctionId; @@ -518,6 +523,7 @@ rivrAnalytics.enableAnalytics = (config) => { auctionObject: {}, adUnits: copiedUnits, clientID: config.options.clientID, + authToken: config.options.authToken, queue: new ExpiringQueue(sendImpressions, sendAuction, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) }; let bannersIds = config.options.bannersIds diff --git a/test/spec/modules/rivrAnalyticsAdapter_spec.js b/test/spec/modules/rivrAnalyticsAdapter_spec.js index 0a462563d02..98505c51228 100644 --- a/test/spec/modules/rivrAnalyticsAdapter_spec.js +++ b/test/spec/modules/rivrAnalyticsAdapter_spec.js @@ -133,11 +133,11 @@ describe('', () => { let timer; before(() => { - ajaxStub = sandbox.stub(ajax, 'ajax'); timer = sandbox.useFakeTimers(0); }); beforeEach(() => { + ajaxStub = sandbox.stub(ajax, 'ajax'); sandbox.stub(events, 'getEvents').callsFake(() => { return [] }); @@ -145,6 +145,7 @@ describe('', () => { afterEach(() => { events.getEvents.restore(); + ajaxStub.restore(); }); it('should be configurable', () => { @@ -315,43 +316,65 @@ describe('', () => { }); }); - it('sends request after timeout', () => { - let impressions = analyticsAdapter.context.auctionObject.imp; - let responses = analyticsAdapter.context.auctionObject.bidResponses; - let requests = analyticsAdapter.context.auctionObject.bidRequests; + describe('when authToken is defined', () => { + it('sends request after timeout', () => { + analyticsAdapter.context.authToken = 'anAuthToken'; + let impressions = analyticsAdapter.context.auctionObject.imp; + let responses = analyticsAdapter.context.auctionObject.bidResponses; + let requests = analyticsAdapter.context.auctionObject.bidRequests; - expect(impressions.length).to.be.eql(1); - expect(responses.length).to.be.eql(3); - expect(requests.length).to.be.eql(3); - expect(ajaxStub.calledOnce).to.be.equal(false); + expect(impressions.length).to.be.eql(1); + expect(responses.length).to.be.eql(3); + expect(requests.length).to.be.eql(3); + expect(ajaxStub.notCalled).to.be.equal(true); - timer.tick(4500); + timer.tick(4500); - let impressionsAfterSend = analyticsAdapter.context.auctionObject.imp; - let responsesAfterSend = analyticsAdapter.context.auctionObject.bidResponses; - let requestsAfterSend = analyticsAdapter.context.auctionObject.bidRequests; + let impressionsAfterSend = analyticsAdapter.context.auctionObject.imp; + let responsesAfterSend = analyticsAdapter.context.auctionObject.bidResponses; + let requestsAfterSend = analyticsAdapter.context.auctionObject.bidRequests; - expect(ajaxStub.calledOnce).to.be.equal(true); - expect(impressionsAfterSend.length).to.be.eql(0); - expect(responsesAfterSend.length).to.be.eql(0); - expect(requestsAfterSend.length).to.be.eql(0); + expect(ajaxStub.calledOnce).to.be.equal(true); + expect(impressionsAfterSend.length).to.be.eql(0); + expect(responsesAfterSend.length).to.be.eql(0); + expect(requestsAfterSend.length).to.be.eql(0); + + analyticsAdapter.context.authToken = undefined; + }); }); describe('sendAuction', () => { - it('clears empty payload properties', () => { - analyticsAdapter.context.auctionObject.nullProperty = null; - analyticsAdapter.context.auctionObject.notNullProperty = 'aValue'; + describe('when authToken is defined', () => { + it('fires call clearing empty payload properties', () => { + analyticsAdapter.context.authToken = 'anAuthToken'; + analyticsAdapter.context.auctionObject.nullProperty = null; + analyticsAdapter.context.auctionObject.notNullProperty = 'aValue'; + + sendAuction(); + + expect(ajaxStub.getCall(0).args[0]).to.match(/http:\/\/tracker.rivr.simplaex.com\/(\w+)\/auctions/); + + const payload = JSON.parse(ajaxStub.getCall(0).args[2]); + + expect(payload.Auction.notNullProperty).to.be.equal('aValue'); + expect(payload.nullProperty).to.be.equal(undefined); + + analyticsAdapter.context.authToken = undefined; + }); + }); - sendAuction(); + describe('when authToken is not defined', () => { + it('does not fire call', () => { + analyticsAdapter.context.authToken = undefined; + analyticsAdapter.context.auctionObject.nullProperty = null; + analyticsAdapter.context.auctionObject.notNullProperty = 'aValue'; - // sendAuction is called automatically. This is the reason why we are testing the second call here. - // Understand how to avoid it and isolate the test. - expect(ajaxStub.getCall(1).args[0]).to.match(/http:\/\/tracker.rivr.simplaex.com\/(\w+)\/auctions/); + expect(ajaxStub.callCount).to.be.equal(0); - const payload = JSON.parse(ajaxStub.getCall(1).args[2]); + sendAuction(); - expect(payload.Auction.notNullProperty).to.be.equal('aValue'); - expect(payload.nullProperty).to.be.equal(undefined); + expect(ajaxStub.callCount).to.be.equal(0); + }); }); }); }); From bc9288506691ae491a172a3492defd8dff049538 Mon Sep 17 00:00:00 2001 From: Alessandro Di Giovanni Date: Mon, 1 Oct 2018 15:14:50 +0000 Subject: [PATCH 25/25] Increase code coverage --- modules/rivrAnalyticsAdapter.js | 51 +- .../spec/modules/rivrAnalyticsAdapter_spec.js | 872 ++++++++++++------ 2 files changed, 621 insertions(+), 302 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 38e857f472a..846c8bd3250 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -17,6 +17,7 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { let handler = null; switch (eventType) { case CONSTANTS.EVENTS.AUCTION_INIT: + logInfo(`CONSTANTS.EVENTS.AUCTION_INIT rivrAnalytics.context.auctionObject`, rivrAnalytics.context.auctionObject); if (rivrAnalytics.context.queue) { rivrAnalytics.context.queue.init(); } @@ -50,10 +51,8 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { }); export function sendAuction() { - console.log('Function called: ============= sendAuction'); - console.log('Function called: ============= sendAuction rivrAnalytics.context.authToken', rivrAnalytics.context.authToken); if (rivrAnalytics.context.authToken) { - removeEmptyProperties(rivrAnalytics.context.auctionObject) + removeEmptyProperties(rivrAnalytics.context.auctionObject); let auctionObject = rivrAnalytics.context.auctionObject; let req = Object.assign({}, {Auction: auctionObject}); rivrAnalytics.context.auctionObject = fulfillAuctionObject(); @@ -62,6 +61,7 @@ export function sendAuction() { `http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientID}/auctions`, () => {}, JSON.stringify(req), + // TODO extract this object to variable { contentType: 'application/json', customHeaders: { @@ -72,8 +72,7 @@ export function sendAuction() { } }; -function sendImpressions() { - console.log('Function called: ============= sendImpressions'); +export function sendImpressions() { if (rivrAnalytics.context.authToken) { let impressions = rivrAnalytics.context.queue.popAll(); if (impressions.length !== 0) { @@ -141,20 +140,23 @@ function setCurrentPublisherId(bidRequested) { } }; -function fetchLocalization() { +export function fetchLocalization() { if (navigator.permissions) { navigator.permissions.query({ name: 'geolocation' }).then((permission) => { if (permission.status === 'granted') { navigator.geolocation.getCurrentPosition((position) => { - let deviceLocation = rivrAnalytics.context.auctionObject.device.geo; - deviceLocation.lat = position.coords.latitude; - deviceLocation.long = position.coords.longitude; + setAuctionAbjectPosition(position); }); } }); } } +export function setAuctionAbjectPosition(position) { + rivrAnalytics.context.auctionObject.device.geo.lat = position.coords.latitude; + rivrAnalytics.context.auctionObject.device.geo.long = position.coords.longitude; +} + function getPlatformType() { if (navigator.userAgent.match(/mobile/i)) { return 'Mobile'; @@ -223,7 +225,7 @@ function createAuctionImpression(bidWonEvent) { } }; -function reportClickEvent(event) { +export function reportClickEvent(event) { let link = event.currentTarget.getElementsByTagName('a')[0]; let clickUrl; if (link) { @@ -237,6 +239,8 @@ function reportClickEvent(event) { 'click_url': clickUrl }; logInfo('Sending click events with parameters: ', req); + + // TODO add Authentication header ajax(`http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientID}/clicks`, () => { }, JSON.stringify(req)); }; @@ -249,7 +253,7 @@ function addDisplayedImpHandler(bannerId) { pinHandlerToHTMLElement(bannerId, dataLoaderForHandler, impHandler); }; -function pinHandlerToHTMLElement(elementId, dataLoaderForHandler, specializedHandler) { +export function pinHandlerToHTMLElement(elementId, dataLoaderForHandler, specializedHandler) { function waitForElement() { let element = document.getElementById(elementId); if (!element) { @@ -261,7 +265,7 @@ function pinHandlerToHTMLElement(elementId, dataLoaderForHandler, specializedHan waitForElement(); } -function dataLoaderForHandler(element, specializedHandler) { +export function dataLoaderForHandler(element, specializedHandler) { function waitForElement() { let iframe = element.getElementsByTagName('iframe')[0]; if (!iframe) { @@ -363,7 +367,7 @@ function fulfillAuctionObject() { 'ext.rivr.optimiser': localStorage.getItem('rivr_should_optimise') || 'unoptimised', modelVersion: localStorage.getItem('rivr_model_version') || null, 'ext.rivr.originalvalues': [] - } + }; return newAuction; }; @@ -374,9 +378,9 @@ function saveUnoptimisedParams() { let allUnits = connectAllUnits(units); allUnits.forEach((adUnit) => { adUnit.bids.forEach((bid) => { - let configForAd = fetchConfigForBidder(bid.bidder); - if (configForAd) { - let unOptimisedParamsField = createUnOptimisedParamsField(bid, configForAd) + let configForBidder = fetchConfigForBidder(bid.bidder); + if (configForBidder) { + let unOptimisedParamsField = createUnOptimisedParamsField(bid, configForBidder); rivrAnalytics.context.auctionObject['ext.rivr.originalvalues'].push(unOptimisedParamsField); } }) @@ -392,15 +396,15 @@ function connectAllUnits(units) { }, []); } -function createUnOptimisedParamsField(unit, config) { +export function createUnOptimisedParamsField(bid, config) { let floorPriceLabel = config['floorPriceLabel']; let currencyLabel = config['currencyLabel']; let pmpLabel = config['pmpLabel']; return { - 'ext.rivr.demand_source_original': unit.bidder, - 'ext.rivr.bidfloor_original': unit.params[floorPriceLabel], - 'ext.rivr.currency_original': unit.params[currencyLabel], - 'ext.rivr.pmp_original': unit.params[pmpLabel], + 'ext.rivr.demand_source_original': bid.bidder, + 'ext.rivr.bidfloor_original': bid.params[floorPriceLabel], + 'ext.rivr.currency_original': bid.params[currencyLabel], + 'ext.rivr.pmp_original': bid.params[pmpLabel], } } @@ -451,12 +455,10 @@ export function ExpiringQueue(sendImpressions, sendAuction, ttl, log) { this.init = reset; function reset() { - console.log('Function called: ============= reset'); if (timeoutId) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { - console.log('Function called: ============= reset -> timeout expired'); sendAuction(); if (queue.length) { sendImpressions(); @@ -526,7 +528,8 @@ rivrAnalytics.enableAnalytics = (config) => { authToken: config.options.authToken, queue: new ExpiringQueue(sendImpressions, sendAuction, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) }; - let bannersIds = config.options.bannersIds + + let bannersIds = config.options.bannersIds; if (bannersIds) { if (bannersIds.length > 0) { addHandlers(config.options.bannersIds); diff --git a/test/spec/modules/rivrAnalyticsAdapter_spec.js b/test/spec/modules/rivrAnalyticsAdapter_spec.js index 98505c51228..dca63bf935e 100644 --- a/test/spec/modules/rivrAnalyticsAdapter_spec.js +++ b/test/spec/modules/rivrAnalyticsAdapter_spec.js @@ -1,4 +1,15 @@ -import analyticsAdapter, {ExpiringQueue, sendAuction} from 'modules/rivrAnalyticsAdapter'; +import * as utils from 'src/utils'; +import analyticsAdapter from 'modules/rivrAnalyticsAdapter'; +import { + ExpiringQueue, + sendAuction, + sendImpressions, + reportClickEvent, + createUnOptimisedParamsField, + dataLoaderForHandler, + pinHandlerToHTMLElement, + setAuctionAbjectPosition, +} from 'modules/rivrAnalyticsAdapter'; import {expect} from 'chai'; import adaptermanager from 'src/adaptermanager'; import * as ajax from 'src/ajax'; @@ -6,50 +17,557 @@ import CONSTANTS from 'src/constants.json'; const events = require('../../../src/events'); -describe('', () => { +describe('RIVR Analytics adapter', () => { + const EXPIRING_QUEUE_TIMEOUT = 4000; + const EXPIRING_QUEUE_TIMEOUT_MOCK = 100; + const PUBLISHER_ID_MOCK = 777; + const EMITTED_AUCTION_ID = 1; + const TRACKER_BASE_URL_MOCK = 'tracker.rivr.simplaex.com'; let sandbox; + let ajaxStub; + let timer; before(() => { sandbox = sinon.sandbox.create(); }); + beforeEach(() => { + timer = sandbox.useFakeTimers(0); + ajaxStub = sandbox.stub(ajax, 'ajax'); + sinon.stub(events, 'getEvents').returns([]); + + adaptermanager.registerAnalyticsAdapter({ + code: 'rivr', + adapter: analyticsAdapter + }); + adaptermanager.enableAnalytics({ + provider: 'rivr', + options: { + pubId: PUBLISHER_ID_MOCK, + adUnits: [utils.deepClone(AD_UNITS_MOCK)] + } + }); + }); + + afterEach(() => { + analyticsAdapter.disableAnalytics(); + events.getEvents.restore(); + ajaxStub.restore(); + timer.restore(); + }); + after(() => { sandbox.restore(); - analyticsAdapter.disableAnalytics(); }); - describe('ExpiringQueue', () => { - let timer; - before(() => { - timer = sandbox.useFakeTimers(0); + it('ExpiringQueue should call sendImpression callback after expiring queue timeout is elapsed', (done) => { + const sendImpressionMock = () => { + let elements = queue.popAll(); + expect(elements).to.be.eql([1, 2, 3, 4]); + elements = queue.popAll(); + expect(elements).to.have.lengthOf(0); + expect(Date.now()).to.be.equal(200); + done(); + }; + const sendAuctionMock = () => {}; + + let queue = new ExpiringQueue( + sendImpressionMock, + sendAuctionMock, + EXPIRING_QUEUE_TIMEOUT_MOCK); + + queue.push(1); + + setTimeout(() => { + queue.push([2, 3]); + timer.tick(50); + }, 50); + setTimeout(() => { + queue.push([4]); + timer.tick(100); + }, 100); + timer.tick(50); + }); + + it('enableAnalytics - should configure host and pubId in adapter context', () => { + // adaptermanager.enableAnalytics() is called in beforeEach. If only called here it doesn't seem to work. + + expect(analyticsAdapter.context).to.have.property('host', TRACKER_BASE_URL_MOCK); + expect(analyticsAdapter.context).to.have.property('pubId', PUBLISHER_ID_MOCK); + }); + + it('Firing AUCTION_INIT should set auction id of context when AUCTION_INIT event is fired', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: EMITTED_AUCTION_ID, config: {}, timeout: 3000}); + const auctionId = analyticsAdapter.context.auctionObject.id; + expect(auctionId).to.be.eql(EMITTED_AUCTION_ID); + }); + + it('Firing AUCTION_INIT when rivr_should_optimise and rivr_model_version are in local storage, sets ext.rivr.optimiser and modelVersion of in auction context', () => { + const RIVR_SHOULD_OPTIMISE_VALUE_MOCK = 'optimise'; + const RIVR_MODEL_VERSION_VALUE_MOCK = 'some model version'; + + localStorage.setItem('rivr_should_optimise', RIVR_SHOULD_OPTIMISE_VALUE_MOCK); + localStorage.setItem('rivr_model_version', RIVR_MODEL_VERSION_VALUE_MOCK); + + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 2, config: {}, timeout: 3000}); + + let auctionObject2 = analyticsAdapter.context.auctionObject; + + expect(auctionObject2['ext.rivr.optimiser']).to.be.eql(RIVR_SHOULD_OPTIMISE_VALUE_MOCK); + expect(auctionObject2['modelVersion']).to.be.eql(RIVR_MODEL_VERSION_VALUE_MOCK); + + localStorage.removeItem('rivr_should_optimise'); + localStorage.removeItem('rivr_model_version'); + }); + + it('Firing AUCTION_INIT , when auction object is already there and rivr_config_string is not in local storage, it does not save unoptimized params in rivr original values', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 3, config: {}, timeout: 3000}); + + expect(analyticsAdapter.context.auctionObject['ext.rivr.originalvalues']).to.be.eql([]); + }); + + it('Firing AUCTION_INIT when rivr_should_optimise and rivr_model_version are NOT in local storage, does not set ext.rivr.optimiser and modelVersion of in auction context', () => { + localStorage.removeItem('rivr_should_optimise'); + localStorage.removeItem('rivr_model_version'); + + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 3, config: {}, timeout: 3000}); + + let auctionObject3 = analyticsAdapter.context.auctionObject; + + expect(auctionObject3['ext.rivr.optimiser']).to.be.eql('unoptimised'); + expect(auctionObject3['modelVersion']).to.be.eql(null); + }); + + it('Firing BID_REQUESTED it sets app and site publisher id in auction object', () => { + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST); + + const sitePubcid = analyticsAdapter.context.auctionObject.site.publisher.id; + const appPubcid = analyticsAdapter.context.auctionObject.app.publisher.id; + expect(sitePubcid).to.be.eql(PUBLISHER_ID_MOCK); + expect(appPubcid).to.be.eql(PUBLISHER_ID_MOCK); + }); + + it('Firing BID_REQUESTED it adds bid request in bid requests array', () => { + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST); + + const requestEvent = analyticsAdapter.context.auctionObject.bidRequests; + expect(requestEvent).to.have.length(1); + expect(requestEvent[0]).to.be.eql({ + bidderCode: 'adapter', + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', + bidderRequestId: '1a6fc81528d0f6', + bids: [{ + bidder: 'adapter', + params: {}, + adUnitCode: 'container-1', + transactionId: 'de90df62-7fd0-4fbc-8787-92d133a7dc06', + sizes: [[300, 250]], + bidId: '208750227436c1', + bidderRequestId: '1a6fc81528d0f6', + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f' + }], + auctionStart: 1509369418387, + timeout: 3000, + start: 1509369418389 }); - after(() => { - timer.restore(); + }); + + it('Firing BID_RESPONSE it inserts bid response object in auctionObject', () => { + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + + events.emit(CONSTANTS.EVENTS.BID_RESPONSE, BID_RESPONSE_MOCK); + const bidResponses = analyticsAdapter.context.auctionObject.bidResponses; + + expect(bidResponses).to.have.length(1); + expect(bidResponses[0]).to.be.eql({ + timestamp: 1509369418832, + status: 1, + 'total_duration': 443, + bidderId: null, + 'bidder_name': 'adapter', + cur: 'EU', + seatbid: [ + { + seat: null, + bid: [ + { + status: 2, + 'clear_price': 0.015, + attr: [], + crid: 999, + cid: null, + id: null, + adid: '208750227436c1', + adomain: [], + iurl: null + } + ] + } + ] }); + }); - it('should notify after timeout period', (done) => { - let queue = new ExpiringQueue(() => { - let elements = queue.popAll(); - expect(elements).to.be.eql([1, 2, 3, 4]); - elements = queue.popAll(); - expect(elements).to.have.lengthOf(0); - expect(Date.now()).to.be.equal(200); - done(); - }, () => {}, 100); - - queue.push(1); - setTimeout(() => { - queue.push([2, 3]); - timer.tick(50); - }, 50); - setTimeout(() => { - queue.push([4]); - timer.tick(100); - }, 100); - timer.tick(50); + it('Firing AUCTION_END it sets auction time end to current time', () => { + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + + const MILLIS_FROM_EPOCH_TO_NOW_MOCK = 477; + timer.tick(MILLIS_FROM_EPOCH_TO_NOW_MOCK); + + events.emit(CONSTANTS.EVENTS.AUCTION_END, BID_RESPONSE_MOCK); + + const endTime = analyticsAdapter.context.auctionTimeEnd; + expect(endTime).to.be.eql(MILLIS_FROM_EPOCH_TO_NOW_MOCK); + }); + + it('Firing AUCTION_END when there are unresponded bid requests should insert then to bidResponses in auctionObject with null duration', () => { + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST2); + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST3); + events.emit(CONSTANTS.EVENTS.AUCTION_END, BID_RESPONSE_MOCK); + + const responses = analyticsAdapter.context.auctionObject.bidResponses; + expect(responses.length).to.be.eql(2); + expect(responses[0].total_duration).to.be.eql(null); + expect(responses[1].total_duration).to.be.eql(null); + }); + + it('Firing BID_WON when it happens after BID_RESPONSE should add won event as auction impression to imp array', () => { + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + + events.emit(CONSTANTS.EVENTS.BID_RESPONSE, BID_RESPONSE_MOCK); + events.emit(CONSTANTS.EVENTS.BID_WON, BID_RESPONSE_MOCK); + + const wonEvent = analyticsAdapter.context.auctionObject.imp; + + expect(wonEvent.length).to.be.eql(1); + expect(wonEvent[0]).to.be.eql({ + tagid: 'container-1', + displaymanager: null, + displaymanagerver: null, + secure: null, + bidfloor: null, + banner: { + w: 300, + h: 250, + pos: null, + expandable: [], + api: [] + } }); }); + it('Firing BID_WON when it happens after BID_RESPONSE should change the status of winning bidResponse to 1', () => { + const BID_STATUS_WON = 1; + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + + events.emit(CONSTANTS.EVENTS.BID_RESPONSE, BID_RESPONSE_MOCK); + events.emit(CONSTANTS.EVENTS.BID_WON, BID_RESPONSE_MOCK); + + const responseWhichIsWonAlso = analyticsAdapter.context.auctionObject.bidResponses[0]; + + expect(responseWhichIsWonAlso.seatbid[0].bid[0].status).to.be.eql(BID_STATUS_WON); + }); + + it('when auction is initialized and authToken is defined and ExpiringQueue ttl expires, it sends the auction', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: EMITTED_AUCTION_ID, config: {}, timeout: 3000}); + analyticsAdapter.context.authToken = 'anAuthToken'; + + expect(ajaxStub.notCalled).to.be.equal(true); + + timer.tick(EXPIRING_QUEUE_TIMEOUT + 500); + + expect(ajaxStub.calledOnce).to.be.equal(true); + }); + + it('when auction is initialized and authToken is defined and ExpiringQueue ttl expires, it clears imp, bidResponses and bidRequests', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: EMITTED_AUCTION_ID, config: {}, timeout: 3000}); + + analyticsAdapter.context.authToken = 'anAuthToken'; + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST); + events.emit(CONSTANTS.EVENTS.BID_RESPONSE, BID_RESPONSE_MOCK); + events.emit(CONSTANTS.EVENTS.BID_WON, BID_RESPONSE_MOCK); + + let impressions = analyticsAdapter.context.auctionObject.imp; + let responses = analyticsAdapter.context.auctionObject.bidResponses; + let requests = analyticsAdapter.context.auctionObject.bidRequests; + + expect(impressions.length).to.be.eql(1); + expect(responses.length).to.be.eql(1); + expect(requests.length).to.be.eql(1); + + timer.tick(EXPIRING_QUEUE_TIMEOUT + 500); + + let impressionsAfterSend = analyticsAdapter.context.auctionObject.imp; + let responsesAfterSend = analyticsAdapter.context.auctionObject.bidResponses; + let requestsAfterSend = analyticsAdapter.context.auctionObject.bidRequests; + + expect(impressionsAfterSend.length).to.be.eql(0); + expect(responsesAfterSend.length).to.be.eql(0); + expect(requestsAfterSend.length).to.be.eql(0); + }); + + it('sendAuction(), when authToken is defined, it fires call clearing empty payload properties', () => { + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + analyticsAdapter.context.authToken = 'anAuthToken'; + analyticsAdapter.context.auctionObject.nullProperty = null; + analyticsAdapter.context.auctionObject.notNullProperty = 'aValue'; + + sendAuction(); + + expect(ajaxStub.getCall(0).args[0]).to.match(/http:\/\/tracker.rivr.simplaex.com\/(\w+)\/auctions/); + + const payload = JSON.parse(ajaxStub.getCall(0).args[2]); + + expect(payload.Auction.notNullProperty).to.be.equal('aValue'); + expect(payload.nullProperty).to.be.equal(undefined); + + analyticsAdapter.context.authToken = undefined; + }); + + it('sendAuction(), when authToken is not defined, it does not fire call', () => { + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + analyticsAdapter.context.authToken = undefined; + analyticsAdapter.context.auctionObject.nullProperty = null; + analyticsAdapter.context.auctionObject.notNullProperty = 'aValue'; + + expect(ajaxStub.callCount).to.be.equal(0); + + sendAuction(); + + expect(ajaxStub.callCount).to.be.equal(0); + }); + + it('sendImpressions(), when authToken is not defined, it does not fire call', () => { + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + analyticsAdapter.context.authToken = undefined; + analyticsAdapter.context.auctionObject.nullProperty = null; + analyticsAdapter.context.auctionObject.notNullProperty = 'aValue'; + + expect(ajaxStub.callCount).to.be.equal(0); + + sendImpressions(); + + expect(ajaxStub.callCount).to.be.equal(0); + }); + + it('sendImpressions(), when authToken is defined and there are impressions, it sends impressions to the tracker', () => { + const aMockString = 'anImpressionPropertyValue'; + const IMPRESSION_MOCK = { anImpressionProperty: aMockString }; + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + analyticsAdapter.context.authToken = 'anAuthToken'; + analyticsAdapter.context.queue = new ExpiringQueue( + () => {}, + () => {}, + EXPIRING_QUEUE_TIMEOUT_MOCK + ); + + analyticsAdapter.context.queue.push(IMPRESSION_MOCK); + + expect(ajaxStub.callCount).to.be.equal(0); + + sendImpressions(); + + const payload = JSON.parse(ajaxStub.getCall(0).args[2]); + + expect(ajaxStub.callCount).to.be.equal(1); + expect(payload.impressions.length).to.be.equal(1); + expect(ajaxStub.getCall(0).args[0]).to.match(/http:\/\/tracker.rivr.simplaex.com\/impressions/); + expect(payload.impressions[0].anImpressionProperty).to.be.equal(aMockString); + }); + + it('reportClickEvent(), when authToken is not defined, it calls endpoint', () => { + const CLIENT_ID_MOCK = 'aClientId'; + const CLICK_URL_MOCK = 'clickURLMock'; + const EVENT_MOCK = { + currentTarget: { + getElementsByTagName: () => { + return [ + { + getAttribute: (attributeName) => { + return CLICK_URL_MOCK; + } + } + ] + } + } + }; + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + analyticsAdapter.context.authToken = undefined; + analyticsAdapter.context.clientID = CLIENT_ID_MOCK; + analyticsAdapter.context.auctionObject.nullProperty = null; + analyticsAdapter.context.auctionObject.notNullProperty = 'aValue'; + + expect(ajaxStub.callCount).to.be.equal(0); + + reportClickEvent(EVENT_MOCK); + + const payload = JSON.parse(ajaxStub.getCall(0).args[2]); + + expect(ajaxStub.callCount).to.be.equal(1); + expect(ajaxStub.getCall(0).args[0]).to.match(/http:\/\/tracker.rivr.simplaex.com\/aClientId\/clicks/); + expect(payload.timestamp).to.be.equal('1970-01-01T00:00:00.000Z'); + expect(payload.request_id).to.be.a('string'); + expect(payload.click_url).to.be.equal(CLICK_URL_MOCK); + }); + + it('createUnOptimisedParamsField(), creates object with unoptimized properties', () => { + const CONFIG_FOR_BIDDER_MOCK = { + floorPriceLabel: 'floorPriceLabelForTestBidder', + currencyLabel: 'currencyLabelForTestBidder', + pmpLabel: 'pmpLabelForTestBidder', + }; + const BID_MOCK = { + bidder: 'aBidder', + params: { + floorPriceLabelForTestBidder: 'theOriginalBidFloor', + currencyLabelForTestBidder: 'theOriginalCurrency', + pmpLabelForTestBidder: 'theOriginalPmp', + }, + }; + + const result = createUnOptimisedParamsField(BID_MOCK, CONFIG_FOR_BIDDER_MOCK); + + expect(result['ext.rivr.demand_source_original']).to.be.equal('aBidder'); + expect(result['ext.rivr.bidfloor_original']).to.be.equal('theOriginalBidFloor'); + expect(result['ext.rivr.currency_original']).to.be.equal('theOriginalCurrency'); + expect(result['ext.rivr.pmp_original']).to.be.equal('theOriginalPmp'); + }); + + it('dataLoaderForHandler(), when iframe and the ad image contained in it are there, it calls the specialized handler', () => { + const MOCK_ELEMENT = { + getElementsByTagName: () => { + return [ + { + contentDocument: { + getElementsByTagName: () => { + return ['displayedImpressionMock'] + } + }, + aDummyProperty: 'aDummyPropertyValue' + } + ] + } + }; + + var specializedHandlerSpy = sinon.spy(); + + expect(specializedHandlerSpy.callCount).to.be.equal(0); + + dataLoaderForHandler(MOCK_ELEMENT, specializedHandlerSpy); + + expect(specializedHandlerSpy.callCount).to.be.equal(1); + expect(specializedHandlerSpy.firstCall.args[0].aDummyProperty).to.be.equal('aDummyPropertyValue'); + expect(specializedHandlerSpy.firstCall.args[0].contentDocument.getElementsByTagName()[0]).to.be.equal('displayedImpressionMock'); + }); + + it('dataLoaderForHandler(), when iframe is not there, it requests animation frame', () => { + const MOCK_ELEMENT = { + getElementsByTagName: () => { + return [ + { + contentDocument: { + getElementsByTagName: () => { + return [] + } + }, + } + ] + } + }; + + const specializedHandlerSpy = sinon.spy(); + const requestAnimationFrameStub = sinon.stub(window, 'requestAnimationFrame'); + expect(requestAnimationFrameStub.callCount).to.be.equal(0); + + dataLoaderForHandler(MOCK_ELEMENT, specializedHandlerSpy); + + expect(requestAnimationFrameStub.callCount).to.be.equal(1); + + requestAnimationFrameStub.restore(); + }); + + it('pinHandlerToHTMLElement(), when element is there, it calls dataLoaderForHandler', () => { + const ELEMENT_MOCK = { + anElementProperty: 'aValue' + } + const dataLoaderForHandlerSpy = sinon.spy(); + sinon.stub(window, 'requestAnimationFrame'); + + sinon.stub(document, 'getElementById').returns(ELEMENT_MOCK); + + expect(dataLoaderForHandlerSpy.callCount).to.be.equal(0); + + pinHandlerToHTMLElement('', dataLoaderForHandlerSpy, () => {}); + + expect(dataLoaderForHandlerSpy.callCount).to.be.equal(1); + expect(dataLoaderForHandlerSpy.firstCall.args[0].anElementProperty).to.be.equal('aValue'); + + window.requestAnimationFrame.restore(); + document.getElementById.restore(); + }); + + it('pinHandlerToHTMLElement(), when element is not there, it requests animation frame', () => { + const dataLoaderForHandlerSpy = sinon.spy(); + const requestAnimationFrameStub = sinon.stub(window, 'requestAnimationFrame'); + + sinon.stub(document, 'getElementById').returns(undefined); + + expect(requestAnimationFrameStub.callCount).to.be.equal(0); + + pinHandlerToHTMLElement('', dataLoaderForHandlerSpy, () => {}); + + expect(dataLoaderForHandlerSpy.callCount).to.be.equal(0); + expect(requestAnimationFrameStub.callCount).to.be.equal(1); + + requestAnimationFrameStub.restore(); + document.getElementById.restore(); + }); + + it('setAuctionAbjectPosition(), it sets latitude and longitude in auction object', () => { + const POSITION_MOCK = { + coords: { + latitude: 'aLatitude', + longitude: 'aLongitude', + } + } + analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + + setAuctionAbjectPosition(POSITION_MOCK); + + expect(analyticsAdapter.context.auctionObject.device.geo.lat).to.be.equal('aLatitude'); + }); + + const AD_UNITS_MOCK = [ + { + code: 'banner-container1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 200], [300, 600]] + } + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: '10433394', + reserve: 0.5 + } + }, + { + bidder: 'huddledmasses', + params: { + placement_id: 0 + } + }, + ] + } + ]; + const REQUEST = { bidderCode: 'adapter', auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', @@ -107,7 +625,7 @@ describe('', () => { start: 1509369418389 }; - const RESPONSE = { + const BID_RESPONSE_MOCK = { bidderCode: 'adapter', width: 300, height: 250, @@ -128,254 +646,52 @@ describe('', () => { size: '300x250' }; - describe('Analytics adapter', () => { - let ajaxStub; - let timer; - - before(() => { - timer = sandbox.useFakeTimers(0); - }); - - beforeEach(() => { - ajaxStub = sandbox.stub(ajax, 'ajax'); - sandbox.stub(events, 'getEvents').callsFake(() => { - return [] - }); - }); - - afterEach(() => { - events.getEvents.restore(); - ajaxStub.restore(); - }); - - it('should be configurable', () => { - adaptermanager.registerAnalyticsAdapter({ - code: 'rivr', - adapter: analyticsAdapter - }); - - adaptermanager.enableAnalytics({ - provider: 'rivr', - options: { - pubId: 777, + const CONTEXT_AFTER_AUCTION_INIT = { + host: TRACKER_BASE_URL_MOCK, + pubId: PUBLISHER_ID_MOCK, + queue: { + mockProp: 'mockValue' + }, + auctionObject: { + id: null, + timestamp: null, + at: null, + bcat: [], + imp: [], + app: { + id: null, + name: null, + domain: window.location.href, + bundle: null, + cat: [], + publisher: { + id: null, + name: null } - }); - - expect(analyticsAdapter.context).to.have.property('host', 'tracker.rivr.simplaex.com'); - expect(analyticsAdapter.context).to.have.property('pubId', 777); - }); - - it('should handle auction init event', () => { - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 1, config: {}, timeout: 3000}); - const auctionId = analyticsAdapter.context.auctionObject.id; - const auctionStart = analyticsAdapter.context.auctionTimeStart; - expect(auctionId).to.be.eql(1); - }); - - it('should map proper response params on auction init', () => { - localStorage.setItem('rivr_should_optimise', 'optimise') - localStorage.setItem('rivr_model_version', 'some model version'); - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 2, config: {}, timeout: 3000}); - let auctionObject2 = analyticsAdapter.context.auctionObject; - - expect(auctionObject2['ext.rivr.optimiser']).to.be.eql('optimise'); - expect(auctionObject2['modelVersion']).to.be.eql('some model version'); - - localStorage.removeItem('rivr_should_optimise'); - localStorage.removeItem('rivr_model_version'); - - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 3, config: {}, timeout: 3000}); - - let auctionObject3 = analyticsAdapter.context.auctionObject; - - expect(auctionObject3['ext.rivr.optimiser']).to.be.eql('unoptimised'); - expect(auctionObject3['modelVersion']).to.be.eql(null); - }) - - it('should handle bid request event', () => { - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST); - const sitePubcid = analyticsAdapter.context.auctionObject.site.publisher.id; - const appPubcid = analyticsAdapter.context.auctionObject.app.publisher.id; - const requestEvent = analyticsAdapter.context.auctionObject.bidRequests; - expect(sitePubcid).to.be.eql(777); - expect(sitePubcid).to.be.eql(appPubcid); - expect(requestEvent).to.have.length(1); - expect(requestEvent[0]).to.be.eql({ - bidderCode: 'adapter', - auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', - bidderRequestId: '1a6fc81528d0f6', - bids: [{ - bidder: 'adapter', - params: {}, - adUnitCode: 'container-1', - transactionId: 'de90df62-7fd0-4fbc-8787-92d133a7dc06', - sizes: [[300, 250]], - bidId: '208750227436c1', - bidderRequestId: '1a6fc81528d0f6', - auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f' - }], - auctionStart: 1509369418387, - timeout: 3000, - start: 1509369418389 - }); - }); - - it('should handle bid response event', () => { - events.emit(CONSTANTS.EVENTS.BID_RESPONSE, RESPONSE); - const responseEvent = analyticsAdapter.context.auctionObject.bidResponses; - expect(responseEvent).to.have.length(1); - expect(responseEvent[0]).to.be.eql({ - timestamp: 1509369418832, - status: 1, - 'total_duration': 443, - bidderId: null, - 'bidder_name': 'adapter', - cur: 'EU', - seatbid: [ - { - seat: null, - bid: [ - { - status: 2, - 'clear_price': 0.015, - attr: [], - crid: 999, - cid: null, - id: null, - adid: '208750227436c1', - adomain: [], - iurl: null - } - ] - } - ] - }); - }); - - it('should handle auction end event', () => { - timer.tick(447); - events.emit(CONSTANTS.EVENTS.AUCTION_END, RESPONSE); - const endTime = analyticsAdapter.context.auctionTimeEnd; - expect(endTime).to.be.eql(447); - }); - - it('should map unresponded requests to empty responded on auction end', () => { - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST2); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST3); - events.emit(CONSTANTS.EVENTS.AUCTION_END, RESPONSE); - const responses = analyticsAdapter.context.auctionObject.bidResponses; - expect(responses.length).to.be.eql(3); - expect(responses[2].total_duration).to.be.eql(null); - }) - - it('should handle winning bid', () => { - events.emit(CONSTANTS.EVENTS.BID_WON, RESPONSE); - const responseWhichIsWonAlso = analyticsAdapter.context.auctionObject.bidResponses[0]; - const wonEvent = analyticsAdapter.context.auctionObject.imp; - expect(wonEvent.length).to.be.eql(1); - expect(wonEvent[0]).to.be.eql({ - tagid: 'container-1', - displaymanager: null, - displaymanagerver: null, - secure: null, - bidfloor: null, - banner: { - w: 300, - h: 250, - pos: null, - expandable: [], - api: [] + }, + site: { + id: null, + name: null, + domain: window.location.href, + cat: [], + publisher: { + id: null, + name: null } - }); - - expect(responseWhichIsWonAlso).to.be.eql({ - timestamp: 1509369418832, - status: 1, - 'total_duration': 443, - bidderId: null, - 'bidder_name': 'adapter', - cur: 'EU', - seatbid: [ - { - seat: null, - bid: [ - { - status: 1, - 'clear_price': 0.015, - attr: [], - crid: 999, - cid: null, - id: null, - adid: '208750227436c1', - adomain: [], - iurl: null - } - ] - } - ] - }); - }); - - describe('when authToken is defined', () => { - it('sends request after timeout', () => { - analyticsAdapter.context.authToken = 'anAuthToken'; - let impressions = analyticsAdapter.context.auctionObject.imp; - let responses = analyticsAdapter.context.auctionObject.bidResponses; - let requests = analyticsAdapter.context.auctionObject.bidRequests; - - expect(impressions.length).to.be.eql(1); - expect(responses.length).to.be.eql(3); - expect(requests.length).to.be.eql(3); - expect(ajaxStub.notCalled).to.be.equal(true); - - timer.tick(4500); - - let impressionsAfterSend = analyticsAdapter.context.auctionObject.imp; - let responsesAfterSend = analyticsAdapter.context.auctionObject.bidResponses; - let requestsAfterSend = analyticsAdapter.context.auctionObject.bidRequests; - - expect(ajaxStub.calledOnce).to.be.equal(true); - expect(impressionsAfterSend.length).to.be.eql(0); - expect(responsesAfterSend.length).to.be.eql(0); - expect(requestsAfterSend.length).to.be.eql(0); - - analyticsAdapter.context.authToken = undefined; - }); - }); - - describe('sendAuction', () => { - describe('when authToken is defined', () => { - it('fires call clearing empty payload properties', () => { - analyticsAdapter.context.authToken = 'anAuthToken'; - analyticsAdapter.context.auctionObject.nullProperty = null; - analyticsAdapter.context.auctionObject.notNullProperty = 'aValue'; - - sendAuction(); - - expect(ajaxStub.getCall(0).args[0]).to.match(/http:\/\/tracker.rivr.simplaex.com\/(\w+)\/auctions/); - - const payload = JSON.parse(ajaxStub.getCall(0).args[2]); - - expect(payload.Auction.notNullProperty).to.be.equal('aValue'); - expect(payload.nullProperty).to.be.equal(undefined); - - analyticsAdapter.context.authToken = undefined; - }); - }); - - describe('when authToken is not defined', () => { - it('does not fire call', () => { - analyticsAdapter.context.authToken = undefined; - analyticsAdapter.context.auctionObject.nullProperty = null; - analyticsAdapter.context.auctionObject.notNullProperty = 'aValue'; - - expect(ajaxStub.callCount).to.be.equal(0); - - sendAuction(); - - expect(ajaxStub.callCount).to.be.equal(0); - }); - }); - }); - }); + }, + device: { + geo: {} + }, + user: { + id: null, + yob: null, + gender: null, + }, + bidResponses: [], + bidRequests: [], + 'ext.rivr.optimiser': 'unoptimised', + modelVersion: null, + 'ext.rivr.originalvalues': [] + } + }; });