diff --git a/modules/openxAnalyticsAdapter.js b/modules/openxAnalyticsAdapter.js index 11481f0703e..529d55b684a 100644 --- a/modules/openxAnalyticsAdapter.js +++ b/modules/openxAnalyticsAdapter.js @@ -41,6 +41,7 @@ const SLOT_LOADED = 'slotOnload'; * @property {Object} utmTagData * @property {string} adIdKey * @property {number} payloadWaitTime + * @property {number} payloadWaitTimePadding * @property {Array}adUnits */ @@ -58,6 +59,7 @@ const DEFAULT_ANALYTICS_CONFIG = { utmTagData: {}, adUnits: [], payloadWaitTime: AUCTION_END_WAIT_TIME, + payloadWaitTimePadding: 100 }; let googletag = window.googletag || {}; @@ -278,7 +280,7 @@ function getAuctionIdByAdId(adId) { utils._each(eventStack, function(auctionInfo) { if (auctionInfo && auctionInfo.events) { auctionInfo.events.forEach(function(eventsInfo) { - if(eventsInfo.eventType === bidWonConst) { + if (eventsInfo.eventType === bidWonConst) { if (eventsInfo.args && eventsInfo.args.adId && eventsInfo.args.adId === adId) { auctionId = eventsInfo.args.auctionId; adUnitCode = eventsInfo.args.adUnitCode; @@ -336,7 +338,7 @@ function onSlotLoaded({ slot }) { let auctionId, adUnitCode; let adUnitInfo = getAuctionIdByAdId(adId); - if(adUnitInfo && adUnitInfo.auctionId && adUnitInfo.adUnitCode) { + if (adUnitInfo && adUnitInfo.auctionId && adUnitInfo.adUnitCode) { auctionId = adUnitInfo.auctionId; adUnitCode = adUnitInfo.adUnitCode; } else { @@ -374,13 +376,13 @@ let openxAdapter = Object.assign(adapter({ urlParam, analyticsType })); openxAdapter.originEnableAnalytics = openxAdapter.enableAnalytics; -openxAdapter.enableAnalytics = function(adapterConfig = {options:{}}) { +openxAdapter.enableAnalytics = function(adapterConfig = {options: {}}) { // Backwards compatibility for external documentation - if(adapterConfig.options.slotLoadWaitTime){ + if (adapterConfig.options.slotLoadWaitTime) { adapterConfig.options.payloadWaitTime = adapterConfig.options.slotLoadWaitTime; } - if(isValidConfig(adapterConfig)){ + if (isValidConfig(adapterConfig)) { analyticsConfig = {...DEFAULT_ANALYTICS_CONFIG, ...adapterConfig.options}; analyticsConfig.utmTagData = this.buildUtmTagData(); utils.logInfo('OpenX Analytics enabled with config', analyticsConfig); @@ -398,7 +400,6 @@ openxAdapter.enableAnalytics = function(adapterConfig = {options:{}}) { onSlotLoadedV2(args); }); }); - } else if (analyticsConfig.enableV2) { // override track method with v2 handlers openxAdapter.track = prebidAnalyticsEventHandlerV2; @@ -422,7 +423,7 @@ openxAdapter.enableAnalytics = function(adapterConfig = {options:{}}) { openxAdapter.originEnableAnalytics(adapterConfig); } - function isValidConfig({options: analyticsOptions}){ + function isValidConfig({options: analyticsOptions}) { const fieldValidations = [ // tuple of property, type, required ['publisherPlatformId', 'string', true], @@ -441,10 +442,10 @@ openxAdapter.enableAnalytics = function(adapterConfig = {options:{}}) { (analyticsOptions.hasOwnProperty(property) && typeof analyticsOptions[property] !== type); }); - if(failedValidation) { + if (failedValidation) { let [property, type, required] = failedValidation; - if(required){ + if (required) { utils.logError(`OpenXAnalyticsAdapter: Expected '${property}' to exist and of type '${type}'`); } else { utils.logError(`OpenXAnalyticsAdapter: Expected '${property}' to be type '${type}'`); @@ -666,13 +667,12 @@ function pushEvent(eventType, args, auctionId) { } function updateLoadedAdSlotsInfo(auctionId, adUnitCode, adPosition) { - - if(auctionId && adUnitCode) { - if(!loadedAdSlots[auctionId]){ + if (auctionId && adUnitCode) { + if (!loadedAdSlots[auctionId]) { loadedAdSlots[auctionId] = {}; } loadedAdSlots[auctionId][adUnitCode] = {}; - if(adPosition) { + if (adPosition) { loadedAdSlots[auctionId][adUnitCode] = { adPosition: adPosition }; } } else { @@ -681,28 +681,23 @@ function updateLoadedAdSlotsInfo(auctionId, adUnitCode, adPosition) { } function getLoadedAdUnitCodes(auctionId) { - return (!auctionId || !loadedAdSlots[auctionId] || typeof loadedAdSlots[auctionId] !== 'object') ? [] : Object.keys(loadedAdSlots[auctionId]); } function pushAdPositionData(auctionId) { - - if(auctionId && eventStack[auctionId] && eventStack[auctionId].events) { - + if (auctionId && eventStack?.[auctionId]?.events) { let adUnitPositionMap = loadedAdSlots[auctionId]; - if(adUnitPositionMap && JSON.stringify(adUnitPositionMap) !== "{}") { - + if (adUnitPositionMap && JSON.stringify(adUnitPositionMap) !== '{}') { eventStack[auctionId].events.filter(function(event) { return event.eventType === auctionEndConst; }).forEach(function (auctionEndEvent) { - - if(auctionEndEvent.args && auctionEndEvent.args.adUnits) { + if (auctionEndEvent.args && auctionEndEvent.args.adUnits) { auctionEndEvent.args.adUnits.forEach(function (adUnitInfo) { - if(adUnitPositionMap[adUnitInfo.code] && adUnitPositionMap[adUnitInfo.code]["adPosition"]) { - adUnitInfo["adPosition"] = adUnitPositionMap[adUnitInfo.code]["adPosition"]; + if (adUnitPositionMap[adUnitInfo.code] && adUnitPositionMap[adUnitInfo.code]['adPosition']) { + adUnitInfo['adPosition'] = adUnitPositionMap[adUnitInfo.code]['adPosition']; } else { - adUnitInfo["adPosition"] = ""; + adUnitInfo['adPosition'] = ''; } }) } @@ -712,8 +707,7 @@ function pushAdPositionData(auctionId) { } function getAdPositionByElementId(elementId) { - - let elem = document.querySelector("#" + elementId); + let elem = document.querySelector('#' + elementId); let adPosition; if (elem) { let bounding = elem.getBoundingClientRect(); @@ -733,13 +727,13 @@ function getAdPositionByElementId(elementId) { let intersectionArea = (intersectionHeight > 0 && intersectionWidth > 0) ? (intersectionHeight * intersectionWidth) : 0; let adSlotArea = (bounding.right - bounding.left) * (bounding.bottom - bounding.top); - if(adSlotArea > 0) { + if (adSlotArea > 0) { // Atleast 50% of intersection in window - adPosition = (intersectionArea * 2 >= adSlotArea) ? "ATF" : "BTF"; + adPosition = (intersectionArea * 2 >= adSlotArea) ? 'ATF' : 'BTF'; } } } else { - utils.logWarn("OX: DOM element not for id " + elementId); + utils.logWarn('OX: DOM element not for id ' + elementId); } return adPosition; } @@ -751,7 +745,7 @@ adapterManager.registerAnalyticsAdapter({ code: 'openx' }); -function prebidAnalyticsEventHandlerV1({eventType, args}){ +function prebidAnalyticsEventHandlerV1({eventType, args}) { if (!checkInitOptions()) { send(eventType, {}, null); return; @@ -807,9 +801,15 @@ function prebidAnalyticsEventHandlerV1({eventType, args}){ //* ******* V2 Code ******* const { - EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, BID_WON } + EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, AUCTION_END, BID_WON } } = CONSTANTS; +export const AUCTION_STATES = { + INIT: 'initialized', // auction has initialized + ENDED: 'ended', // all auction requests have been accounted for + COMPLETED: 'completed' // all slots have rendered +}; + const ENDPOINT = 'https://prebid.openx.net/ox/analytics'; let auctionMap = {}; let auctionOrder = 1; // tracks the number of auctions ran on the page @@ -829,6 +829,9 @@ function prebidAnalyticsEventHandlerV2({eventType, args}) { case BID_TIMEOUT: onBidTimeout(args); break; + case AUCTION_END: + onAuctionEnd(args); + break; case BID_WON: onBidWon(args); break; @@ -838,31 +841,33 @@ function prebidAnalyticsEventHandlerV2({eventType, args}) { } } -/* -TODO: type Auction -auctionId: "526ce090-e42e-4444-996f-ea78cde2244d" -timestamp: 1586675964364 -auctionEnd: undefined -auctionStatus: "inProgress" -adUnits: [{…}] -adUnitCodes: ["video1"] -labels: undefined -bidderRequests: (2) [{…}, {…}] -noBids: [] -bidsReceived: [] -winningBids: [] -timeout: 3000 -config: {publisherPlatformId: "a3aece0c-9e80-4316-8deb-faf804779bd1", publisherAccountId: 537143056, sampling: 1, enableV2: true} +/** + * @typedef {Object} PbAuction + * @property {string} auctionId - Auction ID of the request this bid responded to + * @property {number} timestamp //: 1586675964364 + * @property {number} auctionEnd - timestamp of when auction ended //: 1586675964364 + * @property {string} auctionStatus //: "inProgress" + * @property {Array} adUnits //: [{…}] + * @property {string} adUnitCodes //: ["video1"] + * @property {string} labels //: undefined + * @property {Array} bidderRequests //: (2) [{…}, {…}] + * @property {Array} noBids //: [] + * @property {Array} bidsReceived //: [] + * @property {Array} winningBids //: [] + * @property {number} timeout //: 3000 + * @property {Object} config //: {publisherPlatformId: "a3aece0c-9e80-4316-8deb-faf804779bd1", publisherAccountId: 537143056, sampling: 1, enableV2: true}/* */ + function onAuctionInit({auctionId, timestamp: startTime, timeout, adUnitCodes}) { auctionMap[auctionId] = { id: auctionId, startTime, + endTime: void(0), timeout, auctionOrder, adUnitCodesCount: adUnitCodes.length, adunitCodesRenderedCount: 0, - auctionCompleted: false, + state: AUCTION_STATES.INIT, auctionSendDelayTimer: void (0), }; @@ -956,6 +961,23 @@ function onBidTimeout(args) { }); } +/** + * + * @param {PbAuction} endedAuction + */ +function onAuctionEnd(endedAuction) { + let auction = auctionMap[endedAuction.auctionId]; + + if (!auction) { + return; + } + + clearAuctionTimer(auction); + auction.endTime = endedAuction.auctionEnd; + auction.state = AUCTION_STATES.ENDED; + delayedSend(auction); +} + /** * * @param {BidResponse} bidResponse @@ -978,11 +1000,7 @@ function onSlotLoadedV2({ slot }) { return; // slot is not participating in a prebid auction } - // reset the delay timer to send the auction data - if (auction.auctionSendDelayTimer) { - clearTimeout(auction.auctionSendDelayTimer); - auction.auctionSendDelayTimer = void (0); - } + clearAuctionTimer(auction); // track that an adunit code has completed within an auction auction.adunitCodesRenderedCount++; @@ -998,9 +1016,14 @@ function onSlotLoadedV2({ slot }) { // prepare to send regardless if auction is complete or not as a failsafe in case not all events are tracked // add additional padding when not all slots are rendered + delayedSend(auction); + auction.state = AUCTION_STATES.COMPLETED; +} + +function delayedSend(auction) { const delayTime = auction.adunitCodesRenderedCount === auction.adUnitCodesCount ? analyticsConfig.payloadWaitTime - : analyticsConfig.payloadWaitTime + 500; + : analyticsConfig.payloadWaitTime + analyticsConfig.payloadWaitTimePadding; auction.auctionSendDelayTimer = setTimeout(() => { let payload = JSON.stringify([buildAuctionPayload(auction)]); @@ -1010,8 +1033,14 @@ function onSlotLoadedV2({ slot }) { delete auctionMap[auction.id]; } }, delayTime); +} - auction.auctionCompleted = true; +function clearAuctionTimer(auction) { + // reset the delay timer to send the auction data + if (auction.auctionSendDelayTimer) { + clearTimeout(auction.auctionSendDelayTimer); + auction.auctionSendDelayTimer = void (0); + } } function getAuctionByGoogleTagSLot(slot) { @@ -1030,12 +1059,14 @@ function getAuctionByGoogleTagSLot(slot) { } function buildAuctionPayload(auction) { - let {startTime, timeout, auctionOrder, adUnitCodeToBidderRequestMap} = auction; + let {startTime, endTime, state, timeout, auctionOrder, adUnitCodeToBidderRequestMap} = auction; return { publisherPlatformId: analyticsConfig.publisherPlatformId, publisherAccountId: analyticsConfig.publisherAccountId, + state, startTime, + endTime, timeLimit: timeout, auctionOrder, deviceType: detectMob() ? 'Mobile' : 'Desktop', diff --git a/test/spec/modules/openxAnalyticsAdapter_spec.js b/test/spec/modules/openxAnalyticsAdapter_spec.js index e64248de53d..08659cc7f6a 100644 --- a/test/spec/modules/openxAnalyticsAdapter_spec.js +++ b/test/spec/modules/openxAnalyticsAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import openxAdapterParams from 'modules/openxAnalyticsAdapter.js'; +import openxAdapterParams, {AUCTION_STATES} from 'modules/openxAnalyticsAdapter.js'; import { config } from 'src/config.js'; import events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; @@ -21,7 +21,7 @@ describe('openx analytics adapter', function() { spy = sinon.spy(utils, 'logError'); }); - afterEach(function(){ + afterEach(function() { utils.logError.restore(); }); @@ -676,6 +676,14 @@ describe('openx analytics adapter', function() { auctionId: 'test-auction-id' }; + const auctionEnd = { + auctionId: 'test-auction-id', + timestamp: 1586000000000, + auctionEnd: 1586000000100, + timeout: 3000, + adUnitCodes: [AD_UNIT_CODE], + }; + const bidWonCloseX = { requestId: 'test-closex-request-id', adId: 'test-closex-ad-id', @@ -683,35 +691,40 @@ describe('openx analytics adapter', function() { auctionId: 'test-auction-id' }; + function simulateAuction(events) { let highestBid; events.forEach(event => { const [eventType, args] = event; - openxAdapter.track({ eventType, args }); if (eventType === BID_RESPONSE) { highestBid = highestBid || args; if (highestBid.cpm < args.cpm) { highestBid = args; } } - }); - openxAdapter.track({ - eventType: SLOT_LOADED, - args: { - slot: { - getAdUnitPath: () => { - return '/12345678/test_ad_unit'; - }, - getSlotElementId: () => { - return AD_UNIT_CODE; - }, - getTargeting: sinon - .stub() - .withArgs('hb_adid') - .returns(highestBid ? [highestBid.adId] : []) - } + if (eventType === SLOT_LOADED) { + const slotLoaded = { + slot: { + getAdUnitPath: () => { + return '/12345678/test_ad_unit'; + }, + getSlotElementId: () => { + return AD_UNIT_CODE; + }, + getTargeting: (key) => { + if (key === 'hb_adid') { + return highestBid ? [highestBid.adId] : []; + } else { + return []; + } + } + } + }; + openxAdapter.track({ eventType, args: slotLoaded }); + } else { + openxAdapter.track({ eventType, args }); } }); } @@ -745,10 +758,12 @@ describe('openx analytics adapter', function() { simulateAuction([ [AUCTION_INIT, auctionInit], + [SLOT_LOADED] ]); simulateAuction([ - [AUCTION_INIT, {auctionId: 'second-auction-id', ...auctionInit} ], + [AUCTION_INIT, {...auctionInit, auctionId: 'second-auction-id'} ], + [SLOT_LOADED] ]); clock.tick(SLOT_LOAD_WAIT_TIME); @@ -795,6 +810,7 @@ describe('openx analytics adapter', function() { simulateAuction([ [AUCTION_INIT, auctionInit], + [SLOT_LOADED], ]); clock.tick(SLOT_LOAD_WAIT_TIME); auction = JSON.parse(server.requests[0].requestBody)[0]; @@ -819,7 +835,8 @@ describe('openx analytics adapter', function() { publisherPlatformId: 'test-platform-id', sample: 1.0, enableV2: true, - payloadWaitTime: SLOT_LOAD_WAIT_TIME + payloadWaitTime: SLOT_LOAD_WAIT_TIME, + payloadWaitTimePadding: SLOT_LOAD_WAIT_TIME, } }); @@ -827,8 +844,9 @@ describe('openx analytics adapter', function() { [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedCloseX], [BID_REQUESTED, bidRequestedOpenX], + [SLOT_LOADED], ]); - clock.tick(SLOT_LOAD_WAIT_TIME); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); auction = JSON.parse(server.requests[0].requestBody)[0]; }); @@ -874,7 +892,8 @@ describe('openx analytics adapter', function() { publisherPlatformId: 'test-platform-id', sample: 1.0, enableV2: true, - payloadWaitTime: SLOT_LOAD_WAIT_TIME + payloadWaitTime: SLOT_LOAD_WAIT_TIME, + payloadWaitTimePadding: SLOT_LOAD_WAIT_TIME, } }); @@ -884,8 +903,9 @@ describe('openx analytics adapter', function() { [BID_REQUESTED, bidRequestedOpenX], [BID_TIMEOUT, bidTimeoutCloseX], [BID_TIMEOUT, bidTimeoutOpenX], + [AUCTION_END, auctionEnd] ]); - clock.tick(SLOT_LOAD_WAIT_TIME); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); auction = JSON.parse(server.requests[0].requestBody)[0]; openxBidRequest = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); @@ -915,7 +935,8 @@ describe('openx analytics adapter', function() { publisherPlatformId: 'test-platform-id', sample: 1.0, enableV2: true, - payloadWaitTime: SLOT_LOAD_WAIT_TIME + payloadWaitTime: SLOT_LOAD_WAIT_TIME, + payloadWaitTimePadding: SLOT_LOAD_WAIT_TIME } }); @@ -924,9 +945,11 @@ describe('openx analytics adapter', function() { [BID_REQUESTED, bidRequestedCloseX], [BID_REQUESTED, bidRequestedOpenX], [BID_RESPONSE, bidResponseOpenX], - [BID_RESPONSE, bidResponseCloseX] + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd] ]); - clock.tick(SLOT_LOAD_WAIT_TIME); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); auction = JSON.parse(server.requests[0].requestBody)[0]; openxBidResponse = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx').bidResponses[0]; @@ -985,9 +1008,17 @@ describe('openx analytics adapter', function() { expect(openxBidResponse.currency).to.equal(bidResponseOpenX.currency); expect(closexBidResponse.currency).to.equal(bidResponseCloseX.currency); }); + + it('should track the auction end time', function () { + expect(auction.endTime).to.equal(auctionEnd.auctionEnd); + }); + + it('should track that the auction ended', function () { + expect(auction.state).to.equal(AUCTION_STATES.ENDED); + }); }); - describe('when are bidder wins', function () { + describe('when there are bidder wins', function () { const CURRENT_TIME = 1586000000000; let auction; beforeEach(function () { @@ -997,7 +1028,8 @@ describe('openx analytics adapter', function() { publisherAccountId: 123, sample: 1.0, enableV2: true, - payloadWaitTime: SLOT_LOAD_WAIT_TIME + payloadWaitTime: SLOT_LOAD_WAIT_TIME, + payloadWaitTimePadding: SLOT_LOAD_WAIT_TIME } }); @@ -1010,10 +1042,11 @@ describe('openx analytics adapter', function() { [BID_REQUESTED, bidRequestedCloseX], [BID_RESPONSE, bidResponseOpenX], [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd], [BID_WON, bidWonOpenX] ]); - clock.tick(SLOT_LOAD_WAIT_TIME); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); auction = JSON.parse(server.requests[0].requestBody)[0]; }); @@ -1032,6 +1065,46 @@ describe('openx analytics adapter', function() { let closexBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex'); expect(closexBidder.bidResponses[0]).to.contain({winner: false}); }); + }); + + describe('when a winning bid renders', function () { + const CURRENT_TIME = 1586000000000; + let auction; + beforeEach(function () { + openxAdapter.enableAnalytics({ + options: { + publisherPlatformId: 'test-platform-id', + publisherAccountId: 123, + sample: 1.0, + enableV2: true, + payloadWaitTime: SLOT_LOAD_WAIT_TIME, + payloadWaitTimePadding: SLOT_LOAD_WAIT_TIME + } + }); + + // set current time + clock = sinon.useFakeTimers(CURRENT_TIME); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [BID_REQUESTED, bidRequestedOpenX], + [BID_REQUESTED, bidRequestedCloseX], + [BID_RESPONSE, bidResponseOpenX], + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd], + [BID_WON, bidWonOpenX], + [SLOT_LOADED] + ]); + + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; + }); + + afterEach(function () { + clock.restore(); + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); it('should track that winning bid rendered', function () { let openxBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); @@ -1042,6 +1115,10 @@ describe('openx analytics adapter', function() { let openxBidder = auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); expect(openxBidder.bidResponses[0]).to.contain({renderTime: CURRENT_TIME}); }); + + it('should track that the auction completed', function () { + expect(auction.state).to.equal(AUCTION_STATES.COMPLETED); + }); }); }); @@ -1134,8 +1211,8 @@ describe('openx analytics adapter', function() { 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, 'bids': [{'bidder': 'openx', 'params': {'unit': '540249866', 'delDomain': 'sademo-d.openx.net'}}, - {'bidder': 'closex', - 'params': {'unit': '540249866', 'delDomain': 'sademo-d.openx.net'}}], + {'bidder': 'closex', + 'params': {'unit': '540249866', 'delDomain': 'sademo-d.openx.net'}}], 'sizes': [[300, 250]], 'transactionId': 'test-transaction-id'}]; @@ -1259,7 +1336,7 @@ describe('openx analytics adapter', function() { openxAdapter.disableAnalytics(); }); - it('should send out both payloads', function(){ + it('should send out both payloads', function() { expect(server.requests.length).to.equal(2); }); @@ -1290,7 +1367,6 @@ describe('openx analytics adapter', function() { expect(bidWonEventInfoList.length).to.equal(1); - let openxBidder = v2Auction.bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); expect(openxBidder.bidResponses[0]).to.contain({winner: true}); });