diff --git a/modules/openxAnalyticsAdapter.js b/modules/openxAnalyticsAdapter.js index 68b2d2478ec..5e87474c299 100644 --- a/modules/openxAnalyticsAdapter.js +++ b/modules/openxAnalyticsAdapter.js @@ -361,7 +361,8 @@ function onSlotLoaded({ slot }) { let allSlotsLoaded = false; if (auctionId) { - let adPosition = getAdPositionByElementId(slotElementId); + let {x, y} = getPageOffset(); + let adPosition = isAtf(slotElementId, x, y) ? 'ATF' : 'BTF'; updateLoadedAdSlotsInfo(auctionId, adUnitCode, adPosition); let loadedAdUnitCodes = getLoadedAdUnitCodes(auctionId); let allAdUnitCodes = getAllAdUnitCodesByAuctionId(auctionId); @@ -474,7 +475,7 @@ function buildCampaignFromUtmCodes() { utmTags.forEach(function(utmKey) { let utmValue = queryParams[utmKey]; - if(utmValue){ + if (utmValue) { let key = UTM_TO_CAMPAIGN_PROPERTIES[utmKey]; campaign[key] = utmValue; } @@ -704,9 +705,9 @@ function pushAdPositionData(auctionId) { } } -function getAdPositionByElementId(elementId) { +function isAtf(elementId, scrollLeft = 0, scrollTop = 0) { let elem = document.querySelector('#' + elementId); - let adPosition; + let isInView = false; if (elem) { let bounding = elem.getBoundingClientRect(); if (bounding) { @@ -714,10 +715,10 @@ function getAdPositionByElementId(elementId) { let windowHeight = (window.innerHeight || document.documentElement.clientHeight); // intersection coordinates - let left = Math.max(0, bounding.left); - let right = Math.min(windowWidth, bounding.right); - let bottom = Math.min(windowHeight, bounding.bottom); - let top = Math.max(0, bounding.top); + let left = Math.max(0, bounding.left + scrollLeft); + let right = Math.min(windowWidth, bounding.right + scrollLeft); + let top = Math.max(0, bounding.top + scrollTop); + let bottom = Math.min(windowHeight, bounding.bottom + scrollTop); let intersectionWidth = right - left; let intersectionHeight = bottom - top; @@ -727,13 +728,13 @@ function getAdPositionByElementId(elementId) { if (adSlotArea > 0) { // Atleast 50% of intersection in window - adPosition = (intersectionArea * 2 >= adSlotArea) ? 'ATF' : 'BTF'; + isInView = intersectionArea * 2 >= adSlotArea; } } } else { utils.logWarn('OX: DOM element not for id ' + elementId); } - return adPosition; + return isInView; } openxAdapter.slotOnLoad = onSlotLoaded; @@ -860,7 +861,7 @@ function onAuctionInit({auctionId, timestamp: startTime, timeout, adUnitCodes}) auctionMap[auctionId] = { id: auctionId, startTime, - endTime: void(0), + endTime: void (0), timeout, auctionOrder, adUnitCodesCount: adUnitCodes.length, @@ -870,8 +871,12 @@ function onAuctionInit({auctionId, timestamp: startTime, timeout, adUnitCodes}) }; // setup adunit properties in map - auctionMap[auctionId].adUnitCodeToBidderRequestMap = adUnitCodes.reduce((obj, adunitCode) => { - obj[adunitCode] = {}; + auctionMap[auctionId].adUnitCodeToAdUnitMap = adUnitCodes.reduce((obj, adunitCode) => { + obj[adunitCode] = { + code: adunitCode, + adPosition: void (0), + bidRequestsMap: {} + }; return obj; }, {}); @@ -882,12 +887,12 @@ function onAuctionInit({auctionId, timestamp: startTime, timeout, adUnitCodes}) function onBidRequested(bidRequest) { const {auctionId, auctionStart, refererInfo, bids: bidderRequests, start} = bidRequest; const auction = auctionMap[auctionId]; - const adUnitCodeToBidderRequestMap = auction.adUnitCodeToBidderRequestMap; + const adUnitCodeToAdUnitMap = auction.adUnitCodeToAdUnitMap; bidderRequests.forEach(bidderRequest => { const { adUnitCode, bidder, bidId: requestId, mediaTypes, params, src, userId } = bidderRequest; - adUnitCodeToBidderRequestMap[adUnitCode][requestId] = { + adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId] = { bidder, params, mediaTypes, @@ -928,7 +933,7 @@ function onBidResponse(bidResponse) { meta } = bidResponse; - auctionMap[auctionId].adUnitCodeToBidderRequestMap[adUnitCode][requestId].bids[adId] = { + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId].bids[adId] = { cpm, creativeId, requestTimestamp, @@ -954,12 +959,12 @@ function onBidResponse(bidResponse) { function onBidTimeout(args) { utils._each(args, ({auctionId, adUnitCode, bidId: requestId}) => { - if (auctionMap[auctionId] - && auctionMap[auctionId].adUnitCodeToBidderRequestMap - && auctionMap[auctionId].adUnitCodeToBidderRequestMap[adUnitCode] - && auctionMap[auctionId].adUnitCodeToBidderRequestMap[adUnitCode][requestId] + if (auctionMap[auctionId] && + auctionMap[auctionId].adUnitCodeToAdUnitMap && + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode] && + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId] ) { - auctionMap[auctionId].adUnitCodeToBidderRequestMap[adUnitCode][requestId].timedOut = true; + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId].timedOut = true; } }); } @@ -986,7 +991,14 @@ function onAuctionEnd(endedAuction) { */ function onBidWon(bidResponse) { const { auctionId, adUnitCode, requestId, adId } = bidResponse; - auctionMap[auctionId].adUnitCodeToBidderRequestMap[adUnitCode][requestId].bids[adId].winner = true; + if (auctionMap[auctionId] && + auctionMap[auctionId].adUnitCodeToAdUnitMap && + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode] && + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId] && + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId].bids[adId] + ) { + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId].bids[adId].winner = true; + } } /** @@ -996,10 +1008,18 @@ function onBidWon(bidResponse) { */ function onSlotLoadedV2({ slot }) { const renderTime = Date.now(); - const auction = getAuctionByGoogleTagSLot(slot); + const elementId = slot.getSlotElementId(); + const bidId = slot.getTargeting('hb_adid')[0]; + + let [auction, adUnit, bid] = getPathToBidResponseByBidId(bidId); if (!auction) { - return; // slot is not participating in a prebid auction + // attempt to get auction by adUnitCode + auction = getAuctionByGoogleTagSLot(slot); + + if (!auction) { + return; // slot is not participating in an active prebid auction + } } clearAuctionTimer(auction); @@ -1008,12 +1028,11 @@ function onSlotLoadedV2({ slot }) { auction.adunitCodesRenderedCount++; // mark adunit as rendered - const adId = slot.getTargeting('hb_adid')[0]; - const adUnit = getAdUnitByAuctionAndAdId(auction, adId); - - if (adUnit) { - adUnit.rendered = true; - adUnit.renderTime = renderTime; + if (bid) { + let {x, y} = getPageOffset(); + bid.rendered = true; + bid.renderTime = renderTime; + adUnit.adPosition = isAtf(elementId, x, y) ? 'ATF' : 'BTF'; } if (auction.adunitCodesRenderedCount === auction.adUnitCodesCount) { @@ -1025,6 +1044,18 @@ function onSlotLoadedV2({ slot }) { delayedSend(auction); } +// backwards compatible pageOffset from https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollX +function getPageOffset() { + var x = (window.pageXOffset !== undefined) + ? window.pageXOffset + : (document.documentElement || document.body.parentNode || document.body).scrollLeft; + + var y = (window.pageYOffset !== undefined) + ? window.pageYOffset + : (document.documentElement || document.body.parentNode || document.body).scrollTop; + return {x, y}; +} + function delayedSend(auction) { const delayTime = auction.adunitCodesRenderedCount === auction.adUnitCodesCount ? analyticsConfig.payloadWaitTime @@ -1048,12 +1079,51 @@ function clearAuctionTimer(auction) { } } +/** + * Returns the path to a bid (auction, adunit, bidRequest, and bid) based on a bidId + * @param {string} bidId + * @returns {Array<*>} + */ +function getPathToBidResponseByBidId(bidId) { + let auction; + let adUnit; + let bidResponse; + + if (!bidId) { + return []; + } + + utils._each(auctionMap, currentAuction => { + // skip completed auctions + if (currentAuction.state === AUCTION_STATES.COMPLETED) { + return; + } + + utils._each(currentAuction.adUnitCodeToAdUnitMap, (currentAdunit) => { + utils._each(currentAdunit.bidRequestsMap, currentBiddRequest => { + utils._each(currentBiddRequest.bids, (currentBidResponse, bidResponseId) => { + if (bidId === bidResponseId) { + auction = currentAuction; + adUnit = currentAdunit; + bidResponse = currentBidResponse; + } + }); + }); + }); + }); + return [auction, adUnit, bidResponse]; +} + function getAuctionByGoogleTagSLot(slot) { let slotAdunitCodes = [slot.getSlotElementId(), slot.getAdUnitPath()]; let slotAuction; utils._each(auctionMap, auction => { - utils._each(auction.adUnitCodeToBidderRequestMap, (bidderRequestIdMap, adUnitCode) => { + if (auction.state === AUCTION_STATES.COMPLETED) { + return; + } + + utils._each(auction.adUnitCodeToAdUnitMap, (bidderRequestIdMap, adUnitCode) => { if (slotAdunitCodes.includes(adUnitCode)) { slotAuction = auction; } @@ -1064,7 +1134,7 @@ function getAuctionByGoogleTagSLot(slot) { } function buildAuctionPayload(auction) { - let {startTime, endTime, state, timeout, auctionOrder, adUnitCodeToBidderRequestMap} = auction; + let {startTime, endTime, state, timeout, auctionOrder, adUnitCodeToAdUnitMap} = auction; let {publisherPlatformId, publisherAccountId, campaign} = analyticsConfig; return { @@ -1080,73 +1150,77 @@ function buildAuctionPayload(auction) { deviceOSType: detectOS(), browser: detectBrowser(), testCode: analyticsConfig.testCode, - adUnits: buildBidRequestsPayload(adUnitCodeToBidderRequestMap), + adUnits: buildAdUnitsPayload(adUnitCodeToAdUnitMap), }; - function buildBidRequestsPayload(adUnitCodeToBidderRequestMap) { - return utils._map(adUnitCodeToBidderRequestMap, (bidderRequestMap, adUnitCode) => { - let bidRequests = utils._map(bidderRequestMap, (bidderRequest) => { - let {bidder, source, bids, mediaTypes, timedOut, userId} = bidderRequest; - return { - adUnitCode, - bidder, - source, - // return an array of objects containing the module name and id - userIds: buildUserIdPayload(userId), - hasBidderResponded: Object.keys(bids).length > 0, - availableAdSizes: getMediaTypeSizes(mediaTypes), - availableMediaTypes: getMediaTypes(mediaTypes), - timedOut, - bidResponses: utils._map(bidderRequest.bids, (bidderBidResponse) => { - let { - cpm, - creativeId, - responseTimestamp, - ts, - adId, - meta, - mediaType, - dealId, - ttl, - netRevenue, - currency, - originalCpm, - originalCurrency, - width, - height, - latency, - winner, - rendered, - renderTime - } = bidderBidResponse; - - return { - microCpm: cpm * 1000, - netRevenue, - currency, - mediaType, - height, - width, - size: `${width}x${height}`, - dealId, - latency, - ttl, - winner, - creativeId, - ts, - rendered, - renderTime, - meta - } - }) - } - }); + function buildAdUnitsPayload(adUnitCodeToAdUnitMap) { + return utils._map(adUnitCodeToAdUnitMap, (adUnit) => { + let {code, adPosition} = adUnit; return { - code: adUnitCode, - bidRequests + code, + adPosition, + bidRequests: buildBidRequestPayload(adUnit.bidRequestsMap) }; + function buildBidRequestPayload(bidRequestsMap) { + return utils._map(bidRequestsMap, (bidRequest) => { + let {bidder, source, bids, mediaTypes, timedOut, userId} = bidRequest; + return { + bidder, + source, + // return an array of objects containing the module name and id + userIds: buildUserIdPayload(userId), + hasBidderResponded: Object.keys(bids).length > 0, + availableAdSizes: getMediaTypeSizes(mediaTypes), + availableMediaTypes: getMediaTypes(mediaTypes), + timedOut, + bidResponses: utils._map(bidRequest.bids, (bidderBidResponse) => { + let { + cpm, + creativeId, + responseTimestamp, + ts, + adId, + meta, + mediaType, + dealId, + ttl, + netRevenue, + currency, + originalCpm, + originalCurrency, + width, + height, + latency, + winner, + rendered, + renderTime + } = bidderBidResponse; + + return { + microCpm: cpm * 1000, + netRevenue, + currency, + mediaType, + height, + width, + size: `${width}x${height}`, + dealId, + latency, + ttl, + winner, + creativeId, + ts, + rendered, + renderTime, + meta + } + }) + } + }); + } + function buildUserIdPayload(userId) { return utils._map(userId, (id, module) => { return { @@ -1191,8 +1265,8 @@ function buildAuctionPayload(auction) { function getAdUnitByAuctionAndAdId(auction, adId) { let adunit; - utils._each(auction.adUnitCodeToBidderRequestMap, (bidderRequestIdMap) => { - utils._each(bidderRequestIdMap, bidderRequest => { + utils._each(auction.adUnitCodeToAdUnitMap, (bidderRequestIdMap) => { + utils._each(bidderRequestIdMap.bidRequestsMap, bidderRequest => { utils._each(bidderRequest.bids, (bid, bidId) => { if (bidId === adId) { adunit = bid; @@ -1273,15 +1347,15 @@ openxAdapter.reset = function() { /** * @typedef {Object} BidResponseMeta * @property {string} [networkId] Bidder-specific Network/DSP Id - * @property {string} [networkName] - Network/DSP Name. example: "NetworkN" - * @property {string} [agencyId] - Bidder-specific Agency ID. example: 2222 - * @property {string} [agencyName] - Agency Name. example: "Agency, Inc." - * @property {string} [advertiserId] - Bidder-specific Advertiser ID. example: 3333 - * @property {string} [advertiserName] - Advertiser Name. example: "AdvertiserA" + * @property {string} [networkName] - Network/DSP Name. example: "NetworkN" + * @property {string} [agencyId] - Bidder-specific Agency ID. example: 2222 + * @property {string} [agencyName] - Agency Name. example: "Agency, Inc." + * @property {string} [advertiserId] - Bidder-specific Advertiser ID. example: 3333 + * @property {string} [advertiserName] - Advertiser Name. example: "AdvertiserA" * @property {Array} [advertiserDomains] - Array of Advertiser Domains for the landing page(s). This is an array - * to align with the OpenRTB ‘adomain’ field.. example: ["advertisera.com"] - * @property {string} [brandId] - Bidder-specific Brand ID (some advertisers may have many brands). example: 4444 - * @property {string} [brandName] - Brand Name. example: "BrandB" - * @property {string} [primaryCatId] - Primary IAB category ID. example: "IAB-111" - * @property {Array} [secondaryCatIds] - Array of secondary IAB category IDs. example: ["IAB-222","IAB-333"] + * to align with the OpenRTB ‘adomain’ field.. example: ["advertisera.com"] + * @property {string} [brandId] - Bidder-specific Brand ID (some advertisers may have many brands). example: 4444 + * @property {string} [brandName] - Brand Name. example: "BrandB" + * @property {string} [primaryCatId] - Primary IAB category ID. example: "IAB-111" + * @property {Array} [secondaryCatIds] - Array of secondary IAB category IDs. example: ["IAB-222","IAB-333"] */ diff --git a/test/spec/modules/openxAnalyticsAdapter_spec.js b/test/spec/modules/openxAnalyticsAdapter_spec.js index 468e42b649e..9b0010b8897 100644 --- a/test/spec/modules/openxAnalyticsAdapter_spec.js +++ b/test/spec/modules/openxAnalyticsAdapter_spec.js @@ -771,7 +771,7 @@ describe('openx analytics adapter', function() { ]); simulateAuction([ - [AUCTION_INIT, {...auctionInit, auctionId: 'second-auction-id'} ], + [AUCTION_INIT, {...auctionInit, auctionId: 'second-auction-id'}], [SLOT_LOADED] ]); @@ -930,11 +930,7 @@ describe('openx analytics adapter', function() { }); it('should track the adunit code', function () { - let openxBidder = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); - let closexBidder = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex'); - - expect(openxBidder.adUnitCode).to.equal(AD_UNIT_CODE); - expect(closexBidder.adUnitCode).to.equal(AD_UNIT_CODE); + expect(auction.adUnits[0].code).to.equal(AD_UNIT_CODE); }); it('should track the user ids', function () {