From 1f74a2b890168a67dcee66e0e16f721ba2efa1c1 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Wed, 13 Jan 2021 16:06:07 -0800 Subject: [PATCH 1/2] adding logic to wait for slots to render before message --- modules/rubiconAnalyticsAdapter.js | 64 +++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 670adc9fe6d..a573436fb8b 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -454,24 +454,45 @@ function updateRpaCookie() { return decodedRpaCookie; } +const gamEventFunctions = { + 'slotOnload': (auctionId, bid) => { + cache.auctions[auctionId].gamHasRendered[bid.adUnit.adUnitCode] = true; + }, + 'slotRenderEnded': (auctionId, bid, event) => { + if (event.isEmpty) { + cache.auctions[auctionId].gamHasRendered[bid.adUnit.adUnitCode] = true; + } + bid.adUnit.gam = utils.pick(event, [ + // these come in as `null` from Gpt, which when stringified does not get removed + // so set explicitly to undefined when not a number + 'advertiserId', advertiserId => utils.isNumber(advertiserId) ? advertiserId : undefined, + 'creativeId', creativeId => utils.isNumber(creativeId) ? creativeId : undefined, + 'lineItemId', lineItemId => utils.isNumber(lineItemId) ? lineItemId : undefined, + 'adSlot', () => event.slot.getAdUnitPath(), + 'isSlotEmpty', () => event.isEmpty || undefined + ]); + } +} + function subscribeToGamSlots() { - window.googletag.pubads().addEventListener('slotRenderEnded', event => { - const isMatchingAdSlot = utils.isAdUnitCodeMatchingSlot(event.slot); - // loop through auctions and adUnits and mark the info - Object.keys(cache.auctions).forEach(auctionId => { - (Object.keys(cache.auctions[auctionId].bids) || []).forEach(bidId => { - let bid = cache.auctions[auctionId].bids[bidId]; - // if this slot matches this bids adUnit, add the adUnit info - if (isMatchingAdSlot(bid.adUnit.adUnitCode)) { - bid.adUnit.gam = utils.pick(event, [ - // these come in as `null` from Gpt, which when stringified does not get removed - // so set explicitly to undefined when not a number - 'advertiserId', advertiserId => utils.isNumber(advertiserId) ? advertiserId : undefined, - 'creativeId', creativeId => utils.isNumber(creativeId) ? creativeId : undefined, - 'lineItemId', lineItemId => utils.isNumber(lineItemId) ? lineItemId : undefined, - 'adSlot', () => event.slot.getAdUnitPath(), - 'isSlotEmpty', () => event.isEmpty || undefined - ]); + ['slotOnload', 'slotRenderEnded'].forEach(eventName => { + window.googletag.pubads().addEventListener(eventName, event => { + const isMatchingAdSlot = utils.isAdUnitCodeMatchingSlot(event.slot); + // loop through auctions and adUnits and mark the info + Object.keys(cache.auctions).forEach(auctionId => { + (Object.keys(cache.auctions[auctionId].bids) || []).forEach(bidId => { + let bid = cache.auctions[auctionId].bids[bidId]; + // if this slot matches this bids adUnit, add the adUnit info + if (isMatchingAdSlot(bid.adUnit.adUnitCode)) { + // mark this adUnit as having been rendered by gam + gamEventFunctions[eventName](auctionId, bid, event); + } + }); + // Now if all adUnits have gam rendered, send the payload + if (rubiConf.waitForGamSlots && !cache.auctions[auctionId].sent && Object.keys(cache.auctions[auctionId].gamHasRendered).every(adUnitCode => cache.auctions[auctionId].gamHasRendered[adUnitCode])) { + clearTimeout(cache.timeouts[auctionId]); + delete cache.timeouts[auctionId]; + sendMessage.call(rubiconAdapter, auctionId); } }); }); @@ -538,6 +559,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { ]); cacheEntry.bids = {}; cacheEntry.bidsWon = {}; + cacheEntry.gamHasRendered = {}; cacheEntry.referrer = utils.deepAccess(args, 'bidderRequests.0.refererInfo.referer'); const floorData = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData'); if (floorData) { @@ -557,6 +579,10 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { // mark adUnits we expect bidWon events for cache.auctions[args.auctionId].bidsWon[bid.adUnitCode] = false; + if (rubiConf.waitForGamSlots) { + cache.auctions[args.auctionId].gamHasRendered[bid.adUnitCode] = false; + } + memo[bid.bidId] = utils.pick(bid, [ 'bidder', bidder => bidder.toLowerCase(), 'bidId', @@ -702,7 +728,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { // check if this BID_WON missed the boat, if so send by itself if (auctionCache.sent === true) { sendMessage.call(this, args.auctionId, args.requestId); - } else if (Object.keys(auctionCache.bidsWon).reduce((memo, adUnitCode) => { + } else if (!rubiConf.waitForGamSlots && Object.keys(auctionCache.bidsWon).reduce((memo, adUnitCode) => { // only send if we've received bidWon events for all adUnits in auction memo = memo && auctionCache.bidsWon[adUnitCode]; return memo; @@ -717,7 +743,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { // start timer to send batched payload just in case we don't hear any BID_WON events cache.timeouts[args.auctionId] = setTimeout(() => { sendMessage.call(this, args.auctionId); - }, SEND_TIMEOUT); + }, rubiConf.analyticsBatchTimeout || SEND_TIMEOUT); break; case BID_TIMEOUT: args.forEach(badBid => { From 9567a05d5756b83e6a533348e66d17451af53704 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 14 Jan 2021 11:15:31 -0800 Subject: [PATCH 2/2] Test updates --- .../modules/rubiconAnalyticsAdapter_spec.js | 87 +++++++++++++++++-- 1 file changed, 78 insertions(+), 9 deletions(-) diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 78792ff2ce7..0c19d8dbadf 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -1357,12 +1357,13 @@ describe('rubicon analytics adapter', function () { }); }); describe('with googletag enabled', function () { - let gptSlot0, gptSlot1, gptEvent0, gptEvent1; + let gptSlot0, gptSlot1; + let gptSlotRenderEnded0, gptSlotRenderEnded1; + let gptslotOnload0, gptslotOnload1; beforeEach(function () { mockGpt.enable(); gptSlot0 = mockGpt.makeSlot({code: '/19968336/header-bid-tag-0'}); - gptSlot1 = mockGpt.makeSlot({code: '/19968336/header-bid-tag1'}); - gptEvent0 = { + gptSlotRenderEnded0 = { eventName: 'slotRenderEnded', params: { slot: gptSlot0, @@ -1372,7 +1373,19 @@ describe('rubicon analytics adapter', function () { lineItemId: 3333 } }; - gptEvent1 = { + gptslotOnload0 = { + eventName: 'slotOnload', + params: { + slot: gptSlot0, + isEmpty: false, + advertiserId: 1111, + creativeId: 2222, + lineItemId: 3333 + } + }; + + gptSlot1 = mockGpt.makeSlot({code: '/19968336/header-bid-tag1'}); + gptSlotRenderEnded1 = { eventName: 'slotRenderEnded', params: { slot: gptSlot1, @@ -1382,6 +1395,16 @@ describe('rubicon analytics adapter', function () { lineItemId: 6666 } }; + gptslotOnload1 = { + eventName: 'slotOnload', + params: { + slot: gptSlot1, + isEmpty: false, + advertiserId: 1111, + creativeId: 2222, + lineItemId: 3333 + } + }; }); afterEach(function () { @@ -1389,7 +1412,7 @@ describe('rubicon analytics adapter', function () { }); it('should add necessary gam information if gpt is enabled and slotRender event emmited', function () { - performStandardAuction([gptEvent0, gptEvent1]); + performStandardAuction([gptSlotRenderEnded0, gptSlotRenderEnded1]); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); @@ -1412,7 +1435,7 @@ describe('rubicon analytics adapter', function () { }); it('should handle empty gam renders', function () { - performStandardAuction([gptEvent0, { + performStandardAuction([gptSlotRenderEnded0, { eventName: 'slotRenderEnded', params: { slot: gptSlot1, @@ -1439,7 +1462,7 @@ describe('rubicon analytics adapter', function () { }); it('should still add gam ids if falsy', function () { - performStandardAuction([gptEvent0, { + performStandardAuction([gptSlotRenderEnded0, { eventName: 'slotRenderEnded', params: { slot: gptSlot1, @@ -1470,14 +1493,60 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); - it('should handle empty gam renders', function () { - performStandardAuction([gptEvent0, gptEvent1]); + it('should correctly set adUnit for associated slots', function () { + performStandardAuction([gptSlotRenderEnded0, gptSlotRenderEnded1]); + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + validate(message); + + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + expectedMessage.auctions[0].adUnits[0].gam = { + advertiserId: 1111, + creativeId: 2222, + lineItemId: 3333, + adSlot: '/19968336/header-bid-tag-0' + }; + expectedMessage.auctions[0].adUnits[1].gam = { + advertiserId: 4444, + creativeId: 5555, + lineItemId: 6666, + adSlot: '/19968336/header-bid-tag1' + }; + expect(message).to.deep.equal(expectedMessage); + }); + + it('should send request when waitForGamSlots is present but no bidWons are sent', function () { + config.setConfig({ + rubicon: { + waitForGamSlots: true + } + }); + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + + // should not send if just slotRenderEnded is emmitted for both + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + mockGpt.emitEvent(gptSlotRenderEnded1.eventName, gptSlotRenderEnded1.params); + + expect(server.requests.length).to.equal(0); + + // now emit slotOnload and it should send + mockGpt.emitEvent(gptslotOnload0.eventName, gptslotOnload0.params); + mockGpt.emitEvent(gptslotOnload1.eventName, gptslotOnload1.params); + expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + delete expectedMessage.bidsWon; // should not be any of these expectedMessage.auctions[0].adUnits[0].gam = { advertiserId: 1111, creativeId: 2222,