From 06ea32d72bdada34e23e0c0b27bbdc0ba29b575b Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Mon, 11 Jul 2022 12:22:00 -0700 Subject: [PATCH 01/22] stash --- modules/magniteAnalyticsAdapter.js | 741 +++++++++++++++++++++++++++++ modules/rubiconAnalyticsAdapter.js | 4 +- 2 files changed, 743 insertions(+), 2 deletions(-) create mode 100644 modules/magniteAnalyticsAdapter.js diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js new file mode 100644 index 00000000000..3d6068355a4 --- /dev/null +++ b/modules/magniteAnalyticsAdapter.js @@ -0,0 +1,741 @@ +import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, isGptPubadsDefined, _each, deepSetValue, deepClone, logInfo } from '../src/utils.js'; +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; +import { ajax } from '../src/ajax.js'; +import { config } from '../src/config.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const RUBICON_GVL_ID = 52; +export const storage = getStorageManager({ gvlid: RUBICON_GVL_ID, moduleName: 'rubicon' }); +const COOKIE_NAME = 'rpaSession'; +const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins +const END_EXPIRE_TIME = 21600000; // 6 hours +const MODULE_NAME = 'Rubicon Analytics'; + +// List of known rubicon aliases +// This gets updated on auction init to account for any custom aliases present +let rubiconAliases = ['rubicon']; + +/* + cache used to keep track of data throughout page load + auction: ${auctionId}.adUnits.${transactionId}.bids.${bidId} +*/ + +// const auctions = { +// 'b4904c63-7b26-47d6-92d9-a8e232daaf65': { +// ...auctionData, +// adUnits: { +// 'div-gpt-box': { +// ...adUnitData, +// bids: { +// '2b2f2796098d18': { ...bidData }, +// '3e0c2e1d037ce1': { ...bidData } +// } +// } +// } +// } +// } +const cache = { + auctions: new Map(), + billing: {}, + timeouts: {} +} + +const pbsErrorMap = { + 1: 'timeout-error', + 2: 'input-error', + 3: 'connect-error', + 4: 'request-error', + 999: 'generic-error' +} + +let prebidGlobal = getGlobal(); +const { + EVENTS: { + AUCTION_INIT, + AUCTION_END, + BID_REQUESTED, + BID_RESPONSE, + BIDDER_DONE, + BID_TIMEOUT, + BID_WON, + SET_TARGETING, + BILLABLE_EVENT + }, + STATUS: { + GOOD, + NO_BID + }, + BID_STATUS: { + BID_REJECTED + } +} = CONSTANTS; + +// The saved state of rubicon specific setConfig controls +export let rubiConf = { + pvid: generateUUID().slice(0, 8), + analyticsEventDelay: 0, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + } +}; +config.getConfig('rubicon', config => { + mergeDeep(rubiConf, config.rubicon); + if (deepAccess(config, 'rubicon.updatePageView') === true) { + rubiConf.pvid = generateUUID().slice(0, 8) + } +}); + +// pbs confs +let serverConfig; +config.getConfig('s2sConfig', ({ s2sConfig }) => { + serverConfig = s2sConfig; +}); + +export const SEND_TIMEOUT = 5000; +const DEFAULT_INTEGRATION = 'pbjs'; + +let baseAdapter = adapter({ analyticsType: 'endpoint' }); +let rubiconAdapter = Object.assign({}, baseAdapter, { + MODULE_INITIALIZED_TIME: Date.now(), + referrerHostname: '', + enableAnalytics, + disableAnalytics, + track({ eventType, args }) { + switch (eventType) { + case AUCTION_INIT: + // set the rubicon aliases + setRubiconAliases(adapterManager.aliasRegistry); + + // latest page referer + pageReferer = deepAccess(args, 'bidderRequests.0.refererInfo.referer'); + + // set auction level data + let auctionData = pick(args, [ + 'auctionId', + 'timestamp as auctionStart', + 'timeout as clientTimeoutMillis', + ]); + auctionData.accountId = accountId; + + // Order bidders were called + auctionData.bidderOrder = args.bidderRequests.map(bidderRequest => bidderRequest.bidderCode); + + // Price Floors information + const floorData = deepAccess(args, 'bidderRequests.0.bids.0.floorData'); + if (floorData) { + auctionData.floors = addFloorData(floorData); + } + + // GDPR info + const gdprData = deepAccess(args, 'bidderRequests.0.gdprConsent'); + if (gdprData) { + auctionData.gdpr = pick(gdprData, [ + 'gdprApplies as applies', + 'consentString', + 'apiVersion as version' + ]); + } + + // User ID Data included in auction + const userIds = Object.keys(deepAccess(args, 'bidderRequests.0.bids.0.userId', {})).map(id => { + return { provider: id, hasId: true } + }); + if (userIds.length) { + auctionData.user = { ids: userIds }; + } + + auctionData.serverTimeoutMillis = serverConfig.timeout; + + // adunits saved as map of transactionIds + auctionData.adUnits = args.adUnits.reduce((adMap, adUnit) => { + let ad = pick(adUnit, [ + 'code as adUnitCode', + 'transactionId', + 'mediaTypes', mediaTypes => Object.keys(mediaTypes), + 'sizes as dimensions', sizes => sizes.map(sizeToDimensions), + ]); + ad.pbAdSlot = deepAccess(adUnit, 'ortb2Imp.ext.data.pbadslot'); + ad.pattern = deepAccess(adUnit, 'ortb2Imp.ext.data.aupname'); + ad.gpid = deepAccess(adUnit, 'ortb2Imp.ext.gpid'); + if (deepAccess(bid, 'ortb2Imp.ext.data.adserver.name') === 'gam') { + ad.gam = { adSlot: bid.ortb2Imp.ext.data.adserver.adslot } + } + ad.bids = {}; + ad.status = 'no-bid'; + adMap[adUnit.code] = ad; + return adMap; + }, new Map()); + + cache.auctions[args.auctionId] = auctionData; + break; + case BID_REQUESTED: + args.bids.forEach(bid => { + const adUnit = deepAccess(cache, `auctions.${args.auctionId}.adUnits.${bid.transactionId}`); + adUnit.bids[bid.bidId] = pick(bid, [ + 'bidder', + 'bidId', + 'src as source', + 'status', () => 'no-bid' + ]); + // set acct site zone id on adunit + if ((!adUnit.siteId || !adUnit.zoneId) && rubiconAliases.indexOf(bid.bidder) !== -1) { + if (deepAccess(bid, 'params.accountId') == accountId) { + adUnit.accountId = parseInt(accountId); + adUnit.siteId = parseInt(deepAccess(bid, 'params.siteId')); + adUnit.zoneId = parseInt(deepAccess(bid, 'params.zoneId')); + } + } + }); + break; + case BID_RESPONSE: + let bid = deepAccess(cache, `auctions.${args.auctionId}.adUnits.${args.transactionId}.bids.${args.requestId}`); + + const auctionEntry = deepAccess(cache, `auctions.${args.auctionId}`); + const adUnit = deepAccess(auctionEntry, `adUnits.${args.transactionId}`); + let bid = adUnit.bids[args.requestId]; + + // if this came from multibid, there might now be matching bid, so check + // THIS logic will change when we support multibid per bid request + if (!bid && args.originalRequestId) { + let ogBid = adUnit.bids[args.originalRequestId]; + // create new bid + adUnit.bids[args.requestId] = { + ...ogBid, + bidId: args.requestId, + bidderDetail: args.targetingBidder + }; + bid = adUnit.bids[args.requestId]; + } + + // if we have not set enforcements yet set it (This is hidden from bidders until now so we have to get from here) + if (typeof deepAccess(auctionEntry, 'floors.enforcement') !== 'boolean' && deepAccess(args, 'floorData.enforcements')) { + auctionEntry.floors.enforcement = args.floorData.enforcements.enforceJS; + auctionEntry.floors.dealsEnforced = args.floorData.enforcements.floorDeals; + } + + // Log error if no matching bid! + if (!bid) { + logError(`${MODULE_NAME}: Could not find associated bid request for bid response with requestId: `, args.requestId); + break; + } + + // set bid status + switch (args.getStatusCode()) { + case GOOD: + bid.status = 'success'; + 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 ? BID_REJECTED_IPF : 'no-bid'; + delete bid.error; + break; + default: + bid.status = 'error'; + bid.error = { + code: 'request-error' + }; + } + bid.clientLatencyMillis = bid.timeToRespond || Date.now() - cache.auctions[args.auctionId].auctionStart; + bid.bidResponse = parseBidResponse(args, bid.bidResponse); + break; + case BIDDER_DONE: + const serverError = deepAccess(args, 'serverErrors.0'); + const serverResponseTimeMs = args.serverResponseTimeMs; + args.bids.forEach(bid => { + let cachedBid = deepAccess(cache, `auctions.${args.auctionId}.adUnits.${args.transactionId}.bids.${bid.bidId}`); + if (typeof bid.serverResponseTimeMs !== 'undefined') { + cachedBid.serverLatencyMillis = bid.serverResponseTimeMs; + } else if (serverResponseTimeMs && bid.source === 's2s') { + cachedBid.serverLatencyMillis = serverResponseTimeMs; + } + // if PBS said we had an error, and this bid has not been processed by BID_RESPONSE YET + if (serverError && (!cachedBid.status || ['no-bid', 'error'].indexOf(cachedBid.status) !== -1)) { + cachedBid.status = 'error'; + cachedBid.error = { + code: pbsErrorMap[serverError.code] || pbsErrorMap[999], + description: serverError.message + } + } + }); + + break; + case SET_TARGETING: + // Perhaps we want to do stuff here + break; + case BID_WON: + let bid = deepAccess(cache, `auctions.${args.auctionId}.adUnits.${args.transactionId}.bids.${args.requestId}`); + + // IF caching enabled, find latest auction that matches and has GAM ID's else use its own + let renderingAuctionId; + if (config.getConfig('useBidCache')) { + // reverse iterate through the auction map + // break once found + } + renderingAuctionId = renderingAuctionId || args.auctionId; + // TODO: FIX => formatBidWon, source + render auction ID's + // transactionID ? + payload.bidsWon = [formatBidWon(args, renderingAuctionId)]; + sendOrAddEventToQueue(payload); + break; + case AUCTION_END: + let auctionCache = cache.auctions[args.auctionId]; + // if for some reason the auction did not do its normal thing, this could be undefied so bail + if (!auctionCache) { + break; + } + // If we are not waiting for gam or bidwons, fire it + const payload = getTopLevelDetails(); + payload.auctions = [formatAuction(auctionCache)]; + if (analyticsEventDelay === 0) { + sendEvent(payload); + } else { + // start timer to send batched payload + cache.timeouts[args.auctionId] = setTimeout(() => { + sendEvent(payload); + }, rubiConf.analyticsBatchTimeout || SEND_TIMEOUT); + } + break; + case BID_TIMEOUT: + args.forEach(badBid => { + let bid = deepAccess(cache, `auctions.${badBid.auctionId}.adUnits.${badBid.transactionId}.bids.${badBid.bidId}`, {}); + // might be set already by bidder-done, so do not overwrite + if (bid.status !== 'error') { + bid.status = 'error'; + bid.error = { + code: 'timeout-error', + description: 'prebid.js timeout' // will help us diff if timeout was set by PBS or PBJS + }; + } + }); + break; + case BILLABLE_EVENT: + if (rubiConf.dmBilling.enabled && isBillingEventValid(args)) { + // add to the map indicating it has not been sent yet + deepSetValue(cache.billing, `${args.vendor}.${args.billingId}`, false); + sendOrAddEventToQueue(args); + } else { + logInfo(`${MODULE_NAME}: Billing event ignored`, args); + } + break; + } + } +}); + +const sendEvent = payload => { + // If this is auction event check if billing is there + // if we have not sent any billingEvents send them + const pendingBillingEvents = getPendingBillingEvents(payload); + if (pendingBillingEvents && pendingBillingEvents.length) { + payload.billableEvents = pendingBillingEvents; + } + + ajax( + rubiConf.analyticsEndpoint || endpoint, + null, + JSON.stringify(payload), + { + contentType: 'application/json' + } + ); +} + +function getPendingBillingEvents(payload) { + const billing = deepAccess(payload, 'auctions.0.billing'); + if (billing && billing.length) { + return billing.reduce((accum, billingEvent) => { + if (deepAccess(cache.billing, `${billingEvent.vendor}.${billingEvent.billingId}`) === false) { + accum.push(getBillingPayload(billingEvent)); + } + return accum; + }, []); + } +} + +const formatAuction = auction => { + auction.adUnits = Object.entries(auction.adUnits).map(([tid, adUnit]) => { + adUnit.bids = Object.entries(adUnit.bids).map(([bidId, bid]) => { + return bid; + }); + return adUnit; + }); + return auctionCache; +} + +const formatBidWon = (args, renderingAuctionId) => { + let bid = deepAccess(cache, `auctions.${args.auctionId}.adUnits.${args.transactionId}.bids.${args.requestId}`); + return { + bidder: bid.bidder, + bidderDetail: bid.bidderDetail, + sourceAuctionId: args.auctionId, + renderingAuctionId, + transactionId: args.transactionId, + bidId: args.requestId, + accountId, + siteId: adUnit.siteId, + zoneId: adUnit.zoneId, + } +} + +const isBillingEventValid = event => { + // vendor is whitelisted + const isWhitelistedVendor = rubiConf.dmBilling.vendors.includes(event.vendor); + // event is not duplicated + const isNotDuplicate = typeof deepAccess(cache.billing, `${event.vendor}.${event.billingId}`) !== 'boolean'; + // billingId is defined and a string + return typeof event.billingId === 'string' && isWhitelistedVendor && isNotDuplicate; +} + +const sendOrAddEventToQueue = event => { + // if any auction is not sent yet, then add it to the auction queue + const pendingAuction = Object.keys(cache.auctions).find(auctionId => !cache.auctions[auctionId].sent); + + if (rubiConf.dmBilling.waitForAuction && pendingAuction) { + cache.auctions[pendingAuction].billing = cache.auctions[pendingAuction].billing || []; + cache.auctions[pendingAuction].billing.push(event); + } else { + // send it + const payload = getTopLevelDetails(); + payload.billableEvents = [getBillingPayload(event)]; + sendEvent(payload); + } +} + +function getBillingPayload(event) { + // for now we are mapping all events to type "general", later we will expand support for specific types + let billingEvent = deepClone(event); + billingEvent.type = 'general'; + billingEvent.accountId = accountId; + // mark as sent + deepSetValue(cache.billing, `${event.vendor}.${event.billingId}`, true); + return billingEvent; +} + +const getBidPrice = bid => { + // get the cpm from bidResponse + let cpm; + let currency; + if (bid.status === BID_REJECTED && deepAccess(bid, 'floorData.cpmAfterAdjustments')) { + // if bid was rejected and bid.floorData.cpmAfterAdjustments use it + cpm = bid.floorData.cpmAfterAdjustments; + currency = bid.floorData.floorCurrency; + } else if (typeof bid.currency === 'string' && bid.currency.toUpperCase() === 'USD') { + // bid is in USD use it + return Number(bid.cpm); + } else { + // else grab cpm + cpm = bid.cpm; + currency = bid.currency; + } + // if after this it is still going and is USD then return it. + if (currency === 'USD') { + return Number(cpm); + } + // otherwise we convert and return + try { + return Number(prebidGlobal.convertCurrency(cpm, currency, 'USD')); + } catch (err) { + logWarn(`${MODULE_NAME}: Could not determine the bidPriceUSD of the bid `, bid); + } +} + +const parseBidResponse = (bid, previousBidResponse) => { + // The current bidResponse for this matching requestId/bidRequestId + let responsePrice = getBidPrice(bid) + // we need to compare it with the previous one (if there was one) log highest only + // THIS WILL CHANGE WITH ALLOWING MULTIBID BETTER + if (previousBidResponse && previousBidResponse.bidPriceUSD > responsePrice) { + return previousBidResponse; + } + return pick(bid, [ + 'bidPriceUSD', () => responsePrice, + 'dealId', dealId => dealId || undefined, + 'mediaType', + 'dimensions', () => { + const width = bid.width || bid.playerWidth; + const height = bid.height || bid.playerHeight; + return (width && height) ? { width, height } : undefined; + }, + // Handling use case where pbs sends back 0 or '0' bidIds (these get moved up to bid not bidResponse later) + 'pbsBidId', pbsBidId => pbsBidId == 0 ? generateUUID() : pbsBidId, + 'seatBidId', seatBidId => seatBidId == 0 ? generateUUID() : seatBidId, + 'floorValue', () => deepAccess(bid, 'floorData.floorValue'), + 'floorRuleValue', () => deepAccess(bid, 'floorData.floorRuleValue'), + 'floorRule', () => debugTurnedOn() ? deepAccess(bid, 'floorData.floorRule') : undefined, + 'adomains', () => { + const adomains = deepAccess(bid, 'meta.advertiserDomains'); + const validAdomains = Array.isArray(adomains) && adomains.filter(domain => typeof domain === 'string'); + return validAdomains && validAdomains.length > 0 ? validAdomains.slice(0, 10) : undefined + } + ]); +} + +const addFloorData = floorData => { + if (floorData.location === 'noData') { + auction.floors = pick(floorData, [ + 'location', + 'fetchStatus', + 'floorProvider as provider' + ]); + } else { + auction.floors = pick(floorData, [ + 'location', + 'modelVersion as modelName', + 'modelWeight', + 'modelTimestamp', + 'skipped', + 'enforcement', () => deepAccess(floorData, 'enforcements.enforceJS'), + 'dealsEnforced', () => deepAccess(loorData, 'enforcements.floorDeals'), + 'skipRate', + 'fetchStatus', + 'floorMin', + 'floorProvider as provider' + ]); + } +} + +let pageReferer; + +const getTopLevelDetails = () => { + let cacheEntry = { + channel: 'web', + integration: rubiConf.int_type || DEFAULT_INTEGRATION, + referrerUri: pageReferer, + version: '$prebid.version$', + referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), + } + + // Add DM wrapper details + if (rubiConf.wrapperName) { + cacheEntry.wrapper = { + name: rubiConf.wrapperName, + family: rubiConf.wrapperFamily, + rule: rubiConf.rule_name + } + } + + // Add session info + const sessionData = storage.localStorageIsEnabled() && updateRpaCookie(); + if (sessionData) { + // gather session info + cacheEntry.session = pick(sessionData, [ + 'id', + 'pvid', + 'start', + 'expires' + ]); + if (!isEmpty(sessionData.fpkvs)) { + message.fpkvs = Object.keys(sessionData.fpkvs).map(key => { + return { key, value: sessionData.fpkvs[key] }; + }); + } + } +} + +export function getHostNameFromReferer(referer) { + try { + rubiconAdapter.referrerHostname = parseUrl(referer, { noDecodeWholeURL: true }).hostname; + } catch (e) { + logError(`${MODULE_NAME}: Unable to parse hostname from supplied url: `, referer, e); + rubiconAdapter.referrerHostname = ''; + } + return rubiconAdapter.referrerHostname +}; + +const getRpaCookie = () => { + let encodedCookie = storage.getDataFromLocalStorage(COOKIE_NAME); + if (encodedCookie) { + try { + return JSON.parse(window.atob(encodedCookie)); + } catch (e) { + logError(`${MODULE_NAME}: Unable to decode ${COOKIE_NAME} value: `, e); + } + } + return {}; +} + +const setRpaCookie = (decodedCookie) => { + try { + storage.setDataInLocalStorage(COOKIE_NAME, window.btoa(JSON.stringify(decodedCookie))); + } catch (e) { + logError(`${MODULE_NAME}: Unable to encode ${COOKIE_NAME} value: `, e); + } +} + +const updateRpaCookie = () => { + const currentTime = Date.now(); + let decodedRpaCookie = getRpaCookie(); + if ( + !Object.keys(decodedRpaCookie).length || + (currentTime - decodedRpaCookie.lastSeen) > LAST_SEEN_EXPIRE_TIME || + decodedRpaCookie.expires < currentTime + ) { + decodedRpaCookie = { + id: 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, ...getFpkvs() }; + decodedRpaCookie.pvid = rubiConf.pvid; + setRpaCookie(decodedRpaCookie) + } + return decodedRpaCookie; +} + +/* + Filters and converts URL Params into an object and returns only KVs that match the 'utm_KEY' format +*/ +function getUtmParams() { + let search; + + try { + search = parseQS(getWindowLocation().search); + } catch (e) { + search = {}; + } + + return Object.keys(search).reduce((accum, param) => { + if (param.match(/utm_/)) { + accum[param.replace(/utm_/, '')] = search[param]; + } + return accum; + }, {}); +} + +function getFpkvs() { + rubiConf.fpkvs = Object.assign((rubiConf.fpkvs || {}), getUtmParams()); + + // convert all values to strings + Object.keys(rubiConf.fpkvs).forEach(key => { + rubiConf.fpkvs[key] = rubiConf.fpkvs[key] + ''; + }); + + return rubiConf.fpkvs; +} + +/* + Checks the alias registry for any entries of the rubicon bid adapter. + adds to the rubiconAliases list if found +*/ +const setRubiconAliases = (aliasRegistry) => { + Object.keys(aliasRegistry).forEach(function (alias) { + if (aliasRegistry[alias] === 'rubicon') { + rubiconAliases.push(alias); + } + }); +} + +function sizeToDimensions(size) { + return { + width: size.w || size[0], + height: size.h || size[1] + }; +} + +let accountId; +let endpoint; +const enableAnalytics = (config = {}) => { + let error = false; + // endpoint + endpoint = deepAccess(config, 'options.endpoint'); + if (!endpoint) { + logError(`${MODULE_NAME}: required endpoint missing`); + error = true; + } + // accountId + accountId = deepAccess(config, 'options.accountId'); + if (!accountId) { + logError(`${MODULE_NAME}: required accountId missing`); + error = true; + } + if (!error) { + baseAdapter.enableAnalytics.call(this, config); + } +} + +const subscribeToGamSlots = () => { + window.googletag.pubads().addEventListener('slotRenderEnded', event => { + const isMatchingAdSlot = isAdUnitCodeMatchingSlot(event.slot); + + let renderingAuctionId; + // Loop through auctions in order to find first matching adUnit which has NO gam data + for (const auctionId in cache.auctions) { + const auction = cache.auctions[auctionId]; + // If all adunits in this auction have rendered, skip this auction + if (auction.allGamRendered) break; + // Find first adunit that matches + for (const adUnitCode in auction.adUnits) { + const adUnit = auction[adUnitCode]; + // If this adunit has gam data, skip it + if (adUnit.gamRendered) break; + if (isMatchingAdSlot(adUnitCode)) { + // create new GAM event + const gamEvent = 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 => isNumber(advertiserId) ? advertiserId : undefined, + 'creativeId', creativeId => isNumber(event.sourceAgnosticCreativeId) ? event.sourceAgnosticCreativeId : isNumber(creativeId) ? creativeId : undefined, + 'lineItemId', lineItemId => isNumber(event.sourceAgnosticLineItemId) ? event.sourceAgnosticLineItemId : isNumber(lineItemId) ? lineItemId : undefined, + 'adSlot', slot => slot.getAdUnitPath(), + 'isSlotEmpty', isEmpty => isEmpty || undefined + ]); + gamEvent.auctionId = auctionId; + gamEvent.transactionId = adUnit.transactionId; + // set as ready to send + sendOrAddEventToQueue(gamEvent); + renderingAuctionId = auctionId; + adUnit.gamRendered = true; + break; + } + } + } + // Now if we marked one as rendered, we should see if all have rendered now and send it + if (renderingAuctionId && !cache.auctions[renderingAuctionId].sent && cache.auctions[renderingAuctionId].every(adUnit => adUnit.gamRendered)) { + clearTimeout(cache.timeouts[renderingAuctionId]); + delete cache.timeouts[renderingAuctionId]; + // If we are trying to batch + if (analyticsEventDelay) { + setTimeout(() => { + sendEvent(formatAuction(cache.auctions[renderingAuctionId])); + }, analyticsEventDelay); + return; + } + sendEvent(formatAuction(cache.auctions[renderingAuctionId])); + } + }); +} + +const allAdUnitsRendered = auction => { + auction.adUnits.every(adUnit => adUnit.gamRendered); +} + +window.googletag = window.googletag || {}; +window.googletag.cmd = window.googletag.cmd || []; +window.googletag.cmd.push(function () { + subscribeToGamSlots(); +}); + +const disableAnalytics = () => { + endpoint = undefined; + accountId = undefined; + rubiConf = {}; + cache.gpt.registered = false; + cache.billing = {}; + baseAdapter.disableAnalytics.apply(this, arguments); +} + +adapterManager.registerAnalyticsAdapter({ + adapter: rubiconAdapter, + code: 'rubicon', + gvlid: RUBICON_GVL_ID +}); + +export default rubiconAdapter; diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 69335ff33a8..b546c5f5e3d 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -418,7 +418,7 @@ function getBidPrice(bid) { } } -export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { +export function parseBidResponse(bid, previousBidResponse) { // The current bidResponse for this matching requestId/bidRequestId let responsePrice = getBidPrice(bid) // we need to compare it with the previous one (if there was one) @@ -798,7 +798,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } 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 floor rot besolved gptSlut we have not yet, then update the adUnit to have the adSlot name if (!deepAccess(bid, 'adUnit.gam.adSlot') && deepAccess(args, 'floorData.matchedFields.gptSlot')) { deepSetValue(bid, 'adUnit.gam.adSlot', args.floorData.matchedFields.gptSlot); } From 8d8928f65cdf87d7d84de6a78ef8236e71a0b72f Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Tue, 26 Jul 2022 23:30:43 -0700 Subject: [PATCH 02/22] mgni analytics done? --- modules/magniteAnalyticsAdapter.js | 889 ++++++++++++++++------------- 1 file changed, 489 insertions(+), 400 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 3d6068355a4..e57282d7677 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -1,4 +1,5 @@ -import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, isGptPubadsDefined, _each, deepSetValue, deepClone, logInfo } from '../src/utils.js'; +/* eslint-disable no-console */ +import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, deepSetValue, deepClone, logInfo } from '../src/utils.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; @@ -8,41 +9,17 @@ import { getGlobal } from '../src/prebidGlobal.js'; import { getStorageManager } from '../src/storageManager.js'; const RUBICON_GVL_ID = 52; -export const storage = getStorageManager({ gvlid: RUBICON_GVL_ID, moduleName: 'rubicon' }); -const COOKIE_NAME = 'rpaSession'; +export const storage = getStorageManager({ gvlid: RUBICON_GVL_ID, moduleName: 'magnite' }); +const COOKIE_NAME = 'mgniSession'; const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins const END_EXPIRE_TIME = 21600000; // 6 hours -const MODULE_NAME = 'Rubicon Analytics'; +const MODULE_NAME = 'Magnite Analytics'; +const BID_REJECTED_IPF = 'rejected-ipf'; // List of known rubicon aliases // This gets updated on auction init to account for any custom aliases present let rubiconAliases = ['rubicon']; -/* - cache used to keep track of data throughout page load - auction: ${auctionId}.adUnits.${transactionId}.bids.${bidId} -*/ - -// const auctions = { -// 'b4904c63-7b26-47d6-92d9-a8e232daaf65': { -// ...auctionData, -// adUnits: { -// 'div-gpt-box': { -// ...adUnitData, -// bids: { -// '2b2f2796098d18': { ...bidData }, -// '3e0c2e1d037ce1': { ...bidData } -// } -// } -// } -// } -// } -const cache = { - auctions: new Map(), - billing: {}, - timeouts: {} -} - const pbsErrorMap = { 1: 'timeout-error', 2: 'input-error', @@ -61,7 +38,6 @@ const { BIDDER_DONE, BID_TIMEOUT, BID_WON, - SET_TARGETING, BILLABLE_EVENT }, STATUS: { @@ -74,15 +50,31 @@ const { } = CONSTANTS; // The saved state of rubicon specific setConfig controls -export let rubiConf = { - pvid: generateUUID().slice(0, 8), - analyticsEventDelay: 0, - dmBilling: { - enabled: false, - vendors: [], - waitForAuction: true +export let rubiConf; +// Saving state of all our data we want +let cache; +const resetConfs = () => { + cache = { + auctions: {}, + auctionOrder: [], + timeouts: {}, + billing: {}, + pendingEvents: {}, + eventPending: false } -}; + rubiConf = { + pvid: generateUUID().slice(0, 8), + analyticsEventDelay: 0, + analyticsBatchTimeout: 5000, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + } + } +} +resetConfs(); + config.getConfig('rubicon', config => { mergeDeep(rubiConf, config.rubicon); if (deepAccess(config, 'rubicon.updatePageView') === true) { @@ -96,289 +88,85 @@ config.getConfig('s2sConfig', ({ s2sConfig }) => { serverConfig = s2sConfig; }); -export const SEND_TIMEOUT = 5000; const DEFAULT_INTEGRATION = 'pbjs'; -let baseAdapter = adapter({ analyticsType: 'endpoint' }); -let rubiconAdapter = Object.assign({}, baseAdapter, { - MODULE_INITIALIZED_TIME: Date.now(), - referrerHostname: '', - enableAnalytics, - disableAnalytics, - track({ eventType, args }) { - switch (eventType) { - case AUCTION_INIT: - // set the rubicon aliases - setRubiconAliases(adapterManager.aliasRegistry); - - // latest page referer - pageReferer = deepAccess(args, 'bidderRequests.0.refererInfo.referer'); - - // set auction level data - let auctionData = pick(args, [ - 'auctionId', - 'timestamp as auctionStart', - 'timeout as clientTimeoutMillis', - ]); - auctionData.accountId = accountId; - - // Order bidders were called - auctionData.bidderOrder = args.bidderRequests.map(bidderRequest => bidderRequest.bidderCode); - - // Price Floors information - const floorData = deepAccess(args, 'bidderRequests.0.bids.0.floorData'); - if (floorData) { - auctionData.floors = addFloorData(floorData); - } - - // GDPR info - const gdprData = deepAccess(args, 'bidderRequests.0.gdprConsent'); - if (gdprData) { - auctionData.gdpr = pick(gdprData, [ - 'gdprApplies as applies', - 'consentString', - 'apiVersion as version' - ]); - } - - // User ID Data included in auction - const userIds = Object.keys(deepAccess(args, 'bidderRequests.0.bids.0.userId', {})).map(id => { - return { provider: id, hasId: true } - }); - if (userIds.length) { - auctionData.user = { ids: userIds }; - } - - auctionData.serverTimeoutMillis = serverConfig.timeout; - - // adunits saved as map of transactionIds - auctionData.adUnits = args.adUnits.reduce((adMap, adUnit) => { - let ad = pick(adUnit, [ - 'code as adUnitCode', - 'transactionId', - 'mediaTypes', mediaTypes => Object.keys(mediaTypes), - 'sizes as dimensions', sizes => sizes.map(sizeToDimensions), - ]); - ad.pbAdSlot = deepAccess(adUnit, 'ortb2Imp.ext.data.pbadslot'); - ad.pattern = deepAccess(adUnit, 'ortb2Imp.ext.data.aupname'); - ad.gpid = deepAccess(adUnit, 'ortb2Imp.ext.gpid'); - if (deepAccess(bid, 'ortb2Imp.ext.data.adserver.name') === 'gam') { - ad.gam = { adSlot: bid.ortb2Imp.ext.data.adserver.adslot } - } - ad.bids = {}; - ad.status = 'no-bid'; - adMap[adUnit.code] = ad; - return adMap; - }, new Map()); - - cache.auctions[args.auctionId] = auctionData; - break; - case BID_REQUESTED: - args.bids.forEach(bid => { - const adUnit = deepAccess(cache, `auctions.${args.auctionId}.adUnits.${bid.transactionId}`); - adUnit.bids[bid.bidId] = pick(bid, [ - 'bidder', - 'bidId', - 'src as source', - 'status', () => 'no-bid' - ]); - // set acct site zone id on adunit - if ((!adUnit.siteId || !adUnit.zoneId) && rubiconAliases.indexOf(bid.bidder) !== -1) { - if (deepAccess(bid, 'params.accountId') == accountId) { - adUnit.accountId = parseInt(accountId); - adUnit.siteId = parseInt(deepAccess(bid, 'params.siteId')); - adUnit.zoneId = parseInt(deepAccess(bid, 'params.zoneId')); - } - } - }); - break; - case BID_RESPONSE: - let bid = deepAccess(cache, `auctions.${args.auctionId}.adUnits.${args.transactionId}.bids.${args.requestId}`); - - const auctionEntry = deepAccess(cache, `auctions.${args.auctionId}`); - const adUnit = deepAccess(auctionEntry, `adUnits.${args.transactionId}`); - let bid = adUnit.bids[args.requestId]; - - // if this came from multibid, there might now be matching bid, so check - // THIS logic will change when we support multibid per bid request - if (!bid && args.originalRequestId) { - let ogBid = adUnit.bids[args.originalRequestId]; - // create new bid - adUnit.bids[args.requestId] = { - ...ogBid, - bidId: args.requestId, - bidderDetail: args.targetingBidder - }; - bid = adUnit.bids[args.requestId]; - } - - // if we have not set enforcements yet set it (This is hidden from bidders until now so we have to get from here) - if (typeof deepAccess(auctionEntry, 'floors.enforcement') !== 'boolean' && deepAccess(args, 'floorData.enforcements')) { - auctionEntry.floors.enforcement = args.floorData.enforcements.enforceJS; - auctionEntry.floors.dealsEnforced = args.floorData.enforcements.floorDeals; - } - - // Log error if no matching bid! - if (!bid) { - logError(`${MODULE_NAME}: Could not find associated bid request for bid response with requestId: `, args.requestId); - break; - } +const adUnitIsOnlyInstream = adUnit => { + return adUnit.mediaTypes && Object.keys(adUnit.mediaTypes).length === 1 && deepAccess(adUnit, 'mediaTypes.video.context') === 'instream'; +} - // set bid status - switch (args.getStatusCode()) { - case GOOD: - bid.status = 'success'; - 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 ? BID_REJECTED_IPF : 'no-bid'; - delete bid.error; - break; - default: - bid.status = 'error'; - bid.error = { - code: 'request-error' - }; - } - bid.clientLatencyMillis = bid.timeToRespond || Date.now() - cache.auctions[args.auctionId].auctionStart; - bid.bidResponse = parseBidResponse(args, bid.bidResponse); - break; - case BIDDER_DONE: - const serverError = deepAccess(args, 'serverErrors.0'); - const serverResponseTimeMs = args.serverResponseTimeMs; - args.bids.forEach(bid => { - let cachedBid = deepAccess(cache, `auctions.${args.auctionId}.adUnits.${args.transactionId}.bids.${bid.bidId}`); - if (typeof bid.serverResponseTimeMs !== 'undefined') { - cachedBid.serverLatencyMillis = bid.serverResponseTimeMs; - } else if (serverResponseTimeMs && bid.source === 's2s') { - cachedBid.serverLatencyMillis = serverResponseTimeMs; - } - // if PBS said we had an error, and this bid has not been processed by BID_RESPONSE YET - if (serverError && (!cachedBid.status || ['no-bid', 'error'].indexOf(cachedBid.status) !== -1)) { - cachedBid.status = 'error'; - cachedBid.error = { - code: pbsErrorMap[serverError.code] || pbsErrorMap[999], - description: serverError.message - } - } - }); +const sendPendingEvents = () => { + cache.pendingEvents.trigger = 'batchedEvents'; + sendEvent(cache.pendingEvents); + cache.pendingEvents = {}; + cache.eventPending = false; +} - break; - case SET_TARGETING: - // Perhaps we want to do stuff here - break; - case BID_WON: - let bid = deepAccess(cache, `auctions.${args.auctionId}.adUnits.${args.transactionId}.bids.${args.requestId}`); - - // IF caching enabled, find latest auction that matches and has GAM ID's else use its own - let renderingAuctionId; - if (config.getConfig('useBidCache')) { - // reverse iterate through the auction map - // break once found - } - renderingAuctionId = renderingAuctionId || args.auctionId; - // TODO: FIX => formatBidWon, source + render auction ID's - // transactionID ? - payload.bidsWon = [formatBidWon(args, renderingAuctionId)]; - sendOrAddEventToQueue(payload); - break; - case AUCTION_END: - let auctionCache = cache.auctions[args.auctionId]; - // if for some reason the auction did not do its normal thing, this could be undefied so bail - if (!auctionCache) { - break; - } - // If we are not waiting for gam or bidwons, fire it - const payload = getTopLevelDetails(); - payload.auctions = [formatAuction(auctionCache)]; - if (analyticsEventDelay === 0) { - sendEvent(payload); - } else { - // start timer to send batched payload - cache.timeouts[args.auctionId] = setTimeout(() => { - sendEvent(payload); - }, rubiConf.analyticsBatchTimeout || SEND_TIMEOUT); - } - break; - case BID_TIMEOUT: - args.forEach(badBid => { - let bid = deepAccess(cache, `auctions.${badBid.auctionId}.adUnits.${badBid.transactionId}.bids.${badBid.bidId}`, {}); - // might be set already by bidder-done, so do not overwrite - if (bid.status !== 'error') { - bid.status = 'error'; - bid.error = { - code: 'timeout-error', - description: 'prebid.js timeout' // will help us diff if timeout was set by PBS or PBJS - }; - } - }); - break; - case BILLABLE_EVENT: - if (rubiConf.dmBilling.enabled && isBillingEventValid(args)) { - // add to the map indicating it has not been sent yet - deepSetValue(cache.billing, `${args.vendor}.${args.billingId}`, false); - sendOrAddEventToQueue(args); - } else { - logInfo(`${MODULE_NAME}: Billing event ignored`, args); - } - break; +const addEventToQueue = (event, auctionId, eventName) => { + // If it's auction has not left yet, add it there + if (cache.auctions[auctionId] && !cache.auctions[auctionId].sent) { + cache.auctions[auctionId].pendingEvents = mergeDeep(cache.auctions[auctionId].pendingEvents, event); + } else if (rubiConf.analyticsEventDelay > 0) { + // else if we are trying to batch stuff up, add it to pending events to be fired + cache.pendingEvents = mergeDeep(cache.pendingEvents, event); + + // If no event is pending yet, start a timer for them to be sent and attempted to be gathered together + if (!cache.eventPending) { + setTimeout(sendPendingEvents, rubiConf.analyticsEventDelay); + cache.eventPending = true; } + } else { + // else - send it solo + event.trigger = `solo-${eventName}`; + sendEvent(event); } -}); +} const sendEvent = payload => { - // If this is auction event check if billing is there - // if we have not sent any billingEvents send them - const pendingBillingEvents = getPendingBillingEvents(payload); - if (pendingBillingEvents && pendingBillingEvents.length) { - payload.billableEvents = pendingBillingEvents; + const event = { + ...getTopLevelDetails(), + ...payload } - + console.log('Magnite Analytics: Sending Event: ', event.trigger); ajax( rubiConf.analyticsEndpoint || endpoint, null, - JSON.stringify(payload), + JSON.stringify(event), { contentType: 'application/json' } ); } -function getPendingBillingEvents(payload) { - const billing = deepAccess(payload, 'auctions.0.billing'); - if (billing && billing.length) { - return billing.reduce((accum, billingEvent) => { - if (deepAccess(cache.billing, `${billingEvent.vendor}.${billingEvent.billingId}`) === false) { - accum.push(getBillingPayload(billingEvent)); - } - return accum; - }, []); - } +const sendAuctionEvent = (auctionId, trigger) => { + console.log(`MAGNITE: AUCTION SEND EVENT`); + let auctionCache = cache.auctions[auctionId]; + const auctionEvent = formatAuction(auctionCache.auction); + + auctionCache.sent = true; + sendEvent({ + auctions: [auctionEvent], + ...(auctionCache.pendingEvents || {}), // if any pending events were attached + trigger + }); } const formatAuction = auction => { - auction.adUnits = Object.entries(auction.adUnits).map(([tid, adUnit]) => { + const auctionEvent = deepClone(auction); + + console.log('FORMAT AUCTION: ', JSON.stringify(auctionEvent, null, 2)); + // We stored adUnits and bids as objects for quick lookups, now they are mapped into arrays for PBA + auctionEvent.adUnits = Object.entries(auctionEvent.adUnits).map(([tid, adUnit]) => { adUnit.bids = Object.entries(adUnit.bids).map(([bidId, bid]) => { + // determine adUnit.status from its bid statuses. Use priority below to determine, higher index is better + let statusPriority = ['error', 'no-bid', 'success']; + if (statusPriority.indexOf(bid.status) > statusPriority.indexOf(adUnit.status)) { + adUnit.status = bid.status; + } return bid; }); return adUnit; }); - return auctionCache; -} - -const formatBidWon = (args, renderingAuctionId) => { - let bid = deepAccess(cache, `auctions.${args.auctionId}.adUnits.${args.transactionId}.bids.${args.requestId}`); - return { - bidder: bid.bidder, - bidderDetail: bid.bidderDetail, - sourceAuctionId: args.auctionId, - renderingAuctionId, - transactionId: args.transactionId, - bidId: args.requestId, - accountId, - siteId: adUnit.siteId, - zoneId: adUnit.zoneId, - } + return auctionEvent; } const isBillingEventValid = event => { @@ -390,22 +178,7 @@ const isBillingEventValid = event => { return typeof event.billingId === 'string' && isWhitelistedVendor && isNotDuplicate; } -const sendOrAddEventToQueue = event => { - // if any auction is not sent yet, then add it to the auction queue - const pendingAuction = Object.keys(cache.auctions).find(auctionId => !cache.auctions[auctionId].sent); - - if (rubiConf.dmBilling.waitForAuction && pendingAuction) { - cache.auctions[pendingAuction].billing = cache.auctions[pendingAuction].billing || []; - cache.auctions[pendingAuction].billing.push(event); - } else { - // send it - const payload = getTopLevelDetails(); - payload.billableEvents = [getBillingPayload(event)]; - sendEvent(payload); - } -} - -function getBillingPayload(event) { +const formatBillingEvent = event => { // for now we are mapping all events to type "general", later we will expand support for specific types let billingEvent = deepClone(event); billingEvent.type = 'general'; @@ -443,7 +216,7 @@ const getBidPrice = bid => { } } -const parseBidResponse = (bid, previousBidResponse) => { +export const parseBidResponse = (bid, previousBidResponse) => { // The current bidResponse for this matching requestId/bidRequestId let responsePrice = getBidPrice(bid) // we need to compare it with the previous one (if there was one) log highest only @@ -451,6 +224,13 @@ const parseBidResponse = (bid, previousBidResponse) => { if (previousBidResponse && previousBidResponse.bidPriceUSD > responsePrice) { return previousBidResponse; } + + // if pbs gave us back a bidId, we need to use it and update our bidId to PBA + const pbsId = (bid.pbsBidId == 0 ? generateUUID() : bid.pbsBidId) || (bid.seatBidId == 0 ? generateUUID() : bid.seatBidId); + if (pbsId) { + bid.bidId = pbsId; + } + return pick(bid, [ 'bidPriceUSD', () => responsePrice, 'dealId', dealId => dealId || undefined, @@ -460,9 +240,6 @@ const parseBidResponse = (bid, previousBidResponse) => { const height = bid.height || bid.playerHeight; return (width && height) ? { width, height } : undefined; }, - // Handling use case where pbs sends back 0 or '0' bidIds (these get moved up to bid not bidResponse later) - 'pbsBidId', pbsBidId => pbsBidId == 0 ? generateUUID() : pbsBidId, - 'seatBidId', seatBidId => seatBidId == 0 ? generateUUID() : seatBidId, 'floorValue', () => deepAccess(bid, 'floorData.floorValue'), 'floorRuleValue', () => deepAccess(bid, 'floorData.floorRuleValue'), 'floorRule', () => debugTurnedOn() ? deepAccess(bid, 'floorData.floorRule') : undefined, @@ -476,20 +253,20 @@ const parseBidResponse = (bid, previousBidResponse) => { const addFloorData = floorData => { if (floorData.location === 'noData') { - auction.floors = pick(floorData, [ + return pick(floorData, [ 'location', 'fetchStatus', 'floorProvider as provider' ]); } else { - auction.floors = pick(floorData, [ + return pick(floorData, [ 'location', 'modelVersion as modelName', 'modelWeight', 'modelTimestamp', 'skipped', 'enforcement', () => deepAccess(floorData, 'enforcements.enforceJS'), - 'dealsEnforced', () => deepAccess(loorData, 'enforcements.floorDeals'), + 'dealsEnforced', () => deepAccess(floorData, 'enforcements.floorDeals'), 'skipRate', 'fetchStatus', 'floorMin', @@ -501,17 +278,21 @@ const addFloorData = floorData => { let pageReferer; const getTopLevelDetails = () => { - let cacheEntry = { + let payload = { channel: 'web', integration: rubiConf.int_type || DEFAULT_INTEGRATION, referrerUri: pageReferer, version: '$prebid.version$', - referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), + referrerHostname: magniteAdapter.referrerHostname || getHostNameFromReferer(pageReferer), + timestamps: { + eventTime: Date.now(), + prebidLoaded: magniteAdapter.MODULE_INITIALIZED_TIME + } } // Add DM wrapper details if (rubiConf.wrapperName) { - cacheEntry.wrapper = { + payload.wrapper = { name: rubiConf.wrapperName, family: rubiConf.wrapperFamily, rule: rubiConf.rule_name @@ -522,28 +303,29 @@ const getTopLevelDetails = () => { const sessionData = storage.localStorageIsEnabled() && updateRpaCookie(); if (sessionData) { // gather session info - cacheEntry.session = pick(sessionData, [ + payload.session = pick(sessionData, [ 'id', 'pvid', 'start', 'expires' ]); if (!isEmpty(sessionData.fpkvs)) { - message.fpkvs = Object.keys(sessionData.fpkvs).map(key => { + payload.fpkvs = Object.keys(sessionData.fpkvs).map(key => { return { key, value: sessionData.fpkvs[key] }; }); } } + return payload; } -export function getHostNameFromReferer(referer) { +export const getHostNameFromReferer = referer => { try { - rubiconAdapter.referrerHostname = parseUrl(referer, { noDecodeWholeURL: true }).hostname; + magniteAdapter.referrerHostname = parseUrl(referer, { noDecodeWholeURL: true }).hostname; } catch (e) { logError(`${MODULE_NAME}: Unable to parse hostname from supplied url: `, referer, e); - rubiconAdapter.referrerHostname = ''; + magniteAdapter.referrerHostname = ''; } - return rubiconAdapter.referrerHostname + return magniteAdapter.referrerHostname }; const getRpaCookie = () => { @@ -593,7 +375,7 @@ const updateRpaCookie = () => { /* Filters and converts URL Params into an object and returns only KVs that match the 'utm_KEY' format */ -function getUtmParams() { +const getUtmParams = () => { let search; try { @@ -610,7 +392,7 @@ function getUtmParams() { }, {}); } -function getFpkvs() { +const getFpkvs = () => { rubiConf.fpkvs = Object.assign((rubiConf.fpkvs || {}), getUtmParams()); // convert all values to strings @@ -626,116 +408,423 @@ function getFpkvs() { adds to the rubiconAliases list if found */ const setRubiconAliases = (aliasRegistry) => { - Object.keys(aliasRegistry).forEach(function (alias) { + Object.keys(aliasRegistry).forEach(alias => { if (aliasRegistry[alias] === 'rubicon') { rubiconAliases.push(alias); } }); } -function sizeToDimensions(size) { +const sizeToDimensions = size => { return { width: size.w || size[0], height: size.h || size[1] }; } +const findMatchingAdUnitFromAuctions = (matchesFunction, returnFirstMatch) => { + // finding matching adUnit / auction + let matches = {}; + + // loop through auctions in order and adunits + for (const auctionId of cache.auctionOrder) { + const auction = cache.auctions[auctionId].auction; + for (const adUnitCode in auction.adUnits) { + const adUnit = auction.adUnits[adUnitCode]; + + // check if this matches + let doesMatch; + try { + doesMatch = matchesFunction(adUnit, auction); + } catch (error) { + logWarn(`${MODULE_NAME}: Error running matches function: ${returnFirstMatch}`, error); + doesMatch = false; + } + if (doesMatch) { + matches = { adUnit, auction }; + + // we either return first match or we want last one matching so go to end + if (returnFirstMatch) return matches; + } + } + } + return matches; +} + +const getRenderingAuctionId = bidWonData => { + // if bid caching off -> return the bidWon auciton id + if (!config.getConfig('useBidCache')) { + return bidWonData.auctionId; + } + + // a rendering auction id is the LATEST auction / adunit which contains GAM ID's + const matchingFunction = (adUnit, auction) => { + // does adUnit match our bidWon and gam id's are present + const gamHasRendered = deepAccess(cache, `auctions.${auction.auctionId}.gamRenders.${adUnit.adUnitCode}`); + return adUnit.adUnitCode === bidWonData.adUnitCode && gamHasRendered; + } + let { auction } = findMatchingAdUnitFromAuctions(matchingFunction, false); + // If no match was found, we will use the actual bid won auction id + return (auction && auction.auctionId) || bidWonData.auctionId; +} + +const formatBidWon = bidWonData => { + let renderAuctionId = getRenderingAuctionId(bidWonData); + + // get the bid from the source auction id + let bid = deepAccess(cache, `auctions.${bidWonData.auctionId}.auction.adUnits.${bidWonData.adUnitCode}.bids.${bidWonData.requestId}`); + let adUnit = deepAccess(cache, `auctions.${bidWonData.auctionId}.auction.adUnits.${bidWonData.adUnitCode}`); + return { + ...bid, + sourceAuctionId: bidWonData.auctionId, + renderAuctionId, + transactionId: bidWonData.transactionId, + accountId, + siteId: adUnit.siteId, + zoneId: adUnit.zoneId, + mediaTypes: adUnit.mediaTypes, + adUnitCode: adUnit.adUnitCode + } +} + +const formatGamEvent = (slotEvent, adUnit, auction) => { + const gamEvent = pick(slotEvent, [ + // 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 => isNumber(advertiserId) ? advertiserId : undefined, + 'creativeId', creativeId => isNumber(slotEvent.sourceAgnosticCreativeId) ? slotEvent.sourceAgnosticCreativeId : isNumber(creativeId) ? creativeId : undefined, + 'lineItemId', lineItemId => isNumber(slotEvent.sourceAgnosticLineItemId) ? slotEvent.sourceAgnosticLineItemId : isNumber(lineItemId) ? lineItemId : undefined, + 'adSlot', () => slotEvent.slot.getAdUnitPath(), + 'isSlotEmpty', () => slotEvent.isEmpty || undefined + ]); + gamEvent.auctionId = auction.auctionId; + gamEvent.transactionId = adUnit.transactionId; + return gamEvent; +} + +const subscribeToGamSlots = () => { + window.googletag.pubads().addEventListener('slotRenderEnded', event => { + const isMatchingAdSlot = isAdUnitCodeMatchingSlot(event.slot); + + // We want to find the FIRST auction - adUnit that matches and does not have gam data yet + const matchingFunction = (adUnit, auction) => { + // first it has to match the slot + const matchesSlot = isMatchingAdSlot(adUnit.adUnitCode); + + // next it has to have NOT already been counted as gam rendered + const gamHasRendered = deepAccess(cache, `auctions.${auction.auctionId}.gamRenders.${adUnit.adUnitCode}`); + return matchesSlot && !gamHasRendered; + } + let { adUnit, auction } = findMatchingAdUnitFromAuctions(matchingFunction, true); + + if (!adUnit || !auction) return; // maybe log something here? + + const auctionId = auction.auctionId; + // if we have an adunit, then we need to make a gam event + const gamEvent = formatGamEvent(event, adUnit, auction); + + // marking that this prebid adunit has had its matching gam render found + deepSetValue(cache, `auctions.${auctionId}.gamRenders.${adUnit.adUnitCode}`, true); + + addEventToQueue({ gamRenders: [gamEvent] }, auctionId, 'gam'); + + // If this auction now has all gam slots rendered, fire the payload + if (!cache.auctions[auctionId].sent && Object.keys(cache.auctions[auctionId].gamRenders).every(adUnitCode => cache.auctions[auctionId].gamRenders[adUnitCode])) { + // clear the auction end timeout + clearTimeout(cache.timeouts[auctionId]); + delete cache.timeouts[auctionId]; + + // wait for bid wons a bit or send right away + if (rubiConf.analyticsEventDelay > 0) { + setTimeout(() => { + sendAuctionEvent(auctionId, 'gam'); + }, rubiConf.analyticsEventDelay); + } else { + sendAuctionEvent(auctionId, 'gam'); + } + } + }); +} + +// listen to gam slot renders! +window.googletag = window.googletag || {}; +window.googletag.cmd = window.googletag.cmd || []; +window.googletag.cmd.push(() => subscribeToGamSlots()); + let accountId; let endpoint; -const enableAnalytics = (config = {}) => { + +let magniteAdapter = adapter({ analyticsType: 'endpoint' }); + +magniteAdapter.originEnableAnalytics = magniteAdapter.enableAnalytics; +function enableMgniAnalytics(config = {}) { let error = false; // endpoint + console.log(`setting endpoint to `, deepAccess(config, 'options.endpoint')); endpoint = deepAccess(config, 'options.endpoint'); if (!endpoint) { logError(`${MODULE_NAME}: required endpoint missing`); error = true; } // accountId - accountId = deepAccess(config, 'options.accountId'); + accountId = Number(deepAccess(config, 'options.accountId')); if (!accountId) { logError(`${MODULE_NAME}: required accountId missing`); error = true; } if (!error) { - baseAdapter.enableAnalytics.call(this, config); + console.log('THIS IS ', this); + magniteAdapter.originEnableAnalytics(config); } +}; + +magniteAdapter.enableAnalytics = enableMgniAnalytics; + +magniteAdapter.originDisableAnalytics = magniteAdapter.disableAnalytics; +magniteAdapter.disableAnalytics = function() { + // trick analytics module to register our enable back as main one + magniteAdapter._oldEnable = enableMgniAnalytics; + console.log('MAGNITE DISABLE ANALYTICS'); + endpoint = undefined; + accountId = undefined; + resetConfs(); + magniteAdapter.originDisableAnalytics(); } -const subscribeToGamSlots = () => { - window.googletag.pubads().addEventListener('slotRenderEnded', event => { - const isMatchingAdSlot = isAdUnitCodeMatchingSlot(event.slot); +magniteAdapter.MODULE_INITIALIZED_TIME = Date.now(); +magniteAdapter.referrerHostname = ''; + +magniteAdapter.track = ({ eventType, args }) => { + switch (eventType) { + case AUCTION_INIT: + console.log(`MAGNITE: AUCTION INIT`, args); + // set the rubicon aliases + setRubiconAliases(adapterManager.aliasRegistry); + + // latest page "referer" + pageReferer = deepAccess(args, 'bidderRequests.0.refererInfo.page'); + + // set auction level data + let auctionData = pick(args, [ + 'auctionId', + 'timestamp as auctionStart', + 'timeout as clientTimeoutMillis', + ]); + auctionData.accountId = accountId; + + // Order bidders were called + auctionData.bidderOrder = args.bidderRequests.map(bidderRequest => bidderRequest.bidderCode); + + // Price Floors information + const floorData = deepAccess(args, 'bidderRequests.0.bids.0.floorData'); + if (floorData) { + auctionData.floors = addFloorData(floorData); + } - let renderingAuctionId; - // Loop through auctions in order to find first matching adUnit which has NO gam data - for (const auctionId in cache.auctions) { - const auction = cache.auctions[auctionId]; - // If all adunits in this auction have rendered, skip this auction - if (auction.allGamRendered) break; - // Find first adunit that matches - for (const adUnitCode in auction.adUnits) { - const adUnit = auction[adUnitCode]; - // If this adunit has gam data, skip it - if (adUnit.gamRendered) break; - if (isMatchingAdSlot(adUnitCode)) { - // create new GAM event - const gamEvent = 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 => isNumber(advertiserId) ? advertiserId : undefined, - 'creativeId', creativeId => isNumber(event.sourceAgnosticCreativeId) ? event.sourceAgnosticCreativeId : isNumber(creativeId) ? creativeId : undefined, - 'lineItemId', lineItemId => isNumber(event.sourceAgnosticLineItemId) ? event.sourceAgnosticLineItemId : isNumber(lineItemId) ? lineItemId : undefined, - 'adSlot', slot => slot.getAdUnitPath(), - 'isSlotEmpty', isEmpty => isEmpty || undefined - ]); - gamEvent.auctionId = auctionId; - gamEvent.transactionId = adUnit.transactionId; - // set as ready to send - sendOrAddEventToQueue(gamEvent); - renderingAuctionId = auctionId; - adUnit.gamRendered = true; - break; + // GDPR info + const gdprData = deepAccess(args, 'bidderRequests.0.gdprConsent'); + if (gdprData) { + auctionData.gdpr = pick(gdprData, [ + 'gdprApplies as applies', + 'consentString', + 'apiVersion as version' + ]); + } + + // User ID Data included in auction + const userIds = Object.keys(deepAccess(args, 'bidderRequests.0.bids.0.userId', {})).map(id => { + return { provider: id, hasId: true } + }); + if (userIds.length) { + auctionData.user = { ids: userIds }; + } + + if (serverConfig) { + auctionData.serverTimeoutMillis = serverConfig.timeout; + } + + // lets us keep a map of adunit and wether it had a gam or bid won render yet, used to track when to send events + let gamRenders = {}; + // adunits saved as map of transactionIds + auctionData.adUnits = args.adUnits.reduce((adMap, adUnit) => { + let ad = pick(adUnit, [ + 'code as adUnitCode', + 'transactionId', + 'mediaTypes', mediaTypes => Object.keys(mediaTypes), + 'sizes as dimensions', sizes => sizes.map(sizeToDimensions), + ]); + ad.pbAdSlot = deepAccess(adUnit, 'ortb2Imp.ext.data.pbadslot'); + ad.pattern = deepAccess(adUnit, 'ortb2Imp.ext.data.aupname'); + ad.gpid = deepAccess(adUnit, 'ortb2Imp.ext.gpid'); + if (deepAccess(bid, 'ortb2Imp.ext.data.adserver.name') === 'gam') { + ad.gam = { adSlot: bid.ortb2Imp.ext.data.adserver.adslot } } + ad.bids = {}; + adMap[adUnit.code] = ad; + gamRenders[adUnit.code] = false; + return adMap; + }, {}); + + // holding our pba data to send + cache.auctions[args.auctionId] = { + auction: auctionData, + gamRenders, + pendingEvents: {} } - } - // Now if we marked one as rendered, we should see if all have rendered now and send it - if (renderingAuctionId && !cache.auctions[renderingAuctionId].sent && cache.auctions[renderingAuctionId].every(adUnit => adUnit.gamRendered)) { - clearTimeout(cache.timeouts[renderingAuctionId]); - delete cache.timeouts[renderingAuctionId]; - // If we are trying to batch - if (analyticsEventDelay) { - setTimeout(() => { - sendEvent(formatAuction(cache.auctions[renderingAuctionId])); - }, analyticsEventDelay); - return; + console.log(`MAGNITE: AUCTION cache`, cache); + + // keeping order of auctions and if they have been sent or not + cache.auctionOrder.push(args.auctionId); + break; + case BID_REQUESTED: + console.log(`MAGNITE: BID_REQUESTED`, args); + args.bids.forEach(bid => { + const adUnit = deepAccess(cache, `auctions.${args.auctionId}.auction.adUnits.${bid.adUnitCode}`); + adUnit.bids[bid.bidId] = pick(bid, [ + 'bidder', + 'bidId', + 'src as source', + 'status', () => 'no-bid' + ]); + // set acct site zone id on adunit + if ((!adUnit.siteId || !adUnit.zoneId) && rubiconAliases.indexOf(bid.bidder) !== -1) { + if (deepAccess(bid, 'params.accountId') == accountId) { + adUnit.accountId = parseInt(accountId); + adUnit.siteId = parseInt(deepAccess(bid, 'params.siteId')); + adUnit.zoneId = parseInt(deepAccess(bid, 'params.zoneId')); + } + } + }); + break; + case BID_RESPONSE: + console.log(`MAGNITE: BID_RESPONSE`, args); + const auctionEntry = deepAccess(cache, `auctions.${args.auctionId}.auction`); + const adUnit = deepAccess(auctionEntry, `adUnits.${args.adUnitCode}`); + let bid = adUnit.bids[args.requestId]; + + // if this came from multibid, there might now be matching bid, so check + // THIS logic will change when we support multibid per bid request + if (!bid && args.originalRequestId) { + let ogBid = adUnit.bids[args.originalRequestId]; + // create new bid + adUnit.bids[args.requestId] = { + ...ogBid, + bidId: args.requestId, + bidderDetail: args.targetingBidder + }; + bid = adUnit.bids[args.requestId]; } - sendEvent(formatAuction(cache.auctions[renderingAuctionId])); - } - }); -} -const allAdUnitsRendered = auction => { - auction.adUnits.every(adUnit => adUnit.gamRendered); -} + // if we have not set enforcements yet set it (This is hidden from bidders until now so we have to get from here) + if (typeof deepAccess(auctionEntry, 'floors.enforcement') !== 'boolean' && deepAccess(args, 'floorData.enforcements')) { + auctionEntry.floors.enforcement = args.floorData.enforcements.enforceJS; + auctionEntry.floors.dealsEnforced = args.floorData.enforcements.floorDeals; + } -window.googletag = window.googletag || {}; -window.googletag.cmd = window.googletag.cmd || []; -window.googletag.cmd.push(function () { - subscribeToGamSlots(); -}); + // Log error if no matching bid! + if (!bid) { + logError(`${MODULE_NAME}: Could not find associated bid request for bid response with requestId: `, args.requestId); + break; + } -const disableAnalytics = () => { - endpoint = undefined; - accountId = undefined; - rubiConf = {}; - cache.gpt.registered = false; - cache.billing = {}; - baseAdapter.disableAnalytics.apply(this, arguments); -} + // set bid status + switch (args.getStatusCode()) { + case GOOD: + bid.status = 'success'; + 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 ? BID_REJECTED_IPF : 'no-bid'; + delete bid.error; + break; + default: + bid.status = 'error'; + bid.error = { + code: 'request-error' + }; + } + bid.clientLatencyMillis = args.timeToRespond || Date.now() - cache.auctions[args.auctionId].auctionStart; + bid.bidResponse = parseBidResponse(args, bid.bidResponse); + break; + case BIDDER_DONE: + console.log(`MAGNITE: BIDDER_DONE`, args); + const serverError = deepAccess(args, 'serverErrors.0'); + const serverResponseTimeMs = args.serverResponseTimeMs; + args.bids.forEach(bid => { + let cachedBid = deepAccess(cache, `auctions.${args.auctionId}.auction.adUnits.${args.transactionId}.bids.${bid.bidId}`); + if (typeof bid.serverResponseTimeMs !== 'undefined') { + cachedBid.serverLatencyMillis = bid.serverResponseTimeMs; + } else if (serverResponseTimeMs && bid.source === 's2s') { + cachedBid.serverLatencyMillis = serverResponseTimeMs; + } + // if PBS said we had an error, and this bid has not been processed by BID_RESPONSE YET + if (serverError && (!cachedBid.status || ['no-bid', 'error'].indexOf(cachedBid.status) !== -1)) { + cachedBid.status = 'error'; + cachedBid.error = { + code: pbsErrorMap[serverError.code] || pbsErrorMap[999], + description: serverError.message + } + } + }); + break; + case BID_WON: + console.log(`MAGNITE: BID_WON`, args); + const bidWon = formatBidWon(args); + addEventToQueue({ bidsWon: [bidWon] }, bidWon.renderAuctionId, 'bidWon'); + break; + case AUCTION_END: + console.log(`MAGNITE: AUCTION END`, args); + let auctionCache = cache.auctions[args.auctionId]; + console.log(`MAGNITE: AUCTION END auctionCache`, auctionCache); + // if for some reason the auction did not do its normal thing, this could be undefied so bail + if (!auctionCache) { + break; + } + auctionCache.auction.auctionEnd = args.auctionEnd; + + const isOnlyInstreamAuction = args.adUnits && args.adUnits.every(adUnit => adUnitIsOnlyInstream(adUnit)); + + // if we are not waiting OR it is instream only auction + if (isOnlyInstreamAuction || rubiConf.analyticsBatchTimeout === 0) { + sendAuctionEvent(args.auctionId, 'noBatch'); + } else { + // start timer to send batched payload just in case we don't hear any BID_WON events + cache.timeouts[args.auctionId] = setTimeout(() => { + sendAuctionEvent(args.auctionId, 'auctionEnd'); + }, rubiConf.analyticsBatchTimeout); + } + break; + case BID_TIMEOUT: + console.log(`MAGNITE: BID_TIMEOUT`, args); + args.forEach(badBid => { + let bid = deepAccess(cache, `auctions.${badBid.auctionId}.auction.adUnits.${badBid.adUnitCode}.bids.${badBid.bidId}`, {}); + // might be set already by bidder-done, so do not overwrite + if (bid.status !== 'error') { + bid.status = 'error'; + bid.error = { + code: 'timeout-error', + description: 'prebid.js timeout' // will help us diff if timeout was set by PBS or PBJS + }; + } + }); + break; + case BILLABLE_EVENT: + if (rubiConf.dmBilling.enabled && isBillingEventValid(args)) { + // add to the map indicating it has not been sent yet + deepSetValue(cache.billing, `${args.vendor}.${args.billingId}`, false); + const billingEvent = formatBillingEvent(args); + addEventToQueue({ billableEvents: [billingEvent] }, args.auctionId, 'billing'); + } else { + logInfo(`${MODULE_NAME}: Billing event ignored`, args); + } + break; + } +}; adapterManager.registerAnalyticsAdapter({ - adapter: rubiconAdapter, - code: 'rubicon', + adapter: magniteAdapter, + code: 'magnite', gvlid: RUBICON_GVL_ID }); -export default rubiconAdapter; +export default magniteAdapter; From 6467ed5cc5f1bd78f46c394e44ea21bf38909e2b Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Wed, 27 Jul 2022 00:17:48 -0700 Subject: [PATCH 03/22] test not finished --- .../modules/magniteAnalyticsAdapter_spec.js | 916 ++++++++++++++++++ test/spec/modules/magniteAnalyticsSchema.json | 466 +++++++++ 2 files changed, 1382 insertions(+) create mode 100644 test/spec/modules/magniteAnalyticsAdapter_spec.js create mode 100644 test/spec/modules/magniteAnalyticsSchema.json diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..8067a8cbb77 --- /dev/null +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -0,0 +1,916 @@ +/* eslint-disable no-console */ +import magniteAdapter, { + parseBidResponse, + getHostNameFromReferer, + storage, + rubiConf, +} from '../../../modules/magniteAnalyticsAdapter.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'; + +let Ajv = require('ajv'); +let schema = require('./magniteAnalyticsSchema.json'); +let ajv = new Ajv({ + allErrors: true +}); + +let validator = ajv.compile(schema); + +function validate(message) { + validator(message); + expect(validator.errors).to.deep.equal(null); +} + +let events = require('src/events.js'); +let utils = require('src/utils.js'); + +const { + EVENTS: { + AUCTION_INIT, + AUCTION_END, + BID_REQUESTED, + BID_RESPONSE, + BIDDER_DONE, + BID_WON, + BID_TIMEOUT, + BILLABLE_EVENT + } +} = CONSTANTS; + +const STUBBED_UUID = '12345678-1234-1234-1234-123456789abc'; + +// Mock Event Data +const MOCK = { + AUCTION_INIT: { + 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'timestamp': 1658868383741, + 'adUnits': [ + { + 'code': 'box', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'bids': [ + { + 'bidder': 'rubicon', + 'params': { + 'accountId': 1001, + 'siteId': 267318, + 'zoneId': 1861698 + } + } + ], + 'sizes': [ + [ + 300, + 250 + ] + ], + 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', + 'ortb2Imp': { + 'ext': { + 'tid': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', + 'data': { + 'adserver': { + 'name': 'gam', + 'adslot': '/1234567/prebid-slot' + }, + 'pbadslot': '/1234567/prebid-slot' + }, + 'gpid': '/1234567/prebid-slot' + } + } + } + ], + 'bidderRequests': [ + { + 'bidderCode': 'rubicon', + 'bids': [ + { + 'bidder': 'rubicon', + 'params': { + 'accountId': 1001, + 'siteId': 267318, + 'zoneId': 1861698, + }, + 'adUnitCode': 'box', + 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', + 'bidId': '23fcd8cf4bf0d7', + 'src': 'client', + 'startTime': 1658868383748 + } + ], + 'refererInfo': { + 'page': 'http://a-test-domain.com:8000/test_pages/sanity/TEMP/prebidTest.html?pbjs_debug=true', + }, + } + ], + 'timeout': 3000, + 'config': { + 'accountId': 1001, + 'endpoint': 'https://pba-event-service-alb-dev.use1.fanops.net/event' + } + }, + BID_REQUESTED: { + 'bidderCode': 'rubicon', + 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'bids': [ + { + 'bidder': 'rubicon', + 'params': { + 'accountId': 1001, + 'siteId': 267318, + 'zoneId': 1861698, + }, + 'adUnitCode': 'box', + 'bidId': '23fcd8cf4bf0d7', + 'src': 'client', + } + ] + }, + BID_RESPONSE: { + 'bidderCode': 'rubicon', + 'width': 300, + 'height': 250, + 'adId': '3c0b59947ced11', + 'requestId': '23fcd8cf4bf0d7', + 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', + 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'mediaType': 'banner', + 'source': 'client', + 'currency': 'USD', + 'creativeId': '4954828', + 'cpm': 3.4, + 'ttl': 300, + 'netRevenue': true, + 'ad': '', + 'bidder': 'rubicon', + 'adUnitCode': 'box', + 'timeToRespond': 271, + 'size': '300x250', + 'status': 'rendered', + getStatusCode: () => 1, + }, + AUCTION_END: { + 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'auctionEnd': 1658868384019, + }, + BIDDER_DONE: { + 'bidderCode': 'rubicon', + 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'bids': [ + { + 'bidder': 'rubicon', + 'adUnitCode': 'box', + 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', + 'bidId': '23fcd8cf4bf0d7', + 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'src': 'client', + } + ] + }, + BID_WON: { + 'bidderCode': 'rubicon', + 'adId': '3c0b59947ced11', + 'requestId': '23fcd8cf4bf0d7', + 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', + 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'mediaType': 'banner', + 'currency': 'USD', + 'cpm': 3.4, + 'ttl': 300, + 'bidder': 'rubicon', + 'adUnitCode': 'box', + 'status': 'rendered', + } +} + +const ANALYTICS_MESSAGE = { + 'channel': 'web', + 'integration': 'pbjs', + 'referrerUri': 'http://a-test-domain.com:8000/test_pages/sanity/TEMP/prebidTest.html?pbjs_debug=true', + 'version': '7.8.0-pre', + 'referrerHostname': 'a-test-domain.com', + 'timestamps': { + 'eventTime': 1519767018781, + 'prebidLoaded': magniteAdapter.MODULE_INITIALIZED_TIME + }, + 'wrapper': { + 'name': '10000_fakewrapper_test' + }, + 'session': { + 'id': '12345678-1234-1234-1234-123456789abc', + 'pvid': '12345678', + 'start': 1519767018781, + 'expires': 1519788618781 + }, + 'auctions': [ + { + 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'auctionStart': 1658868383741, + 'clientTimeoutMillis': 3000, + 'accountId': 1001, + 'bidderOrder': [ + 'rubicon' + ], + 'serverTimeoutMillis': 1000, + 'adUnits': [ + { + 'adUnitCode': 'box', + 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', + 'mediaTypes': [ + 'banner' + ], + 'dimensions': [ + { + 'width': 300, + 'height': 250 + } + ], + 'pbAdSlot': '/1234567/prebid-slot', + 'gpid': '/1234567/prebid-slot', + 'bids': [ + { + 'bidder': 'rubicon', + 'bidId': '23fcd8cf4bf0d7', + 'source': 'client', + 'status': 'success', + 'clientLatencyMillis': 271, + 'bidResponse': { + 'bidPriceUSD': 3.4, + 'mediaType': 'banner', + 'dimensions': { + 'width': 300, + 'height': 250 + } + } + } + ], + 'accountId': 1001, + 'siteId': 267318, + 'zoneId': 1861698, + 'status': 'success' + } + ], + 'auctionEnd': 1658868384019 + } + ], + 'bidsWon': [ + { + 'bidder': 'rubicon', + 'bidId': '23fcd8cf4bf0d7', + 'source': 'client', + 'status': 'success', + 'clientLatencyMillis': 271, + 'bidResponse': { + 'bidPriceUSD': 3.4, + 'mediaType': 'banner', + 'dimensions': { + 'width': 300, + 'height': 250 + } + }, + 'sourceAuctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'renderAuctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', + 'accountId': 1001, + 'siteId': 267318, + 'zoneId': 1861698, + 'mediaTypes': [ + 'banner' + ], + 'adUnitCode': 'box' + } + ], + 'trigger': 'auctionEnd' +} + +describe('magnite analytics adapter', function () { + let sandbox; + let clock; + let getDataFromLocalStorageStub, setDataInLocalStorageStub, localStorageIsEnabledStub; + beforeEach(function () { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + setDataInLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + mockGpt.disable(); + sandbox = sinon.sandbox.create(); + + localStorageIsEnabledStub.returns(true); + + sandbox.stub(events, 'getEvents').returns([]); + + sandbox.stub(utils, 'generateUUID').returns(STUBBED_UUID); + + clock = sandbox.useFakeTimers(1519767013781); + + magniteAdapter.referrerHostname = ''; + + config.setConfig({ + s2sConfig: { + timeout: 1000, + accountId: 10000, + }, + rubicon: { + wrapperName: '10000_fakewrapper_test' + } + }) + }); + + afterEach(function () { + sandbox.restore(); + config.resetConfig(); + mockGpt.enable(); + getDataFromLocalStorageStub.restore(); + setDataInLocalStorageStub.restore(); + localStorageIsEnabledStub.restore(); + magniteAdapter.disableAnalytics(); + }); + + it('should require accountId', function () { + sandbox.stub(utils, 'logError'); + + magniteAdapter.enableAnalytics({ + options: { + endpoint: '//localhost:9999/event' + } + }); + + expect(utils.logError.called).to.equal(true); + }); + + it('should require endpoint', function () { + sandbox.stub(utils, 'logError'); + + magniteAdapter.enableAnalytics({ + options: { + accountId: 1001 + } + }); + + expect(utils.logError.called).to.equal(true); + }); + + describe('config subscribe', function () { + it('should update the pvid if user asks', function () { + expect(utils.generateUUID.called).to.equal(false); + config.setConfig({ rubicon: { updatePageView: true } }); + expect(utils.generateUUID.called).to.equal(true); + }); + it('should merge in and preserve older set configs', function () { + config.setConfig({ + rubicon: { + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + source: 'fb' + }, + updatePageView: true + } + }); + expect(rubiConf).to.deep.equal({ + analyticsEventDelay: 0, + analyticsBatchTimeout: 5000, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + }, + pvid: '12345678', + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + source: 'fb' + }, + updatePageView: true + }); + + // update it with stuff + config.setConfig({ + rubicon: { + analyticsBatchTimeout: 3000, + fpkvs: { + link: 'email' + } + } + }); + expect(rubiConf).to.deep.equal({ + analyticsEventDelay: 0, + analyticsBatchTimeout: 3000, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + }, + pvid: '12345678', + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + source: 'fb', + link: 'email' + }, + updatePageView: true + }); + + // overwriting specific edge keys should update them + config.setConfig({ + rubicon: { + fpkvs: { + link: 'iMessage', + source: 'twitter' + } + } + }); + expect(rubiConf).to.deep.equal({ + analyticsEventDelay: 0, + analyticsBatchTimeout: 3000, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + }, + pvid: '12345678', + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + link: 'iMessage', + source: 'twitter' + }, + updatePageView: true + }); + }); + }); + + describe('when handling events', function () { + function performStandardAuction(gptEvents, auctionId = MOCK.AUCTION_INIT.auctionId) { + events.emit(AUCTION_INIT, { ...MOCK.AUCTION_INIT, auctionId }); + events.emit(BID_REQUESTED, { ...MOCK.BID_REQUESTED, auctionId }); + events.emit(BID_RESPONSE, { ...MOCK.BID_RESPONSE, auctionId }); + events.emit(BIDDER_DONE, { ...MOCK.BIDDER_DONE, auctionId }); + events.emit(AUCTION_END, { ...MOCK.AUCTION_END, auctionId }); + + if (gptEvents && gptEvents.length) { + gptEvents.forEach(gptEvent => mockGpt.emitEvent(gptEvent.eventName, gptEvent.params)); + } + + events.emit(BID_WON, { ...MOCK.BID_WON, auctionId }); + clock.tick(rubiConf.analyticsBatchTimeout + 1000); + } + + beforeEach(function () { + console.log('RUNNING BEFORE EACH'); + magniteAdapter.enableAnalytics({ + options: { + endpoint: '//localhost:9999/event', + accountId: 1001 + } + }); + config.setConfig({ rubicon: { updatePageView: true } }); + }); + + // afterEach(function () { + // console.log('RUNNING afterEach') + // magniteAdapter.disableAnalytics(); + // }); + + it('should build a batched message from prebid events', function () { + 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); + + console.log(`ANALYTICS PAYLOAD: \n`, JSON.stringify(message, null, 2)); + + expect(message).to.deep.equal(ANALYTICS_MESSAGE); + }); + + it('should pass along bidderOrder correctly', function () { + const auctionInit = utils.deepClone(MOCK.AUCTION_INIT); + + auctionInit.bidderRequests = auctionInit.bidderRequests.concat([ + { bidderCode: 'pubmatic' }, + { bidderCode: 'ix' }, + { bidderCode: 'appnexus' } + ]) + + events.emit(AUCTION_INIT, auctionInit); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + clock.tick(rubiConf.analyticsBatchTimeout + 1000); + + let message = JSON.parse(server.requests[0].requestBody); + expect(message.auctions[0].bidderOrder).to.deep.equal([ + 'rubicon', + 'pubmatic', + 'ix', + 'appnexus' + ]); + }); + + it('should pass along user ids', function () { + let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); + auctionInit.bidderRequests[0].bids[0].userId = { + criteoId: 'sadfe4334', + lotamePanoramaId: 'asdf3gf4eg', + pubcid: 'dsfa4545-svgdfs5', + sharedId: { id1: 'asdf', id2: 'sadf4344' } + }; + + events.emit(AUCTION_INIT, auctionInit); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + clock.tick(rubiConf.analyticsBatchTimeout + 1000); + + let message = JSON.parse(server.requests[0].requestBody); + + expect(message.auctions[0].user).to.deep.equal({ + ids: [ + { provider: 'criteoId', 'hasId': true }, + { provider: 'lotamePanoramaId', 'hasId': true }, + { provider: 'pubcid', 'hasId': true }, + { provider: 'sharedId', 'hasId': true }, + ] + }); + }); + + // A-Domain tests + [ + { input: ['magnite.com'], expected: ['magnite.com'] }, + { input: ['magnite.com', 'prebid.org'], expected: ['magnite.com', 'prebid.org'] }, + { input: [123, 'prebid.org', false, true, [], 'magnite.com', {}], expected: ['prebid.org', 'magnite.com'] }, + { input: 'not array', expected: undefined }, + { input: [], expected: undefined }, + ].forEach((test, index) => { + it(`should handle adomain correctly - #${index + 1}`, function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + let bidResponse = utils.deepClone(MOCK.BID_RESPONSE); + bidResponse.meta = { + advertiserDomains: test.input + } + + events.emit(BID_RESPONSE, bidResponse); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(BID_WON, MOCK.BID_WON); + clock.tick(rubiConf.analyticsBatchTimeout + 1000); + + let message = JSON.parse(server.requests[0].requestBody); + expect(message.auctions[0].adUnits[0].bids[0].bidResponse.adomains).to.deep.equal(test.expected); + }); + }); + + describe('with session handling', function () { + const expectedPvid = STUBBED_UUID.slice(0, 8); + beforeEach(function () { + config.setConfig({ rubicon: { updatePageView: true } }); + }); + + 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; + 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 () { + config.setConfig({ + rubicon: { + fpkvs: { + 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 = STUBBED_UUID.slice(0, 8); + expectedMessage.fpkvs = [ + { key: 'source', value: 'fb' }, + { key: 'link', value: 'email' } + ] + expect(message).to.deep.equal(expectedMessage); + }); + + it('should convert kvs to strings before sending', function () { + config.setConfig({ + rubicon: { + fpkvs: { + number: 24, + boolean: false, + string: 'hello', + array: ['one', 2, 'three'], + object: { one: 'two' } + } + } + }); + 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 = STUBBED_UUID.slice(0, 8); + expectedMessage.fpkvs = [ + { key: 'number', value: '24' }, + { key: 'boolean', value: 'false' }, + { key: 'string', value: 'hello' }, + { key: 'array', value: 'one,2,three' }, + { key: 'object', value: '[object Object]' } + ] + expect(message).to.deep.equal(expectedMessage); + }); + + it('should use the query utm param rubicon kv value and pass updated kv and pvid when defined', function () { + sandbox.stub(utils, 'getWindowLocation').returns({ 'search': '?utm_source=other', 'pbjs_debug': 'true' }); + + config.setConfig({ + rubicon: { + fpkvs: { + 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 = STUBBED_UUID.slice(0, 8); + expectedMessage.fpkvs = [ + { key: 'source', value: 'other' }, + { key: 'link', value: 'email' } + ] + + message.fpkvs.sort((left, right) => left.key < right.key); + expectedMessage.fpkvs.sort((left, right) => left.key < right.key); + + expect(message).to.deep.equal(expectedMessage); + }); + + // it('should pick up existing localStorage and use its values', function () { + // // set some localStorage + // let inputlocalStorage = { + // id: '987654', + // start: 1519767017881, // 15 mins before "now" + // expires: 1519767039481, // six hours later + // lastSeen: 1519766113781, + // fpkvs: { source: 'tw' } + // }; + // getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + // config.setConfig({ + // rubicon: { + // fpkvs: { + // link: 'email' // should merge this with what is in the localStorage! + // } + // } + // }); + // 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: 1519767017881, + // expires: 1519767039481, + // pvid: expectedPvid + // } + // expectedMessage.fpkvs = [ + // { key: 'source', value: 'tw' }, + // { key: 'link', value: 'email' } + // ] + // expect(message).to.deep.equal(expectedMessage); + + // let calledWith; + // try { + // calledWith = JSON.parse(atob(setDataInLocalStorageStub.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: expectedPvid // new pvid stored + // }); + // }); + + // it('should overwrite matching localstorge value and use its remaining values', function () { + // sandbox.stub(utils, 'getWindowLocation').returns({ 'search': '?utm_source=fb&utm_click=dog' }); + + // // set some localStorage + // let inputlocalStorage = { + // id: '987654', + // start: 1519766113781, // 15 mins before "now" + // expires: 1519787713781, // six hours later + // lastSeen: 1519766113781, + // fpkvs: { source: 'tw', link: 'email' } + // }; + // getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + // config.setConfig({ + // rubicon: { + // fpkvs: { + // link: 'email' // should merge this with what is in the localStorage! + // } + // } + // }); + // 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: expectedPvid + // } + // expectedMessage.fpkvs = [ + // { key: 'source', value: 'fb' }, + // { key: 'link', value: 'email' }, + // { key: 'click', value: 'dog' } + // ] + + // message.fpkvs.sort((left, right) => left.key < right.key); + // expectedMessage.fpkvs.sort((left, right) => left.key < right.key); + + // expect(message).to.deep.equal(expectedMessage); + + // let calledWith; + // try { + // calledWith = JSON.parse(atob(setDataInLocalStorageStub.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: 'fb', link: 'email', click: 'dog' }, // link merged in + // pvid: expectedPvid // new pvid stored + // }); + // }); + + // it('should throw out session if lastSeen > 30 mins ago and create new one', function () { + // // 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' } + // }; + // getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + // config.setConfig({ + // rubicon: { + // fpkvs: { + // link: 'email' // should merge this with what is in the localStorage! + // } + // } + // }); + + // 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 = expectedPvid; + + // // 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(setDataInLocalStorageStub.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: expectedPvid // new pvid stored + // }); + // }); + + // it('should throw out session if past expires time and create new one', function () { + // // 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' } + // }; + // getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + // config.setConfig({ + // rubicon: { + // fpkvs: { + // link: 'email' // should merge this with what is in the localStorage! + // } + // } + // }); + + // 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 = expectedPvid; + + // // 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(setDataInLocalStorageStub.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: expectedPvid // new pvid stored + // }); + // }); + }); + + }); +}); diff --git a/test/spec/modules/magniteAnalyticsSchema.json b/test/spec/modules/magniteAnalyticsSchema.json new file mode 100644 index 00000000000..5abd960ff2c --- /dev/null +++ b/test/spec/modules/magniteAnalyticsSchema.json @@ -0,0 +1,466 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Prebid Auctions", + "description": "A batched data object describing the lifecycle of an auction or multiple auction across a single page view.", + "type": "object", + "required": [ + "integration", + "version" + ], + "anyOf": [ + { + "required": [ + "auctions" + ] + }, + { + "required": [ + "bidsWon" + ] + }, + { + "required": [ + "billableEvents" + ] + } + ], + "properties": { + "integration": { + "type": "string", + "description": "Integration type that generated this event.", + "default": "pbjs" + }, + "version": { + "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, + "items": { + "type": "object", + "required": [ + "adUnits" + ], + "properties": { + "clientTimeoutMillis": { + "type": "integer", + "description": "Timeout given in client for given auction in milliseconds (if applicable)." + }, + "serverTimeoutMillis": { + "type": "integer", + "description": "Timeout configured for server adapter request in milliseconds (if applicable)." + }, + "accountId": { + "type": "number", + "description": "The account id for prebid server (if applicable)." + }, + "adUnits": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "description": "An array of adUnits involved in this auction.", + "required": [ + "status", + "adUnitCode", + "transactionId", + "mediaTypes", + "dimensions", + "bids" + ], + "properties": { + "status": { + "type": "string", + "description": "The status of the adUnit" + }, + "adUnitCode": { + "type": "string", + "description": "The adUnit.code identifier" + }, + "transactionId": { + "type": "string", + "description": "The UUID generated id to represent this adunit in this auction." + }, + "adSlot": { + "type": "string" + }, + "mediaTypes": { + "$ref": "#/definitions/mediaTypes" + }, + "videoAdFormat": { + "$ref": "#/definitions/videoAdFormat" + }, + "dimensions": { + "type": "array", + "description": "All valid sizes included in this auction (note: may be sizeConfig filtered).", + "minItems": 1, + "items": { + "$ref": "#/definitions/dimensions" + } + }, + "adserverTargeting": { + "$ref": "#/definitions/adserverTargeting" + }, + "bids": { + "type": "array", + "description": "An array that contains a combination of the bids from the adUnit combined with their responses.", + "minItems": 1, + "items": { + "$ref": "#/definitions/bid" + } + }, + "accountId": { + "type": "number", + "description": "The Rubicon AccountId associated with this adUnit - Removed if null" + }, + "siteId": { + "type": "number", + "description": "The Rubicon siteId associated with this adUnit - Removed if null" + }, + "zoneId": { + "type": "number", + "description": "The Rubicon zoneId associated with this adUnit - Removed if null" + }, + "gam": { + "$ref": "#/definitions/gam" + } + } + } + } + } + } + }, + "bidsWon": { + "type": "array", + "minItems": 1, + "items": { + "allOf": [ + { + "$ref": "#/definitions/bid" + }, + { + "required": [ + "transactionId", + "accountId", + "mediaTypes", + "adUnitCode" + ], + "properties": { + "transactionId": { + "type": "string" + }, + "accountId": { + "type": "number" + }, + "adUnitCode": { + "type": "string" + }, + "videoAdFormat": { + "$ref": "#/definitions/videoAdFormat" + }, + "mediaTypes": { + "$ref": "#/definitions/mediaTypes" + }, + "adserverTargeting": { + "$ref": "#/definitions/adserverTargeting" + }, + "siteId": { + "type": "number", + "description": "The Rubicon siteId associated with this adUnit - Removed if null" + }, + "zoneId": { + "type": "number", + "description": "The Rubicon zoneId associated with this adUnit - Removed if null" + } + } + } + ] + } + }, + "billableEvents":{ + "type":"array", + "minItems":1, + "items":{ + "type":"object", + "required":[ + "accountId", + "vendor", + "type", + "billingId" + ], + "properties":{ + "vendor":{ + "type":"string", + "description":"The name of the vendor who emitted the billable event" + }, + "type":{ + "type":"string", + "description":"The type of billable event", + "enum":[ + "impression", + "pageLoad", + "auction", + "request", + "general" + ] + }, + "billingId":{ + "type":"string", + "description":"A UUID which is responsible more mapping this event to" + }, + "accountId": { + "type": "number", + "description": "The account id for the rubicon publisher" + } + } + } + } + }, + "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", + "patternProperties": { + ".+": { + "type": "string" + } + } + }, + "videoAdFormat": { + "type": "string", + "description": "This value only provided for video specifies the ad format", + "enum": [ + "pre-roll", + "interstitial", + "outstream", + "mid-roll", + "post-roll", + "vertical" + ] + }, + "mediaTypes": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "native", + "video", + "banner" + ] + } + }, + "dimensions": { + "type": "object", + "description": "Size object representing the dimensions of creative in pixels.", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + } + }, + "bid": { + "type": "object", + "required": [ + "bidder", + "bidId", + "status", + "source" + ], + "properties": { + "bidder": { + "type": "string" + }, + "bidId": { + "type": "string", + "description": "UUID representing this individual bid request in this auction." + }, + "params": { + "description": "A copy of the bid.params from the adUnit.bids", + "anyOf": [ + { + "type": "object" + }, + { + "$ref": "#/definitions/params/rubicon" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "success", + "no-bid", + "error", + "rejected-gdpr", + "rejected-ipf" + ] + }, + "error": { + "type": "object", + "additionalProperties": false, + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "enum": [ + "request-error", + "connect-error", + "timeout-error" + ] + }, + "description": { + "type": "string" + } + } + }, + "source": { + "type": "string", + "enum": [ + "client", + "server" + ] + }, + "clientLatencyMillis": { + "type": "integer", + "description": "Latency from auction start to bid response recieved in milliseconds." + }, + "serverLatencyMillis": { + "type": "integer", + "description": "Latency returned by prebid server (response_time_ms)." + }, + "bidResponse": { + "type": "object", + "required": [ + "mediaType", + "bidPriceUSD" + ], + "properties": { + "dimensions": { + "$ref": "#/definitions/dimensions" + }, + "mediaType": { + "type": "string", + "enum": [ + "native", + "video", + "banner" + ] + }, + "bidPriceUSD": { + "type": "number", + "description": "The bid value denoted in USD" + }, + "dealId": { + "type": "integer", + "description": "The id associated with any potential deals" + } + } + } + } + }, + "params": { + "rubicon": { + "type": "object", + "properties": { + "accountId": { + "type": "number" + }, + "siteId": { + "type": "number" + }, + "zoneId": { + "type": "number" + } + } + } + } + } +} From f990c5733c6885574119af572bc44803732dd644 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Wed, 27 Jul 2022 00:56:11 -0700 Subject: [PATCH 04/22] remove log --- modules/magniteAnalyticsAdapter.js | 15 - .../modules/magniteAnalyticsAdapter_spec.js | 446 +++++++++--------- 2 files changed, 218 insertions(+), 243 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index e57282d7677..4a19c0f198c 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, deepSetValue, deepClone, logInfo } from '../src/utils.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; @@ -126,7 +125,6 @@ const sendEvent = payload => { ...getTopLevelDetails(), ...payload } - console.log('Magnite Analytics: Sending Event: ', event.trigger); ajax( rubiConf.analyticsEndpoint || endpoint, null, @@ -138,7 +136,6 @@ const sendEvent = payload => { } const sendAuctionEvent = (auctionId, trigger) => { - console.log(`MAGNITE: AUCTION SEND EVENT`); let auctionCache = cache.auctions[auctionId]; const auctionEvent = formatAuction(auctionCache.auction); @@ -560,7 +557,6 @@ magniteAdapter.originEnableAnalytics = magniteAdapter.enableAnalytics; function enableMgniAnalytics(config = {}) { let error = false; // endpoint - console.log(`setting endpoint to `, deepAccess(config, 'options.endpoint')); endpoint = deepAccess(config, 'options.endpoint'); if (!endpoint) { logError(`${MODULE_NAME}: required endpoint missing`); @@ -573,7 +569,6 @@ function enableMgniAnalytics(config = {}) { error = true; } if (!error) { - console.log('THIS IS ', this); magniteAdapter.originEnableAnalytics(config); } }; @@ -584,7 +579,6 @@ magniteAdapter.originDisableAnalytics = magniteAdapter.disableAnalytics; magniteAdapter.disableAnalytics = function() { // trick analytics module to register our enable back as main one magniteAdapter._oldEnable = enableMgniAnalytics; - console.log('MAGNITE DISABLE ANALYTICS'); endpoint = undefined; accountId = undefined; resetConfs(); @@ -597,7 +591,6 @@ magniteAdapter.referrerHostname = ''; magniteAdapter.track = ({ eventType, args }) => { switch (eventType) { case AUCTION_INIT: - console.log(`MAGNITE: AUCTION INIT`, args); // set the rubicon aliases setRubiconAliases(adapterManager.aliasRegistry); @@ -671,13 +664,11 @@ magniteAdapter.track = ({ eventType, args }) => { gamRenders, pendingEvents: {} } - console.log(`MAGNITE: AUCTION cache`, cache); // keeping order of auctions and if they have been sent or not cache.auctionOrder.push(args.auctionId); break; case BID_REQUESTED: - console.log(`MAGNITE: BID_REQUESTED`, args); args.bids.forEach(bid => { const adUnit = deepAccess(cache, `auctions.${args.auctionId}.auction.adUnits.${bid.adUnitCode}`); adUnit.bids[bid.bidId] = pick(bid, [ @@ -697,7 +688,6 @@ magniteAdapter.track = ({ eventType, args }) => { }); break; case BID_RESPONSE: - console.log(`MAGNITE: BID_RESPONSE`, args); const auctionEntry = deepAccess(cache, `auctions.${args.auctionId}.auction`); const adUnit = deepAccess(auctionEntry, `adUnits.${args.adUnitCode}`); let bid = adUnit.bids[args.requestId]; @@ -747,7 +737,6 @@ magniteAdapter.track = ({ eventType, args }) => { bid.bidResponse = parseBidResponse(args, bid.bidResponse); break; case BIDDER_DONE: - console.log(`MAGNITE: BIDDER_DONE`, args); const serverError = deepAccess(args, 'serverErrors.0'); const serverResponseTimeMs = args.serverResponseTimeMs; args.bids.forEach(bid => { @@ -768,14 +757,11 @@ magniteAdapter.track = ({ eventType, args }) => { }); break; case BID_WON: - console.log(`MAGNITE: BID_WON`, args); const bidWon = formatBidWon(args); addEventToQueue({ bidsWon: [bidWon] }, bidWon.renderAuctionId, 'bidWon'); break; case AUCTION_END: - console.log(`MAGNITE: AUCTION END`, args); let auctionCache = cache.auctions[args.auctionId]; - console.log(`MAGNITE: AUCTION END auctionCache`, auctionCache); // if for some reason the auction did not do its normal thing, this could be undefied so bail if (!auctionCache) { break; @@ -795,7 +781,6 @@ magniteAdapter.track = ({ eventType, args }) => { } break; case BID_TIMEOUT: - console.log(`MAGNITE: BID_TIMEOUT`, args); args.forEach(badBid => { let bid = deepAccess(cache, `auctions.${badBid.auctionId}.auction.adUnits.${badBid.adUnitCode}.bids.${badBid.bidId}`, {}); // might be set already by bidder-done, so do not overwrite diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index 8067a8cbb77..c174d959e0a 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import magniteAdapter, { parseBidResponse, getHostNameFromReferer, @@ -471,7 +470,6 @@ describe('magnite analytics adapter', function () { } beforeEach(function () { - console.log('RUNNING BEFORE EACH'); magniteAdapter.enableAnalytics({ options: { endpoint: '//localhost:9999/event', @@ -481,11 +479,6 @@ describe('magnite analytics adapter', function () { config.setConfig({ rubicon: { updatePageView: true } }); }); - // afterEach(function () { - // console.log('RUNNING afterEach') - // magniteAdapter.disableAnalytics(); - // }); - it('should build a batched message from prebid events', function () { performStandardAuction(); @@ -497,8 +490,6 @@ describe('magnite analytics adapter', function () { let message = JSON.parse(request.requestBody); validate(message); - console.log(`ANALYTICS PAYLOAD: \n`, JSON.stringify(message, null, 2)); - expect(message).to.deep.equal(ANALYTICS_MESSAGE); }); @@ -692,225 +683,224 @@ describe('magnite analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); - // it('should pick up existing localStorage and use its values', function () { - // // set some localStorage - // let inputlocalStorage = { - // id: '987654', - // start: 1519767017881, // 15 mins before "now" - // expires: 1519767039481, // six hours later - // lastSeen: 1519766113781, - // fpkvs: { source: 'tw' } - // }; - // getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); - - // config.setConfig({ - // rubicon: { - // fpkvs: { - // link: 'email' // should merge this with what is in the localStorage! - // } - // } - // }); - // 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: 1519767017881, - // expires: 1519767039481, - // pvid: expectedPvid - // } - // expectedMessage.fpkvs = [ - // { key: 'source', value: 'tw' }, - // { key: 'link', value: 'email' } - // ] - // expect(message).to.deep.equal(expectedMessage); - - // let calledWith; - // try { - // calledWith = JSON.parse(atob(setDataInLocalStorageStub.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: expectedPvid // new pvid stored - // }); - // }); - - // it('should overwrite matching localstorge value and use its remaining values', function () { - // sandbox.stub(utils, 'getWindowLocation').returns({ 'search': '?utm_source=fb&utm_click=dog' }); - - // // set some localStorage - // let inputlocalStorage = { - // id: '987654', - // start: 1519766113781, // 15 mins before "now" - // expires: 1519787713781, // six hours later - // lastSeen: 1519766113781, - // fpkvs: { source: 'tw', link: 'email' } - // }; - // getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); - - // config.setConfig({ - // rubicon: { - // fpkvs: { - // link: 'email' // should merge this with what is in the localStorage! - // } - // } - // }); - // 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: expectedPvid - // } - // expectedMessage.fpkvs = [ - // { key: 'source', value: 'fb' }, - // { key: 'link', value: 'email' }, - // { key: 'click', value: 'dog' } - // ] - - // message.fpkvs.sort((left, right) => left.key < right.key); - // expectedMessage.fpkvs.sort((left, right) => left.key < right.key); - - // expect(message).to.deep.equal(expectedMessage); - - // let calledWith; - // try { - // calledWith = JSON.parse(atob(setDataInLocalStorageStub.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: 'fb', link: 'email', click: 'dog' }, // link merged in - // pvid: expectedPvid // new pvid stored - // }); - // }); - - // it('should throw out session if lastSeen > 30 mins ago and create new one', function () { - // // 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' } - // }; - // getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); - - // config.setConfig({ - // rubicon: { - // fpkvs: { - // link: 'email' // should merge this with what is in the localStorage! - // } - // } - // }); - - // 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 = expectedPvid; - - // // 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(setDataInLocalStorageStub.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: expectedPvid // new pvid stored - // }); - // }); - - // it('should throw out session if past expires time and create new one', function () { - // // 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' } - // }; - // getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); - - // config.setConfig({ - // rubicon: { - // fpkvs: { - // link: 'email' // should merge this with what is in the localStorage! - // } - // } - // }); - - // 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 = expectedPvid; - - // // 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(setDataInLocalStorageStub.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: expectedPvid // new pvid stored - // }); - // }); - }); + it('should pick up existing localStorage and use its values', function () { + // set some localStorage + let inputlocalStorage = { + id: '987654', + start: 1519767017881, // 15 mins before "now" + expires: 1519767039481, // six hours later + lastSeen: 1519766113781, + fpkvs: { source: 'tw' } + }; + getDataFromLocalStorageStub.withArgs('mgniSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + config.setConfig({ + rubicon: { + fpkvs: { + link: 'email' // should merge this with what is in the localStorage! + } + } + }); + 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: 1519767017881, + expires: 1519767039481, + pvid: expectedPvid + } + expectedMessage.fpkvs = [ + { key: 'source', value: 'tw' }, + { key: 'link', value: 'email' } + ] + expect(message).to.deep.equal(expectedMessage); + + let calledWith; + try { + calledWith = JSON.parse(atob(setDataInLocalStorageStub.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: expectedPvid // new pvid stored + }); + }); + + it('should overwrite matching localstorge value and use its remaining values', function () { + sandbox.stub(utils, 'getWindowLocation').returns({ 'search': '?utm_source=fb&utm_click=dog' }); + // set some localStorage + let inputlocalStorage = { + id: '987654', + start: 1519766113781, // 15 mins before "now" + expires: 1519787713781, // six hours later + lastSeen: 1519766113781, + fpkvs: { source: 'tw', link: 'email' } + }; + getDataFromLocalStorageStub.withArgs('mgniSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + config.setConfig({ + rubicon: { + fpkvs: { + link: 'email' // should merge this with what is in the localStorage! + } + } + }); + 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: expectedPvid + } + expectedMessage.fpkvs = [ + { key: 'source', value: 'fb' }, + { key: 'link', value: 'email' }, + { key: 'click', value: 'dog' } + ] + + message.fpkvs.sort((left, right) => left.key < right.key); + expectedMessage.fpkvs.sort((left, right) => left.key < right.key); + + expect(message).to.deep.equal(expectedMessage); + + let calledWith; + try { + calledWith = JSON.parse(atob(setDataInLocalStorageStub.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: 'fb', link: 'email', click: 'dog' }, // link merged in + pvid: expectedPvid // new pvid stored + }); + }); + + it('should throw out session if lastSeen > 30 mins ago and create new one', function () { + // 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' } + }; + getDataFromLocalStorageStub.withArgs('mgniSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + config.setConfig({ + rubicon: { + fpkvs: { + link: 'email' // should merge this with what is in the localStorage! + } + } + }); + + 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 = expectedPvid; + + // 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(setDataInLocalStorageStub.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: expectedPvid // new pvid stored + }); + }); + + it('should throw out session if past expires time and create new one', function () { + // 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' } + }; + getDataFromLocalStorageStub.withArgs('mgniSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + config.setConfig({ + rubicon: { + fpkvs: { + link: 'email' // should merge this with what is in the localStorage! + } + } + }); + + 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 = expectedPvid; + + // 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(setDataInLocalStorageStub.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: expectedPvid // new pvid stored + }); + }); + }); }); }); From 01160032aea9c1f6a07aa560969730dad3491014 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Wed, 27 Jul 2022 09:18:50 -0700 Subject: [PATCH 05/22] get rid of console log --- modules/magniteAnalyticsAdapter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 4a19c0f198c..772923a25ec 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -150,7 +150,6 @@ const sendAuctionEvent = (auctionId, trigger) => { const formatAuction = auction => { const auctionEvent = deepClone(auction); - console.log('FORMAT AUCTION: ', JSON.stringify(auctionEvent, null, 2)); // We stored adUnits and bids as objects for quick lookups, now they are mapped into arrays for PBA auctionEvent.adUnits = Object.entries(auctionEvent.adUnits).map(([tid, adUnit]) => { adUnit.bids = Object.entries(adUnit.bids).map(([bidId, bid]) => { From 797e0683ca35b1119b1211660b9f37f228f280ef Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Fri, 5 Aug 2022 11:33:01 -0700 Subject: [PATCH 06/22] pass render transaction id --- modules/magniteAnalyticsAdapter.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 772923a25ec..4ce35819440 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -447,7 +447,7 @@ const findMatchingAdUnitFromAuctions = (matchesFunction, returnFirstMatch) => { return matches; } -const getRenderingAuctionId = bidWonData => { +const getRenderingIds = bidWonData => { // if bid caching off -> return the bidWon auciton id if (!config.getConfig('useBidCache')) { return bidWonData.auctionId; @@ -459,13 +459,17 @@ const getRenderingAuctionId = bidWonData => { const gamHasRendered = deepAccess(cache, `auctions.${auction.auctionId}.gamRenders.${adUnit.adUnitCode}`); return adUnit.adUnitCode === bidWonData.adUnitCode && gamHasRendered; } - let { auction } = findMatchingAdUnitFromAuctions(matchingFunction, false); + let { adUnit, auction } = findMatchingAdUnitFromAuctions(matchingFunction, false); // If no match was found, we will use the actual bid won auction id - return (auction && auction.auctionId) || bidWonData.auctionId; + return { + renderTransactionId: (adUnit && adUnit.transactionId) || adUnit.transactionId, + renderAuctionId: (auction && auction.auctionId) || bidWonData.auctionId + } } const formatBidWon = bidWonData => { - let renderAuctionId = getRenderingAuctionId(bidWonData); + // get transaction and auction id of where this "rendered" + const { renderTransactionId, renderAuctionId } = getRenderingIds(bidWonData); // get the bid from the source auction id let bid = deepAccess(cache, `auctions.${bidWonData.auctionId}.auction.adUnits.${bidWonData.adUnitCode}.bids.${bidWonData.requestId}`); @@ -475,6 +479,7 @@ const formatBidWon = bidWonData => { sourceAuctionId: bidWonData.auctionId, renderAuctionId, transactionId: bidWonData.transactionId, + renderTransactionId, accountId, siteId: adUnit.siteId, zoneId: adUnit.zoneId, From da977a11dc4bcee6e1057186e0b607ee9d6f2868 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Mon, 22 Aug 2022 14:04:28 -0700 Subject: [PATCH 07/22] add bid Id --- modules/magniteAnalyticsAdapter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 4ce35819440..12c55229038 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -479,6 +479,7 @@ const formatBidWon = bidWonData => { sourceAuctionId: bidWonData.auctionId, renderAuctionId, transactionId: bidWonData.transactionId, + bidId: bidWonData.bidId, renderTransactionId, accountId, siteId: adUnit.siteId, From eaccfeee0c9fde742c77def72618f95c2494a0c7 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 25 Aug 2022 16:17:16 -0700 Subject: [PATCH 08/22] fix bidder done --- modules/magniteAnalyticsAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 12c55229038..9a199b85b91 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -745,7 +745,7 @@ magniteAdapter.track = ({ eventType, args }) => { const serverError = deepAccess(args, 'serverErrors.0'); const serverResponseTimeMs = args.serverResponseTimeMs; args.bids.forEach(bid => { - let cachedBid = deepAccess(cache, `auctions.${args.auctionId}.auction.adUnits.${args.transactionId}.bids.${bid.bidId}`); + let cachedBid = deepAccess(cache, `auctions.${bid.auctionId}.auction.adUnits.${bid.adUnitCode}.bids.${bid.bidId}`); if (typeof bid.serverResponseTimeMs !== 'undefined') { cachedBid.serverLatencyMillis = bid.serverResponseTimeMs; } else if (serverResponseTimeMs && bid.source === 's2s') { From 66c7d21288bf1cbc92437dd896c489922a4fb939 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 25 Aug 2022 17:55:57 -0700 Subject: [PATCH 09/22] fix tests --- modules/magniteAnalyticsAdapter.js | 24 ++++--- .../modules/magniteAnalyticsAdapter_spec.js | 70 +++++++++++++------ 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 9a199b85b91..d9ee881cdb0 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, deepSetValue, deepClone, logInfo } from '../src/utils.js'; +import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, deepSetValue, deepClone, logInfo, isGptPubadsDefined } from '../src/utils.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; @@ -63,7 +63,7 @@ const resetConfs = () => { } rubiConf = { pvid: generateUUID().slice(0, 8), - analyticsEventDelay: 0, + analyticsEventDelay: 200, analyticsBatchTimeout: 5000, dmBilling: { enabled: false, @@ -450,7 +450,10 @@ const findMatchingAdUnitFromAuctions = (matchesFunction, returnFirstMatch) => { const getRenderingIds = bidWonData => { // if bid caching off -> return the bidWon auciton id if (!config.getConfig('useBidCache')) { - return bidWonData.auctionId; + return { + renderTransactionId: bidWonData.transactionId, + renderAuctionId: bidWonData.auctionId + }; } // a rendering auction id is the LATEST auction / adunit which contains GAM ID's @@ -462,7 +465,7 @@ const getRenderingIds = bidWonData => { let { adUnit, auction } = findMatchingAdUnitFromAuctions(matchingFunction, false); // If no match was found, we will use the actual bid won auction id return { - renderTransactionId: (adUnit && adUnit.transactionId) || adUnit.transactionId, + renderTransactionId: (adUnit && adUnit.transactionId) || bidWonData.transactionId, renderAuctionId: (auction && auction.auctionId) || bidWonData.auctionId } } @@ -548,11 +551,6 @@ const subscribeToGamSlots = () => { }); } -// listen to gam slot renders! -window.googletag = window.googletag || {}; -window.googletag.cmd = window.googletag.cmd || []; -window.googletag.cmd.push(() => subscribeToGamSlots()); - let accountId; let endpoint; @@ -576,6 +574,14 @@ function enableMgniAnalytics(config = {}) { if (!error) { magniteAdapter.originEnableAnalytics(config); } + // listen to gam slot renders! + if (isGptPubadsDefined()) { + subscribeToGamSlots(); + } else { + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(() => subscribeToGamSlots()); + } }; magniteAdapter.enableAnalytics = enableMgniAnalytics; diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index c174d959e0a..faca1ecc950 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -183,6 +183,7 @@ const MOCK = { }, BID_WON: { 'bidderCode': 'rubicon', + 'bidId': '23fcd8cf4bf0d7', 'adId': '3c0b59947ced11', 'requestId': '23fcd8cf4bf0d7', 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', @@ -204,7 +205,7 @@ const ANALYTICS_MESSAGE = { 'version': '7.8.0-pre', 'referrerHostname': 'a-test-domain.com', 'timestamps': { - 'eventTime': 1519767018781, + 'eventTime': 1519767013981, 'prebidLoaded': magniteAdapter.MODULE_INITIALIZED_TIME }, 'wrapper': { @@ -213,8 +214,8 @@ const ANALYTICS_MESSAGE = { 'session': { 'id': '12345678-1234-1234-1234-123456789abc', 'pvid': '12345678', - 'start': 1519767018781, - 'expires': 1519788618781 + 'start': 1519767013981, + 'expires': 1519788613981 }, 'auctions': [ { @@ -267,6 +268,16 @@ const ANALYTICS_MESSAGE = { 'auctionEnd': 1658868384019 } ], + 'gamRenders': [ + { + 'adSlot': 'box', + 'advertiserId': 1111, + 'creativeId': 2222, + 'lineItemId': 3333, + 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a' + } + ], 'bidsWon': [ { 'bidder': 'rubicon', @@ -284,6 +295,7 @@ const ANALYTICS_MESSAGE = { }, 'sourceAuctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', 'renderAuctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'renderTransactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', 'accountId': 1001, 'siteId': 267318, @@ -294,18 +306,31 @@ const ANALYTICS_MESSAGE = { 'adUnitCode': 'box' } ], - 'trigger': 'auctionEnd' + 'trigger': 'gam' } describe('magnite analytics adapter', function () { let sandbox; let clock; let getDataFromLocalStorageStub, setDataInLocalStorageStub, localStorageIsEnabledStub; + let gptSlot0; + let gptSlotRenderEnded0; beforeEach(function () { + mockGpt.enable(); + gptSlot0 = mockGpt.makeSlot({ code: 'box' }); + gptSlotRenderEnded0 = { + eventName: 'slotRenderEnded', + params: { + slot: gptSlot0, + isEmpty: false, + advertiserId: 1111, + sourceAgnosticCreativeId: 2222, + sourceAgnosticLineItemId: 3333 + } + }; getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); setDataInLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); - mockGpt.disable(); sandbox = sinon.sandbox.create(); localStorageIsEnabledStub.returns(true); @@ -337,6 +362,7 @@ describe('magnite analytics adapter', function () { setDataInLocalStorageStub.restore(); localStorageIsEnabledStub.restore(); magniteAdapter.disableAnalytics(); + mockGpt.disable(); }); it('should require accountId', function () { @@ -381,7 +407,7 @@ describe('magnite analytics adapter', function () { } }); expect(rubiConf).to.deep.equal({ - analyticsEventDelay: 0, + analyticsEventDelay: 200, analyticsBatchTimeout: 5000, dmBilling: { enabled: false, @@ -407,7 +433,7 @@ describe('magnite analytics adapter', function () { } }); expect(rubiConf).to.deep.equal({ - analyticsEventDelay: 0, + analyticsEventDelay: 200, analyticsBatchTimeout: 3000, dmBilling: { enabled: false, @@ -434,7 +460,7 @@ describe('magnite analytics adapter', function () { } }); expect(rubiConf).to.deep.equal({ - analyticsEventDelay: 0, + analyticsEventDelay: 200, analyticsBatchTimeout: 3000, dmBilling: { enabled: false, @@ -454,7 +480,7 @@ describe('magnite analytics adapter', function () { }); describe('when handling events', function () { - function performStandardAuction(gptEvents, auctionId = MOCK.AUCTION_INIT.auctionId) { + function performStandardAuction(gptEvents = [gptSlotRenderEnded0], auctionId = MOCK.AUCTION_INIT.auctionId) { events.emit(AUCTION_INIT, { ...MOCK.AUCTION_INIT, auctionId }); events.emit(BID_REQUESTED, { ...MOCK.BID_REQUESTED, auctionId }); events.emit(BID_RESPONSE, { ...MOCK.BID_RESPONSE, auctionId }); @@ -466,7 +492,7 @@ describe('magnite analytics adapter', function () { } events.emit(BID_WON, { ...MOCK.BID_WON, auctionId }); - clock.tick(rubiConf.analyticsBatchTimeout + 1000); + clock.tick(rubiConf.analyticsEventDelay + 100); } beforeEach(function () { @@ -729,9 +755,9 @@ describe('magnite analytics adapter', function () { 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" + start: 1519767017881, // should have stayed same + expires: 1519767039481, // should have stayed same + lastSeen: 1519767013981, // lastSeen updated to our "now" fpkvs: { source: 'tw', link: 'email' }, // link merged in pvid: expectedPvid // new pvid stored }); @@ -792,7 +818,7 @@ describe('magnite analytics adapter', function () { 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" + lastSeen: 1519767013981, // lastSeen updated to our "now" fpkvs: { source: 'fb', link: 'email', click: 'dog' }, // link merged in pvid: expectedPvid // new pvid stored }); @@ -841,10 +867,10 @@ describe('magnite analytics adapter', function () { } 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" + id: STUBBED_UUID, // should have generated not used input + start: 1519767013981, // should have stayed same + expires: 1519788613981, // should have stayed same + lastSeen: 1519767013981, // lastSeen updated to our "now" fpkvs: { link: 'email' }, // link merged in pvid: expectedPvid // new pvid stored }); @@ -893,10 +919,10 @@ describe('magnite analytics adapter', function () { } 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" + id: STUBBED_UUID, // should have generated and not used same one + start: 1519767013981, // should have stayed same + expires: 1519788613981, // should have stayed same + lastSeen: 1519767013981, // lastSeen updated to our "now" fpkvs: { link: 'email' }, // link merged in pvid: expectedPvid // new pvid stored }); From 56b0b1b1eb68082888f80b8808a828ce6e99f0f2 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Fri, 26 Aug 2022 09:43:27 -0700 Subject: [PATCH 10/22] Handle PPI elementids case --- modules/magniteAnalyticsAdapter.js | 20 +++++++- .../modules/magniteAnalyticsAdapter_spec.js | 49 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index d9ee881cdb0..39d82bba88f 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -59,7 +59,8 @@ const resetConfs = () => { timeouts: {}, billing: {}, pendingEvents: {}, - eventPending: false + eventPending: false, + elementIdMap: {} } rubiConf = { pvid: generateUUID().slice(0, 8), @@ -514,7 +515,9 @@ const subscribeToGamSlots = () => { // We want to find the FIRST auction - adUnit that matches and does not have gam data yet const matchingFunction = (adUnit, auction) => { // first it has to match the slot - const matchesSlot = isMatchingAdSlot(adUnit.adUnitCode); + // if the code is present in the elementIdMap then we use the matched id as code here + const elementIds = cache.elementIdMap[adUnit.adUnitCode] || [adUnit.adUnitCode]; + const matchesSlot = elementIds.some(isMatchingAdSlot); // next it has to have NOT already been counted as gam rendered const gamHasRendered = deepAccess(cache, `auctions.${auction.auctionId}.gamRenders.${adUnit.adUnitCode}`); @@ -666,6 +669,19 @@ magniteAdapter.track = ({ eventType, args }) => { ad.bids = {}; adMap[adUnit.code] = ad; gamRenders[adUnit.code] = false; + + // Handle case elementId's (div Id's) are set on adUnit - PPI + const elementIds = deepAccess(adUnit, 'ortb2Imp.ext.data.elementid'); + if (elementIds) { + cache.elementIdMap[adUnit.code] = cache.elementIdMap[adUnit.code] || []; + // set it to array if set to string to be careful (should be array of strings) + const newIds = typeof elementIds === 'string' ? [elementIds] : elementIds; + newIds.forEach(id => { + if (!cache.elementIdMap[adUnit.code].includes(id)) { + cache.elementIdMap[adUnit.code].push(id); + } + }); + } return adMap; }, {}); diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index faca1ecc950..6a5bb789c2e 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -928,5 +928,54 @@ describe('magnite analytics adapter', function () { }); }); }); + + it('should send gam data if adunit has elementid ortb2 fields', function () { + // update auction init mock to have the elementids in the adunit + // and change adUnitCode to be hashes + let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); + auctionInit.adUnits[0].ortb2Imp.ext.data.elementid = [gptSlot0.getSlotElementId()]; + auctionInit.adUnits[0].code = '1a2b3c4d'; + + // bid request + let bidRequested = utils.deepClone(MOCK.BID_REQUESTED); + bidRequested.bids[0].adUnitCode = '1a2b3c4d'; + + // bid response + let bidResponse = utils.deepClone(MOCK.BID_RESPONSE); + bidResponse.adUnitCode = '1a2b3c4d'; + + // bidder done + let bidderDone = utils.deepClone(MOCK.BIDDER_DONE); + bidderDone.bids[0].adUnitCode = '1a2b3c4d'; + + // bidder done + let bidWon = utils.deepClone(MOCK.BID_WON); + bidWon.adUnitCode = '1a2b3c4d'; + + // Run auction + events.emit(AUCTION_INIT, auctionInit); + events.emit(BID_REQUESTED, bidRequested); + events.emit(BID_RESPONSE, bidResponse); + events.emit(BIDDER_DONE, bidderDone); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + // emmit gpt events and bidWon + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + + events.emit(BID_WON, bidWon); + + // hit the eventDelay + clock.tick(rubiConf.analyticsEventDelay + 100); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + + // new adUnitCodes in payload + expectedMessage.auctions[0].adUnits[0].adUnitCode = '1a2b3c4d'; + expectedMessage.bidsWon[0].adUnitCode = '1a2b3c4d'; + expect(message).to.deep.equal(expectedMessage); + }); }); }); From 15ea0d38d46f532d46330f20fa7cd5c665261cac Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Fri, 26 Aug 2022 18:28:24 -0700 Subject: [PATCH 11/22] added lots of test + minor fixes --- modules/magniteAnalyticsAdapter.js | 44 +- .../modules/magniteAnalyticsAdapter_spec.js | 936 +++++++++++++++++- test/spec/modules/magniteAnalyticsSchema.json | 466 --------- 3 files changed, 938 insertions(+), 508 deletions(-) delete mode 100644 test/spec/modules/magniteAnalyticsSchema.json diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 39d82bba88f..592893bd1b1 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -95,7 +95,7 @@ const adUnitIsOnlyInstream = adUnit => { } const sendPendingEvents = () => { - cache.pendingEvents.trigger = 'batchedEvents'; + cache.pendingEvents.trigger = `batched-${Object.keys(cache.pendingEvents).sort().join('-')}`; sendEvent(cache.pendingEvents); cache.pendingEvents = {}; cache.eventPending = false; @@ -159,6 +159,13 @@ const formatAuction = auction => { if (statusPriority.indexOf(bid.status) > statusPriority.indexOf(adUnit.status)) { adUnit.status = bid.status; } + + // If PBS told us to overwrite the bid ID, do so + if (bid.pbsBidId) { + bid.oldBidId = bid.bidId; + bid.bidId = bid.pbsBidId; + delete bid.pbsBidId; + } return bid; }); return adUnit; @@ -222,12 +229,6 @@ export const parseBidResponse = (bid, previousBidResponse) => { return previousBidResponse; } - // if pbs gave us back a bidId, we need to use it and update our bidId to PBA - const pbsId = (bid.pbsBidId == 0 ? generateUUID() : bid.pbsBidId) || (bid.seatBidId == 0 ? generateUUID() : bid.seatBidId); - if (pbsId) { - bid.bidId = pbsId; - } - return pick(bid, [ 'bidPriceUSD', () => responsePrice, 'dealId', dealId => dealId || undefined, @@ -449,7 +450,7 @@ const findMatchingAdUnitFromAuctions = (matchesFunction, returnFirstMatch) => { } const getRenderingIds = bidWonData => { - // if bid caching off -> return the bidWon auciton id + // if bid caching off -> return the bidWon auction id if (!config.getConfig('useBidCache')) { return { renderTransactionId: bidWonData.transactionId, @@ -478,12 +479,13 @@ const formatBidWon = bidWonData => { // get the bid from the source auction id let bid = deepAccess(cache, `auctions.${bidWonData.auctionId}.auction.adUnits.${bidWonData.adUnitCode}.bids.${bidWonData.requestId}`); let adUnit = deepAccess(cache, `auctions.${bidWonData.auctionId}.auction.adUnits.${bidWonData.adUnitCode}`); - return { + let bidWon = { ...bid, sourceAuctionId: bidWonData.auctionId, renderAuctionId, transactionId: bidWonData.transactionId, - bidId: bidWonData.bidId, + sourceTransactionId: bidWonData.transactionId, + bidId: bid.pbsBidId || bidWonData.bidId, // if PBS had us overwrite bidId, use that as signal renderTransactionId, accountId, siteId: adUnit.siteId, @@ -491,6 +493,8 @@ const formatBidWon = bidWonData => { mediaTypes: adUnit.mediaTypes, adUnitCode: adUnit.adUnitCode } + delete bidWon.pbsBidId; // if pbsBidId is there delete it (no need to pass it) + return bidWon; } const formatGamEvent = (slotEvent, adUnit, auction) => { @@ -545,7 +549,7 @@ const subscribeToGamSlots = () => { // wait for bid wons a bit or send right away if (rubiConf.analyticsEventDelay > 0) { setTimeout(() => { - sendAuctionEvent(auctionId, 'gam'); + sendAuctionEvent(auctionId, 'gam-delayed'); }, rubiConf.analyticsEventDelay); } else { sendAuctionEvent(auctionId, 'gam'); @@ -663,9 +667,6 @@ magniteAdapter.track = ({ eventType, args }) => { ad.pbAdSlot = deepAccess(adUnit, 'ortb2Imp.ext.data.pbadslot'); ad.pattern = deepAccess(adUnit, 'ortb2Imp.ext.data.aupname'); ad.gpid = deepAccess(adUnit, 'ortb2Imp.ext.gpid'); - if (deepAccess(bid, 'ortb2Imp.ext.data.adserver.name') === 'gam') { - ad.gam = { adSlot: bid.ortb2Imp.ext.data.adserver.adslot } - } ad.bids = {}; adMap[adUnit.code] = ad; gamRenders[adUnit.code] = false; @@ -760,8 +761,14 @@ magniteAdapter.track = ({ eventType, args }) => { code: 'request-error' }; } - bid.clientLatencyMillis = args.timeToRespond || Date.now() - cache.auctions[args.auctionId].auctionStart; + bid.clientLatencyMillis = args.timeToRespond || Date.now() - cache.auctions[args.auctionId].auction.auctionStart; bid.bidResponse = parseBidResponse(args, bid.bidResponse); + + // if pbs gave us back a bidId, we need to use it and update our bidId to PBA + const pbsBidId = (args.pbsBidId == 0 ? generateUUID() : args.pbsBidId) || (args.seatBidId == 0 ? generateUUID() : args.seatBidId); + if (pbsBidId) { + bid.pbsBidId = pbsBidId; + } break; case BIDDER_DONE: const serverError = deepAccess(args, 'serverErrors.0'); @@ -781,6 +788,11 @@ magniteAdapter.track = ({ eventType, args }) => { description: serverError.message } } + + // set client latency if not done yet + if (!cachedBid.clientLatencyMillis) { + cachedBid.clientLatencyMillis = Date.now() - cache.auctions[args.auctionId].auction.auctionStart; + } }); break; case BID_WON: @@ -799,7 +811,7 @@ magniteAdapter.track = ({ eventType, args }) => { // if we are not waiting OR it is instream only auction if (isOnlyInstreamAuction || rubiConf.analyticsBatchTimeout === 0) { - sendAuctionEvent(args.auctionId, 'noBatch'); + sendAuctionEvent(args.auctionId, 'solo-auction'); } else { // start timer to send batched payload just in case we don't hear any BID_WON events cache.timeouts[args.auctionId] = setTimeout(() => { diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index 6a5bb789c2e..80dc3852a4f 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -12,19 +12,7 @@ import { setConfig, addBidResponseHook, } from 'modules/currency.js'; - -let Ajv = require('ajv'); -let schema = require('./magniteAnalyticsSchema.json'); -let ajv = new Ajv({ - allErrors: true -}); - -let validator = ajv.compile(schema); - -function validate(message) { - validator(message); - expect(validator.errors).to.deep.equal(null); -} +import { deepAccess } from '../../../src/utils.js'; let events = require('src/events.js'); let utils = require('src/utils.js'); @@ -295,6 +283,7 @@ const ANALYTICS_MESSAGE = { }, 'sourceAuctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', 'renderAuctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'sourceTransactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', 'renderTransactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', 'accountId': 1001, @@ -306,7 +295,7 @@ const ANALYTICS_MESSAGE = { 'adUnitCode': 'box' } ], - 'trigger': 'gam' + 'trigger': 'gam-delayed' } describe('magnite analytics adapter', function () { @@ -480,7 +469,12 @@ describe('magnite analytics adapter', function () { }); describe('when handling events', function () { - function performStandardAuction(gptEvents = [gptSlotRenderEnded0], auctionId = MOCK.AUCTION_INIT.auctionId) { + function performStandardAuction({ + gptEvents = [gptSlotRenderEnded0], + auctionId = MOCK.AUCTION_INIT.auctionId, + eventDelay = rubiConf.analyticsEventDelay, + sendBidWon = true + } = {}) { events.emit(AUCTION_INIT, { ...MOCK.AUCTION_INIT, auctionId }); events.emit(BID_REQUESTED, { ...MOCK.BID_REQUESTED, auctionId }); events.emit(BID_RESPONSE, { ...MOCK.BID_RESPONSE, auctionId }); @@ -491,8 +485,13 @@ describe('magnite analytics adapter', function () { gptEvents.forEach(gptEvent => mockGpt.emitEvent(gptEvent.eventName, gptEvent.params)); } - events.emit(BID_WON, { ...MOCK.BID_WON, auctionId }); - clock.tick(rubiConf.analyticsEventDelay + 100); + if (sendBidWon) { + events.emit(BID_WON, { ...MOCK.BID_WON, auctionId }); + } + + if (eventDelay > 0) { + clock.tick(eventDelay); + } } beforeEach(function () { @@ -514,7 +513,6 @@ describe('magnite analytics adapter', function () { expect(request.url).to.equal('//localhost:9999/event'); let message = JSON.parse(request.requestBody); - validate(message); expect(message).to.deep.equal(ANALYTICS_MESSAGE); }); @@ -620,7 +618,6 @@ describe('magnite analytics adapter', function () { expect(request.url).to.equal('//localhost:9999/event'); let message = JSON.parse(request.requestBody); - validate(message); expect(message).to.deep.equal(expectedMessage); }); @@ -638,7 +635,6 @@ describe('magnite analytics adapter', function () { 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 = STUBBED_UUID.slice(0, 8); @@ -665,7 +661,6 @@ describe('magnite analytics adapter', function () { 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 = STUBBED_UUID.slice(0, 8); @@ -694,7 +689,6 @@ describe('magnite analytics adapter', function () { 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 = STUBBED_UUID.slice(0, 8); @@ -731,7 +725,6 @@ describe('magnite analytics adapter', function () { 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 = { @@ -787,7 +780,6 @@ describe('magnite analytics adapter', function () { 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 = { @@ -847,7 +839,6 @@ describe('magnite analytics adapter', function () { 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 @@ -899,7 +890,6 @@ describe('magnite analytics adapter', function () { 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 @@ -977,5 +967,899 @@ describe('magnite analytics adapter', function () { expectedMessage.bidsWon[0].adUnitCode = '1a2b3c4d'; expect(message).to.deep.equal(expectedMessage); }); + + it('should delay the event call depending on analyticsEventDelay config', function () { + config.setConfig({ + rubicon: { + analyticsEventDelay: 2000 + } + }); + performStandardAuction({ eventDelay: 0 }); + + // Should not be sent until delay + expect(server.requests.length).to.equal(0); + + // tick the clock and it should fire + clock.tick(2000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + // The timestamps should be changed from the default by 1800 (set eventDelay - eventDelay default (200)) + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + expectedMessage.session.expires = expectedMessage.session.expires + 1800; + expectedMessage.session.start = expectedMessage.session.start + 1800; + expectedMessage.timestamps.eventTime = expectedMessage.timestamps.eventTime + 1800; + + expect(message).to.deep.equal(expectedMessage); + }); + + ['seatBidId', 'pbsBidId'].forEach(pbsParam => { + it(`should overwrite prebid bidId with incoming PBS ${pbsParam}`, function () { + // bid response + let seatBidResponse = utils.deepClone(MOCK.BID_RESPONSE); + seatBidResponse[pbsParam] = 'abc-123-do-re-me'; + + // Run auction + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, seatBidResponse); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + // emmit gpt events and bidWon + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + + events.emit(BID_WON, MOCK.BID_WON); + + // hit the eventDelay + clock.tick(rubiConf.analyticsEventDelay); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + + // new adUnitCodes in payload + expectedMessage.auctions[0].adUnits[0].bids[0].bidId = 'abc-123-do-re-me'; + expectedMessage.auctions[0].adUnits[0].bids[0].oldBidId = '23fcd8cf4bf0d7'; + expectedMessage.bidsWon[0].bidId = 'abc-123-do-re-me'; + expect(message).to.deep.equal(expectedMessage); + }); + }); + + [0, '0'].forEach(pbsParam => { + it(`should generate new bidId if incoming pbsBidId is ${pbsParam}`, function () { + // bid response + let seatBidResponse = utils.deepClone(MOCK.BID_RESPONSE); + seatBidResponse.pbsBidId = pbsParam; + + // Run auction + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, seatBidResponse); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + // emmit gpt events and bidWon + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + + events.emit(BID_WON, MOCK.BID_WON); + + // hit the eventDelay + clock.tick(rubiConf.analyticsEventDelay); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + + // new adUnitCodes in payload + expectedMessage.auctions[0].adUnits[0].bids[0].bidId = STUBBED_UUID; + expectedMessage.auctions[0].adUnits[0].bids[0].oldBidId = '23fcd8cf4bf0d7'; + expectedMessage.bidsWon[0].bidId = STUBBED_UUID; + expect(message).to.deep.equal(expectedMessage); + }); + }); + + it(`should pick highest cpm if more than one bidResponse comes in`, function () { + // Run auction + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + const bidResp = utils.deepClone(MOCK.BID_RESPONSE); + + // emit some bid responses + [1.0, 5.5, 0.1].forEach(cpm => { + events.emit(BID_RESPONSE, { ...bidResp, cpm }); + }); + + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + // emmit gpt events and bidWon + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + + events.emit(BID_WON, MOCK.BID_WON); + + // hit the eventDelay + clock.tick(rubiConf.analyticsEventDelay); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + + // highest cpm in payload + expectedMessage.auctions[0].adUnits[0].bids[0].bidResponse.bidPriceUSD = 5.5; + expectedMessage.bidsWon[0].bidResponse.bidPriceUSD = 5.5; + expect(message).to.deep.equal(expectedMessage); + }); + + it('should send bid won events by themselves if emitted after auction pba payload is sent', function () { + performStandardAuction({ sendBidWon: false }); + + // Now send bidWon + events.emit(BID_WON, MOCK.BID_WON); + + // tick the event delay time + clock.tick(rubiConf.analyticsEventDelay); + + // should see two server requests + expect(server.requests.length).to.equal(2); + + // first is normal analytics event without bidWon + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + delete expectedMessage.bidsWon; + + let message = JSON.parse(server.requests[0].requestBody); + expect(message).to.deep.equal(expectedMessage); + + // second is just a bidWon (remove gam and auction event) + message = JSON.parse(server.requests[1].requestBody); + + let expectedMessage2 = utils.deepClone(ANALYTICS_MESSAGE); + delete expectedMessage2.auctions; + delete expectedMessage2.gamRenders; + + // second event should be event delay time after first one + expectedMessage2.session.expires = expectedMessage.session.expires + rubiConf.analyticsEventDelay; + expectedMessage2.session.start = expectedMessage.session.start + rubiConf.analyticsEventDelay; + expectedMessage2.timestamps.eventTime = expectedMessage.timestamps.eventTime + rubiConf.analyticsEventDelay; + + // trigger is `batched-bidsWon` + expectedMessage2.trigger = 'batched-bidsWon'; + + expect(message).to.deep.equal(expectedMessage2); + }); + + it('should send gamRender events by themselves if emitted after auction pba payload is sent', function () { + // dont send extra events and hit the batch timeout + performStandardAuction({ gptEvents: [], sendBidWon: false, eventDelay: rubiConf.analyticsBatchTimeout }); + + // Now send gptEvent and bidWon + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + events.emit(BID_WON, MOCK.BID_WON); + + // tick the event delay time + clock.tick(rubiConf.analyticsEventDelay); + + // should see two server requests + expect(server.requests.length).to.equal(2); + + // first is normal analytics event without bidWon or gam + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + delete expectedMessage.bidsWon; + delete expectedMessage.gamRenders; + + // timing changes a bit -> timestamps should be batchTimeout - event delay later + const expectedExtraTime = rubiConf.analyticsBatchTimeout - rubiConf.analyticsEventDelay; + expectedMessage.session.expires = expectedMessage.session.expires + expectedExtraTime; + expectedMessage.session.start = expectedMessage.session.start + expectedExtraTime; + expectedMessage.timestamps.eventTime = expectedMessage.timestamps.eventTime + expectedExtraTime; + + // since gam event did not fire, the trigger should be auctionEnd + expectedMessage.trigger = 'auctionEnd'; + + let message = JSON.parse(server.requests[0].requestBody); + expect(message).to.deep.equal(expectedMessage); + + // second is gam and bid won + message = JSON.parse(server.requests[1].requestBody); + + let expectedMessage2 = utils.deepClone(ANALYTICS_MESSAGE); + // second event should be event delay time after first one + expectedMessage2.session.expires = expectedMessage.session.expires + rubiConf.analyticsEventDelay; + expectedMessage2.session.start = expectedMessage.session.start + rubiConf.analyticsEventDelay; + expectedMessage2.timestamps.eventTime = expectedMessage.timestamps.eventTime + rubiConf.analyticsEventDelay; + delete expectedMessage2.auctions; + + // trigger should be `batched-bidsWon-gamRender` + expectedMessage2.trigger = 'batched-bidsWon-gamRenders'; + + expect(message).to.deep.equal(expectedMessage2); + }); + + it('should send all events solo if delay and batch set to 0', function () { + const defaultDelay = rubiConf.analyticsEventDelay; + config.setConfig({ + rubicon: { + analyticsBatchTimeout: 0, + analyticsEventDelay: 0 + } + }); + + performStandardAuction({eventDelay: 0}); + + // should be 3 requests + expect(server.requests.length).to.equal(3); + + // grab expected 3 requests from default message + let { auctions, gamRenders, bidsWon, ...rest } = utils.deepClone(ANALYTICS_MESSAGE); + + // rest of payload should have timestamps changed to be - default eventDelay since we changed it to 0 + rest.session.expires = rest.session.expires - defaultDelay; + rest.session.start = rest.session.start - defaultDelay; + rest.timestamps.eventTime = rest.timestamps.eventTime - defaultDelay; + + // loop through and assert events fired in correct order with correct stuff + [ + { expectedMessage: { auctions, ...rest }, trigger: 'solo-auction' }, + { expectedMessage: { gamRenders, ...rest }, trigger: 'solo-gam' }, + { expectedMessage: { bidsWon, ...rest }, trigger: 'solo-bidWon' }, + ].forEach((stuff, requestNum) => { + let message = JSON.parse(server.requests[requestNum].requestBody); + stuff.expectedMessage.trigger = stuff.trigger; + expect(message).to.deep.equal(stuff.expectedMessage); + }); + }); + + it(`should correctly mark bids as timed out`, function () { + // Run auction (simulate bidder timed out in 1000 ms) + const auctionStart = Date.now() - 1000; + events.emit(AUCTION_INIT, { ...MOCK.AUCTION_INIT, timestamp: auctionStart }); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + // emit bid timeout + events.emit(BID_TIMEOUT, [ + { + auctionId: MOCK.AUCTION_INIT.auctionId, + adUnitCode: MOCK.AUCTION_INIT.adUnits[0].code, + bidId: MOCK.BID_REQUESTED.bids[0].bidId + } + ]); + + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + // emmit gpt events and bidWon + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + + // hit the eventDelay + clock.tick(rubiConf.analyticsEventDelay); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + + // should see error time out bid + expectedMessage.auctions[0].adUnits[0].bids[0].status = 'error'; + expectedMessage.auctions[0].adUnits[0].bids[0].error = { + code: 'timeout-error', + description: 'prebid.js timeout' // will help us diff if timeout was set by PBS or PBJS + }; + + // should not see bidResponse or bidsWon + delete expectedMessage.auctions[0].adUnits[0].bids[0].bidResponse; + delete expectedMessage.bidsWon; + + // adunit should be marked as error + expectedMessage.auctions[0].adUnits[0].status = 'error'; + + // timed out in 1000 ms + expectedMessage.auctions[0].adUnits[0].bids[0].clientLatencyMillis = 1000; + + expectedMessage.auctions[0].auctionStart = auctionStart; + + expect(message).to.deep.equal(expectedMessage); + }); + + [ + { name: 'aupname', adUnitPath: 'adUnits.0.ortb2Imp.ext.data.aupname', eventPath: 'auctions.0.adUnits.0.pattern', input: '1234/mycoolsite/*&gpt_leaderboard&deviceType=mobile' }, + { name: 'gpid', adUnitPath: 'adUnits.0.ortb2Imp.ext.gpid', eventPath: 'auctions.0.adUnits.0.gpid', input: '1234/gpid/path' }, + { name: 'pbadslot', adUnitPath: 'adUnits.0.ortb2Imp.ext.data.pbadslot', eventPath: 'auctions.0.adUnits.0.pbAdSlot', input: '1234/pbadslot/path' } + ].forEach(test => { + it(`should correctly pass ${test.name}`, function () { + // bid response + let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); + utils.deepSetValue(auctionInit, test.adUnitPath, test.input); + + // Run auction + events.emit(AUCTION_INIT, auctionInit); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + // emmit gpt events and bidWon + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + + events.emit(BID_WON, MOCK.BID_WON); + + // hit the eventDelay + clock.tick(rubiConf.analyticsEventDelay); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + // pattern in payload + expect(deepAccess(message, test.eventPath)).to.equal(test.input); + }); + }); + + it('should pass bidderDetail for multibid auctions', function () { + let bidResponse = utils.deepClone(MOCK.BID_RESPONSE); + bidResponse.targetingBidder = 'rubi2'; + bidResponse.originalRequestId = bidResponse.requestId; + bidResponse.requestId = '1a2b3c4d5e6f7g8h9'; + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, bidResponse); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + // emmit gpt events and bidWon + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + + let bidWon = utils.deepClone(MOCK.BID_WON); + bidWon.bidId = bidWon.requestId = '1a2b3c4d5e6f7g8h9'; + bidWon.bidderDetail = 'rubi2'; + events.emit(BID_WON, bidWon); + + // hit the eventDelay + clock.tick(rubiConf.analyticsEventDelay); + + expect(server.requests.length).to.equal(1); + + let message = JSON.parse(server.requests[0].requestBody); + + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + + // expect an extra bid added + expectedMessage.auctions[0].adUnits[0].bids.push({ + ...ANALYTICS_MESSAGE.auctions[0].adUnits[0].bids[0], + bidderDetail: 'rubi2', + bidId: '1a2b3c4d5e6f7g8h9' + }); + + // bid won is our extra bid + expectedMessage.bidsWon[0].bidderDetail = 'rubi2'; + expectedMessage.bidsWon[0].bidId = '1a2b3c4d5e6f7g8h9'; + + expect(message).to.deep.equal(expectedMessage); + }); + + it('should pass bidderDetail for multibid auctions', function () { + // Set the rates + setConfig({ + adServerCurrency: 'JPY', + rates: { + USD: { + JPY: 100 + } + } + }); + + // set our bid response to JPY + let bidResponse = utils.deepClone(MOCK.BID_RESPONSE); + bidResponse.currency = 'JPY'; + bidResponse.cpm = 100; + + // Now add the bidResponse hook which hooks on the currenct conversion function onto the bid response + let innerBid; + addBidResponseHook(function (adCodeId, bid) { + innerBid = bid; + }, 'elementId', bidResponse); + + // Use the rubi analytics parseBidResponse Function to get the resulting cpm from the bid response! + const bidResponseObj = parseBidResponse(innerBid); + expect(bidResponseObj).to.have.property('bidPriceUSD'); + expect(bidResponseObj.bidPriceUSD).to.equal(1.0); + }); + + it('should use the integration type provided in the config instead of the default', () => { + config.setConfig({ + rubicon: { + int_type: 'testType' + } + }) + + performStandardAuction(); + + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message.integration).to.equal('testType'); + }); + describe('when handling bid caching', () => { + let auctionInits, bidRequests, bidResponses, bidsWon; + beforeEach(function () { + // set timing stuff to 0 so we clearly know when things fire + config.setConfig({ + useBidCache: true, + rubicon: { + analyticsEventDelay: 0, + analyticsBatchTimeout: 0 + } + }); + + // setup 3 auctions + auctionInits = [ + { ...MOCK.AUCTION_INIT, auctionId: 'auctionId-1', adUnits: [{ ...MOCK.AUCTION_INIT.adUnits[0], transactionId: 'tid-1' }] }, + { ...MOCK.AUCTION_INIT, auctionId: 'auctionId-2', adUnits: [{ ...MOCK.AUCTION_INIT.adUnits[0], transactionId: 'tid-2' }] }, + { ...MOCK.AUCTION_INIT, auctionId: 'auctionId-3', adUnits: [{ ...MOCK.AUCTION_INIT.adUnits[0], transactionId: 'tid-3' }] } + ]; + bidRequests = [ + { ...MOCK.BID_REQUESTED, auctionId: 'auctionId-1', bids: [{ ...MOCK.BID_REQUESTED.bids[0], bidId: 'bidId-1' }] }, + { ...MOCK.BID_REQUESTED, auctionId: 'auctionId-2', bids: [{ ...MOCK.BID_REQUESTED.bids[0], bidId: 'bidId-2' }] }, + { ...MOCK.BID_REQUESTED, auctionId: 'auctionId-3', bids: [{ ...MOCK.BID_REQUESTED.bids[0], bidId: 'bidId-3' }] } + ]; + bidResponses = [ + { ...MOCK.BID_RESPONSE, auctionId: 'auctionId-1', transactionId: 'tid-1', requestId: 'bidId-1' }, + { ...MOCK.BID_RESPONSE, auctionId: 'auctionId-2', transactionId: 'tid-2', requestId: 'bidId-2' }, + { ...MOCK.BID_RESPONSE, auctionId: 'auctionId-3', transactionId: 'tid-3', requestId: 'bidId-3' }, + ]; + bidsWon = [ + { ...MOCK.BID_WON, auctionId: 'auctionId-1', transactionId: 'tid-1', bidId: 'bidId-1', requestId: 'bidId-1' }, + { ...MOCK.BID_WON, auctionId: 'auctionId-2', transactionId: 'tid-2', bidId: 'bidId-2', requestId: 'bidId-2' }, + { ...MOCK.BID_WON, auctionId: 'auctionId-3', transactionId: 'tid-3', bidId: 'bidId-3', requestId: 'bidId-3' }, + ]; + }); + function runBasicAuction(auctionNum) { + events.emit(AUCTION_INIT, auctionInits[auctionNum]); + events.emit(BID_REQUESTED, bidRequests[auctionNum]); + events.emit(BID_RESPONSE, bidResponses[auctionNum]); + events.emit(BIDDER_DONE, { ...MOCK.BIDDER_DONE, auctionId: auctionInits[auctionNum].auctionId }); + events.emit(AUCTION_END, { ...MOCK.AUCTION_END, auctionId: auctionInits[auctionNum].auctionId }); + } + it('should select earliest auction to attach to', () => { + // get 3 auctions pending to send events + runBasicAuction(0); + runBasicAuction(1); + runBasicAuction(2); + + // emmit a gptEvent should attach to first auction + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + + // should be 4 requests so far (3 auctions + 1 gamRender) + expect(server.requests.length).to.equal(4); + + // 4th should be gamRender and should have Auciton # 1's id's + const message = JSON.parse(server.requests[3].requestBody); + const expectedMessage = { + ...ANALYTICS_MESSAGE.gamRenders[0], + auctionId: 'auctionId-1', + transactionId: 'tid-1' + }; + expect(message.gamRenders).to.deep.equal([expectedMessage]); + + // emit bidWon from first auction + events.emit(BID_WON, bidsWon[0]); + + // another request which is bidWon + expect(server.requests.length).to.equal(5); + const message1 = JSON.parse(server.requests[4].requestBody); + const expectedMessage1 = { + ...ANALYTICS_MESSAGE.bidsWon[0], + sourceAuctionId: 'auctionId-1', + renderAuctionId: 'auctionId-1', + sourceTransactionId: 'tid-1', + renderTransactionId: 'tid-1', + transactionId: 'tid-1', + bidId: 'bidId-1', + }; + expect(message1.bidsWon).to.deep.equal([expectedMessage1]); + }); + + [ + { useBidCache: true, expectedRenderId: 3 }, + { useBidCache: false, expectedRenderId: 2 } + ].forEach(test => { + it(`should match bidWon to correct render auction if useBidCache is ${test.useBidCache}`, () => { + config.setConfig({ useBidCache: test.useBidCache }); + // get 3 auctions pending to send events + runBasicAuction(0); + runBasicAuction(1); + runBasicAuction(2); + + // emmit 3 gpt Events, first two "empty" + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, { + slot: gptSlot0, + isEmpty: true, + }); + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, { + slot: gptSlot0, + isEmpty: true, + }); + // last one is valid + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + + // should be 6 requests so far (3 auctions + 3 gamRender) + expect(server.requests.length).to.equal(6); + + // 4th should be gamRender and should have Auciton # 1's id's + const message = JSON.parse(server.requests[3].requestBody); + const expectedMessage = { + auctionId: 'auctionId-1', + transactionId: 'tid-1', + isSlotEmpty: true, + adSlot: 'box' + }; + expect(message.gamRenders).to.deep.equal([expectedMessage]); + + // 5th should be gamRender and should have Auciton # 2's id's + const message1 = JSON.parse(server.requests[4].requestBody); + const expectedMessage1 = { + auctionId: 'auctionId-2', + transactionId: 'tid-2', + isSlotEmpty: true, + adSlot: 'box' + }; + expect(message1.gamRenders).to.deep.equal([expectedMessage1]); + + // 6th should be gamRender and should have Auciton # 3's id's + const message2 = JSON.parse(server.requests[5].requestBody); + const expectedMessage2 = { + ...ANALYTICS_MESSAGE.gamRenders[0], + auctionId: 'auctionId-3', + transactionId: 'tid-3' + }; + expect(message2.gamRenders).to.deep.equal([expectedMessage2]); + + // emit bidWon from second auction + // it should pick out render information from 3rd auction and source from 1st + events.emit(BID_WON, bidsWon[1]); + + // another request which is bidWon + expect(server.requests.length).to.equal(7); + const message3 = JSON.parse(server.requests[6].requestBody); + const expectedMessage3 = { + ...ANALYTICS_MESSAGE.bidsWon[0], + sourceAuctionId: 'auctionId-2', + renderAuctionId: `auctionId-${test.expectedRenderId}`, + sourceTransactionId: 'tid-2', + renderTransactionId: `tid-${test.expectedRenderId}`, + transactionId: 'tid-2', + bidId: 'bidId-2', + }; + expect(message3.bidsWon).to.deep.equal([expectedMessage3]); + }); + }); + + it('should still fire bidWon if no gam match found', () => { + // get 3 auctions pending to send events + runBasicAuction(0); + runBasicAuction(1); + runBasicAuction(2); + + // emit bidWon from 3rd auction - it should still fire even though no associated gamRender found + events.emit(BID_WON, bidsWon[2]); + + // another request which is bidWon + expect(server.requests.length).to.equal(4); + const message1 = JSON.parse(server.requests[3].requestBody); + const expectedMessage1 = { + ...ANALYTICS_MESSAGE.bidsWon[0], + sourceAuctionId: 'auctionId-3', + renderAuctionId: 'auctionId-3', + sourceTransactionId: 'tid-3', + renderTransactionId: 'tid-3', + transactionId: 'tid-3', + bidId: 'bidId-3', + }; + expect(message1.bidsWon).to.deep.equal([expectedMessage1]); + }); + }); + }); + + describe('billing events integration', () => { + beforeEach(function () { + magniteAdapter.enableAnalytics({ + options: { + endpoint: '//localhost:9999/event', + accountId: 1001 + } + }); + // default dmBilling + config.setConfig({ + rubicon: { + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + } + } + }) + }); + afterEach(function () { + magniteAdapter.disableAnalytics(); + }); + const basicBillingAuction = (billingEvents = []) => { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + billingEvents.forEach(ev => events.emit(BILLABLE_EVENT, ev)); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + events.emit(BID_WON, MOCK.BID_WON); + + clock.tick(rubiConf.analyticsEventDelay); + } + it('should ignore billing events when not enabled', () => { + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message.billableEvents).to.be.undefined; + }); + it('should ignore billing events when enabled but vendor is not whitelisted', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true + } + } + }); + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message.billableEvents).to.be.undefined; + }); + it('should ignore billing events if billingId is not defined or billingId is not a string', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + basicBillingAuction([ + { + vendor: 'vendorName', + type: 'auction', + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: true + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: 1233434 + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: null + } + ]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message.billableEvents).to.be.undefined; + }); + it('should pass along billing event in same payload if same auctionId', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965', + auctionId: MOCK.AUCTION_INIT.auctionId + }]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message).to.haveOwnProperty('auctions'); + expect(message.billableEvents).to.deep.equal([{ + accountId: 1001, + vendor: 'vendorName', + type: 'general', // mapping all events to endpoint as 'general' for now + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965', + auctionId: MOCK.AUCTION_INIT.auctionId + }]); + }); + it('should pass NOT pass along billing event in same payload if no auctionId', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965', + }]); + expect(server.requests.length).to.equal(2); + + // first is the billing event + let message = JSON.parse(server.requests[0].requestBody); + expect(message).to.not.haveOwnProperty('auctions'); + expect(message.billableEvents).to.deep.equal([{ + accountId: 1001, + vendor: 'vendorName', + type: 'general', // mapping all events to endpoint as 'general' for now + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + + // second is auctions + message = JSON.parse(server.requests[1].requestBody); + expect(message).to.haveOwnProperty('auctions'); + expect(message).to.not.haveOwnProperty('billableEvents'); + }); + it('should pass along multiple billing events but filter out duplicates', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + basicBillingAuction([ + { + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965', + auctionId: MOCK.AUCTION_INIT.auctionId + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: '743db6e3-21f2-44d4-917f-cb3488c6076f', + auctionId: MOCK.AUCTION_INIT.auctionId + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965', + auctionId: MOCK.AUCTION_INIT.auctionId + } + ]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message).to.haveOwnProperty('auctions'); + expect(message.billableEvents).to.deep.equal([ + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965', + auctionId: MOCK.AUCTION_INIT.auctionId + }, + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: '743db6e3-21f2-44d4-917f-cb3488c6076f', + auctionId: MOCK.AUCTION_INIT.auctionId + } + ]); + }); + it('should pass along event right away if no pending auction', () => { + // off by default + config.setConfig({ + rubicon: { + analyticsEventDelay: 0, + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + + events.emit(BILLABLE_EVENT, { + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message).to.not.haveOwnProperty('auctions'); + expect(message.billableEvents).to.deep.equal([ + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + } + ]); + }); + it('should pass along event right away if pending auction but not waiting', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'], + waitForAuction: false + } + } + }); + // should fire right away, and then auction later + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + expect(server.requests.length).to.equal(2); + const billingRequest = server.requests[0]; + const billingMessage = JSON.parse(billingRequest.requestBody); + expect(billingMessage).to.not.haveOwnProperty('auctions'); + expect(billingMessage.billableEvents).to.deep.equal([ + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + } + ]); + // auction event after + const auctionRequest = server.requests[1]; + const auctionMessage = JSON.parse(auctionRequest.requestBody); + // should not double pass events! + expect(auctionMessage).to.not.haveOwnProperty('billableEvents'); + }); + }); + + it('getHostNameFromReferer correctly grabs hostname from an input URL', function () { + let inputUrl = 'https://www.prebid.org/some/path?pbjs_debug=true'; + expect(getHostNameFromReferer(inputUrl)).to.equal('www.prebid.org'); + inputUrl = 'https://www.prebid.com/some/path?pbjs_debug=true'; + expect(getHostNameFromReferer(inputUrl)).to.equal('www.prebid.com'); + inputUrl = 'https://prebid.org/some/path?pbjs_debug=true'; + expect(getHostNameFromReferer(inputUrl)).to.equal('prebid.org'); + inputUrl = 'http://xn--p8j9a0d9c9a.xn--q9jyb4c/'; + expect(typeof getHostNameFromReferer(inputUrl)).to.equal('string'); + + // not non-UTF char's in query / path which break if noDecodeWholeURL not set + inputUrl = 'https://prebid.org/search_results/%95x%8Em%92%CA/?category=000'; + expect(getHostNameFromReferer(inputUrl)).to.equal('prebid.org'); }); }); diff --git a/test/spec/modules/magniteAnalyticsSchema.json b/test/spec/modules/magniteAnalyticsSchema.json deleted file mode 100644 index 5abd960ff2c..00000000000 --- a/test/spec/modules/magniteAnalyticsSchema.json +++ /dev/null @@ -1,466 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Prebid Auctions", - "description": "A batched data object describing the lifecycle of an auction or multiple auction across a single page view.", - "type": "object", - "required": [ - "integration", - "version" - ], - "anyOf": [ - { - "required": [ - "auctions" - ] - }, - { - "required": [ - "bidsWon" - ] - }, - { - "required": [ - "billableEvents" - ] - } - ], - "properties": { - "integration": { - "type": "string", - "description": "Integration type that generated this event.", - "default": "pbjs" - }, - "version": { - "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, - "items": { - "type": "object", - "required": [ - "adUnits" - ], - "properties": { - "clientTimeoutMillis": { - "type": "integer", - "description": "Timeout given in client for given auction in milliseconds (if applicable)." - }, - "serverTimeoutMillis": { - "type": "integer", - "description": "Timeout configured for server adapter request in milliseconds (if applicable)." - }, - "accountId": { - "type": "number", - "description": "The account id for prebid server (if applicable)." - }, - "adUnits": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "description": "An array of adUnits involved in this auction.", - "required": [ - "status", - "adUnitCode", - "transactionId", - "mediaTypes", - "dimensions", - "bids" - ], - "properties": { - "status": { - "type": "string", - "description": "The status of the adUnit" - }, - "adUnitCode": { - "type": "string", - "description": "The adUnit.code identifier" - }, - "transactionId": { - "type": "string", - "description": "The UUID generated id to represent this adunit in this auction." - }, - "adSlot": { - "type": "string" - }, - "mediaTypes": { - "$ref": "#/definitions/mediaTypes" - }, - "videoAdFormat": { - "$ref": "#/definitions/videoAdFormat" - }, - "dimensions": { - "type": "array", - "description": "All valid sizes included in this auction (note: may be sizeConfig filtered).", - "minItems": 1, - "items": { - "$ref": "#/definitions/dimensions" - } - }, - "adserverTargeting": { - "$ref": "#/definitions/adserverTargeting" - }, - "bids": { - "type": "array", - "description": "An array that contains a combination of the bids from the adUnit combined with their responses.", - "minItems": 1, - "items": { - "$ref": "#/definitions/bid" - } - }, - "accountId": { - "type": "number", - "description": "The Rubicon AccountId associated with this adUnit - Removed if null" - }, - "siteId": { - "type": "number", - "description": "The Rubicon siteId associated with this adUnit - Removed if null" - }, - "zoneId": { - "type": "number", - "description": "The Rubicon zoneId associated with this adUnit - Removed if null" - }, - "gam": { - "$ref": "#/definitions/gam" - } - } - } - } - } - } - }, - "bidsWon": { - "type": "array", - "minItems": 1, - "items": { - "allOf": [ - { - "$ref": "#/definitions/bid" - }, - { - "required": [ - "transactionId", - "accountId", - "mediaTypes", - "adUnitCode" - ], - "properties": { - "transactionId": { - "type": "string" - }, - "accountId": { - "type": "number" - }, - "adUnitCode": { - "type": "string" - }, - "videoAdFormat": { - "$ref": "#/definitions/videoAdFormat" - }, - "mediaTypes": { - "$ref": "#/definitions/mediaTypes" - }, - "adserverTargeting": { - "$ref": "#/definitions/adserverTargeting" - }, - "siteId": { - "type": "number", - "description": "The Rubicon siteId associated with this adUnit - Removed if null" - }, - "zoneId": { - "type": "number", - "description": "The Rubicon zoneId associated with this adUnit - Removed if null" - } - } - } - ] - } - }, - "billableEvents":{ - "type":"array", - "minItems":1, - "items":{ - "type":"object", - "required":[ - "accountId", - "vendor", - "type", - "billingId" - ], - "properties":{ - "vendor":{ - "type":"string", - "description":"The name of the vendor who emitted the billable event" - }, - "type":{ - "type":"string", - "description":"The type of billable event", - "enum":[ - "impression", - "pageLoad", - "auction", - "request", - "general" - ] - }, - "billingId":{ - "type":"string", - "description":"A UUID which is responsible more mapping this event to" - }, - "accountId": { - "type": "number", - "description": "The account id for the rubicon publisher" - } - } - } - } - }, - "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", - "patternProperties": { - ".+": { - "type": "string" - } - } - }, - "videoAdFormat": { - "type": "string", - "description": "This value only provided for video specifies the ad format", - "enum": [ - "pre-roll", - "interstitial", - "outstream", - "mid-roll", - "post-roll", - "vertical" - ] - }, - "mediaTypes": { - "type": "array", - "uniqueItems": true, - "minItems": 1, - "items": { - "type": "string", - "enum": [ - "native", - "video", - "banner" - ] - } - }, - "dimensions": { - "type": "object", - "description": "Size object representing the dimensions of creative in pixels.", - "required": [ - "width", - "height" - ], - "properties": { - "width": { - "type": "integer", - "minimum": 1 - }, - "height": { - "type": "integer", - "minimum": 1 - } - } - }, - "bid": { - "type": "object", - "required": [ - "bidder", - "bidId", - "status", - "source" - ], - "properties": { - "bidder": { - "type": "string" - }, - "bidId": { - "type": "string", - "description": "UUID representing this individual bid request in this auction." - }, - "params": { - "description": "A copy of the bid.params from the adUnit.bids", - "anyOf": [ - { - "type": "object" - }, - { - "$ref": "#/definitions/params/rubicon" - } - ] - }, - "status": { - "type": "string", - "enum": [ - "success", - "no-bid", - "error", - "rejected-gdpr", - "rejected-ipf" - ] - }, - "error": { - "type": "object", - "additionalProperties": false, - "required": [ - "code" - ], - "properties": { - "code": { - "type": "string", - "enum": [ - "request-error", - "connect-error", - "timeout-error" - ] - }, - "description": { - "type": "string" - } - } - }, - "source": { - "type": "string", - "enum": [ - "client", - "server" - ] - }, - "clientLatencyMillis": { - "type": "integer", - "description": "Latency from auction start to bid response recieved in milliseconds." - }, - "serverLatencyMillis": { - "type": "integer", - "description": "Latency returned by prebid server (response_time_ms)." - }, - "bidResponse": { - "type": "object", - "required": [ - "mediaType", - "bidPriceUSD" - ], - "properties": { - "dimensions": { - "$ref": "#/definitions/dimensions" - }, - "mediaType": { - "type": "string", - "enum": [ - "native", - "video", - "banner" - ] - }, - "bidPriceUSD": { - "type": "number", - "description": "The bid value denoted in USD" - }, - "dealId": { - "type": "integer", - "description": "The id associated with any potential deals" - } - } - } - } - }, - "params": { - "rubicon": { - "type": "object", - "properties": { - "accountId": { - "type": "number" - }, - "siteId": { - "type": "number" - }, - "zoneId": { - "type": "number" - } - } - } - } - } -} From 5a36174e5a8b2f7e9d85d2702e0b5d545f97c5d1 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Fri, 26 Aug 2022 19:08:00 -0700 Subject: [PATCH 12/22] implement Michele review comments fix clashing rubicon configs in tests --- modules/magniteAnalyticsAdapter.js | 9 +++----- modules/rubiconAnalyticsAdapter.js | 21 ++++++++++++------- .../modules/magniteAnalyticsAdapter_spec.js | 1 - .../modules/rubiconAnalyticsAdapter_spec.js | 5 ++--- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 592893bd1b1..dabc2743ae6 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -127,7 +127,7 @@ const sendEvent = payload => { ...payload } ajax( - rubiConf.analyticsEndpoint || endpoint, + endpoint, null, JSON.stringify(event), { @@ -406,11 +406,8 @@ const getFpkvs = () => { adds to the rubiconAliases list if found */ const setRubiconAliases = (aliasRegistry) => { - Object.keys(aliasRegistry).forEach(alias => { - if (aliasRegistry[alias] === 'rubicon') { - rubiconAliases.push(alias); - } - }); + const otherAliases = Object.keys(aliasRegistry).filter(alias => aliasRegistry[alias] === 'rubicon'); + rubiconAliases.push(...otherAliases); } const sizeToDimensions = size => { diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 23a576bd660..e822c7e0a4c 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -62,15 +62,20 @@ const cache = { const BID_REJECTED_IPF = 'rejected-ipf'; -export let rubiConf = { - pvid: generateUUID().slice(0, 8), - analyticsEventDelay: 0, - dmBilling: { - enabled: false, - vendors: [], - waitForAuction: true +export let rubiConf; +export const resetRubiConf = () => { + rubiConf = { + pvid: generateUUID().slice(0, 8), + analyticsEventDelay: 0, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + } } -}; +} +resetRubiConf(); + // we are saving these as global to this module so that if a pub accidentally overwrites the entire // rubicon object, then we do not lose other data config.getConfig('rubicon', config => { diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index 80dc3852a4f..4d255b7ea55 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -351,7 +351,6 @@ describe('magnite analytics adapter', function () { setDataInLocalStorageStub.restore(); localStorageIsEnabledStub.restore(); magniteAdapter.disableAnalytics(); - mockGpt.disable(); }); it('should require accountId', function () { diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index bef3eda0afc..b410607ad48 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -4,6 +4,7 @@ import rubiconAnalyticsAdapter, { getHostNameFromReferer, storage, rubiConf, + resetRubiConf } from 'modules/rubiconAnalyticsAdapter.js'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config.js'; @@ -667,6 +668,7 @@ describe('rubicon analytics adapter', function () { expect(utils.generateUUID.called).to.equal(true); }); it('should merge in and preserve older set configs', function () { + resetRubiConf(); config.setConfig({ rubicon: { wrapperName: '1001_general', @@ -689,7 +691,6 @@ describe('rubicon analytics adapter', function () { fpkvs: { source: 'fb' }, - updatePageView: true }); // update it with stuff @@ -714,7 +715,6 @@ describe('rubicon analytics adapter', function () { source: 'fb', link: 'email' }, - updatePageView: true }); // overwriting specific edge keys should update them @@ -740,7 +740,6 @@ describe('rubicon analytics adapter', function () { link: 'iMessage', source: 'twitter' }, - updatePageView: true }); }); }); From 13676427398ff841bf2951d5908800bf4c741f83 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Mon, 29 Aug 2022 12:30:26 -0700 Subject: [PATCH 13/22] michele fixes --- modules/magniteAnalyticsAdapter.js | 2 +- test/spec/modules/magniteAnalyticsAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index dabc2743ae6..3c2a5ac26d1 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -1,5 +1,5 @@ import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, deepSetValue, deepClone, logInfo, isGptPubadsDefined } from '../src/utils.js'; -import adapter from '../src/AnalyticsAdapter.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; import { ajax } from '../src/ajax.js'; diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index 4d255b7ea55..1bdaf27dbc5 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -190,7 +190,7 @@ const ANALYTICS_MESSAGE = { 'channel': 'web', 'integration': 'pbjs', 'referrerUri': 'http://a-test-domain.com:8000/test_pages/sanity/TEMP/prebidTest.html?pbjs_debug=true', - 'version': '7.8.0-pre', + 'version': '$prebid.version$', 'referrerHostname': 'a-test-domain.com', 'timestamps': { 'eventTime': 1519767013981, From 75ff5f0b9c0e3e9e38b28c6416f7f700d0401f2d Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Tue, 30 Aug 2022 14:59:30 -0700 Subject: [PATCH 14/22] sometimes bidWons do not have "bidId" so use "requestId" --- modules/magniteAnalyticsAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 3c2a5ac26d1..7f974b97636 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -482,7 +482,7 @@ const formatBidWon = bidWonData => { renderAuctionId, transactionId: bidWonData.transactionId, sourceTransactionId: bidWonData.transactionId, - bidId: bid.pbsBidId || bidWonData.bidId, // if PBS had us overwrite bidId, use that as signal + bidId: bid.pbsBidId || bidWonData.bidId || bidWonData.requestId, // if PBS had us overwrite bidId, use that as signal renderTransactionId, accountId, siteId: adUnit.siteId, From 15c6bcc82a6e7afb0c3915e3cd4cd916373ac778 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Tue, 13 Sep 2022 14:19:41 -0700 Subject: [PATCH 15/22] add logs so we can debug on page --- modules/magniteAnalyticsAdapter.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 7f974b97636..634156425a6 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -473,6 +473,14 @@ const formatBidWon = bidWonData => { // get transaction and auction id of where this "rendered" const { renderTransactionId, renderAuctionId } = getRenderingIds(bidWonData); + logInfo(`${MODULE_NAME}: Bid Won : `, { + isCachedBid: renderTransactionId !== bidWonData.transactionId, + renderAuctionId, + renderTransactionId, + sourceAuctionId: bidWonData.auctionId, + sourceTransactionId: bidWonData.transactionId, + }); + // get the bid from the source auction id let bid = deepAccess(cache, `auctions.${bidWonData.auctionId}.auction.adUnits.${bidWonData.adUnitCode}.bids.${bidWonData.requestId}`); let adUnit = deepAccess(cache, `auctions.${bidWonData.auctionId}.auction.adUnits.${bidWonData.adUnitCode}`); @@ -526,9 +534,23 @@ const subscribeToGamSlots = () => { } let { adUnit, auction } = findMatchingAdUnitFromAuctions(matchingFunction, true); - if (!adUnit || !auction) return; // maybe log something here? + const slotName = `${event.slot.getAdUnitPath()} - ${event.slot.getSlotElementId()}`; + if (!adUnit || !auction) { + logInfo(`${MODULE_NAME}: Could not find matching adUnit for Gam Render: `, { + slotName + }); + return; + } const auctionId = auction.auctionId; + + logInfo(`${MODULE_NAME}: Gam Render: `, { + slotName, + transactionId: adUnit.transactionId, + auctionId: auctionId, + adUnit: adUnit, + }); + // if we have an adunit, then we need to make a gam event const gamEvent = formatGamEvent(event, adUnit, auction); From 49df701a9e09544593b40776bf4510abcadf0309 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Mon, 19 Sep 2022 15:01:17 -0700 Subject: [PATCH 16/22] handle currency conversion errors better --- modules/magniteAnalyticsAdapter.js | 11 ++++- .../modules/magniteAnalyticsAdapter_spec.js | 40 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 634156425a6..a0e494d231a 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -196,7 +196,7 @@ const getBidPrice = bid => { // get the cpm from bidResponse let cpm; let currency; - if (bid.status === BID_REJECTED && deepAccess(bid, 'floorData.cpmAfterAdjustments')) { + if (bid.status === BID_REJECTED && typeof deepAccess(bid, 'floorData.cpmAfterAdjustments') === 'number') { // if bid was rejected and bid.floorData.cpmAfterAdjustments use it cpm = bid.floorData.cpmAfterAdjustments; currency = bid.floorData.floorCurrency; @@ -217,6 +217,10 @@ const getBidPrice = bid => { return Number(prebidGlobal.convertCurrency(cpm, currency, 'USD')); } catch (err) { logWarn(`${MODULE_NAME}: Could not determine the bidPriceUSD of the bid `, bid); + bid.conversionError = true; + bid.ogCurrency = currency; + bid.ogPrice = cpm; + return 0; } } @@ -245,7 +249,10 @@ export const parseBidResponse = (bid, previousBidResponse) => { const adomains = deepAccess(bid, 'meta.advertiserDomains'); const validAdomains = Array.isArray(adomains) && adomains.filter(domain => typeof domain === 'string'); return validAdomains && validAdomains.length > 0 ? validAdomains.slice(0, 10) : undefined - } + }, + 'conversionError', conversionError => conversionError === true || undefined, // only pass if exactly true + 'ogCurrency', + 'ogPrice' ]); } diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index 1bdaf27dbc5..aa0e2cf7254 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -12,6 +12,7 @@ import { setConfig, addBidResponseHook, } from 'modules/currency.js'; +import { getGlobal } from '../../../src/prebidGlobal.js'; import { deepAccess } from '../../../src/utils.js'; let events = require('src/events.js'); @@ -1861,4 +1862,43 @@ describe('magnite analytics adapter', function () { inputUrl = 'https://prebid.org/search_results/%95x%8Em%92%CA/?category=000'; expect(getHostNameFromReferer(inputUrl)).to.equal('prebid.org'); }); + + describe(`handle currency conversions`, () => { + const origConvertCurrency = getGlobal().convertCurrency; + afterEach(() => { + if (origConvertCurrency != null) { + getGlobal().convertCurrency = origConvertCurrency; + } else { + delete getGlobal().convertCurrency; + } + }); + + it(`should convert successfully`, () => { + getGlobal().convertCurrency = () => 1.0; + const bidCopy = utils.deepClone(MOCK.BID_RESPONSE); + bidCopy.currency = 'JPY'; + bidCopy.cpm = 100; + + const bidResponseObj = parseBidResponse(bidCopy); + expect(bidResponseObj.conversionError).to.equal(undefined); + expect(bidResponseObj.ogCurrency).to.equal(undefined); + expect(bidResponseObj.ogPrice).to.equal(undefined); + expect(bidResponseObj.bidPriceUSD).to.equal(1.0); + }); + + it(`should catch error and set to zero with conversionError flag true`, () => { + getGlobal().convertCurrency = () => { + throw new Error('I am an error'); + }; + const bidCopy = utils.deepClone(MOCK.BID_RESPONSE); + bidCopy.currency = 'JPY'; + bidCopy.cpm = 100; + + const bidResponseObj = parseBidResponse(bidCopy); + expect(bidResponseObj.conversionError).to.equal(true); + expect(bidResponseObj.ogCurrency).to.equal('JPY'); + expect(bidResponseObj.ogPrice).to.equal(100); + expect(bidResponseObj.bidPriceUSD).to.equal(0); + }); + }); }); From 677183f76fa317db3fdec217e0bd0189ce02f4af Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 22 Sep 2022 08:51:14 -0700 Subject: [PATCH 17/22] bug fixes --- modules/magniteAnalyticsAdapter.js | 12 ++++-- .../modules/magniteAnalyticsAdapter_spec.js | 37 ++++++++++++++++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index a0e494d231a..5a5848fa3d9 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -151,6 +151,8 @@ const sendAuctionEvent = (auctionId, trigger) => { const formatAuction = auction => { const auctionEvent = deepClone(auction); + auctionEvent.samplingFactor = 1; + // We stored adUnits and bids as objects for quick lookups, now they are mapped into arrays for PBA auctionEvent.adUnits = Object.entries(auctionEvent.adUnits).map(([tid, adUnit]) => { adUnit.bids = Object.entries(adUnit.bids).map(([bidId, bid]) => { @@ -480,8 +482,9 @@ const formatBidWon = bidWonData => { // get transaction and auction id of where this "rendered" const { renderTransactionId, renderAuctionId } = getRenderingIds(bidWonData); + const isCachedBid = renderTransactionId !== bidWonData.transactionId; logInfo(`${MODULE_NAME}: Bid Won : `, { - isCachedBid: renderTransactionId !== bidWonData.transactionId, + isCachedBid, renderAuctionId, renderTransactionId, sourceAuctionId: bidWonData.auctionId, @@ -503,7 +506,8 @@ const formatBidWon = bidWonData => { siteId: adUnit.siteId, zoneId: adUnit.zoneId, mediaTypes: adUnit.mediaTypes, - adUnitCode: adUnit.adUnitCode + adUnitCode: adUnit.adUnitCode, + isCachedBid: isCachedBid || undefined // only send if it is true (save some space) } delete bidWon.pbsBidId; // if pbsBidId is there delete it (no need to pass it) return bidWon; @@ -620,7 +624,7 @@ function enableMgniAnalytics(config = {}) { magniteAdapter.enableAnalytics = enableMgniAnalytics; magniteAdapter.originDisableAnalytics = magniteAdapter.disableAnalytics; -magniteAdapter.disableAnalytics = function() { +magniteAdapter.disableAnalytics = function () { // trick analytics module to register our enable back as main one magniteAdapter._oldEnable = enableMgniAnalytics; endpoint = undefined; @@ -728,7 +732,7 @@ magniteAdapter.track = ({ eventType, args }) => { adUnit.bids[bid.bidId] = pick(bid, [ 'bidder', 'bidId', - 'src as source', + 'source', () => bid.src === 's2s' ? 'server' : 'client', 'status', () => 'no-bid' ]); // set acct site zone id on adunit diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index aa0e2cf7254..96c9abcc7b8 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -210,6 +210,7 @@ const ANALYTICS_MESSAGE = { { 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', 'auctionStart': 1658868383741, + 'samplingFactor': 1, 'clientTimeoutMillis': 3000, 'accountId': 1001, 'bidderOrder': [ @@ -1190,7 +1191,7 @@ describe('magnite analytics adapter', function () { } }); - performStandardAuction({eventDelay: 0}); + performStandardAuction({ eventDelay: 0 }); // should be 3 requests expect(server.requests.length).to.equal(3); @@ -1386,6 +1387,37 @@ describe('magnite analytics adapter', function () { const message = JSON.parse(request.requestBody); expect(message.integration).to.equal('testType'); }); + + it('should correctly pass bid.source when is s2s', () => { + // Run auction + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + + const bidReq = utils.deepClone(MOCK.BID_REQUESTED); + bidReq.bids[0].src = 's2s'; + + events.emit(BID_REQUESTED, bidReq); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + // emmit gpt events and bidWon + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + events.emit(BID_WON, MOCK.BID_WON); + + // hit the eventDelay + clock.tick(rubiConf.analyticsEventDelay); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + + // bid source should be 'server' + expectedMessage.auctions[0].adUnits[0].bids[0].source = 'server'; + expectedMessage.bidsWon[0].source = 'server'; + expect(message).to.deep.equal(expectedMessage); + }); + describe('when handling bid caching', () => { let auctionInits, bidRequests, bidResponses, bidsWon; beforeEach(function () { @@ -1535,8 +1567,9 @@ describe('magnite analytics adapter', function () { sourceTransactionId: 'tid-2', renderTransactionId: `tid-${test.expectedRenderId}`, transactionId: 'tid-2', - bidId: 'bidId-2', + bidId: 'bidId-2' }; + if (test.useBidCache) expectedMessage3.isCachedBid = true expect(message3.bidsWon).to.deep.equal([expectedMessage3]); }); }); From aabc12c9dd76f5290f97b2614c641f8dfac0a76c Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Tue, 27 Sep 2022 12:22:45 -0700 Subject: [PATCH 18/22] time since page load --- modules/magniteAnalyticsAdapter.js | 1 + test/spec/modules/magniteAnalyticsAdapter_spec.js | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 5a5848fa3d9..c04eef046aa 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -292,6 +292,7 @@ const getTopLevelDetails = () => { version: '$prebid.version$', referrerHostname: magniteAdapter.referrerHostname || getHostNameFromReferer(pageReferer), timestamps: { + timeSincePageLoad: performance.now(), eventTime: Date.now(), prebidLoaded: magniteAdapter.MODULE_INITIALIZED_TIME } diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index 96c9abcc7b8..bbaec1ae95a 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -194,6 +194,7 @@ const ANALYTICS_MESSAGE = { 'version': '$prebid.version$', 'referrerHostname': 'a-test-domain.com', 'timestamps': { + 'timeSincePageLoad': 200, 'eventTime': 1519767013981, 'prebidLoaded': magniteAdapter.MODULE_INITIALIZED_TIME }, @@ -992,6 +993,7 @@ describe('magnite analytics adapter', function () { expectedMessage.session.expires = expectedMessage.session.expires + 1800; expectedMessage.session.start = expectedMessage.session.start + 1800; expectedMessage.timestamps.eventTime = expectedMessage.timestamps.eventTime + 1800; + expectedMessage.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + 1800; expect(message).to.deep.equal(expectedMessage); }); @@ -1128,6 +1130,7 @@ describe('magnite analytics adapter', function () { expectedMessage2.session.expires = expectedMessage.session.expires + rubiConf.analyticsEventDelay; expectedMessage2.session.start = expectedMessage.session.start + rubiConf.analyticsEventDelay; expectedMessage2.timestamps.eventTime = expectedMessage.timestamps.eventTime + rubiConf.analyticsEventDelay; + expectedMessage2.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + rubiConf.analyticsEventDelay; // trigger is `batched-bidsWon` expectedMessage2.trigger = 'batched-bidsWon'; @@ -1159,6 +1162,7 @@ describe('magnite analytics adapter', function () { expectedMessage.session.expires = expectedMessage.session.expires + expectedExtraTime; expectedMessage.session.start = expectedMessage.session.start + expectedExtraTime; expectedMessage.timestamps.eventTime = expectedMessage.timestamps.eventTime + expectedExtraTime; + expectedMessage.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + expectedExtraTime; // since gam event did not fire, the trigger should be auctionEnd expectedMessage.trigger = 'auctionEnd'; @@ -1174,6 +1178,7 @@ describe('magnite analytics adapter', function () { expectedMessage2.session.expires = expectedMessage.session.expires + rubiConf.analyticsEventDelay; expectedMessage2.session.start = expectedMessage.session.start + rubiConf.analyticsEventDelay; expectedMessage2.timestamps.eventTime = expectedMessage.timestamps.eventTime + rubiConf.analyticsEventDelay; + expectedMessage2.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + rubiConf.analyticsEventDelay; delete expectedMessage2.auctions; // trigger should be `batched-bidsWon-gamRender` @@ -1203,6 +1208,7 @@ describe('magnite analytics adapter', function () { rest.session.expires = rest.session.expires - defaultDelay; rest.session.start = rest.session.start - defaultDelay; rest.timestamps.eventTime = rest.timestamps.eventTime - defaultDelay; + rest.timestamps.timeSincePageLoad = rest.timestamps.timeSincePageLoad - defaultDelay; // loop through and assert events fired in correct order with correct stuff [ From f8ee6a8e9423e636192a4f149c6939a3dfece8ff Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Tue, 4 Oct 2022 07:40:13 -0700 Subject: [PATCH 19/22] Magnite MD --- modules/magniteAnalyticsAdapter.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 modules/magniteAnalyticsAdapter.md diff --git a/modules/magniteAnalyticsAdapter.md b/modules/magniteAnalyticsAdapter.md new file mode 100644 index 00000000000..a9ad0f4345b --- /dev/null +++ b/modules/magniteAnalyticsAdapter.md @@ -0,0 +1,18 @@ +# Magnite Analytics Adapter + +``` +Module Name: Magnite Analytics Adapter +Module Type: Analytics Adapter +Maintainer: demand-manager-support@magnite.com +``` + +## How to configure? +``` +pbjs.enableAnalytics({ + provider: 'magnite', + options: { + accountId: 12345, // The account id assigned to you by the Magnite Team + endpoint: 'http:localhost:9999/event' // Given by the Magnite Team + } +}); +``` From 8e0603d270e86672d454f709e3775399b4cd1817 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Tue, 4 Oct 2022 09:01:47 -0700 Subject: [PATCH 20/22] Move session to auction init --- modules/magniteAnalyticsAdapter.js | 22 +++++---- .../modules/magniteAnalyticsAdapter_spec.js | 46 ++++++++----------- 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index c04eef046aa..8d3eb7d4e4f 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -60,7 +60,8 @@ const resetConfs = () => { billing: {}, pendingEvents: {}, eventPending: false, - elementIdMap: {} + elementIdMap: {}, + sessionData: {} } rubiConf = { pvid: generateUUID().slice(0, 8), @@ -185,9 +186,9 @@ const isBillingEventValid = event => { } const formatBillingEvent = event => { - // for now we are mapping all events to type "general", later we will expand support for specific types let billingEvent = deepClone(event); - billingEvent.type = 'general'; + // Pass along type if is string and not empty else general + billingEvent.type = (typeof event.type === 'string' && event.type) || 'general'; billingEvent.accountId = accountId; // mark as sent deepSetValue(cache.billing, `${event.vendor}.${event.billingId}`, true); @@ -307,19 +308,18 @@ const getTopLevelDetails = () => { } } - // Add session info - const sessionData = storage.localStorageIsEnabled() && updateRpaCookie(); - if (sessionData) { + if (cache.sessionData) { // gather session info - payload.session = pick(sessionData, [ + payload.session = pick(cache.sessionData, [ 'id', 'pvid', 'start', 'expires' ]); - if (!isEmpty(sessionData.fpkvs)) { - payload.fpkvs = Object.keys(sessionData.fpkvs).map(key => { - return { key, value: sessionData.fpkvs[key] }; + // Any FPKVS set? + if (!isEmpty(cache.sessionData.fpkvs)) { + payload.fpkvs = Object.keys(cache.sessionData.fpkvs).map(key => { + return { key, value: cache.sessionData.fpkvs[key] }; }); } } @@ -640,6 +640,8 @@ magniteAdapter.referrerHostname = ''; magniteAdapter.track = ({ eventType, args }) => { switch (eventType) { case AUCTION_INIT: + // Update session + cache.sessionData = storage.localStorageIsEnabled() && updateRpaCookie(); // set the rubicon aliases setRubiconAliases(adapterManager.aliasRegistry); diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index bbaec1ae95a..c57b44226a5 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -204,8 +204,8 @@ const ANALYTICS_MESSAGE = { 'session': { 'id': '12345678-1234-1234-1234-123456789abc', 'pvid': '12345678', - 'start': 1519767013981, - 'expires': 1519788613981 + 'start': 1519767013781, + 'expires': 1519788613781 }, 'auctions': [ { @@ -752,7 +752,7 @@ describe('magnite analytics adapter', function () { id: '987654', // should have stayed same start: 1519767017881, // should have stayed same expires: 1519767039481, // should have stayed same - lastSeen: 1519767013981, // lastSeen updated to our "now" + lastSeen: 1519767013781, // lastSeen updated to our auction init time fpkvs: { source: 'tw', link: 'email' }, // link merged in pvid: expectedPvid // new pvid stored }); @@ -812,7 +812,7 @@ describe('magnite analytics adapter', function () { id: '987654', // should have stayed same start: 1519766113781, // should have stayed same expires: 1519787713781, // should have stayed same - lastSeen: 1519767013981, // lastSeen updated to our "now" + lastSeen: 1519767013781, // lastSeen updated to our auction init time fpkvs: { source: 'fb', link: 'email', click: 'dog' }, // link merged in pvid: expectedPvid // new pvid stored }); @@ -861,9 +861,9 @@ describe('magnite analytics adapter', function () { expect(calledWith).to.deep.equal({ id: STUBBED_UUID, // should have generated not used input - start: 1519767013981, // should have stayed same - expires: 1519788613981, // should have stayed same - lastSeen: 1519767013981, // lastSeen updated to our "now" + start: 1519767013781, // updated to whenever auction init started + expires: 1519788613781, // 6 hours after start + lastSeen: 1519767013781, // lastSeen updated to our "now" fpkvs: { link: 'email' }, // link merged in pvid: expectedPvid // new pvid stored }); @@ -912,9 +912,9 @@ describe('magnite analytics adapter', function () { expect(calledWith).to.deep.equal({ id: STUBBED_UUID, // should have generated and not used same one - start: 1519767013981, // should have stayed same - expires: 1519788613981, // should have stayed same - lastSeen: 1519767013981, // lastSeen updated to our "now" + start: 1519767013781, // updated to whenever auction init started + expires: 1519788613781, // 6 hours after start + lastSeen: 1519767013781, // lastSeen updated to our "now" fpkvs: { link: 'email' }, // link merged in pvid: expectedPvid // new pvid stored }); @@ -990,8 +990,6 @@ describe('magnite analytics adapter', function () { // The timestamps should be changed from the default by 1800 (set eventDelay - eventDelay default (200)) let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); - expectedMessage.session.expires = expectedMessage.session.expires + 1800; - expectedMessage.session.start = expectedMessage.session.start + 1800; expectedMessage.timestamps.eventTime = expectedMessage.timestamps.eventTime + 1800; expectedMessage.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + 1800; @@ -1127,8 +1125,6 @@ describe('magnite analytics adapter', function () { delete expectedMessage2.gamRenders; // second event should be event delay time after first one - expectedMessage2.session.expires = expectedMessage.session.expires + rubiConf.analyticsEventDelay; - expectedMessage2.session.start = expectedMessage.session.start + rubiConf.analyticsEventDelay; expectedMessage2.timestamps.eventTime = expectedMessage.timestamps.eventTime + rubiConf.analyticsEventDelay; expectedMessage2.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + rubiConf.analyticsEventDelay; @@ -1159,8 +1155,6 @@ describe('magnite analytics adapter', function () { // timing changes a bit -> timestamps should be batchTimeout - event delay later const expectedExtraTime = rubiConf.analyticsBatchTimeout - rubiConf.analyticsEventDelay; - expectedMessage.session.expires = expectedMessage.session.expires + expectedExtraTime; - expectedMessage.session.start = expectedMessage.session.start + expectedExtraTime; expectedMessage.timestamps.eventTime = expectedMessage.timestamps.eventTime + expectedExtraTime; expectedMessage.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + expectedExtraTime; @@ -1175,8 +1169,6 @@ describe('magnite analytics adapter', function () { let expectedMessage2 = utils.deepClone(ANALYTICS_MESSAGE); // second event should be event delay time after first one - expectedMessage2.session.expires = expectedMessage.session.expires + rubiConf.analyticsEventDelay; - expectedMessage2.session.start = expectedMessage.session.start + rubiConf.analyticsEventDelay; expectedMessage2.timestamps.eventTime = expectedMessage.timestamps.eventTime + rubiConf.analyticsEventDelay; expectedMessage2.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + rubiConf.analyticsEventDelay; delete expectedMessage2.auctions; @@ -1205,8 +1197,6 @@ describe('magnite analytics adapter', function () { let { auctions, gamRenders, bidsWon, ...rest } = utils.deepClone(ANALYTICS_MESSAGE); // rest of payload should have timestamps changed to be - default eventDelay since we changed it to 0 - rest.session.expires = rest.session.expires - defaultDelay; - rest.session.start = rest.session.start - defaultDelay; rest.timestamps.eventTime = rest.timestamps.eventTime - defaultDelay; rest.timestamps.timeSincePageLoad = rest.timestamps.timeSincePageLoad - defaultDelay; @@ -1721,7 +1711,7 @@ describe('magnite analytics adapter', function () { }); basicBillingAuction([{ vendor: 'vendorName', - type: 'auction', + type: 'pageView', billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965', auctionId: MOCK.AUCTION_INIT.auctionId }]); @@ -1732,7 +1722,7 @@ describe('magnite analytics adapter', function () { expect(message.billableEvents).to.deep.equal([{ accountId: 1001, vendor: 'vendorName', - type: 'general', // mapping all events to endpoint as 'general' for now + type: 'pageView', billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965', auctionId: MOCK.AUCTION_INIT.auctionId }]); @@ -1760,7 +1750,7 @@ describe('magnite analytics adapter', function () { expect(message.billableEvents).to.deep.equal([{ accountId: 1001, vendor: 'vendorName', - type: 'general', // mapping all events to endpoint as 'general' for now + type: 'auction', billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' }]); @@ -1788,7 +1778,7 @@ describe('magnite analytics adapter', function () { }, { vendor: 'vendorName', - type: 'auction', + type: 'impression', billingId: '743db6e3-21f2-44d4-917f-cb3488c6076f', auctionId: MOCK.AUCTION_INIT.auctionId }, @@ -1807,14 +1797,14 @@ describe('magnite analytics adapter', function () { { accountId: 1001, vendor: 'vendorName', - type: 'general', + type: 'auction', billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965', auctionId: MOCK.AUCTION_INIT.auctionId }, { accountId: 1001, vendor: 'vendorName', - type: 'general', + type: 'impression', billingId: '743db6e3-21f2-44d4-917f-cb3488c6076f', auctionId: MOCK.AUCTION_INIT.auctionId } @@ -1845,7 +1835,7 @@ describe('magnite analytics adapter', function () { { accountId: 1001, vendor: 'vendorName', - type: 'general', + type: 'auction', billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' } ]); @@ -1875,7 +1865,7 @@ describe('magnite analytics adapter', function () { { accountId: 1001, vendor: 'vendorName', - type: 'general', + type: 'auction', billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' } ]); From c1f0cd6faa6c3b7694e59e546d8852115015cbad Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Sat, 8 Oct 2022 10:52:49 -0700 Subject: [PATCH 21/22] Change adUnitMap to transactionId Add delay processing to Bid Won --- modules/magniteAnalyticsAdapter.js | 55 +++++++----- .../modules/magniteAnalyticsAdapter_spec.js | 83 ++++++++++--------- 2 files changed, 81 insertions(+), 57 deletions(-) diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 8d3eb7d4e4f..dba847e1b07 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -65,8 +65,9 @@ const resetConfs = () => { } rubiConf = { pvid: generateUUID().slice(0, 8), - analyticsEventDelay: 200, + analyticsEventDelay: 500, analyticsBatchTimeout: 5000, + analyticsProcessDelay: 1, dmBilling: { enabled: false, vendors: [], @@ -434,8 +435,8 @@ const findMatchingAdUnitFromAuctions = (matchesFunction, returnFirstMatch) => { // loop through auctions in order and adunits for (const auctionId of cache.auctionOrder) { const auction = cache.auctions[auctionId].auction; - for (const adUnitCode in auction.adUnits) { - const adUnit = auction.adUnits[adUnitCode]; + for (const transactionId in auction.adUnits) { + const adUnit = auction.adUnits[transactionId]; // check if this matches let doesMatch; @@ -468,7 +469,7 @@ const getRenderingIds = bidWonData => { // a rendering auction id is the LATEST auction / adunit which contains GAM ID's const matchingFunction = (adUnit, auction) => { // does adUnit match our bidWon and gam id's are present - const gamHasRendered = deepAccess(cache, `auctions.${auction.auctionId}.gamRenders.${adUnit.adUnitCode}`); + const gamHasRendered = deepAccess(cache, `auctions.${auction.auctionId}.gamRenders.${adUnit.transactionId}`); return adUnit.adUnitCode === bidWonData.adUnitCode && gamHasRendered; } let { adUnit, auction } = findMatchingAdUnitFromAuctions(matchingFunction, false); @@ -493,8 +494,8 @@ const formatBidWon = bidWonData => { }); // get the bid from the source auction id - let bid = deepAccess(cache, `auctions.${bidWonData.auctionId}.auction.adUnits.${bidWonData.adUnitCode}.bids.${bidWonData.requestId}`); - let adUnit = deepAccess(cache, `auctions.${bidWonData.auctionId}.auction.adUnits.${bidWonData.adUnitCode}`); + let bid = deepAccess(cache, `auctions.${bidWonData.auctionId}.auction.adUnits.${bidWonData.transactionId}.bids.${bidWonData.requestId}`); + let adUnit = deepAccess(cache, `auctions.${bidWonData.auctionId}.auction.adUnits.${bidWonData.transactionId}`); let bidWon = { ...bid, sourceAuctionId: bidWonData.auctionId, @@ -541,7 +542,7 @@ const subscribeToGamSlots = () => { const matchesSlot = elementIds.some(isMatchingAdSlot); // next it has to have NOT already been counted as gam rendered - const gamHasRendered = deepAccess(cache, `auctions.${auction.auctionId}.gamRenders.${adUnit.adUnitCode}`); + const gamHasRendered = deepAccess(cache, `auctions.${auction.auctionId}.gamRenders.${adUnit.transactionId}`); return matchesSlot && !gamHasRendered; } let { adUnit, auction } = findMatchingAdUnitFromAuctions(matchingFunction, true); @@ -567,12 +568,12 @@ const subscribeToGamSlots = () => { const gamEvent = formatGamEvent(event, adUnit, auction); // marking that this prebid adunit has had its matching gam render found - deepSetValue(cache, `auctions.${auctionId}.gamRenders.${adUnit.adUnitCode}`, true); + deepSetValue(cache, `auctions.${auctionId}.gamRenders.${adUnit.transactionId}`, true); addEventToQueue({ gamRenders: [gamEvent] }, auctionId, 'gam'); // If this auction now has all gam slots rendered, fire the payload - if (!cache.auctions[auctionId].sent && Object.keys(cache.auctions[auctionId].gamRenders).every(adUnitCode => cache.auctions[auctionId].gamRenders[adUnitCode])) { + if (!cache.auctions[auctionId].sent && Object.keys(cache.auctions[auctionId].gamRenders).every(tid => cache.auctions[auctionId].gamRenders[tid])) { // clear the auction end timeout clearTimeout(cache.timeouts[auctionId]); delete cache.timeouts[auctionId]; @@ -622,6 +623,11 @@ function enableMgniAnalytics(config = {}) { } }; +const handleBidWon = args => { + const bidWon = formatBidWon(args); + addEventToQueue({ bidsWon: [bidWon] }, bidWon.renderAuctionId, 'bidWon'); +} + magniteAdapter.enableAnalytics = enableMgniAnalytics; magniteAdapter.originDisableAnalytics = magniteAdapter.disableAnalytics; @@ -701,8 +707,8 @@ magniteAdapter.track = ({ eventType, args }) => { ad.pattern = deepAccess(adUnit, 'ortb2Imp.ext.data.aupname'); ad.gpid = deepAccess(adUnit, 'ortb2Imp.ext.gpid'); ad.bids = {}; - adMap[adUnit.code] = ad; - gamRenders[adUnit.code] = false; + adMap[adUnit.transactionId] = ad; + gamRenders[adUnit.transactionId] = false; // Handle case elementId's (div Id's) are set on adUnit - PPI const elementIds = deepAccess(adUnit, 'ortb2Imp.ext.data.elementid'); @@ -725,13 +731,10 @@ magniteAdapter.track = ({ eventType, args }) => { gamRenders, pendingEvents: {} } - - // keeping order of auctions and if they have been sent or not - cache.auctionOrder.push(args.auctionId); break; case BID_REQUESTED: args.bids.forEach(bid => { - const adUnit = deepAccess(cache, `auctions.${args.auctionId}.auction.adUnits.${bid.adUnitCode}`); + const adUnit = deepAccess(cache, `auctions.${args.auctionId}.auction.adUnits.${bid.transactionId}`); adUnit.bids[bid.bidId] = pick(bid, [ 'bidder', 'bidId', @@ -750,7 +753,7 @@ magniteAdapter.track = ({ eventType, args }) => { break; case BID_RESPONSE: const auctionEntry = deepAccess(cache, `auctions.${args.auctionId}.auction`); - const adUnit = deepAccess(auctionEntry, `adUnits.${args.adUnitCode}`); + const adUnit = deepAccess(auctionEntry, `adUnits.${args.transactionId}`); let bid = adUnit.bids[args.requestId]; // if this came from multibid, there might now be matching bid, so check @@ -807,7 +810,7 @@ magniteAdapter.track = ({ eventType, args }) => { const serverError = deepAccess(args, 'serverErrors.0'); const serverResponseTimeMs = args.serverResponseTimeMs; args.bids.forEach(bid => { - let cachedBid = deepAccess(cache, `auctions.${bid.auctionId}.auction.adUnits.${bid.adUnitCode}.bids.${bid.bidId}`); + let cachedBid = deepAccess(cache, `auctions.${bid.auctionId}.auction.adUnits.${bid.transactionId}.bids.${bid.bidId}`); if (typeof bid.serverResponseTimeMs !== 'undefined') { cachedBid.serverLatencyMillis = bid.serverResponseTimeMs; } else if (serverResponseTimeMs && bid.source === 's2s') { @@ -829,8 +832,16 @@ magniteAdapter.track = ({ eventType, args }) => { }); break; case BID_WON: - const bidWon = formatBidWon(args); - addEventToQueue({ bidsWon: [bidWon] }, bidWon.renderAuctionId, 'bidWon'); + // Allowing us to delay bidWon handling so it happens at right time + // we expect it to happen after gpt slotRenderEnded, but have seen it happen before when testing + // this will ensure it happens after if set + if (rubiConf.analyticsProcessDelay > 0) { + setTimeout(() => { + handleBidWon(args); + }, rubiConf.analyticsProcessDelay); + } else { + handleBidWon(args); + } break; case AUCTION_END: let auctionCache = cache.auctions[args.auctionId]; @@ -838,8 +849,12 @@ magniteAdapter.track = ({ eventType, args }) => { if (!auctionCache) { break; } + // Set this auction as being done auctionCache.auction.auctionEnd = args.auctionEnd; + // keeping order of auctions and if the payload has been sent or not + cache.auctionOrder.push(args.auctionId); + const isOnlyInstreamAuction = args.adUnits && args.adUnits.every(adUnit => adUnitIsOnlyInstream(adUnit)); // if we are not waiting OR it is instream only auction @@ -854,7 +869,7 @@ magniteAdapter.track = ({ eventType, args }) => { break; case BID_TIMEOUT: args.forEach(badBid => { - let bid = deepAccess(cache, `auctions.${badBid.auctionId}.auction.adUnits.${badBid.adUnitCode}.bids.${badBid.bidId}`, {}); + let bid = deepAccess(cache, `auctions.${badBid.auctionId}.auction.adUnits.${badBid.transactionId}.bids.${badBid.bidId}`, {}); // might be set already by bidder-done, so do not overwrite if (bid.status !== 'error') { bid.status = 'error'; diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index c57b44226a5..eb9ba190e33 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -115,6 +115,7 @@ const MOCK = { BID_REQUESTED: { 'bidderCode': 'rubicon', 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', 'bids': [ { 'bidder': 'rubicon', @@ -125,6 +126,7 @@ const MOCK = { }, 'adUnitCode': 'box', 'bidId': '23fcd8cf4bf0d7', + 'transactionId': '7b10a106-89ea-4e19-bc51-9b2e970fc42a', 'src': 'client', } ] @@ -194,8 +196,8 @@ const ANALYTICS_MESSAGE = { 'version': '$prebid.version$', 'referrerHostname': 'a-test-domain.com', 'timestamps': { - 'timeSincePageLoad': 200, - 'eventTime': 1519767013981, + 'timeSincePageLoad': 500, + 'eventTime': 1519767014281, 'prebidLoaded': magniteAdapter.MODULE_INITIALIZED_TIME }, 'wrapper': { @@ -398,8 +400,9 @@ describe('magnite analytics adapter', function () { } }); expect(rubiConf).to.deep.equal({ - analyticsEventDelay: 200, + analyticsEventDelay: 500, analyticsBatchTimeout: 5000, + analyticsProcessDelay: 1, dmBilling: { enabled: false, vendors: [], @@ -424,8 +427,9 @@ describe('magnite analytics adapter', function () { } }); expect(rubiConf).to.deep.equal({ - analyticsEventDelay: 200, + analyticsEventDelay: 500, analyticsBatchTimeout: 3000, + analyticsProcessDelay: 1, dmBilling: { enabled: false, vendors: [], @@ -451,8 +455,9 @@ describe('magnite analytics adapter', function () { } }); expect(rubiConf).to.deep.equal({ - analyticsEventDelay: 200, + analyticsEventDelay: 500, analyticsBatchTimeout: 3000, + analyticsProcessDelay: 1, dmBilling: { enabled: false, vendors: [], @@ -956,8 +961,8 @@ describe('magnite analytics adapter', function () { events.emit(BID_WON, bidWon); - // hit the eventDelay - clock.tick(rubiConf.analyticsEventDelay + 100); + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); expect(server.requests.length).to.equal(1); let request = server.requests[0]; @@ -988,10 +993,10 @@ describe('magnite analytics adapter', function () { let request = server.requests[0]; let message = JSON.parse(request.requestBody); - // The timestamps should be changed from the default by 1800 (set eventDelay - eventDelay default (200)) + // The timestamps should be changed from the default by (set eventDelay (2000) - eventDelay default (500)) let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); - expectedMessage.timestamps.eventTime = expectedMessage.timestamps.eventTime + 1800; - expectedMessage.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + 1800; + expectedMessage.timestamps.eventTime = expectedMessage.timestamps.eventTime + 1500; + expectedMessage.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + 1500; expect(message).to.deep.equal(expectedMessage); }); @@ -1014,8 +1019,8 @@ describe('magnite analytics adapter', function () { events.emit(BID_WON, MOCK.BID_WON); - // hit the eventDelay - clock.tick(rubiConf.analyticsEventDelay); + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); expect(server.requests.length).to.equal(1); let request = server.requests[0]; @@ -1048,8 +1053,8 @@ describe('magnite analytics adapter', function () { events.emit(BID_WON, MOCK.BID_WON); - // hit the eventDelay - clock.tick(rubiConf.analyticsEventDelay); + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); expect(server.requests.length).to.equal(1); let request = server.requests[0]; @@ -1084,8 +1089,8 @@ describe('magnite analytics adapter', function () { events.emit(BID_WON, MOCK.BID_WON); - // hit the eventDelay - clock.tick(rubiConf.analyticsEventDelay); + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); expect(server.requests.length).to.equal(1); let request = server.requests[0]; @@ -1104,8 +1109,8 @@ describe('magnite analytics adapter', function () { // Now send bidWon events.emit(BID_WON, MOCK.BID_WON); - // tick the event delay time - clock.tick(rubiConf.analyticsEventDelay); + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); // should see two server requests expect(server.requests.length).to.equal(2); @@ -1125,8 +1130,8 @@ describe('magnite analytics adapter', function () { delete expectedMessage2.gamRenders; // second event should be event delay time after first one - expectedMessage2.timestamps.eventTime = expectedMessage.timestamps.eventTime + rubiConf.analyticsEventDelay; - expectedMessage2.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + rubiConf.analyticsEventDelay; + expectedMessage2.timestamps.eventTime = expectedMessage.timestamps.eventTime + rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay; + expectedMessage2.timestamps.timeSincePageLoad = expectedMessage.timestamps.timeSincePageLoad + rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay; // trigger is `batched-bidsWon` expectedMessage2.trigger = 'batched-bidsWon'; @@ -1142,8 +1147,8 @@ describe('magnite analytics adapter', function () { mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); events.emit(BID_WON, MOCK.BID_WON); - // tick the event delay time - clock.tick(rubiConf.analyticsEventDelay); + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); // should see two server requests expect(server.requests.length).to.equal(2); @@ -1184,7 +1189,8 @@ describe('magnite analytics adapter', function () { config.setConfig({ rubicon: { analyticsBatchTimeout: 0, - analyticsEventDelay: 0 + analyticsEventDelay: 0, + analyticsProcessDelay: 0 } }); @@ -1223,7 +1229,8 @@ describe('magnite analytics adapter', function () { { auctionId: MOCK.AUCTION_INIT.auctionId, adUnitCode: MOCK.AUCTION_INIT.adUnits[0].code, - bidId: MOCK.BID_REQUESTED.bids[0].bidId + bidId: MOCK.BID_REQUESTED.bids[0].bidId, + transactionId: MOCK.AUCTION_INIT.adUnits[0].transactionId, } ]); @@ -1233,8 +1240,8 @@ describe('magnite analytics adapter', function () { // emmit gpt events and bidWon mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); - // hit the eventDelay - clock.tick(rubiConf.analyticsEventDelay); + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); expect(server.requests.length).to.equal(1); let request = server.requests[0]; @@ -1285,8 +1292,8 @@ describe('magnite analytics adapter', function () { events.emit(BID_WON, MOCK.BID_WON); - // hit the eventDelay - clock.tick(rubiConf.analyticsEventDelay); + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); expect(server.requests.length).to.equal(1); let request = server.requests[0]; @@ -1318,8 +1325,8 @@ describe('magnite analytics adapter', function () { bidWon.bidderDetail = 'rubi2'; events.emit(BID_WON, bidWon); - // hit the eventDelay - clock.tick(rubiConf.analyticsEventDelay); + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); expect(server.requests.length).to.equal(1); @@ -1400,8 +1407,8 @@ describe('magnite analytics adapter', function () { mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); events.emit(BID_WON, MOCK.BID_WON); - // hit the eventDelay - clock.tick(rubiConf.analyticsEventDelay); + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); expect(server.requests.length).to.equal(1); let request = server.requests[0]; @@ -1422,7 +1429,8 @@ describe('magnite analytics adapter', function () { useBidCache: true, rubicon: { analyticsEventDelay: 0, - analyticsBatchTimeout: 0 + analyticsBatchTimeout: 0, + analyticsProcessDelay: 0 } }); @@ -1433,9 +1441,9 @@ describe('magnite analytics adapter', function () { { ...MOCK.AUCTION_INIT, auctionId: 'auctionId-3', adUnits: [{ ...MOCK.AUCTION_INIT.adUnits[0], transactionId: 'tid-3' }] } ]; bidRequests = [ - { ...MOCK.BID_REQUESTED, auctionId: 'auctionId-1', bids: [{ ...MOCK.BID_REQUESTED.bids[0], bidId: 'bidId-1' }] }, - { ...MOCK.BID_REQUESTED, auctionId: 'auctionId-2', bids: [{ ...MOCK.BID_REQUESTED.bids[0], bidId: 'bidId-2' }] }, - { ...MOCK.BID_REQUESTED, auctionId: 'auctionId-3', bids: [{ ...MOCK.BID_REQUESTED.bids[0], bidId: 'bidId-3' }] } + { ...MOCK.BID_REQUESTED, auctionId: 'auctionId-1', bids: [{ ...MOCK.BID_REQUESTED.bids[0], bidId: 'bidId-1', transactionId: 'tid-1' }] }, + { ...MOCK.BID_REQUESTED, auctionId: 'auctionId-2', bids: [{ ...MOCK.BID_REQUESTED.bids[0], bidId: 'bidId-2', transactionId: 'tid-2' }] }, + { ...MOCK.BID_REQUESTED, auctionId: 'auctionId-3', bids: [{ ...MOCK.BID_REQUESTED.bids[0], bidId: 'bidId-3', transactionId: 'tid-3' }] } ]; bidResponses = [ { ...MOCK.BID_RESPONSE, auctionId: 'auctionId-1', transactionId: 'tid-1', requestId: 'bidId-1' }, @@ -1631,7 +1639,8 @@ describe('magnite analytics adapter', function () { mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); events.emit(BID_WON, MOCK.BID_WON); - clock.tick(rubiConf.analyticsEventDelay); + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); } it('should ignore billing events when not enabled', () => { basicBillingAuction([{ From b24b7ed310a07ff9c07f7d088e2370a072ffea37 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Sat, 8 Oct 2022 11:00:24 -0700 Subject: [PATCH 22/22] oops --- modules/rubiconAnalyticsAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 9483635214c..76043b71c64 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -804,7 +804,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } let bid = auctionEntry.bids[args.requestId]; - // If floor rot besolved gptSlut we have not yet, then update the adUnit to have the adSlot name + // If floor resolved gptSlot but we have not yet, then update the adUnit to have the adSlot name if (!deepAccess(bid, 'adUnit.gam.adSlot') && deepAccess(args, 'floorData.matchedFields.gptSlot')) { deepSetValue(bid, 'adUnit.gam.adSlot', args.floorData.matchedFields.gptSlot); }