From 33ecab2874ec2f7d75a1ed0ae00476db4da62380 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Wed, 26 Aug 2020 11:48:15 -0700 Subject: [PATCH 01/12] temp check in --- karma.conf.maker.js | 8 ++ modules/rubiconAnalyticsAdapter.js | 146 +++++++++++++++++++++++++++-- pbaModules.json | 10 ++ src/AnalyticsAdapter.js | 4 +- 4 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 pbaModules.json diff --git a/karma.conf.maker.js b/karma.conf.maker.js index 712ef14caa1..683d61f6775 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -170,6 +170,14 @@ module.exports = function(codeCoverage, browserstack, watchMode, file) { plugins: plugins } + // To ensure that, we are able to run single spec file + // here we are adding preprocessors, when file is passed + if (file) { + config.files.forEach((file) => { + config.preprocessors[file] = ['webpack', 'sourcemap']; + }); + delete config.preprocessors['test/test_index.js']; + } setReporters(config, codeCoverage, browserstack); setBrowsers(config, browserstack); return config; diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 0bee4d926fc..3e890ce2fff 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -5,9 +5,17 @@ import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import * as utils from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const RUBICON_GVL_ID = 52; +const storage = getStorageManager(RUBICON_GVL_ID, 'rubicon'); +const COOKIE_NAME = 'rpaSession'; +const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins +const END_EXPIRE_TIME = 21600000; // 6 hours const { EVENTS: { + REQUEST_BIDS, AUCTION_INIT, AUCTION_END, BID_REQUESTED, @@ -15,7 +23,8 @@ const { BIDDER_DONE, BID_TIMEOUT, BID_WON, - SET_TARGETING + SET_TARGETING, + TCF2_ENFORCEMENT }, STATUS: { GOOD, @@ -38,6 +47,7 @@ const cache = { auctions: {}, targeting: {}, timeouts: {}, + gpt: {}, }; export function getHostNameFromReferer(referer) { @@ -126,7 +136,7 @@ function sendMessage(auctionId, bidWonId) { }); } let auctionCache = cache.auctions[auctionId]; - let referrer = config.getConfig('pageUrl') || auctionCache.referrer; + let referrer = config.getConfig('pageUrl') || auctionCache && auctionCache.referrer; let message = { eventTimeMillis: Date.now(), integration: config.getConfig('rubicon.int_type') || DEFAULT_INTEGRATION, @@ -149,7 +159,8 @@ function sendMessage(auctionId, bidWonId) { 'mediaTypes', 'dimensions', 'adserverTargeting', () => stringProperties(cache.targeting[bid.adUnit.adUnitCode] || {}), - 'adSlot' + 'gam', + 'pbAdSlot' ]); adUnit.bids = []; adUnit.status = 'no-bid'; // default it to be no bid @@ -215,6 +226,26 @@ function sendMessage(auctionId, bidWonId) { } } + // gather gdpr info + if (auctionCache.gdprConsent) { + auction.gdpr = utils.pick(auctionCache.gdprConsent, [ + 'gdprApplies as applies', + 'consentString', + 'apiVersion as version' + ]); + } + + // gather session info + if (auctionCache.session) { + message.session = utils.pick(auctionCache.session, [ + 'id', + 'pvid', + 'start', + 'expires' + ]); + auction.fpkvs = auctionCache.session.fpkvs || undefined; + } + if (serverConfig) { auction.serverTimeoutMillis = serverConfig.timeout; } @@ -300,6 +331,19 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { ]); } +function getDynamicKvps() { + if (pbjs.rp && typeof pbjs.rp.getCustomTargeting === 'function') { + return pbjs.rp.getCustomTargeting(); + } + return {}; +} + +function getPageViewId() { + if (pbjs.rp && typeof pbjs.rp.getPageViewId === 'function') { + return pbjs.rp.getPageViewId(); + } +} + let samplingFactor = 1; let accountId; // List of known rubicon aliases @@ -318,6 +362,73 @@ function setRubiconAliases(aliasRegistry) { }); } +function getRpaCookie() { + let encodedCookie = storage.getCookie(COOKIE_NAME); + if (encodedCookie) { + try { + return JSON.parse(window.atob(encodedCookie)); + } catch (e) { + utils.logError('Rubicon Analytics: Unable to decode: ', e); + } + } + return {}; +} + +function setRpaCookie(decodedCookie) { + try { + storage.setCookie(COOKIE_NAME, window.btoa(JSON.stringify(decodedCookie))); + } catch (e) { + utils.logError('Rubicon Analytics: Unable to encode: ', e); + } +} + +function updateRpaCookie() { + const currentTime = Date.now(); + let decodedRpaCookie = getRpaCookie(); + if ( + !Object.keys(decodedRpaCookie).length || + (decodedRpaCookie.lastSeen - currentTime) > LAST_SEEN_EXPIRE_TIME || + decodedRpaCookie.expires < currentTime + ) { + decodedRpaCookie = { + id: utils.generateUUID(), + start: currentTime, + expires: currentTime + END_EXPIRE_TIME, // six hours later, + } + } + // possible that decodedRpaCookie is undefined, and if it is, we probably are blocked by storage or some other exception + if (Object.keys(decodedRpaCookie).length) { + decodedRpaCookie.lastSeen = currentTime; + decodedRpaCookie.fpkvs = {...decodedRpaCookie.fpkvs, ...getDynamicKvps()}; + decodedRpaCookie.pvid = getPageViewId(); + setRpaCookie(decodedRpaCookie) + } + return decodedRpaCookie; +} + +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() + ]); + } + }); + }); + }); +} + let baseAdapter = adapter({analyticsType: 'endpoint'}); let rubiconAdapter = Object.assign({}, baseAdapter, { referrerHostname: '', @@ -367,7 +478,14 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { }, track({eventType, args}) { switch (eventType) { + case REQUEST_BIDS: + console.log('THE REQUEST_BIDS args are: ', args); + break; case AUCTION_INIT: + // register to listen to gpt events if not done yet + if (!cache.gpt.registered && utils.isGptPubadsDefined()) { + subscribeToGamSlots(); + } // set the rubicon aliases setRubiconAliases(adapterManager.aliasRegistry); let cacheEntry = utils.pick(args, [ @@ -376,11 +494,13 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { ]); cacheEntry.bids = {}; cacheEntry.bidsWon = {}; - cacheEntry.referrer = args.bidderRequests[0].refererInfo.referer; + cacheEntry.referrer = utils.deepAccess(args, 'bidderRequests[0].refererInfo.referer'); const floorData = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData'); if (floorData) { cacheEntry.floorData = {...floorData}; } + cacheEntry.gdprConsent = utils.deepAccess(args, 'bidderRequests.0.gdprConsent'); + cacheEntry.session = updateRpaCookie(); cache.auctions[args.auctionId] = cacheEntry; break; case BID_REQUESTED: @@ -452,6 +572,12 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } return ['banner']; }, + 'gam', () => { + if (utils.deepAccess(bid, 'fpd.context.adServer.name') === 'gam') { + return {adSlot: bid.fpd.context.adServer.adSlot} + } + }, + 'pbAdSlot', () => utils.deepAccess(bid, 'fpd.context.pbAdSlot') ]) ]); return memo; @@ -461,8 +587,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { let auctionEntry = cache.auctions[args.auctionId]; let bid = auctionEntry.bids[args.requestId]; // If floor resolved gptSlot but we have not yet, then update the adUnit to have the adSlot name - if (!utils.deepAccess(bid, 'adUnit.adSlot') && utils.deepAccess(args, 'floorData.matchedFields.gptSlot')) { - bid.adUnit.adSlot = args.floorData.matchedFields.gptSlot; + if (!utils.deepAccess(bid, 'adUnit.gam.adSlot') && utils.deepAccess(args, 'floorData.matchedFields.gptSlot')) { + utils.deepSetValue(bid, 'adUnit.gam.adSlot', args.floorData.matchedFields.gptSlot); } // if we have not set enforcements yet set it if (!utils.deepAccess(auctionEntry, 'floorData.enforcements') && utils.deepAccess(args, 'floorData.enforcements')) { @@ -479,7 +605,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { delete bid.error; // it's possible for this to be set by a previous timeout break; case NO_BID: - bid.status = args.status === BID_REJECTED ? 'rejected' : 'no-bid'; + bid.status = args.status === BID_REJECTED ? 'rejected-ipf' : 'no-bid'; delete bid.error; break; default: @@ -542,13 +668,17 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { }; }); break; + case TCF2_ENFORCEMENT: + console.log('THE TCF2_ENFORCEMENT args are: ', args); + break; } } }); adapterManager.registerAnalyticsAdapter({ adapter: rubiconAdapter, - code: 'rubicon' + code: 'rubicon', + gvlid: RUBICON_GVL_ID }); export default rubiconAdapter; diff --git a/pbaModules.json b/pbaModules.json new file mode 100644 index 00000000000..201ba24ac88 --- /dev/null +++ b/pbaModules.json @@ -0,0 +1,10 @@ +[ + "rubiconBidAdapter", + "appnexusBidAdapter", + "rubiconAnalyticsAdapter", + "prebidServerBidAdapter", + "currency", + "priceFloors", + "consentManagement", + "gdprEnforcement" +] \ No newline at end of file diff --git a/src/AnalyticsAdapter.js b/src/AnalyticsAdapter.js index f3297412a35..c6355218de8 100644 --- a/src/AnalyticsAdapter.js +++ b/src/AnalyticsAdapter.js @@ -18,7 +18,8 @@ const { BIDDER_DONE, SET_TARGETING, AD_RENDER_FAILED, - ADD_AD_UNITS + ADD_AD_UNITS, + TCF2_ENFORCEMENT } } = CONSTANTS; @@ -101,6 +102,7 @@ export default function AnalyticsAdapter({ url, analyticsType, global, handler } // Next register event listeners to send data immediately _handlers = { + [TCF2_ENFORCEMENT]: args => this.enqueue({ eventType: TCF2_ENFORCEMENT, args }), [REQUEST_BIDS]: args => this.enqueue({ eventType: REQUEST_BIDS, args }), [BID_REQUESTED]: args => this.enqueue({ eventType: BID_REQUESTED, args }), [BID_RESPONSE]: args => this.enqueue({ eventType: BID_RESPONSE, args }), From ebe5d8ce7be929732bc201a919e2298dc8ddc301 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Wed, 26 Aug 2020 13:30:48 -0700 Subject: [PATCH 02/12] add --- pbaModules.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pbaModules.json b/pbaModules.json index 201ba24ac88..a03c82621ef 100644 --- a/pbaModules.json +++ b/pbaModules.json @@ -6,5 +6,6 @@ "currency", "priceFloors", "consentManagement", - "gdprEnforcement" + "gdprEnforcement", + "gptPreAuction" ] \ No newline at end of file From 0c88c495b445ae8d41d20a5813e4c969fbe0d013 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Fri, 28 Aug 2020 12:55:10 -0700 Subject: [PATCH 03/12] dev almost done --- modules/rubiconAnalyticsAdapter.js | 29 ++++++++----------- .../modules/rubiconAnalyticsAdapter_spec.js | 8 ++--- test/spec/modules/rubiconAnalyticsSchema.json | 3 +- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 3e890ce2fff..c76950180d7 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -13,9 +13,9 @@ const COOKIE_NAME = 'rpaSession'; const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins const END_EXPIRE_TIME = 21600000; // 6 hours +let prebidGlobal = getGlobal(); const { EVENTS: { - REQUEST_BIDS, AUCTION_INIT, AUCTION_END, BID_REQUESTED, @@ -24,7 +24,6 @@ const { BID_TIMEOUT, BID_WON, SET_TARGETING, - TCF2_ENFORCEMENT }, STATUS: { GOOD, @@ -136,7 +135,7 @@ function sendMessage(auctionId, bidWonId) { }); } let auctionCache = cache.auctions[auctionId]; - let referrer = config.getConfig('pageUrl') || auctionCache && auctionCache.referrer; + let referrer = config.getConfig('pageUrl') || (auctionCache && auctionCache.referrer); let message = { eventTimeMillis: Date.now(), integration: config.getConfig('rubicon.int_type') || DEFAULT_INTEGRATION, @@ -243,7 +242,9 @@ function sendMessage(auctionId, bidWonId) { 'start', 'expires' ]); - auction.fpkvs = auctionCache.session.fpkvs || undefined; + if (auctionCache.session.fpkvs && Object.keys(auctionCache.session.fpkvs).length) { + auction.fpkvs = auctionCache.session.fpkvs; + } } if (serverConfig) { @@ -303,7 +304,7 @@ function getBidPrice(bid) { } // otherwise we convert and return try { - return Number(getGlobal().convertCurrency(cpm, currency, 'USD')); + return Number(prebidGlobal.convertCurrency(cpm, currency, 'USD')); } catch (err) { utils.logWarn('Rubicon Analytics Adapter: Could not determine the bidPriceUSD of the bid ', bid); } @@ -332,15 +333,15 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { } function getDynamicKvps() { - if (pbjs.rp && typeof pbjs.rp.getCustomTargeting === 'function') { - return pbjs.rp.getCustomTargeting(); + if (prebidGlobal.rp && typeof prebidGlobal.rp.getCustomTargeting === 'function') { + return prebidGlobal.rp.getCustomTargeting(); } return {}; } function getPageViewId() { - if (pbjs.rp && typeof pbjs.rp.getPageViewId === 'function') { - return pbjs.rp.getPageViewId(); + if (prebidGlobal.rp && typeof prebidGlobal.rp.getPageViewId === 'function') { + return prebidGlobal.rp.getPageViewId(); } } @@ -396,7 +397,7 @@ function updateRpaCookie() { expires: currentTime + END_EXPIRE_TIME, // six hours later, } } - // possible that decodedRpaCookie is undefined, and if it is, we probably are blocked by storage or some other exception + // possible that decodedRpaCookie is undefined, and if it is, we probably are blocked by storage or some other exception if (Object.keys(decodedRpaCookie).length) { decodedRpaCookie.lastSeen = currentTime; decodedRpaCookie.fpkvs = {...decodedRpaCookie.fpkvs, ...getDynamicKvps()}; @@ -478,9 +479,6 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { }, track({eventType, args}) { switch (eventType) { - case REQUEST_BIDS: - console.log('THE REQUEST_BIDS args are: ', args); - break; case AUCTION_INIT: // register to listen to gpt events if not done yet if (!cache.gpt.registered && utils.isGptPubadsDefined()) { @@ -494,7 +492,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { ]); cacheEntry.bids = {}; cacheEntry.bidsWon = {}; - cacheEntry.referrer = utils.deepAccess(args, 'bidderRequests[0].refererInfo.referer'); + cacheEntry.referrer = utils.deepAccess(args, 'bidderRequests.0.refererInfo.referer'); const floorData = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData'); if (floorData) { cacheEntry.floorData = {...floorData}; @@ -668,9 +666,6 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { }; }); break; - case TCF2_ENFORCEMENT: - console.log('THE TCF2_ENFORCEMENT args are: ', args); - break; } } }); diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 0c2c83a4b37..18cea73b69d 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -747,17 +747,17 @@ describe('rubicon analytics adapter', function () { provider: 'rubicon' }); // first adUnit's adSlot - expect(message.auctions[0].adUnits[0].adSlot).to.equal('12345/sports'); + expect(message.auctions[0].adUnits[0].gam.adSlot).to.equal('12345/sports'); // since no other bids, we set adUnit status to no-bid expect(message.auctions[0].adUnits[0].status).to.equal('no-bid'); // first adUnits bid is rejected - expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected'); + expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected-ipf'); expect(message.auctions[0].adUnits[0].bids[0].bidResponse.floorValue).to.equal(4); // if bid rejected should take cpmAfterAdjustments val expect(message.auctions[0].adUnits[0].bids[0].bidResponse.bidPriceUSD).to.equal(2.1); // second adUnit's adSlot - expect(message.auctions[0].adUnits[1].adSlot).to.equal('12345/news'); + expect(message.auctions[0].adUnits[1].gam.adSlot).to.equal('12345/news'); // top level adUnit status is success expect(message.auctions[0].adUnits[1].status).to.equal('success'); // second adUnits bid is success @@ -786,7 +786,7 @@ describe('rubicon analytics adapter', function () { // since no other bids, we set adUnit status to no-bid expect(message.auctions[0].adUnits[0].status).to.equal('no-bid'); // first adUnits bid is rejected - expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected'); + expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected-ipf'); expect(message.auctions[0].adUnits[0].bids[0].bidResponse.floorValue).to.equal(4); // if bid rejected should take cpmAfterAdjustments val expect(message.auctions[0].adUnits[0].bids[0].bidResponse.bidPriceUSD).to.equal(2.1); diff --git a/test/spec/modules/rubiconAnalyticsSchema.json b/test/spec/modules/rubiconAnalyticsSchema.json index 16cca629d8c..32659b27d39 100644 --- a/test/spec/modules/rubiconAnalyticsSchema.json +++ b/test/spec/modules/rubiconAnalyticsSchema.json @@ -293,7 +293,8 @@ "success", "no-bid", "error", - "rejected" + "rejected-gdpr", + "rejected-ipf" ] }, "error": { From c72cd54d497ebc6002138c2a9650ad9bdabde776 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Wed, 2 Sep 2020 09:25:36 -0700 Subject: [PATCH 04/12] add rule name to payload --- modules/rubiconAnalyticsAdapter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index c76950180d7..78f022f341f 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -139,6 +139,7 @@ function sendMessage(auctionId, bidWonId) { let message = { eventTimeMillis: Date.now(), integration: config.getConfig('rubicon.int_type') || DEFAULT_INTEGRATION, + ruleId: config.getConfig('rubicon.rule_name'), version: '$prebid.version$', referrerUri: referrer, referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer) @@ -422,7 +423,8 @@ function subscribeToGamSlots() { '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() + 'adSlot', () => event.slot.getAdUnitPath(), + 'isSlotEmpty', () => event.isEmpty || undefined ]); } }); From 2161cc118e3bfaad8c645cb1f984aa9b961ce33f Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 3 Sep 2020 15:17:34 -0700 Subject: [PATCH 05/12] update rubi analytics tests --- modules/rubiconAnalyticsAdapter.js | 20 +- .../modules/rubiconAnalyticsAdapter_spec.js | 359 +++++++++++++++++- test/spec/modules/rubiconAnalyticsSchema.json | 75 ++++ 3 files changed, 440 insertions(+), 14 deletions(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 78f022f341f..570b6087bcb 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -8,7 +8,7 @@ import { getGlobal } from '../src/prebidGlobal.js'; import { getStorageManager } from '../src/storageManager.js'; const RUBICON_GVL_ID = 52; -const storage = getStorageManager(RUBICON_GVL_ID, 'rubicon'); +export const storage = getStorageManager(RUBICON_GVL_ID, 'rubicon'); const COOKIE_NAME = 'rpaSession'; const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins const END_EXPIRE_TIME = 21600000; // 6 hours @@ -244,7 +244,9 @@ function sendMessage(auctionId, bidWonId) { 'expires' ]); if (auctionCache.session.fpkvs && Object.keys(auctionCache.session.fpkvs).length) { - auction.fpkvs = auctionCache.session.fpkvs; + message.fpkvs = Object.keys(auctionCache.session.fpkvs).map(key => { + return { key, value: auctionCache.session.fpkvs[key] }; + }); } } @@ -341,8 +343,8 @@ function getDynamicKvps() { } function getPageViewId() { - if (prebidGlobal.rp && typeof prebidGlobal.rp.getPageViewId === 'function') { - return prebidGlobal.rp.getPageViewId(); + if (prebidGlobal.rp && typeof prebidGlobal.rp.generatePageViewId === 'function') { + return prebidGlobal.rp.generatePageViewId(false); } } @@ -389,7 +391,7 @@ function updateRpaCookie() { let decodedRpaCookie = getRpaCookie(); if ( !Object.keys(decodedRpaCookie).length || - (decodedRpaCookie.lastSeen - currentTime) > LAST_SEEN_EXPIRE_TIME || + (currentTime - decodedRpaCookie.lastSeen) > LAST_SEEN_EXPIRE_TIME || decodedRpaCookie.expires < currentTime ) { decodedRpaCookie = { @@ -482,10 +484,6 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { track({eventType, args}) { switch (eventType) { case AUCTION_INIT: - // register to listen to gpt events if not done yet - if (!cache.gpt.registered && utils.isGptPubadsDefined()) { - subscribeToGamSlots(); - } // set the rubicon aliases setRubiconAliases(adapterManager.aliasRegistry); let cacheEntry = utils.pick(args, [ @@ -502,6 +500,10 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.gdprConsent = utils.deepAccess(args, 'bidderRequests.0.gdprConsent'); cacheEntry.session = updateRpaCookie(); cache.auctions[args.auctionId] = cacheEntry; + // register to listen to gpt events if not done yet + if (!cache.gpt.registered && utils.isGptPubadsDefined()) { + subscribeToGamSlots(); + } break; case BID_REQUESTED: Object.assign(cache.auctions[args.auctionId].bids, args.bids.reduce((memo, bid) => { diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 18cea73b69d..391d96d7948 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -2,16 +2,19 @@ import rubiconAnalyticsAdapter, { SEND_TIMEOUT, parseBidResponse, getHostNameFromReferer, + storage, } from 'modules/rubiconAnalyticsAdapter.js'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr.js'; - +import * as mockGpt from '../integration/faker/googletag.js'; import { setConfig, addBidResponseHook, } from 'modules/currency.js'; +import { getGlobal } from 'src/prebidGlobal.js'; +let prebidGlobal = getGlobal(); let Ajv = require('ajv'); let schema = require('./rubiconAnalyticsSchema.json'); let ajv = new Ajv({ @@ -272,11 +275,18 @@ const MOCK = { ] }; +const STUBBED_UUID = '12345678-1234-1234-1234-123456789abc'; + const ANALYTICS_MESSAGE = { 'eventTimeMillis': 1519767013781, 'integration': 'pbjs', 'version': '$prebid.version$', 'referrerUri': 'http://www.test.com/page.html', + 'session': { + 'expires': 1519788613781, + 'id': STUBBED_UUID, + 'start': 1519767013781 + }, 'referrerHostname': 'www.test.com', 'auctions': [ { @@ -466,13 +476,18 @@ const ANALYTICS_MESSAGE = { 'wrapperName': '10000_fakewrapper_test' }; -function performStandardAuction() { +function performStandardAuction(gptEvents) { 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); + + if (gptEvents && gptEvents.length) { + gptEvents.forEach(gptEvent => mockGpt.emitEvent(gptEvent.eventName, gptEvent.params)); + } + events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[0]); events.emit(BID_WON, MOCK.BID_WON[1]); @@ -483,10 +498,13 @@ describe('rubicon analytics adapter', function () { let clock; beforeEach(function () { + mockGpt.disable(); sandbox = sinon.sandbox.create(); sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'generateUUID').returns(STUBBED_UUID); + clock = sandbox.useFakeTimers(1519767013781); rubiconAnalyticsAdapter.referrerHostname = ''; @@ -505,6 +523,7 @@ describe('rubicon analytics adapter', function () { afterEach(function () { sandbox.restore(); config.resetConfig(); + mockGpt.enable(); }); it('should require accountId', function () { @@ -767,7 +786,7 @@ describe('rubicon analytics adapter', function () { }); it('should still send floor info if provider is not rubicon', function () { - let message = performFloorAuction('randomProvider') + let message = performFloorAuction('randomProvider'); // verify our floor stuff is passed // top level floor info @@ -782,7 +801,7 @@ describe('rubicon analytics adapter', function () { provider: 'randomProvider' }); // first adUnit's adSlot - expect(message.auctions[0].adUnits[0].adSlot).to.equal('12345/sports'); + expect(message.auctions[0].adUnits[0].gam.adSlot).to.equal('12345/sports'); // since no other bids, we set adUnit status to no-bid expect(message.auctions[0].adUnits[0].status).to.equal('no-bid'); // first adUnits bid is rejected @@ -792,7 +811,7 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[0].bids[0].bidResponse.bidPriceUSD).to.equal(2.1); // second adUnit's adSlot - expect(message.auctions[0].adUnits[1].adSlot).to.equal('12345/news'); + expect(message.auctions[0].adUnits[1].gam.adSlot).to.equal('12345/news'); // top level adUnit status is success expect(message.auctions[0].adUnits[1].status).to.equal('success'); // second adUnits bid is success @@ -801,6 +820,336 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); }); + describe('with session handling', function () { + let getCookieStub, setCookieStub, pvid, kvps; + beforeEach(function () { + getCookieStub = sinon.stub(storage, 'getCookie'); + setCookieStub = sinon.stub(storage, 'setCookie'); + + // custom dm stuff + prebidGlobal.rp = { + getCustomTargeting: () => kvps, + generatePageViewId: () => pvid + } + }); + + afterEach(function () { + getCookieStub.restore(); + setCookieStub.restore(); + + prebidGlobal.rp = pvid = kvps = undefined; + }); + + it('should should pass along custom rubicon kv and pvid when defined', function () { + pvid = '1a2b3c'; + kvps = { + source: 'fb', + link: 'email' + }; + + performStandardAuction(); + 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.session.pvid = '1a2b3c'; + expectedMessage.fpkvs = [ + {key: 'source', value: 'fb'}, + {key: 'link', value: 'email'} + ] + expect(message).to.deep.equal(expectedMessage); + }); + + it('should pick up existing cookie and use its values', function () { + // set some cookie + let inputCookie = { + id: '987654', + start: 1519766113781, // 15 mins before "now" + expires: 1519787713781, // six hours later + lastSeen: 1519766113781, + fpkvs: { source: 'tw' } + }; + getCookieStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputCookie))); + + pvid = '1a2b3c'; + kvps = { + link: 'email' // should merge this with what is in the cookie! + }; + + performStandardAuction(); + 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.session = { + id: '987654', + start: 1519766113781, + expires: 1519787713781, + pvid: '1a2b3c' + } + expectedMessage.fpkvs = [ + {key: 'source', value: 'tw'}, + {key: 'link', value: 'email'} + ] + expect(message).to.deep.equal(expectedMessage); + + let calledWith; + try { + calledWith = JSON.parse(atob(setCookieStub.getCall(0).args[1])); + } catch (e) { + calledWith = {}; + } + + expect(calledWith).to.deep.equal({ + id: '987654', // should have stayed same + start: 1519766113781, // should have stayed same + expires: 1519787713781, // should have stayed same + lastSeen: 1519767013781, // lastSeen updated to our "now" + fpkvs: { source: 'tw', link: 'email' }, // link merged in + pvid: '1a2b3c' // new pvid stored + }); + }); + + it('should throw out session if lastSeen > 30 mins ago and create new one', function () { + // set some cookie + let inputCookie = { + id: '987654', + start: 1519764313781, // 45 mins before "now" + expires: 1519785913781, // six hours later + lastSeen: 1519764313781, // 45 mins before "now" + fpkvs: { source: 'tw' } + }; + getCookieStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputCookie))); + + pvid = '1a2b3c'; + kvps = { + link: 'email' // should merge this with what is in the cookie! + }; + + performStandardAuction(); + 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); + // session should match what is already in ANALYTICS_MESSAGE, just need to add pvid + expectedMessage.session.pvid = '1a2b3c'; + + // the saved fpkvs should have been thrown out since session expired + expectedMessage.fpkvs = [ + {key: 'link', value: 'email'} + ] + expect(message).to.deep.equal(expectedMessage); + + let calledWith; + try { + calledWith = JSON.parse(atob(setCookieStub.getCall(0).args[1])); + } catch (e) { + calledWith = {}; + } + + expect(calledWith).to.deep.equal({ + id: STUBBED_UUID, // should have stayed same + start: 1519767013781, // should have stayed same + expires: 1519788613781, // should have stayed same + lastSeen: 1519767013781, // lastSeen updated to our "now" + fpkvs: { link: 'email' }, // link merged in + pvid: '1a2b3c' // new pvid stored + }); + }); + + it('should throw out session if past expires time and create new one', function () { + // set some cookie + let inputCookie = { + id: '987654', + start: 1519745353781, // 6 hours before "expires" + expires: 1519766953781, // little more than six hours ago + lastSeen: 1519767008781, // 5 seconds ago + fpkvs: { source: 'tw' } + }; + getCookieStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputCookie))); + + pvid = '1a2b3c'; + kvps = { + link: 'email' // should merge this with what is in the cookie! + }; + + performStandardAuction(); + 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); + // session should match what is already in ANALYTICS_MESSAGE, just need to add pvid + expectedMessage.session.pvid = '1a2b3c'; + + // the saved fpkvs should have been thrown out since session expired + expectedMessage.fpkvs = [ + {key: 'link', value: 'email'} + ] + expect(message).to.deep.equal(expectedMessage); + + let calledWith; + try { + calledWith = JSON.parse(atob(setCookieStub.getCall(0).args[1])); + } catch (e) { + calledWith = {}; + } + + expect(calledWith).to.deep.equal({ + id: STUBBED_UUID, // should have stayed same + start: 1519767013781, // should have stayed same + expires: 1519788613781, // should have stayed same + lastSeen: 1519767013781, // lastSeen updated to our "now" + fpkvs: { link: 'email' }, // link merged in + pvid: '1a2b3c' // new pvid stored + }); + }); + }); + describe('with googletag enabled', function () { + let gptSlot0, gptSlot1, gptEvent0, gptEvent1; + beforeEach(function () { + mockGpt.enable(); + gptSlot0 = mockGpt.makeSlot({code: '/19968336/header-bid-tag-0'}); + gptSlot1 = mockGpt.makeSlot({code: '/19968336/header-bid-tag1'}); + gptEvent0 = { + eventName: 'slotRenderEnded', + params: { + slot: gptSlot0, + isEmpty: false, + advertiserId: 1111, + creativeId: 2222, + lineItemId: 3333 + } + }; + gptEvent1 = { + eventName: 'slotRenderEnded', + params: { + slot: gptSlot1, + isEmpty: false, + advertiserId: 4444, + creativeId: 5555, + lineItemId: 6666 + } + }; + }); + + afterEach(function () { + mockGpt.disable(); + }); + + it('should add necessary gam information if gpt is enabled and slotRender event emmited', function () { + performStandardAuction([gptEvent0, gptEvent1]); + 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 handle empty gam renders', function () { + performStandardAuction([gptEvent0, { + eventName: 'slotRenderEnded', + params: { + slot: gptSlot1, + isEmpty: true + } + }]); + 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 = { + isSlotEmpty: true, + adSlot: '/19968336/header-bid-tag1' + }; + expect(message).to.deep.equal(expectedMessage); + }); + + it('should still add gam ids if falsy', function () { + performStandardAuction([gptEvent0, { + eventName: 'slotRenderEnded', + params: { + slot: gptSlot1, + isEmpty: false, + advertiserId: 0, + creativeId: 0, + lineItemId: 0 + } + }]); + 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: 0, + creativeId: 0, + lineItemId: 0, + adSlot: '/19968336/header-bid-tag1' + }; + expect(message).to.deep.equal(expectedMessage); + }); + + it('should handle empty gam renders', function () { + performStandardAuction([gptEvent0, gptEvent1]); + 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 correctly overwrite bidId if seatBidId is on the bidResponse', function () { // Only want one bid request in our mock auction let bidRequested = utils.deepClone(MOCK.BID_REQUESTED); diff --git a/test/spec/modules/rubiconAnalyticsSchema.json b/test/spec/modules/rubiconAnalyticsSchema.json index 32659b27d39..407dfd18be3 100644 --- a/test/spec/modules/rubiconAnalyticsSchema.json +++ b/test/spec/modules/rubiconAnalyticsSchema.json @@ -34,6 +34,53 @@ "type": "string", "description": "Version of Prebid.js responsible for the auctions contained within." }, + "fpkvs": { + "type": "array", + "description": "List of any dynamic key value pairs set by publisher.", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "session": { + "type": "object", + "description": "The session information for a given event", + "required": [ + "id", + "start", + "expires" + ], + "properties": { + "id": { + "type": "string", + "description": "UUID of session." + }, + "start": { + "type": "integer", + "description": "Unix timestamp of time of creation for this session in milliseconds." + }, + "expires": { + "type": "integer", + "description": "Unix timestamp of the maximum allowed time in milliseconds of the session." + }, + "pvid": { + "type": "string", + "description": "id to track page view." + } + } + }, "auctions": { "type": "array", "minItems": 1, @@ -125,6 +172,9 @@ "zoneId": { "type": "number", "description": "The Rubicon zoneId associated with this adUnit - Removed if null" + }, + "gam": { + "$ref": "#/definitions/gam" } } } @@ -197,6 +247,31 @@ } }, "definitions": { + "gam": { + "type": "object", + "description": "The gam information for a given ad unit", + "required": [ + "adSlot" + ], + "properties": { + "adSlot": { + "type": "string" + }, + "advertiserId": { + "type": "integer" + }, + "creativeId": { + "type": "integer" + }, + "LineItemId": { + "type": "integer" + }, + "isSlotEmpty": { + "type": "boolean", + "enum": [true] + } + } + }, "adserverTargeting": { "type": "object", "description": "The adserverTargeting key/value pairs", From 57195c2246d97cd3da5aacfd7671b531e65bb6b5 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 3 Sep 2020 15:17:45 -0700 Subject: [PATCH 06/12] add functionality to mock gpt --- test/spec/integration/faker/googletag.js | 64 ++++++++++++++++++------ 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/test/spec/integration/faker/googletag.js b/test/spec/integration/faker/googletag.js index a0ce04402f7..9d91bf315d9 100644 --- a/test/spec/integration/faker/googletag.js +++ b/test/spec/integration/faker/googletag.js @@ -43,18 +43,52 @@ export function makeSlot() { return slot; } -window.googletag = { - _slots: [], - pubads: function () { - var self = this; - return { - getSlots: function () { - return self._slots; - }, - - setSlots: function (slots) { - self._slots = slots; - } - }; - } -}; +export function emitEvent(eventName, params) { + (window.googletag._callbackMap[eventName] || []).forEach(eventCb => eventCb({...params, eventName})); +} + +export function enable() { + window.googletag = { + _slots: [], + _callbackMap: {}, + pubads: function () { + var self = this; + return { + getSlots: function () { + return self._slots; + }, + + setSlots: function (slots) { + self._slots = slots; + }, + + setTargeting: function(key, arrayOfValues) { + self._targeting[key] = Array.isArray(arrayOfValues) ? arrayOfValues : [arrayOfValues]; + }, + + getTargeting: function(key) { + return self._targeting[key] || []; + }, + + getTargetingKeys: function() { + return Object.getOwnPropertyNames(self._targeting); + }, + + clearTargeting: function() { + self._targeting = {}; + }, + + addEventListener: function (eventName, cb) { + self._callbackMap[eventName] = self._callbackMap[eventName] || []; + self._callbackMap[eventName].push(cb); + } + }; + } + }; +} + +export function disable() { + window.googletag = undefined; +} + +enable(); From ab19c72751122c0122c404d37dbaaaccd126a5f1 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 3 Sep 2020 15:17:54 -0700 Subject: [PATCH 07/12] revert analyticsAdapter update --- src/AnalyticsAdapter.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/AnalyticsAdapter.js b/src/AnalyticsAdapter.js index c6355218de8..f3297412a35 100644 --- a/src/AnalyticsAdapter.js +++ b/src/AnalyticsAdapter.js @@ -18,8 +18,7 @@ const { BIDDER_DONE, SET_TARGETING, AD_RENDER_FAILED, - ADD_AD_UNITS, - TCF2_ENFORCEMENT + ADD_AD_UNITS } } = CONSTANTS; @@ -102,7 +101,6 @@ export default function AnalyticsAdapter({ url, analyticsType, global, handler } // Next register event listeners to send data immediately _handlers = { - [TCF2_ENFORCEMENT]: args => this.enqueue({ eventType: TCF2_ENFORCEMENT, args }), [REQUEST_BIDS]: args => this.enqueue({ eventType: REQUEST_BIDS, args }), [BID_REQUESTED]: args => this.enqueue({ eventType: BID_REQUESTED, args }), [BID_RESPONSE]: args => this.enqueue({ eventType: BID_RESPONSE, args }), From dbe91cece9bec4c7decdb9cd691f61ddef7d0ef8 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 3 Sep 2020 15:27:53 -0700 Subject: [PATCH 08/12] minor cleanup --- karma.conf.maker.js | 2 ++ modules/rubiconAnalyticsAdapter.js | 2 +- pbaModules.json | 11 ----------- 3 files changed, 3 insertions(+), 12 deletions(-) delete mode 100644 pbaModules.json diff --git a/karma.conf.maker.js b/karma.conf.maker.js index 683d61f6775..8af216d6262 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -170,6 +170,7 @@ module.exports = function(codeCoverage, browserstack, watchMode, file) { plugins: plugins } + // To ensure that, we are able to run single spec file // here we are adding preprocessors, when file is passed if (file) { @@ -178,6 +179,7 @@ module.exports = function(codeCoverage, browserstack, watchMode, file) { }); delete config.preprocessors['test/test_index.js']; } + setReporters(config, codeCoverage, browserstack); setBrowsers(config, browserstack); return config; diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 570b6087bcb..2b9c185ed83 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -23,7 +23,7 @@ const { BIDDER_DONE, BID_TIMEOUT, BID_WON, - SET_TARGETING, + SET_TARGETING }, STATUS: { GOOD, diff --git a/pbaModules.json b/pbaModules.json deleted file mode 100644 index a03c82621ef..00000000000 --- a/pbaModules.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - "rubiconBidAdapter", - "appnexusBidAdapter", - "rubiconAnalyticsAdapter", - "prebidServerBidAdapter", - "currency", - "priceFloors", - "consentManagement", - "gdprEnforcement", - "gptPreAuction" -] \ No newline at end of file From aa0197f046c7ae4a2dc5ac823fde7160d00fc7de Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Tue, 8 Sep 2020 18:32:53 -0700 Subject: [PATCH 09/12] handle edge cases more gracefully --- modules/rubiconAnalyticsAdapter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 2b9c185ed83..426482f615b 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -243,7 +243,7 @@ function sendMessage(auctionId, bidWonId) { 'start', 'expires' ]); - if (auctionCache.session.fpkvs && Object.keys(auctionCache.session.fpkvs).length) { + if (!utils.isEmpty(auctionCache.session.fpkvs)) { message.fpkvs = Object.keys(auctionCache.session.fpkvs).map(key => { return { key, value: auctionCache.session.fpkvs[key] }; }); @@ -372,7 +372,7 @@ function getRpaCookie() { try { return JSON.parse(window.atob(encodedCookie)); } catch (e) { - utils.logError('Rubicon Analytics: Unable to decode: ', e); + utils.logError(`Rubicon Analytics: Unable to decode ${COOKIE_NAME} value: `, e); } } return {}; @@ -382,7 +382,7 @@ function setRpaCookie(decodedCookie) { try { storage.setCookie(COOKIE_NAME, window.btoa(JSON.stringify(decodedCookie))); } catch (e) { - utils.logError('Rubicon Analytics: Unable to encode: ', e); + utils.logError(`Rubicon Analytics: Unable to encode ${COOKIE_NAME} value: `, e); } } @@ -498,7 +498,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.floorData = {...floorData}; } cacheEntry.gdprConsent = utils.deepAccess(args, 'bidderRequests.0.gdprConsent'); - cacheEntry.session = updateRpaCookie(); + cacheEntry.session = storage.cookiesAreEnabled() && updateRpaCookie(); cache.auctions[args.auctionId] = cacheEntry; // register to listen to gpt events if not done yet if (!cache.gpt.registered && utils.isGptPubadsDefined()) { From 3be598754940d194ac518c6243b47e24c4125e6b Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Wed, 9 Sep 2020 12:40:09 -0700 Subject: [PATCH 10/12] logic for when cookies not enabled new param `channel` tests --- modules/rubiconAnalyticsAdapter.js | 3 +- .../modules/rubiconAnalyticsAdapter_spec.js | 39 +++++++++++++++---- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 426482f615b..8ea5a14275e 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -142,7 +142,8 @@ function sendMessage(auctionId, bidWonId) { ruleId: config.getConfig('rubicon.rule_name'), version: '$prebid.version$', referrerUri: referrer, - referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer) + referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), + channel: 'web' }; const wrapperName = config.getConfig('rubicon.wrapperName'); if (wrapperName) { diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 391d96d7948..51a26b6a835 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -278,6 +278,7 @@ const MOCK = { const STUBBED_UUID = '12345678-1234-1234-1234-123456789abc'; const ANALYTICS_MESSAGE = { + 'channel': 'web', 'eventTimeMillis': 1519767013781, 'integration': 'pbjs', 'version': '$prebid.version$', @@ -496,11 +497,16 @@ function performStandardAuction(gptEvents) { describe('rubicon analytics adapter', function () { let sandbox; let clock; - + let getCookieStub, setCookieStub, cookiesAreEnabledStub; beforeEach(function () { + getCookieStub = sinon.stub(storage, 'getCookie'); + setCookieStub = sinon.stub(storage, 'setCookie'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); mockGpt.disable(); sandbox = sinon.sandbox.create(); + cookiesAreEnabledStub.returns(true); + sandbox.stub(events, 'getEvents').returns([]); sandbox.stub(utils, 'generateUUID').returns(STUBBED_UUID); @@ -524,6 +530,9 @@ describe('rubicon analytics adapter', function () { sandbox.restore(); config.resetConfig(); mockGpt.enable(); + getCookieStub.restore(); + setCookieStub.restore(); + cookiesAreEnabledStub.restore(); }); it('should require accountId', function () { @@ -821,11 +830,8 @@ describe('rubicon analytics adapter', function () { }); describe('with session handling', function () { - let getCookieStub, setCookieStub, pvid, kvps; + let pvid, kvps; beforeEach(function () { - getCookieStub = sinon.stub(storage, 'getCookie'); - setCookieStub = sinon.stub(storage, 'setCookie'); - // custom dm stuff prebidGlobal.rp = { getCustomTargeting: () => kvps, @@ -834,12 +840,29 @@ describe('rubicon analytics adapter', function () { }); afterEach(function () { - getCookieStub.restore(); - setCookieStub.restore(); - prebidGlobal.rp = pvid = kvps = undefined; }); + it('shouldnot log any session data if cookies are not enabled', function () { + cookiesAreEnabledStub.returns(false); + + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + delete expectedMessage.session; + delete expectedMessage.fpkvs; + + performStandardAuction(); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + + expect(request.url).to.equal('//localhost:9999/event'); + + let message = JSON.parse(request.requestBody); + validate(message); + + expect(message).to.deep.equal(expectedMessage); + }); + it('should should pass along custom rubicon kv and pvid when defined', function () { pvid = '1a2b3c'; kvps = { From a0996e569c09dfc3c3ce871b426fd46e033e5fbb Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Wed, 9 Sep 2020 13:16:58 -0700 Subject: [PATCH 11/12] use timeToRespond if available on bidResponse --- modules/rubiconAnalyticsAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 8ea5a14275e..240f3387f80 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -617,7 +617,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { code: 'request-error' }; } - bid.clientLatencyMillis = Date.now() - cache.auctions[args.auctionId].timestamp; + bid.clientLatencyMillis = bid.timeToRespond || Date.now() - cache.auctions[args.auctionId].timestamp; bid.bidResponse = parseBidResponse(args, bid.bidResponse); break; case BIDDER_DONE: From 524a2aa72240e5a207c39129a3d2feda8e9425d2 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Tue, 15 Sep 2020 10:32:34 -0700 Subject: [PATCH 12/12] Updating with review comments --- modules/rubiconAnalyticsAdapter.js | 12 +++-- .../modules/rubiconAnalyticsAdapter_spec.js | 52 +++++++++---------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 240f3387f80..54aa108ed78 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -49,6 +49,8 @@ const cache = { gpt: {}, }; +const BID_REJECTED_IPF = 'rejected-ipf'; + export function getHostNameFromReferer(referer) { try { rubiconAdapter.referrerHostname = utils.parseUrl(referer, {noDecodeWholeURL: true}).hostname; @@ -368,7 +370,7 @@ function setRubiconAliases(aliasRegistry) { } function getRpaCookie() { - let encodedCookie = storage.getCookie(COOKIE_NAME); + let encodedCookie = storage.getDataFromLocalStorage(COOKIE_NAME); if (encodedCookie) { try { return JSON.parse(window.atob(encodedCookie)); @@ -381,7 +383,7 @@ function getRpaCookie() { function setRpaCookie(decodedCookie) { try { - storage.setCookie(COOKIE_NAME, window.btoa(JSON.stringify(decodedCookie))); + storage.setDataInLocalStorage(COOKIE_NAME, window.btoa(JSON.stringify(decodedCookie))); } catch (e) { utils.logError(`Rubicon Analytics: Unable to encode ${COOKIE_NAME} value: `, e); } @@ -480,6 +482,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { disableAnalytics() { this.getUrl = baseAdapter.getUrl; accountId = null; + cache.gpt.registered = false; baseAdapter.disableAnalytics.apply(this, arguments); }, track({eventType, args}) { @@ -499,11 +502,12 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.floorData = {...floorData}; } cacheEntry.gdprConsent = utils.deepAccess(args, 'bidderRequests.0.gdprConsent'); - cacheEntry.session = storage.cookiesAreEnabled() && updateRpaCookie(); + cacheEntry.session = storage.localStorageIsEnabled() && updateRpaCookie(); cache.auctions[args.auctionId] = cacheEntry; // register to listen to gpt events if not done yet if (!cache.gpt.registered && utils.isGptPubadsDefined()) { subscribeToGamSlots(); + cache.gpt.registered = true; } break; case BID_REQUESTED: @@ -608,7 +612,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { delete bid.error; // it's possible for this to be set by a previous timeout break; case NO_BID: - bid.status = args.status === BID_REJECTED ? 'rejected-ipf' : 'no-bid'; + bid.status = args.status === BID_REJECTED ? BID_REJECTED_IPF : 'no-bid'; delete bid.error; break; default: diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 51a26b6a835..a2999dfed8c 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -497,15 +497,15 @@ function performStandardAuction(gptEvents) { describe('rubicon analytics adapter', function () { let sandbox; let clock; - let getCookieStub, setCookieStub, cookiesAreEnabledStub; + let getDataFromLocalStorageStub, setDataInLocalStorageStub, localStorageIsEnabledStub; beforeEach(function () { - getCookieStub = sinon.stub(storage, 'getCookie'); - setCookieStub = sinon.stub(storage, 'setCookie'); - cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + setDataInLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); mockGpt.disable(); sandbox = sinon.sandbox.create(); - cookiesAreEnabledStub.returns(true); + localStorageIsEnabledStub.returns(true); sandbox.stub(events, 'getEvents').returns([]); @@ -530,9 +530,9 @@ describe('rubicon analytics adapter', function () { sandbox.restore(); config.resetConfig(); mockGpt.enable(); - getCookieStub.restore(); - setCookieStub.restore(); - cookiesAreEnabledStub.restore(); + getDataFromLocalStorageStub.restore(); + setDataInLocalStorageStub.restore(); + localStorageIsEnabledStub.restore(); }); it('should require accountId', function () { @@ -843,8 +843,8 @@ describe('rubicon analytics adapter', function () { prebidGlobal.rp = pvid = kvps = undefined; }); - it('shouldnot log any session data if cookies are not enabled', function () { - cookiesAreEnabledStub.returns(false); + it('should not log any session data if local storage is not enabled', function () { + localStorageIsEnabledStub.returns(false); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); delete expectedMessage.session; @@ -885,20 +885,20 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); - it('should pick up existing cookie and use its values', function () { - // set some cookie - let inputCookie = { + it('should pick up existing localStorage and use its values', function () { + // set some localStorage + let inputlocalStorage = { id: '987654', start: 1519766113781, // 15 mins before "now" expires: 1519787713781, // six hours later lastSeen: 1519766113781, fpkvs: { source: 'tw' } }; - getCookieStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputCookie))); + getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); pvid = '1a2b3c'; kvps = { - link: 'email' // should merge this with what is in the cookie! + link: 'email' // should merge this with what is in the localStorage! }; performStandardAuction(); @@ -922,7 +922,7 @@ describe('rubicon analytics adapter', function () { let calledWith; try { - calledWith = JSON.parse(atob(setCookieStub.getCall(0).args[1])); + calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); } catch (e) { calledWith = {}; } @@ -938,19 +938,19 @@ describe('rubicon analytics adapter', function () { }); it('should throw out session if lastSeen > 30 mins ago and create new one', function () { - // set some cookie - let inputCookie = { + // set some localStorage + let inputlocalStorage = { id: '987654', start: 1519764313781, // 45 mins before "now" expires: 1519785913781, // six hours later lastSeen: 1519764313781, // 45 mins before "now" fpkvs: { source: 'tw' } }; - getCookieStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputCookie))); + getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); pvid = '1a2b3c'; kvps = { - link: 'email' // should merge this with what is in the cookie! + link: 'email' // should merge this with what is in the localStorage! }; performStandardAuction(); @@ -971,7 +971,7 @@ describe('rubicon analytics adapter', function () { let calledWith; try { - calledWith = JSON.parse(atob(setCookieStub.getCall(0).args[1])); + calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); } catch (e) { calledWith = {}; } @@ -987,19 +987,19 @@ describe('rubicon analytics adapter', function () { }); it('should throw out session if past expires time and create new one', function () { - // set some cookie - let inputCookie = { + // set some localStorage + let inputlocalStorage = { id: '987654', start: 1519745353781, // 6 hours before "expires" expires: 1519766953781, // little more than six hours ago lastSeen: 1519767008781, // 5 seconds ago fpkvs: { source: 'tw' } }; - getCookieStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputCookie))); + getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); pvid = '1a2b3c'; kvps = { - link: 'email' // should merge this with what is in the cookie! + link: 'email' // should merge this with what is in the localStorage! }; performStandardAuction(); @@ -1020,7 +1020,7 @@ describe('rubicon analytics adapter', function () { let calledWith; try { - calledWith = JSON.parse(atob(setCookieStub.getCall(0).args[1])); + calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); } catch (e) { calledWith = {}; }