From 49ea705266c4714b7b6cb48bade4b24eef6ae296 Mon Sep 17 00:00:00 2001 From: AlessandroDG Date: Wed, 19 Dec 2018 15:32:08 +0100 Subject: [PATCH 1/3] Rivr thin adapter (#3351) * Rivr thin adapter (#5) * RVR-2049 - change impression event * RVR-2049 - fix user id value returned on creation of the cookie * RVR-2049 - remove blockedCategories and browser properties from Auction object * RVR-2060 - rewrite rendering detection for impression and click events * RVR-2060 - rewrite tests for rendering detection for impression and click events * RVR-2049 - send ad unit code in click event * RVR-2049 - adapt tests for click event * RVR-2072 - split impression detection depending on the ad server - DFP/none * RVR-2056 - set creativeId in Auction -> received bids * RVR-2056 - set bid.clearPrice to cmp * RVR-2087 - extract trackAuctionInit implementation to rivraddon * RVR-2087 - extract implementation to rivraddon WIP * RVR-2087 - extract implementation to rivraddon * RVR-2087 - extract implementation to rivraddon * RVR-2087 - Update package-lock * RVR-2087 - Add addons check * Rivr Thin Adapter - part 2 (#6) * RVR-2087 - Remove sendAuction call from ExpiringQueue * RVR-2147 - Fix test failing on IE 11.0.0 * RVR-2087 - Remove ExpiringQueue --- modules/rivrAnalyticsAdapter.js | 445 +------ .../spec/modules/rivrAnalyticsAdapter_spec.js | 1067 +---------------- 2 files changed, 70 insertions(+), 1442 deletions(-) diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index 14143f5f21d..867cc3d68bc 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -1,43 +1,27 @@ import {ajax} from 'src/ajax'; import adapter from 'src/AnalyticsAdapter'; -import find from 'core-js/library/fn/array/find'; import CONSTANTS from 'src/constants.json'; import adaptermanager from 'src/adaptermanager'; -import { logInfo, generateUUID, timestamp } from 'src/utils'; +import * as utils from 'src/utils'; const analyticsType = 'endpoint'; -const rivrUsrIdCookieKey = 'rvr_usr_id'; -const DEFAULT_HOST = 'tracker.rivr.simplaex.com'; -const DEFAULT_QUEUE_TIMEOUT = 4000; let rivrAnalytics = Object.assign(adapter({analyticsType}), { track({ eventType, args }) { - if (!rivrAnalytics.context) { + if (!window.rivraddon || !window.rivraddon.analytics || !window.rivraddon.analytics.getContext()) { return; } - logInfo(`ARGUMENTS FOR TYPE: ============= ${eventType}`, args); + utils.logInfo(`ARGUMENTS FOR TYPE: ============= ${eventType}`, args); 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(); - } - if (rivrAnalytics.context.auctionObject) { - rivrAnalytics.context.auctionObject = createNewAuctionObject(); - saveUnoptimisedAdUnits(); - fetchLocalization(); - } - handler = trackAuctionInit; - break; - case CONSTANTS.EVENTS.BID_WON: - handler = trackBidWon; - break; - case CONSTANTS.EVENTS.BID_TIMEOUT: - handler = trackBidTimeout; + handler = window.rivraddon.analytics.trackAuctionInit; break; case CONSTANTS.EVENTS.AUCTION_END: - handler = trackAuctionEnd; + handler = window.rivraddon.analytics.trackAuctionEnd; + break; + case CONSTANTS.EVENTS.BID_WON: + handler = window.rivraddon.analytics.trackBidWon; break; } if (handler) { @@ -46,422 +30,15 @@ let rivrAnalytics = Object.assign(adapter({analyticsType}), { } }); -export function sendAuction() { - if (rivrAnalytics.context.authToken) { - removeEmptyProperties(rivrAnalytics.context.auctionObject); - let auctionObject = rivrAnalytics.context.auctionObject; - let req = Object.assign({}, {Auction: auctionObject}); - rivrAnalytics.context.auctionObject = createNewAuctionObject(); - logInfo('sending request to analytics => ', req); - ajax( - `http://${rivrAnalytics.context.host}/${rivrAnalytics.context.clientID}/auctions`, - () => {}, - JSON.stringify(req), - { - contentType: 'application/json', - customHeaders: { - 'Authorization': 'Basic ' + rivrAnalytics.context.authToken - } - } - ); - } -}; - -export function 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}/${rivrAnalytics.context.clientID}/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; -}; - -function trackBidWon(args) { - setWinningBidStatus(args); -}; - -function setWinningBidStatus(args) { - let auctionObject = rivrAnalytics.context.auctionObject; - const bidderObjectForThisWonBid = find(auctionObject.bidders, (bidder) => { - return bidder.id === args.bidderCode; - }); - if (bidderObjectForThisWonBid) { - const bidObjectForThisWonBid = find(bidderObjectForThisWonBid.bids, (bid) => { - return bid.impId === args.adUnitCode; - }); - if (bidObjectForThisWonBid) { - bidObjectForThisWonBid.status = 1; - } - } -}; - -export function trackAuctionEnd(args) { - rivrAnalytics.context.auctionTimeEnd = Date.now(); - rivrAnalytics.context.auctionObject.bidders = buildBiddersArrayFromAuctionEnd(args); - rivrAnalytics.context.auctionObject.impressions = buildImpressionsArrayFromAuctionEnd(args); -}; - -function buildImpressionsArrayFromAuctionEnd(auctionEndEvent) { - return auctionEndEvent.adUnits.map((adUnit) => { - const impression = {}; - impression.id = adUnit.code; - impression.adType = 'unknown'; - impression.acceptedSizes = []; - const bidReceivedForThisAdUnit = find(auctionEndEvent.bidsReceived, (bidReceived) => { - return adUnit.code === bidReceived.adUnitCode; - }); - if (adUnit.mediaTypes) { - if (adUnit.mediaTypes.banner) { - buildAdTypeDependentFieldsForImpression(impression, 'banner', adUnit, bidReceivedForThisAdUnit); - } else if (adUnit.mediaTypes.video) { - buildAdTypeDependentFieldsForImpression(impression, 'video', adUnit, bidReceivedForThisAdUnit); - } - } - return impression; - }); -} - -function buildAdTypeDependentFieldsForImpression(impression, adType, adUnit, bidReceivedForThisAdUnit) { - impression.adType = adType; - impression.acceptedSizes = adUnit.mediaTypes[adType].sizes.map((acceptedSize) => { - return { - w: acceptedSize[0], - h: acceptedSize[1] - }; - }); - if (bidReceivedForThisAdUnit) { - impression[adType] = { - w: bidReceivedForThisAdUnit.width, - h: bidReceivedForThisAdUnit.height - }; - } -} - -function buildBiddersArrayFromAuctionEnd(auctionEndEvent) { - return auctionEndEvent.bidderRequests.map((bidderRequest) => { - const bidder = {}; - bidder.id = bidderRequest.bidderCode; - bidder.bids = bidderRequest.bids.map((bid) => { - const bidReceivedForThisRequest = find(auctionEndEvent.bidsReceived, (bidReceived) => { - return bidderRequest.bidderCode === bidReceived.bidderCode && - bid.bidId === bidReceived.adId && - bid.adUnitCode === bidReceived.adUnitCode; - }); - return { - adomain: [''], - clearPrice: 0.0, - impId: bid.adUnitCode, - price: bidReceivedForThisRequest ? bidReceivedForThisRequest.cpm : 0.0, - status: 0 - }; - }); - return bidder; - }); -} - -function trackBidTimeout(args) { - return [args]; -}; - -export function fetchLocalization() { - if (navigator.permissions) { - navigator.permissions.query({ name: 'geolocation' }).then((permission) => { - if (permission.status === 'granted') { - navigator.geolocation.getCurrentPosition((position) => { - 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) || navigator.userAgent.match(/iPad|Android|Touch/i)) { - return 1; - } else { - return 2; - } -}; - -export 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}/${rivrAnalytics.context.clientID}/clicks`, - () => {}, - JSON.stringify(req), - { - contentType: 'application/json', - customHeaders: { - 'Authorization': 'Basic ' + rivrAnalytics.context.authToken - } - } - ); -}; - -function addClickHandler(bannerId) { - pinHandlerToHTMLElement(bannerId, dataLoaderForHandler, addClickListener); -}; - -function addDisplayedImpHandler(bannerId) { - pinHandlerToHTMLElement(bannerId, dataLoaderForHandler, impHandler); -}; - -export function pinHandlerToHTMLElement(elementId, dataLoaderForHandler, specializedHandler) { - function waitForElement() { - let element = document.getElementById(elementId); - if (!element) { - window.requestAnimationFrame(waitForElement); - } else { - dataLoaderForHandler(element, specializedHandler); - } - } - waitForElement(); -} - -export 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); - } - } - } - waitForElement(); -}; - -function addClickListener(iframe) { - iframe.contentDocument.addEventListener('click', reportClickEvent); -} - -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); - } -} - -function addHandlers(bannersIds) { - bannersIds.forEach((bannerId) => { - addClickHandler(bannerId); - addDisplayedImpHandler(bannerId); - }) -}; - -export function createNewAuctionObject() { - const auction = { - id: '', - publisher: rivrAnalytics.context.clientID, - blockedCategories: [''], - timestamp: timestamp(), - user: { - id: rivrAnalytics.context.userId - }, - site: { - domain: window.location.host, - page: window.location.pathname, - categories: rivrAnalytics.context.siteCategories - }, - impressions: [], - bidders: [], - device: { - userAgent: navigator.userAgent, - browser: '', - deviceType: getPlatformType() - }, - 'ext.rivr.originalvalues': [], - 'ext.rivr.optimiser': localStorage.getItem('rivr_should_optimise') || 'unoptimised', - modelVersion: localStorage.getItem('rivr_model_version') || null, - } - - return auction; -}; - -export function saveUnoptimisedAdUnits() { - let units = rivrAnalytics.context.adUnits; - if (units) { - if (units.length > 0) { - let allUnits = concatAllUnits(units); - allUnits.forEach((adUnit) => { - adUnit.bids.forEach((bid) => { - let configForBidder = fetchConfigForBidder(bid.bidder); - if (configForBidder) { - let unOptimisedParamsField = createUnOptimisedParamsField(bid, configForBidder); - rivrAnalytics.context.auctionObject['ext.rivr.originalvalues'].push(unOptimisedParamsField); - } - }) - }); - } - } -}; - -export function concatAllUnits(units) { - return Array.prototype.concat.apply([], units); -} - -export function createUnOptimisedParamsField(bid, config) { - let floorPriceLabel = config['floorPriceLabel']; - let currencyLabel = config['currencyLabel']; - let pmpLabel = config['pmpLabel']; - return { - '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], - } -} - -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 - * @param ttl - * @constructor - */ -export function ExpiringQueue(sendImpressions, sendAuction, ttl, log) { - 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(() => { - sendAuction(); - if (queue.length) { - sendImpressions(); - } - }, ttl); - } -}; - -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] - }); -}; - -function getCookie(name) { - var value = '; ' + document.cookie; - var parts = value.split('; ' + name + '='); - if (parts.length == 2) return parts.pop().split(';').shift(); -} - -function storeAndReturnRivrUsrIdCookie() { - return document.cookie = 'rvr_usr_id=' + generateUUID(); -} - // 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) => { - let copiedUnits; - if (config.options.adUnits) { - let stringifiedAdUnits = JSON.stringify(config.options.adUnits); - copiedUnits = JSON.parse(stringifiedAdUnits); - } - rivrAnalytics.context = { - userId: getCookie(rivrUsrIdCookieKey) || storeAndReturnRivrUsrIdCookie(), - host: config.options.host || DEFAULT_HOST, - auctionObject: {}, - adUnits: copiedUnits, - siteCategories: config.options.siteCategories || [], - clientID: config.options.clientID, - authToken: config.options.authToken, - queue: new ExpiringQueue(sendImpressions, sendAuction, config.options.queueTimeout || DEFAULT_QUEUE_TIMEOUT) - }; - - let bannersIds = config.options.bannersIds; - if (bannersIds) { - if (bannersIds.length > 0) { - addHandlers(config.options.bannersIds); - } + if (window.rivraddon && window.rivraddon.analytics) { + window.rivraddon.analytics.enableAnalytics(config, {utils, ajax}); + rivrAnalytics.originEnableAnalytics(config); } - logInfo('Rivr Analytics enabled with config', rivrAnalytics.context); - rivrAnalytics.originEnableAnalytics(config); }; adaptermanager.registerAnalyticsAdapter({ diff --git a/test/spec/modules/rivrAnalyticsAdapter_spec.js b/test/spec/modules/rivrAnalyticsAdapter_spec.js index 0fc20171e0a..5bd526579ac 100644 --- a/test/spec/modules/rivrAnalyticsAdapter_spec.js +++ b/test/spec/modules/rivrAnalyticsAdapter_spec.js @@ -1,10 +1,8 @@ import * as utils from 'src/utils'; import analyticsAdapter from 'modules/rivrAnalyticsAdapter'; import { - ExpiringQueue, - sendAuction, sendImpressions, - reportClickEvent, + handleClickEventWithClosureScope, createUnOptimisedParamsField, dataLoaderForHandler, pinHandlerToHTMLElement, @@ -12,6 +10,11 @@ import { createNewAuctionObject, concatAllUnits, trackAuctionEnd, + handleImpression, + getCookie, + storeAndReturnRivrUsrIdCookie, + arrayDifference, + activelyWaitForBannersToRender, } from 'modules/rivrAnalyticsAdapter'; import {expect} from 'chai'; import adaptermanager from 'src/adaptermanager'; @@ -28,12 +31,27 @@ describe('RIVR Analytics adapter', () => { const EMITTED_AUCTION_ID = 1; const TRACKER_BASE_URL_MOCK = 'tracker.rivr.simplaex.com'; const UUID_REG_EXP = new RegExp('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', 'i'); + const MOCK_RIVRADDON_CONTEXT = {}; let sandbox; let ajaxStub; + let rivraddonsEnableAnalyticsStub; + let rivraddonsTrackAuctionInitStub; + let rivraddonsTrackAuctionEndStub; + let rivraddonsTrackBidWonStub; let timer; before(() => { sandbox = sinon.sandbox.create(); + window.rivraddon = { + analytics: { + enableAnalytics: () => {}, + getContext: () => { return MOCK_RIVRADDON_CONTEXT; }, + trackAuctionInit: () => {}, + trackAuctionEnd: () => {}, + trackBidWon: () => {}, + } + }; + rivraddonsEnableAnalyticsStub = sandbox.stub(window.rivraddon.analytics, 'enableAnalytics'); }); beforeEach(() => { @@ -64,455 +82,77 @@ describe('RIVR Analytics adapter', () => { after(() => { sandbox.restore(); + delete window.rivraddon; }); - 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); + it('enableAnalytics - should call rivraddon enableAnalytics with the correct arguments', () => { + // adaptermanager.enableAnalytics() is called in beforeEach. If just called here it doesn't seem to work. + const firstArgument = rivraddonsEnableAnalyticsStub.getCall(0).args[0]; + const secondArgument = rivraddonsEnableAnalyticsStub.getCall(0).args[1]; - queue.push(1); + expect(firstArgument.provider).to.be.equal('rivr'); - setTimeout(() => { - queue.push([2, 3]); - timer.tick(50); - }, 50); - setTimeout(() => { - queue.push([4]); - timer.tick(100); - }, 100); - timer.tick(50); + expect(secondArgument).to.have.property('utils'); + expect(secondArgument).to.have.property('ajax'); }); - it('enableAnalytics - should configure host and clientID in adapter context', () => { - // adaptermanager.enableAnalytics() is called in beforeEach. If only called here it doesn't seem to work. + it('Firing an event when rivraddon context is not defined it should do nothing', () => { + let rivraddonsGetContextStub = sandbox.stub(window.rivraddon.analytics, 'getContext'); + rivraddonsTrackAuctionInitStub = sandbox.stub(window.rivraddon.analytics, 'trackAuctionInit'); - expect(analyticsAdapter.context).to.have.property('host', TRACKER_BASE_URL_MOCK); - expect(analyticsAdapter.context).to.have.property('clientID', RVR_CLIENT_ID_MOCK); - }); - - it('enableAnalytics - should set a cookie containing a user id', () => { - expect(UUID_REG_EXP.test(analyticsAdapter.context.userId)).to.equal(true); - }); + expect(rivraddonsTrackAuctionInitStub.callCount).to.be.equal(0); - 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 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); + expect(rivraddonsTrackAuctionInitStub.callCount).to.be.equal(0); - const endTime = analyticsAdapter.context.auctionTimeEnd; - expect(endTime).to.be.eql(MILLIS_FROM_EPOCH_TO_NOW_MOCK); + window.rivraddon.analytics.getContext.restore(); + window.rivraddon.analytics.trackAuctionInit.restore(); }); - it('Firing AUCTION_END populates impressions array in auction object', () => { - analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); + it('Firing AUCTION_INIT should call rivraddon trackAuctionInit passing the parameters', () => { + rivraddonsTrackAuctionInitStub = sandbox.stub(window.rivraddon.analytics, 'trackAuctionInit'); - events.emit(CONSTANTS.EVENTS.AUCTION_END, AUCTION_END_EVENT_WITH_AD_UNITS_AND_BID_RESPONSES_MOCK); + expect(rivraddonsTrackAuctionInitStub.callCount).to.be.equal(0); - const impressions = analyticsAdapter.context.auctionObject.impressions; - expect(impressions.length).to.be.eql(3); - }); - - it('Firing BID_WON should set to 1 the status of the corresponding bid', () => { - analyticsAdapter.context.auctionObject = utils.deepClone(AUCTION_OBJECT_AFTER_AUCTION_END_MOCK); - - events.emit(CONSTANTS.EVENTS.BID_WON, BID_WON_MOCK); - - expect(analyticsAdapter.context.auctionObject.bidders.length).to.be.equal(3); - - expect(analyticsAdapter.context.auctionObject.bidders[0].bids[0].status).to.be.equal(0); - - expect(analyticsAdapter.context.auctionObject.bidders[1].bids[0].status).to.be.equal(0); - - expect(analyticsAdapter.context.auctionObject.bidders[2].bids[0].status).to.be.equal(1); - expect(analyticsAdapter.context.auctionObject.bidders[2].bids[1].status).to.be.equal(0); - }); - - 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 resets auctionObject', () => { events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: EMITTED_AUCTION_ID, config: {}, timeout: 3000}); - analyticsAdapter.context.authToken = 'anAuthToken'; - events.emit(CONSTANTS.EVENTS.AUCTION_END, AUCTION_END_EVENT_WITH_AD_UNITS_AND_BID_RESPONSES_MOCK); - - let impressions = analyticsAdapter.context.auctionObject.impressions; - - expect(impressions.length).to.be.eql(3); - - timer.tick(EXPIRING_QUEUE_TIMEOUT + 500); - - let impressionsAfterSend = analyticsAdapter.context.auctionObject.impressions; - let biddersAfterSend = analyticsAdapter.context.auctionObject.bidders; - - expect(impressionsAfterSend.length).to.be.eql(0); - expect(biddersAfterSend.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 }; - const CLIENT_ID_MOCK = 'aClientID'; - analyticsAdapter.context = utils.deepClone(CONTEXT_AFTER_AUCTION_INIT); - analyticsAdapter.context.authToken = 'anAuthToken'; - analyticsAdapter.context.clientID = CLIENT_ID_MOCK; - 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\/aClientID\/impressions/); - expect(payload.impressions[0].anImpressionProperty).to.be.equal(aMockString); - }); - - it('reportClickEvent() calls endpoint', () => { - const CLIENT_ID_MOCK = 'aClientId'; - const AUTH_TOKEN_MOCK = 'aToken'; - 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 = AUTH_TOKEN_MOCK; - 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]); - const options = ajaxStub.getCall(0).args[3]; - - expect(ajaxStub.callCount).to.be.equal(1); - expect(ajaxStub.getCall(0).args[0]).to.match(/http:\/\/tracker.rivr.simplaex.com\/aClientId\/clicks/); - expect(options.customHeaders.Authorization).to.equal('Basic aToken'); - 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(rivraddonsTrackAuctionInitStub.callCount).to.be.equal(1); - expect(requestAnimationFrameStub.callCount).to.be.equal(1); + const firstArgument = rivraddonsTrackAuctionInitStub.getCall(0).args[0]; + expect(firstArgument.auctionId).to.be.equal(EMITTED_AUCTION_ID); - requestAnimationFrameStub.restore(); + window.rivraddon.analytics.trackAuctionInit.restore(); }); - it('pinHandlerToHTMLElement(), when element is there, it calls dataLoaderForHandler', () => { - const ELEMENT_MOCK = { - anElementProperty: 'aValue' - } - const dataLoaderForHandlerSpy = sinon.spy(); - sinon.stub(window, 'requestAnimationFrame'); + it('Firing AUCTION_END should call rivraddon trackAuctionEnd passing the parameters', () => { + rivraddonsTrackAuctionEndStub = sandbox.stub(window.rivraddon.analytics, 'trackAuctionEnd'); - sinon.stub(document, 'getElementById').returns(ELEMENT_MOCK); + expect(rivraddonsTrackAuctionEndStub.callCount).to.be.equal(0); - expect(dataLoaderForHandlerSpy.callCount).to.be.equal(0); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: EMITTED_AUCTION_ID}); - pinHandlerToHTMLElement('', dataLoaderForHandlerSpy, () => {}); + expect(rivraddonsTrackAuctionEndStub.callCount).to.be.equal(1); - expect(dataLoaderForHandlerSpy.callCount).to.be.equal(1); - expect(dataLoaderForHandlerSpy.firstCall.args[0].anElementProperty).to.be.equal('aValue'); + const firstArgument = rivraddonsTrackAuctionEndStub.getCall(0).args[0]; + expect(firstArgument.auctionId).to.be.equal(EMITTED_AUCTION_ID); - window.requestAnimationFrame.restore(); - document.getElementById.restore(); + window.rivraddon.analytics.trackAuctionEnd.restore(); }); - it('pinHandlerToHTMLElement(), when element is not there, it requests animation frame', () => { - const dataLoaderForHandlerSpy = sinon.spy(); - const requestAnimationFrameStub = sinon.stub(window, 'requestAnimationFrame'); + it('Firing BID_WON should call rivraddon trackBidWon passing the parameters', () => { + rivraddonsTrackBidWonStub = sandbox.stub(window.rivraddon.analytics, 'trackBidWon'); - sinon.stub(document, 'getElementById').returns(undefined); + expect(rivraddonsTrackBidWonStub.callCount).to.be.equal(0); - expect(requestAnimationFrameStub.callCount).to.be.equal(0); + events.emit(CONSTANTS.EVENTS.BID_WON, {auctionId: EMITTED_AUCTION_ID}); - pinHandlerToHTMLElement('', dataLoaderForHandlerSpy, () => {}); + expect(rivraddonsTrackBidWonStub.callCount).to.be.equal(1); - expect(dataLoaderForHandlerSpy.callCount).to.be.equal(0); - expect(requestAnimationFrameStub.callCount).to.be.equal(1); + const firstArgument = rivraddonsTrackBidWonStub.getCall(0).args[0]; + expect(firstArgument.auctionId).to.be.equal(EMITTED_AUCTION_ID); - 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'); - }); - - it('createNewAuctionObject(), it creates a new auction object', () => { - const MILLIS_FROM_EPOCH_TO_NOW_MOCK = 123456; - timer.tick(MILLIS_FROM_EPOCH_TO_NOW_MOCK); - - const result = createNewAuctionObject(); - - expect(result.device.deviceType).to.be.equal(2); - expect(result.publisher).to.be.equal(RVR_CLIENT_ID_MOCK); - expect(result.device.userAgent).to.be.equal(navigator.userAgent); - expect(result.timestamp).to.be.equal(MILLIS_FROM_EPOCH_TO_NOW_MOCK); - expect(result.site.domain.substring(0, 9)).to.be.equal('localhost'); - expect(result.site.page).to.be.equal('/context.html'); - expect(result.site.categories).to.be.equal(SITE_CATEGORIES_MOCK); - }); - - it('concatAllUnits(), returns a flattened array with all banner and video adunits', () => { - const allAdUnits = [BANNER_AD_UNITS_MOCK, VIDEO_AD_UNITS_MOCK]; - - const result = concatAllUnits(allAdUnits); - - expect(result.length).to.be.eql(2); - expect(result[0].code).to.be.eql('banner-container1'); - expect(result[1].code).to.be.eql('video'); - }); - - it('trackAuctionEnd(), populates the bidders array from bidderRequests and bidsReceived', () => { - trackAuctionEnd(AUCTION_END_EVENT_WITH_BID_REQUESTS_AND_BID_RESPONSES_MOCK); - - const result = analyticsAdapter.context.auctionObject.bidders; - - expect(result.length).to.be.eql(3); - - expect(result[0].id).to.be.eql('vuble'); - expect(result[0].bids[0].price).to.be.eql(0); - - expect(result[1].id).to.be.eql('vertamedia'); - expect(result[1].bids[0].price).to.be.eql(0); - - expect(result[2].id).to.be.eql('appnexus'); - expect(result[2].bids[0].price).to.be.eql(0.5); - expect(result[2].bids[0].impId).to.be.eql('/19968336/header-bid-tag-0'); - expect(result[2].bids[1].price).to.be.eql(0.7); - expect(result[2].bids[1].impId).to.be.eql('/19968336/header-bid-tag-1'); - }); - - it('trackAuctionEnd(), populates the impressions array from adUnits', () => { - trackAuctionEnd(AUCTION_END_EVENT_WITH_AD_UNITS_AND_BID_RESPONSES_MOCK); - - const result = analyticsAdapter.context.auctionObject.impressions; - - expect(result.length).to.be.eql(3); - - expect(result[0].id).to.be.eql('/19968336/header-bid-tag-0'); - expect(result[0].adType).to.be.eql('banner'); - - expect(result[1].id).to.be.eql('/19968336/header-bid-tag-1'); - expect(result[1].adType).to.be.eql('banner'); - expect(result[1].acceptedSizes).to.be.eql([{w: 728, h: 90}, {w: 970, h: 250}]); - expect(result[1].banner).to.be.eql({w: 300, h: 250}); - - expect(result[2].id).to.be.eql('video'); - expect(result[2].adType).to.be.eql('video'); - expect(result[2].acceptedSizes).to.be.eql([{w: 640, h: 360}, {w: 640, h: 480}]); + window.rivraddon.analytics.trackBidWon.restore(); }); const BANNER_AD_UNITS_MOCK = [ @@ -540,593 +180,4 @@ describe('RIVR Analytics adapter', () => { ] } ]; - - const VIDEO_AD_UNITS_MOCK = [ - { - code: 'video', - mediaTypes: { - video: { - context: 'outstream', - sizes: [[640, 360], [640, 480]] - } - }, - bids: [ - { - bidder: 'vuble', - params: { - env: 'net', - pubId: '18', - zoneId: '12345', - referrer: 'http://www.vuble.tv/', // optional - floorPrice: 5.00 // optional - } - }, - { - bidder: 'vertamedia', - params: { - aid: 331133 - } - } - ] - }]; - - 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 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 BID_RESPONSE_MOCK = { - 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' - }; - - const BID_WON_MOCK = { - bidderCode: 'appnexus', - width: 300, - height: 600, - statusMessage: 'Bid available', - adId: '63301dc59deb3b', - mediaType: 'banner', - source: 'client', - requestId: '63301dc59deb3b', - cpm: 0.5, - creativeId: 98493581, - currency: 'USD', - netRevenue: true, - ttl: 300, - appnexus: { - buyerMemberId: 9325 - }, - ad: '...HTML CONTAINING THE AD...', - auctionId: '1825871c-b4c2-401a-b219-64549d412495', - responseTimestamp: 1540560447955, - requestTimestamp: 1540560447622, - bidder: 'appnexus', - adUnitCode: '/19968336/header-bid-tag-0', - timeToRespond: 333, - pbLg: '0.50', - pbMg: '0.50', - pbHg: '0.50', - pbAg: '0.50', - pbDg: '0.50', - pbCg: '', - size: '300x600', - adserverTargeting: { - hb_bidder: 'appnexus', - hb_adid: '63301dc59deb3b', - hb_pb: '0.50', - hb_size: '300x600', - hb_source: 'client', - hb_format: 'banner' - }, - status: 'rendered', - params: [ - { - placementId: 13144370 - } - ] - }; - - const CONTEXT_AFTER_AUCTION_INIT = { - host: TRACKER_BASE_URL_MOCK, - clientID: RVR_CLIENT_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 - } - }, - site: { - id: null, - name: null, - domain: window.location.href, - cat: [], - publisher: { - id: null, - name: null - } - }, - device: { - geo: {} - }, - user: { - id: null, - yob: null, - gender: null, - }, - bidResponses: [], - bidRequests: [], - 'ext.rivr.optimiser': 'unoptimised', - modelVersion: null, - 'ext.rivr.originalvalues': [] - } - }; - - const AUCTION_END_EVENT_WITH_AD_UNITS_AND_BID_RESPONSES_MOCK = { - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - auctionStart: 1540560217395, - auctionEnd: 1540560217703, - auctionStatus: 'completed', - adUnits: [ - { - code: '/19968336/header-bid-tag-0', - mediaTypes: { - banner: { - sizes: [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] - ] - } - }, - bids: [ - { - bidder: 'appnexus', - params: { - placementId: 13144370 - }, - crumbs: { - pubcid: '87eb6b0e-e1a8-42a9-b58d-e93a382e2d9b' - } - } - ], - transactionId: 'aee9bf8d-6d8f-425b-a42a-52c875371ebc', - sizes: [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] - ] - }, - { - code: '/19968336/header-bid-tag-1', - mediaTypes: { - banner: { - sizes: [ - [ - 728, - 90 - ], - [ - 970, - 250 - ] - ] - } - }, - bids: [ - { - bidder: 'appnexus', - params: { - placementId: 13144370 - }, - crumbs: { - pubcid: '87eb6b0e-e1a8-42a9-b58d-e93a382e2d9b' - } - } - ], - transactionId: '3d5f0f89-e9cd-4714-b314-3f0fb7fcf8e3', - sizes: [ - [ - 728, - 90 - ], - [ - 970, - 250 - ] - ] - }, - { - code: 'video', - mediaTypes: { - video: { - context: 'outstream', - sizes: [ - [ - 640, - 360 - ], - [ - 640, - 480 - ] - ] - } - }, - bids: [ - { - bidder: 'vuble', - params: { - env: 'net', - pubId: '18', - zoneId: '12345', - referrer: 'http: //www.vuble.tv/', - floorPrice: 5 - }, - crumbs: { - pubcid: '87eb6b0e-e1a8-42a9-b58d-e93a382e2d9b' - } - }, - { - bidder: 'vertamedia', - params: { - aid: 331133 - }, - crumbs: { - pubcid: '87eb6b0e-e1a8-42a9-b58d-e93a382e2d9b' - } - } - ], - transactionId: 'df11a105-4eef-4ceb-bbc3-a49224f7c49d' - } - ], - adUnitCodes: [ - '/19968336/header-bid-tag-0', - '/19968336/header-bid-tag-1', - 'video' - ], - bidderRequests: [], - bidsReceived: [ - { - bidderCode: 'appnexus', - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: '6de82e80757293', - mediaType: 'banner', - source: 'client', - requestId: '6de82e80757293', - cpm: 0.5, - creativeId: 96846035, - currency: 'USD', - netRevenue: true, - ttl: 300, - appnexus: { - buyerMemberId: 9325 - }, - ad: '...HTML CONTAINING THE AD...', - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - responseTimestamp: 1540560217636, - requestTimestamp: 1540560217403, - bidder: 'appnexus', - adUnitCode: '/19968336/header-bid-tag-1', - timeToRespond: 233, - pbLg: '0.50', - pbMg: '0.50', - pbHg: '0.50', - pbAg: '0.50', - pbDg: '0.50', - pbCg: '', - size: '728x90', - adserverTargeting: { - hb_bidder: 'appnexus', - hb_adid: '7e1a45d85bd57c', - hb_pb: '0.50', - hb_size: '728x90', - hb_source: 'client', - hb_format: 'banner' - } - } - ], - winningBids: [], - timeout: 3000 - }; - - const AUCTION_OBJECT_AFTER_AUCTION_END_MOCK = { - bidders: [ - { - id: 'vuble', - bids: [ - { - adomain: [ - '' - ], - clearPrice: 0, - impId: 'video', - price: 0, - status: 0 - } - ] - }, - { - id: 'vertamedia', - bids: [ - { - adomain: [ - '' - ], - clearPrice: 0, - impId: 'video', - price: 0, - status: 0 - } - ] - }, - { - id: 'appnexus', - bids: [ - { - adomain: [ - '' - ], - clearPrice: 0, - impId: '/19968336/header-bid-tag-0', - price: 0.5, - status: 0 - }, - { - adomain: [ - '' - ], - clearPrice: 0, - impId: '/19968336/header-bid-tag-1', - price: 0.7, - status: 0 - } - ] - } - ], - impressions: [] - }; - - const AUCTION_END_EVENT_WITH_BID_REQUESTS_AND_BID_RESPONSES_MOCK = { - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - auctionStart: 1540560217395, - auctionEnd: 1540560217703, - auctionStatus: 'completed', - adUnits: [], - adUnitCodes: [ - '/19968336/header-bid-tag-0', - '/19968336/header-bid-tag-1', - 'video' - ], - bidderRequests: [ - { - bidderCode: 'vuble', - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - bidderRequestId: '1bb11e055665bc', - bids: [ - { - bidder: 'vuble', - crumbs: { - pubcid: '87eb6b0e-e1a8-42a9-b58d-e93a382e2d9b' - }, - adUnitCode: 'video', - transactionId: 'df11a105-4eef-4ceb-bbc3-a49224f7c49d', - bidId: '2859b890da7418', - bidderRequestId: '1bb11e055665bc', - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - src: 'client', - bidRequestsCount: 1 - } - ], - auctionStart: 1540560217395, - timeout: 3000, - refererInfo: { - referer: 'http: //localhost: 8080/', - reachedTop: true, - numIframes: 0, - stack: [ - 'http://localhost:8080/' - ] - }, - start: 1540560217401, - doneCbCallCount: 0 - }, - { - bidderCode: 'vertamedia', - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - bidderRequestId: '3c2cbf7f1466cb', - bids: [ - { - bidder: 'vertamedia', - params: { - aid: 331133 - }, - crumbs: { - pubcid: '87eb6b0e-e1a8-42a9-b58d-e93a382e2d9b' - }, - adUnitCode: 'video', - transactionId: 'df11a105-4eef-4ceb-bbc3-a49224f7c49d', - bidId: '45b3ad5c2dc794', - bidderRequestId: '3c2cbf7f1466cb', - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - bidRequestsCount: 1 - } - ], - auctionStart: 1540560217395, - timeout: 3000, - start: 1540560217401 - }, - { - bidderCode: 'appnexus', - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - bidderRequestId: '5312eef4418cd7', - bids: [ - { - bidder: 'appnexus', - params: { - placementId: 13144370 - }, - crumbs: { - pubcid: '87eb6b0e-e1a8-42a9-b58d-e93a382e2d9b' - }, - adUnitCode: '/19968336/header-bid-tag-0', - transactionId: 'aee9bf8d-6d8f-425b-a42a-52c875371ebc', - bidId: '6de82e80757293', - bidderRequestId: '5312eef4418cd7', - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - src: 'client', - bidRequestsCount: 1 - }, - { - bidder: 'appnexus', - params: { - placementId: 13144370 - }, - crumbs: { - pubcid: '87eb6b0e-e1a8-42a9-b58d-e93a382e2d9b' - }, - adUnitCode: '/19968336/header-bid-tag-1', - transactionId: '3d5f0f89-e9cd-4714-b314-3f0fb7fcf8e3', - bidId: '7e1a45d85bd57c', - bidderRequestId: '5312eef4418cd7', - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - src: 'client', - bidRequestsCount: 1 - } - ], - auctionStart: 1540560217395, - timeout: 3000, - start: 1540560217403, - doneCbCallCount: 0 - } - ], - bidsReceived: [ - { - bidderCode: 'appnexus', - adId: '6de82e80757293', - mediaType: 'banner', - source: 'client', - requestId: '6de82e80757293', - cpm: 0.5, - creativeId: 96846035, - appnexus: { - buyerMemberId: 9325 - }, - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - bidder: 'appnexus', - adUnitCode: '/19968336/header-bid-tag-0', - }, - { - bidderCode: 'appnexus', - adId: '7e1a45d85bd57c', - mediaType: 'banner', - source: 'client', - requestId: '7e1a45d85bd57c', - cpm: 0.7, - creativeId: 96846035, - appnexus: { - buyerMemberId: 9325 - }, - auctionId: 'f6c1d093-14a3-4ade-bc7d-1de37e7cbdb2', - bidder: 'appnexus', - adUnitCode: '/19968336/header-bid-tag-1', - } - ], - winningBids: [], - timeout: 3000 - }; }); From e5f425531ed300350f88b32749bef13baaee56bc Mon Sep 17 00:00:00 2001 From: "N. Faure" Date: Wed, 19 Dec 2018 16:24:54 +0100 Subject: [PATCH 2/3] Fix multi-bid adId in Criteo bid adapter (#3340) * Fix multi-bid adId in Criteo bid adapter A single bid request can generate multiple bids if it has multiple sizes. However, by default 'adId' will be filled with the bid request's 'bidId' field, so if we create two bids from the same bid request, they will share the same 'adId'. Because of this, Prebid will not know which bid to use once DFP makes either of those bids win, always taking the first one (see 'auctionManager.findBidByAdId' used in 'pbjs.renderAd'). * Increment Criteo adapter version * Add test for Criteo multi-bid fix --- modules/criteoBidAdapter.js | 3 +- test/spec/modules/criteoBidAdapter_spec.js | 38 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 8bf92c07f00..7ed6c42ac23 100755 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -6,7 +6,7 @@ import find from 'core-js/library/fn/array/find'; import JSEncrypt from 'jsencrypt/bin/jsencrypt'; import sha256 from 'crypto-js/sha256'; -const ADAPTER_VERSION = 15; +const ADAPTER_VERSION = 16; const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = '//bidder.criteo.com/cdb'; const CRITEO_VENDOR_ID = 91; @@ -98,6 +98,7 @@ export const spec = { const bidId = bidRequest.bidId; const bid = { requestId: bidId, + adId: slot.bidId || utils.getUniqueIdentifierStr(), cpm: slot.cpm, currency: slot.currency, netRevenue: true, diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index 6dbf51932a0..34fa111260b 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -1,5 +1,7 @@ import { expect } from 'chai'; import { cryptoVerify, spec, FAST_BID_PUBKEY } from 'modules/criteoBidAdapter'; +import * as bidfactory from 'src/bidfactory'; +import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils'; describe('The Criteo bidding adapter', function () { @@ -241,6 +243,7 @@ describe('The Criteo bidding adapter', function () { body: { slots: [{ impid: 'test-requestId', + bidId: 'abc123', cpm: 1.23, creative: 'test-ad', width: 728, @@ -261,6 +264,7 @@ describe('The Criteo bidding adapter', function () { const bids = spec.interpretResponse(response, request); expect(bids).to.have.lengthOf(1); expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].adId).to.equal('abc123'); expect(bids[0].cpm).to.equal(1.23); expect(bids[0].ad).to.equal('test-ad'); expect(bids[0].width).to.equal(728); @@ -297,6 +301,40 @@ describe('The Criteo bidding adapter', function () { expect(bids[0].width).to.equal(728); expect(bids[0].height).to.equal(90); }); + + it('should generate unique adIds if none are returned by the endpoint', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + cpm: 1.23, + creative: 'test-ad', + width: 300, + height: 250, + }, { + impid: 'test-requestId', + cpm: 4.56, + creative: 'test-ad', + width: 728, + height: 90, + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + sizes: [[300, 250], [728, 90]], + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(2); + const prebidBids = bids.map(bid => Object.assign(bidfactory.createBid(CONSTANTS.STATUS.GOOD, request.bidRequests[0]), bid)); + expect(prebidBids[0].adId).to.not.equal(prebidBids[1].adId); + }); }); describe('cryptoVerify', function () { From e47602142febf3469672fdcfa6f1f1574ec29a46 Mon Sep 17 00:00:00 2001 From: telariaEng <36203956+telariaEng@users.noreply.github.com> Date: Thu, 20 Dec 2018 12:04:41 -0800 Subject: [PATCH 3/3] Modified the Telaria Bid Adapter to use bid.mediaTypes.video.playerSize instead of bid.sizes (#3377) * Added telaria bid adapter * more documentation * Added more test cases. And improved some code in the adapter * Removed the check for optional params, they are handled in the server. Also updated certain param names used in the test spec. * added some spaces to fix CircleCI tests * added some spaces to fix CircleCI tests * fixed code indentation in /spec/AnalyticsAdapter_spec.js which causing the CircleCI tests to fail. * Reverted the changes * merged with prebid master. * creative Id is required when we build a response but our server doesn't always have the crid, so using a sentinel value when we don't have the crid. * - removed an un used method - Removed the package-lock file. * merging to master * updated telaria bid adapter to use player size provided by the bid.mediaTypes.video.playerSize instead of bid.sizes. https://github.com/prebid/Prebid.js/issues/3331 * - removed the requirement for having player size - updated the test spec to reflect the above change - removed changes to the package-lock.json file. * added a param to the ad call url to let us know that the request is coming via hb. * to lower casing the bidder code. --- modules/telariaBidAdapter.js | 50 ++++++----- test/spec/modules/telariaBidAdapter_spec.js | 92 +++++++++++++-------- 2 files changed, 87 insertions(+), 55 deletions(-) diff --git a/modules/telariaBidAdapter.js b/modules/telariaBidAdapter.js index e59ed6cd0f6..7bbd7573307 100644 --- a/modules/telariaBidAdapter.js +++ b/modules/telariaBidAdapter.js @@ -1,7 +1,6 @@ import * as utils from 'src/utils'; import * as bidfactory from 'src/bidfactory'; import {registerBidder} from 'src/adapters/bidderFactory'; -import {config} from 'src/config'; import {VIDEO} from '../src/mediaTypes'; import {STATUS} from 'src/constants'; @@ -24,17 +23,18 @@ export const spec = { /** * Make a server request from the list of BidRequests. * @param validBidRequests list of valid bid requests that have passed isBidRequestValid check + * @param bidderRequest * @returns {Array} of url objects */ - buildRequests: function (validBidRequests) { + buildRequests: function (validBidRequests, bidderRequest) { let requests = []; validBidRequests.forEach(bid => { - let url = generateUrl(bid); + let url = generateUrl(bid, bidderRequest); if (url) { requests.push({ method: 'GET', - url: generateUrl(bid), + url: generateUrl(bid, bidderRequest), bidId: bid.bidId, vastUrl: url.split('&fmt=json')[0] }); @@ -84,7 +84,7 @@ export const spec = { utils.logError(errorMessage); } else if (bidResult.seatbid && bidResult.seatbid.length > 0) { bidResult.seatbid[0].bid.forEach(tag => { - bids.push(createBid(STATUS.GOOD, bidderRequest, tag, width, height, bidResult.seatbid[0].seat)); + bids.push(createBid(STATUS.GOOD, bidderRequest, tag, width, height, bidResult.seatbid[0].seat.toLowerCase())); }); } @@ -112,28 +112,36 @@ export const spec = { * Generates the url based on the parameters given. Sizes, supplyCode & adCode are required. * The format is: [L,W] or [[L1,W1],...] * @param bid + * @param bidderRequest * @returns {string} */ -function generateUrl(bid) { - let width, height; - if (!bid.sizes) { - return ''; +function generateUrl(bid, bidderRequest) { + let playerSize = (bid.mediaTypes && bid.mediaTypes.video && bid.mediaTypes.video.playerSize); + if (!playerSize) { + utils.logWarn('Although player size isn\'t required it is highly recommended'); } - if (utils.isArray(bid.sizes) && (bid.sizes.length === 2) && (!isNaN(bid.sizes[0]) && !isNaN(bid.sizes[1]))) { - width = bid.sizes[0]; - height = bid.sizes[1]; - } else if (typeof bid.sizes === 'object') { - // take the primary (first) size from the array - width = bid.sizes[0][0]; - height = bid.sizes[0][1]; + let width, height; + if (playerSize) { + if (utils.isArray(playerSize) && (playerSize.length === 2) && (!isNaN(playerSize[0]) && !isNaN(playerSize[1]))) { + width = playerSize[0]; + height = playerSize[1]; + } else if (typeof playerSize === 'object') { + width = playerSize[0][0]; + height = playerSize[0][1]; + } } - if (width && height && bid.params.supplyCode && bid.params.adCode) { + + if (bid.params.supplyCode && bid.params.adCode) { let scheme = ((document.location.protocol === 'https:') ? 'https' : 'http') + '://'; let url = scheme + bid.params.supplyCode + ENDPOINT + '?adCode=' + bid.params.adCode; - url += ('&playerWidth=' + width); - url += ('&playerHeight=' + height); + if (width) { + url += ('&playerWidth=' + width); + } + if (height) { + url += ('&playerHeight=' + height); + } for (let key in bid.params) { if (bid.params.hasOwnProperty(key) && bid.params[key]) { @@ -145,8 +153,8 @@ function generateUrl(bid) { url += ('&srcPageUrl=' + encodeURIComponent(document.location.href)); } - url += ('&transactionId=' + bid.transactionId); - url += ('&referrer=' + config.getConfig('pageUrl') || utils.getTopWindowUrl()); + url += ('&transactionId=' + bid.transactionId + '&hb=1'); + url += ('&referrer=' + encodeURIComponent(bidderRequest.refererInfo.referer)); return (url + '&fmt=json'); } diff --git a/test/spec/modules/telariaBidAdapter_spec.js b/test/spec/modules/telariaBidAdapter_spec.js index 6b5278c20ae..88b61844ea4 100644 --- a/test/spec/modules/telariaBidAdapter_spec.js +++ b/test/spec/modules/telariaBidAdapter_spec.js @@ -8,7 +8,12 @@ const SUPPLY_CODE = 'ssp-demo-rm6rh'; const SIZES = [640, 480]; const REQUEST = { 'code': 'video1', - 'sizes': [640, 480], + 'mediaTypes': { + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream' + } + }, 'mediaType': 'video', 'bids': [{ 'bidder': 'tremor', @@ -19,6 +24,12 @@ const REQUEST = { }] }; +const BIDDER_REQUEST = { + 'refererInfo': { + 'referer': 'www.test.com' + } +}; + const RESPONSE = { 'cur': 'USD', 'id': '3dba13e35f3d42f998bc7e65fd871889', @@ -34,26 +45,26 @@ const RESPONSE = { }] }; -describe('TelariaAdapter', function () { +describe('TelariaAdapter', () => { const adapter = newBidder(spec); - describe('inherited functions', function () { - it('exists and is a function', function () { + describe('inherited functions', () => { + it('exists and is a function', () => { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); }); - describe('isBidRequestValid', function () { + describe('isBidRequestValid', () => { let bid = REQUEST.bids[0]; - it('should return true when required params found', function () { + it('should return true when required params found', () => { let tempBid = bid; tempBid.params.adCode = 'ssp-!demo!-lufip'; tempBid.params.supplyCode = 'ssp-demo-rm6rh'; expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should return true when required params found', function () { + it('should return true when required params found', () => { let tempBid = bid; delete tempBid.params; tempBid.params = { @@ -64,17 +75,22 @@ describe('TelariaAdapter', function () { expect(spec.isBidRequestValid(tempBid)).to.equal(true); }); - it('should return false when required params are not passed', function () { + it('should return false when required params are not passed', () => { let tempBid = bid; tempBid.params = {}; expect(spec.isBidRequestValid(tempBid)).to.equal(false); }); }); - describe('buildRequests', function () { + describe('buildRequests', () => { const stub = [{ + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + } + }, bidder: 'tremor', - sizes: [[300, 250], [300, 600]], params: { supplyCode: 'ssp-demo-rm6rh', adCode: 'ssp-!demo!-lufip', @@ -82,17 +98,17 @@ describe('TelariaAdapter', function () { } }]; - it('exists and is a function', function () { + it('exists and is a function', () => { expect(spec.buildRequests).to.exist.and.to.be.a('function'); }); - it('requires supply code, ad code and sizes to make a request', function () { - const tempRequest = spec.buildRequests(stub); + it('requires supply code & ad code to make a request', () => { + const tempRequest = spec.buildRequests(stub, BIDDER_REQUEST); expect(tempRequest.length).to.equal(1); }); - it('generates an array of requests with 4 params, method, url, bidId and vastUrl', function () { - const tempRequest = spec.buildRequests(stub); + it('generates an array of requests with 4 params, method, url, bidId and vastUrl', () => { + const tempRequest = spec.buildRequests(stub, BIDDER_REQUEST); expect(tempRequest.length).to.equal(1); expect(tempRequest[0].method).to.equal('GET'); @@ -101,36 +117,44 @@ describe('TelariaAdapter', function () { expect(tempRequest[0].vastUrl).to.exist; }); - it('requires sizes to make a request', function () { + it('doesn\'t require player size but is highly recommended', () => { let tempBid = stub; - tempBid[0].sizes = null; - const tempRequest = spec.buildRequests(tempBid); + tempBid[0].mediaTypes.video.playerSize = null; + const tempRequest = spec.buildRequests(tempBid, BIDDER_REQUEST); - expect(tempRequest.length).to.equal(0); + expect(tempRequest.length).to.equal(1); }); - it('generates a valid request with sizes as an array of two elements', function () { + it('generates a valid request with sizes as an array of two elements', () => { let tempBid = stub; - tempBid[0].sizes = [640, 480]; - expect(spec.buildRequests(tempBid).length).to.equal(1); + tempBid[0].mediaTypes.video.playerSize = [640, 480]; + tempBid[0].params.adCode = 'ssp-!demo!-lufip'; + tempBid[0].params.supplyCode = 'ssp-demo-rm6rh'; + let builtRequests = spec.buildRequests(tempBid, BIDDER_REQUEST); + expect(builtRequests.length).to.equal(1); }); - it('requires ad code and supply code to make a request', function () { + it('requires ad code and supply code to make a request', () => { let tempBid = stub; tempBid[0].params.adCode = null; tempBid[0].params.supplyCode = null; - const tempRequest = spec.buildRequests(tempBid); + const tempRequest = spec.buildRequests(tempBid, BIDDER_REQUEST); expect(tempRequest.length).to.equal(0); }); }); - describe('interpretResponse', function () { + describe('interpretResponse', () => { const responseStub = RESPONSE; const stub = [{ + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + } + }, bidder: 'tremor', - sizes: [[300, 250], [300, 600]], params: { supplyCode: 'ssp-demo-rm6rh', adCode: 'ssp-!demo!-lufip', @@ -138,40 +162,40 @@ describe('TelariaAdapter', function () { } }]; - it('should get correct bid response', function () { + it('should get correct bid response', () => { let expectedResponseKeys = ['bidderCode', 'width', 'height', 'statusMessage', 'adId', 'mediaType', 'source', 'getStatusCode', 'getSize', 'requestId', 'cpm', 'creativeId', 'vastXml', 'vastUrl', 'currency', 'netRevenue', 'ttl', 'ad']; - let bidRequest = spec.buildRequests(stub)[0]; + let bidRequest = spec.buildRequests(stub, BIDDER_REQUEST)[0]; bidRequest.bidId = '1234'; let result = spec.interpretResponse({body: responseStub}, bidRequest); expect(Object.keys(result[0])).to.have.members(expectedResponseKeys); }); - it('handles nobid responses', function () { + it('handles nobid responses', () => { let tempResponse = responseStub; tempResponse.seatbid = []; - let bidRequest = spec.buildRequests(stub)[0]; + let bidRequest = spec.buildRequests(stub, BIDDER_REQUEST)[0]; bidRequest.bidId = '1234'; let result = spec.interpretResponse({body: tempResponse}, bidRequest); expect(result.length).to.equal(0); }); - it('handles invalid responses', function () { + it('handles invalid responses', () => { let result = spec.interpretResponse(null, {bbidderCode: 'telaria'}); expect(result.length).to.equal(0); }); - it('handles error responses', function () { + it('handles error responses', () => { let result = spec.interpretResponse({body: {error: 'Invalid request'}}, {bbidderCode: 'telaria'}); expect(result.length).to.equal(0); }); }); - describe('getUserSyncs', function () { + describe('getUserSyncs', () => { const responses = [{body: RESPONSE}]; responses[0].body.ext = { telaria: { @@ -182,7 +206,7 @@ describe('TelariaAdapter', function () { } }; - it('should get the correct number of sync urls', function () { + it('should get the correct number of sync urls', () => { let urls = spec.getUserSyncs({pixelEnabled: true}, responses); expect(urls.length).to.equal(2); });