UNPKG

mk9-prebid

Version:

Header Bidding Management Library

822 lines (761 loc) 29.5 kB
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 * as utils from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { getStorageManager } from '../src/storageManager.js'; const RUBICON_GVL_ID = 52; export const storage = getStorageManager(RUBICON_GVL_ID, 'rubicon'); const COOKIE_NAME = 'rpaSession'; const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins const END_EXPIRE_TIME = 21600000; // 6 hours 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 }, STATUS: { GOOD, NO_BID }, BID_STATUS: { BID_REJECTED } } = CONSTANTS; let serverConfig; config.getConfig('s2sConfig', ({s2sConfig}) => { serverConfig = s2sConfig; }); export const SEND_TIMEOUT = 3000; const DEFAULT_INTEGRATION = 'pbjs'; const cache = { auctions: {}, targeting: {}, timeouts: {}, gpt: {}, }; const BID_REJECTED_IPF = 'rejected-ipf'; export let rubiConf = { pvid: utils.generateUUID().slice(0, 8), analyticsEventDelay: 0 }; // 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 => { utils.mergeDeep(rubiConf, config.rubicon); if (utils.deepAccess(config, 'rubicon.updatePageView') === true) { rubiConf.pvid = utils.generateUUID().slice(0, 8) } }); export function getHostNameFromReferer(referer) { try { rubiconAdapter.referrerHostname = utils.parseUrl(referer, {noDecodeWholeURL: true}).hostname; } catch (e) { utils.logError('Rubicon Analytics: Unable to parse hostname from supplied url: ', referer, e); rubiconAdapter.referrerHostname = ''; } return rubiconAdapter.referrerHostname }; function stringProperties(obj) { return Object.keys(obj).reduce((newObj, prop) => { let value = obj[prop]; if (typeof value === 'number') { value = value.toFixed(3); } else if (typeof value !== 'string') { value = String(value); } newObj[prop] = value; return newObj; }, {}); } function sizeToDimensions(size) { return { width: size.w || size[0], height: size.h || size[1] }; } function validMediaType(type) { return ['banner', 'native', 'video'].indexOf(type) !== -1; } function formatSource(src) { if (typeof src === 'undefined') { src = 'client'; } else if (src === 's2s') { src = 'server'; } return src.toLowerCase(); } function sendMessage(auctionId, bidWonId, trigger) { function formatBid(bid) { return utils.pick(bid, [ 'bidder', 'bidderDetail', 'bidId', bidId => utils.deepAccess(bid, 'bidResponse.pbsBidId') || utils.deepAccess(bid, 'bidResponse.seatBidId') || bidId, 'status', 'error', 'source', (source, bid) => { if (source) { return source; } return serverConfig && Array.isArray(serverConfig.bidders) && serverConfig.bidders.some(s2sBidder => s2sBidder.toLowerCase() === bid.bidder) !== -1 ? 'server' : 'client' }, 'clientLatencyMillis', 'serverLatencyMillis', 'params', 'bidResponse', bidResponse => bidResponse ? utils.pick(bidResponse, [ 'bidPriceUSD', 'dealId', 'dimensions', 'mediaType', 'floorValue', 'floorRuleValue', 'floorRule', 'adomains' ]) : undefined ]); } function formatBidWon(bid) { return Object.assign(formatBid(bid), utils.pick(bid.adUnit, [ 'adUnitCode', 'transactionId', 'videoAdFormat', () => bid.videoAdFormat, 'mediaTypes' ]), { adserverTargeting: !utils.isEmpty(cache.targeting[bid.adUnit.adUnitCode]) ? stringProperties(cache.targeting[bid.adUnit.adUnitCode]) : undefined, bidwonStatus: 'success', // hard-coded for now accountId, siteId: bid.siteId, zoneId: bid.zoneId, samplingFactor }); } let auctionCache = cache.auctions[auctionId]; let referrer = config.getConfig('pageUrl') || (auctionCache && auctionCache.referrer); let message = { timestamps: { prebidLoaded: rubiconAdapter.MODULE_INITIALIZED_TIME, auctionEnded: auctionCache.endTs, eventTime: Date.now() }, trigger, integration: rubiConf.int_type || DEFAULT_INTEGRATION, version: '$prebid.version$', referrerUri: referrer, referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), channel: 'web', }; if (rubiConf.wrapperName) { message.wrapper = { name: rubiConf.wrapperName, family: rubiConf.wrapperFamily, rule: rubiConf.rule_name } } if (auctionCache && !auctionCache.sent) { let adUnitMap = Object.keys(auctionCache.bids).reduce((adUnits, bidId) => { let bid = auctionCache.bids[bidId]; let adUnit = adUnits[bid.adUnit.adUnitCode]; if (!adUnit) { adUnit = adUnits[bid.adUnit.adUnitCode] = utils.pick(bid.adUnit, [ 'adUnitCode', 'transactionId', 'mediaTypes', 'dimensions', 'adserverTargeting', () => !utils.isEmpty(cache.targeting[bid.adUnit.adUnitCode]) ? stringProperties(cache.targeting[bid.adUnit.adUnitCode]) : undefined, 'gam', gam => !utils.isEmpty(gam) ? gam : undefined, 'pbAdSlot', 'pattern' ]); adUnit.bids = []; adUnit.status = 'no-bid'; // default it to be no bid } // Add site and zone id if not there and if we found a rubicon bidder if ((!adUnit.siteId || !adUnit.zoneId) && rubiconAliases.indexOf(bid.bidder) !== -1) { if (utils.deepAccess(bid, 'params.accountId') == accountId) { adUnit.accountId = parseInt(accountId); adUnit.siteId = parseInt(utils.deepAccess(bid, 'params.siteId')); adUnit.zoneId = parseInt(utils.deepAccess(bid, 'params.zoneId')); } } if (bid.videoAdFormat && !adUnit.videoAdFormat) { adUnit.videoAdFormat = bid.videoAdFormat; } // 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; } adUnit.bids.push(formatBid(bid)); return adUnits; }, {}); // We need to mark each cached bid response with its appropriate rubicon site-zone id // This allows the bidWon events to have these params even in the case of a delayed render Object.keys(auctionCache.bids).forEach(function (bidId) { let adCode = auctionCache.bids[bidId].adUnit.adUnitCode; Object.assign(auctionCache.bids[bidId], utils.pick(adUnitMap[adCode], ['accountId', 'siteId', 'zoneId'])); }); let auction = { clientTimeoutMillis: auctionCache.timeout, samplingFactor, accountId, adUnits: Object.keys(adUnitMap).map(i => adUnitMap[i]), requestId: auctionId }; // pick our of top level floor data we want to send! if (auctionCache.floorData) { if (auctionCache.floorData.location === 'noData') { auction.floors = utils.pick(auctionCache.floorData, [ 'location', 'fetchStatus', 'floorProvider as provider' ]); } else { auction.floors = utils.pick(auctionCache.floorData, [ 'location', 'modelVersion as modelName', 'modelWeight', 'modelTimestamp', 'skipped', 'enforcement', () => utils.deepAccess(auctionCache.floorData, 'enforcements.enforceJS'), 'dealsEnforced', () => utils.deepAccess(auctionCache.floorData, 'enforcements.floorDeals'), 'skipRate', 'fetchStatus', 'floorMin', 'floorProvider as provider' ]); } } // gather gdpr info if (auctionCache.gdprConsent) { auction.gdpr = utils.pick(auctionCache.gdprConsent, [ 'gdprApplies as applies', 'consentString', 'apiVersion as version' ]); } // gather session info if (auctionCache.session) { message.session = utils.pick(auctionCache.session, [ 'id', 'pvid', 'start', 'expires' ]); if (!utils.isEmpty(auctionCache.session.fpkvs)) { message.fpkvs = Object.keys(auctionCache.session.fpkvs).map(key => { return { key, value: auctionCache.session.fpkvs[key] }; }); } } if (serverConfig) { auction.serverTimeoutMillis = serverConfig.timeout; } if (auctionCache.userIds.length) { auction.user = {ids: auctionCache.userIds}; } message.auctions = [auction]; let bidsWon = Object.keys(auctionCache.bidsWon).reduce((memo, adUnitCode) => { let bidId = auctionCache.bidsWon[adUnitCode]; if (bidId) { memo.push(formatBidWon(auctionCache.bids[bidId])); } return memo; }, []); if (bidsWon.length > 0) { message.bidsWon = bidsWon; } auctionCache.sent = true; } else if (bidWonId && auctionCache && auctionCache.bids[bidWonId]) { message.bidsWon = [ formatBidWon(auctionCache.bids[bidWonId]) ]; } ajax( this.getUrl(), null, JSON.stringify(message), { contentType: 'application/json' } ); } function adUnitIsOnlyInstream(adUnit) { return adUnit.mediaTypes && Object.keys(adUnit.mediaTypes).length === 1 && utils.deepAccess(adUnit, 'mediaTypes.video.context') === 'instream'; } function getBidPrice(bid) { // get the cpm from bidResponse let cpm; let currency; if (bid.status === BID_REJECTED && utils.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) { utils.logWarn('Rubicon Analytics Adapter: Could not determine the bidPriceUSD of the bid ', bid); } } export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { // 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) if (previousBidResponse && previousBidResponse.bidPriceUSD > responsePrice) { return previousBidResponse; } return utils.pick(bid, [ 'bidPriceUSD', () => responsePrice, 'dealId', 'status', '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 'pbsBidId', pbsBidId => pbsBidId == 0 ? utils.generateUUID() : pbsBidId, 'seatBidId', seatBidId => seatBidId == 0 ? utils.generateUUID() : seatBidId, 'floorValue', () => utils.deepAccess(bid, 'floorData.floorValue'), 'floorRuleValue', () => utils.deepAccess(bid, 'floorData.floorRuleValue'), 'floorRule', () => utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined, 'adomains', () => { let adomains = utils.deepAccess(bid, 'meta.advertiserDomains'); return Array.isArray(adomains) && adomains.length > 0 ? adomains.slice(0, 10) : undefined } ]); } /* Filters and converts URL Params into an object and returns only KVs that match the 'utm_KEY' format */ function getUtmParams() { let search; try { search = utils.parseQS(utils.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; } let samplingFactor = 1; let accountId; // List of known rubicon aliases // This gets updated on auction init to account for any custom aliases present let rubiconAliases = ['rubicon']; /* Checks the alias registry for any entries of the rubicon bid adapter. adds to the rubiconAliases list if found */ function setRubiconAliases(aliasRegistry) { Object.keys(aliasRegistry).forEach(function (alias) { if (aliasRegistry[alias] === 'rubicon') { rubiconAliases.push(alias); } }); } function getRpaCookie() { let encodedCookie = storage.getDataFromLocalStorage(COOKIE_NAME); if (encodedCookie) { try { return JSON.parse(window.atob(encodedCookie)); } catch (e) { utils.logError(`Rubicon Analytics: Unable to decode ${COOKIE_NAME} value: `, e); } } return {}; } function setRpaCookie(decodedCookie) { try { storage.setDataInLocalStorage(COOKIE_NAME, window.btoa(JSON.stringify(decodedCookie))); } catch (e) { utils.logError(`Rubicon Analytics: Unable to encode ${COOKIE_NAME} value: `, e); } } function 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: utils.generateUUID(), start: currentTime, expires: currentTime + END_EXPIRE_TIME, // six hours later, } } // possible that decodedRpaCookie is undefined, and if it is, we probably are blocked by storage or some other exception if (Object.keys(decodedRpaCookie).length) { decodedRpaCookie.lastSeen = currentTime; decodedRpaCookie.fpkvs = {...decodedRpaCookie.fpkvs, ...getFpkvs()}; decodedRpaCookie.pvid = rubiConf.pvid; setRpaCookie(decodedRpaCookie) } return decodedRpaCookie; } function subscribeToGamSlots() { window.googletag.pubads().addEventListener('slotRenderEnded', event => { const isMatchingAdSlot = utils.isAdUnitCodeMatchingSlot(event.slot); // loop through auctions and adUnits and mark the info Object.keys(cache.auctions).forEach(auctionId => { (Object.keys(cache.auctions[auctionId].bids) || []).forEach(bidId => { let bid = cache.auctions[auctionId].bids[bidId]; // if this slot matches this bids adUnit, add the adUnit info if (isMatchingAdSlot(bid.adUnit.adUnitCode)) { // mark this adUnit as having been rendered by gam cache.auctions[auctionId].gamHasRendered[bid.adUnit.adUnitCode] = true; bid.adUnit.gam = utils.pick(event, [ // these come in as `null` from Gpt, which when stringified does not get removed // so set explicitly to undefined when not a number 'advertiserId', advertiserId => utils.isNumber(advertiserId) ? advertiserId : undefined, 'creativeId', creativeId => utils.isNumber(event.sourceAgnosticCreativeId) ? event.sourceAgnosticCreativeId : utils.isNumber(creativeId) ? creativeId : undefined, 'lineItemId', lineItemId => utils.isNumber(event.sourceAgnosticLineItemId) ? event.sourceAgnosticLineItemId : utils.isNumber(lineItemId) ? lineItemId : undefined, 'adSlot', () => event.slot.getAdUnitPath(), 'isSlotEmpty', () => event.isEmpty || undefined ]); } }); // Now if all adUnits have gam rendered, send the payload if (rubiConf.waitForGamSlots && !cache.auctions[auctionId].sent && Object.keys(cache.auctions[auctionId].gamHasRendered).every(adUnitCode => cache.auctions[auctionId].gamHasRendered[adUnitCode])) { clearTimeout(cache.timeouts[auctionId]); delete cache.timeouts[auctionId]; if (rubiConf.analyticsEventDelay > 0) { setTimeout(() => sendMessage.call(rubiconAdapter, auctionId, undefined, 'delayedGam'), rubiConf.analyticsEventDelay) } else { sendMessage.call(rubiconAdapter, auctionId, undefined, 'gam') } } }); }); } let baseAdapter = adapter({analyticsType: 'endpoint'}); let rubiconAdapter = Object.assign({}, baseAdapter, { MODULE_INITIALIZED_TIME: Date.now(), referrerHostname: '', enableAnalytics(config = {}) { let error = false; samplingFactor = 1; if (typeof config.options === 'object') { if (config.options.accountId) { accountId = Number(config.options.accountId); } if (config.options.endpoint) { this.getUrl = () => config.options.endpoint; } else { utils.logError('required endpoint missing from rubicon analytics'); error = true; } if (typeof config.options.sampling !== 'undefined') { samplingFactor = 1 / parseFloat(config.options.sampling); } if (typeof config.options.samplingFactor !== 'undefined') { if (typeof config.options.sampling !== 'undefined') { utils.logWarn('Both options.samplingFactor and options.sampling enabled in rubicon analytics, defaulting to samplingFactor'); } samplingFactor = parseFloat(config.options.samplingFactor); config.options.sampling = 1 / samplingFactor; } } let validSamplingFactors = [1, 10, 20, 40, 100]; if (validSamplingFactors.indexOf(samplingFactor) === -1) { error = true; utils.logError('invalid samplingFactor for rubicon analytics: ' + samplingFactor + ', must be one of ' + validSamplingFactors.join(', ')); } else if (!accountId) { error = true; utils.logError('required accountId missing for rubicon analytics'); } if (!error) { baseAdapter.enableAnalytics.call(this, config); } }, disableAnalytics() { this.getUrl = baseAdapter.getUrl; accountId = undefined; rubiConf = {}; cache.gpt.registered = false; baseAdapter.disableAnalytics.apply(this, arguments); }, track({eventType, args}) { switch (eventType) { case AUCTION_INIT: // set the rubicon aliases setRubiconAliases(adapterManager.aliasRegistry); let cacheEntry = utils.pick(args, [ 'timestamp', 'timeout' ]); cacheEntry.bids = {}; cacheEntry.bidsWon = {}; cacheEntry.gamHasRendered = {}; cacheEntry.referrer = utils.deepAccess(args, 'bidderRequests.0.refererInfo.referer'); const floorData = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData'); if (floorData) { cacheEntry.floorData = {...floorData}; } cacheEntry.gdprConsent = utils.deepAccess(args, 'bidderRequests.0.gdprConsent'); cacheEntry.session = storage.localStorageIsEnabled() && updateRpaCookie(); cacheEntry.userIds = Object.keys(utils.deepAccess(args, 'bidderRequests.0.bids.0.userId', {})).map(id => { return {provider: id, hasId: true} }); cache.auctions[args.auctionId] = cacheEntry; // register to listen to gpt events if not done yet if (!cache.gpt.registered && utils.isGptPubadsDefined()) { subscribeToGamSlots(); cache.gpt.registered = true; } else if (!cache.gpt.registered) { cache.gpt.registered = true; window.googletag = window.googletag || {}; window.googletag.cmd = window.googletag.cmd || []; window.googletag.cmd.push(function() { subscribeToGamSlots(); }); } break; case BID_REQUESTED: Object.assign(cache.auctions[args.auctionId].bids, args.bids.reduce((memo, bid) => { // mark adUnits we expect bidWon events for cache.auctions[args.auctionId].bidsWon[bid.adUnitCode] = false; if (rubiConf.waitForGamSlots && !adUnitIsOnlyInstream(bid)) { cache.auctions[args.auctionId].gamHasRendered[bid.adUnitCode] = false; } memo[bid.bidId] = utils.pick(bid, [ 'bidder', bidder => bidder.toLowerCase(), 'bidId', 'status', () => 'no-bid', // default a bid to no-bid until response is recieved or bid is timed out 'source', () => formatSource(bid.src), 'params', (params, bid) => { switch (bid.bidder) { // specify bidder params we want here case 'rubicon': return utils.pick(params, [ 'accountId', 'siteId', 'zoneId' ]); } }, 'videoAdFormat', (_, cachedBid) => { if (cachedBid.bidder === 'rubicon') { return ({ 201: 'pre-roll', 202: 'interstitial', 203: 'outstream', 204: 'mid-roll', 205: 'post-roll', 207: 'vertical' })[utils.deepAccess(bid, 'params.video.size_id')]; } else { let startdelay = parseInt(utils.deepAccess(bid, 'params.video.startdelay'), 10); if (!isNaN(startdelay)) { if (startdelay > 0) { return 'mid-roll'; } return ({ '0': 'pre-roll', '-1': 'mid-roll', '-2': 'post-roll' })[startdelay] } } }, 'adUnit', () => utils.pick(bid, [ 'adUnitCode', 'transactionId', 'sizes as dimensions', sizes => sizes.map(sizeToDimensions), 'mediaTypes', (types) => { if (bid.mediaType && validMediaType(bid.mediaType)) { return [bid.mediaType]; } if (Array.isArray(types)) { return types.filter(validMediaType); } if (typeof types === 'object') { if (!bid.sizes) { bid.dimensions = []; utils._each(types, (type) => bid.dimensions = bid.dimensions.concat( type.sizes.map(sizeToDimensions) ) ); } return Object.keys(types).filter(validMediaType); } return ['banner']; }, 'gam', () => { if (utils.deepAccess(bid, 'ortb2Imp.ext.data.adserver.name') === 'gam') { return {adSlot: bid.ortb2Imp.ext.data.adserver.adslot} } }, 'pbAdSlot', () => utils.deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'), 'pattern', () => utils.deepAccess(bid, 'ortb2Imp.ext.data.aupname') ]) ]); return memo; }, {})); break; case BID_RESPONSE: let auctionEntry = cache.auctions[args.auctionId]; if (!auctionEntry.bids[args.requestId] && args.originalRequestId) { auctionEntry.bids[args.requestId] = {...auctionEntry.bids[args.originalRequestId]}; auctionEntry.bids[args.requestId].bidId = args.requestId; auctionEntry.bids[args.requestId].bidderDetail = args.targetingBidder; } let bid = auctionEntry.bids[args.requestId]; // If floor resolved gptSlot but we have not yet, then update the adUnit to have the adSlot name if (!utils.deepAccess(bid, 'adUnit.gam.adSlot') && utils.deepAccess(args, 'floorData.matchedFields.gptSlot')) { utils.deepSetValue(bid, 'adUnit.gam.adSlot', args.floorData.matchedFields.gptSlot); } // if we have not set enforcements yet set it if (!utils.deepAccess(auctionEntry, 'floorData.enforcements') && utils.deepAccess(args, 'floorData.enforcements')) { auctionEntry.floorData.enforcements = {...args.floorData.enforcements}; } if (!bid) { utils.logError('Rubicon Anlytics Adapter Error: Could not find associated bid request for bid response with requestId: ', args.requestId); break; } bid.source = formatSource(bid.source || args.source); 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].timestamp; bid.bidResponse = parseBidResponse(args, bid.bidResponse); break; case BIDDER_DONE: const serverError = utils.deepAccess(args, 'serverErrors.0'); const serverResponseTimeMs = args.serverResponseTimeMs; args.bids.forEach(bid => { let cachedBid = cache.auctions[bid.auctionId].bids[bid.bidId || bid.requestId]; 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 } } if (!cachedBid.status) { cachedBid.status = 'no-bid'; } if (!cachedBid.clientLatencyMillis) { cachedBid.clientLatencyMillis = Date.now() - cache.auctions[bid.auctionId].timestamp; } }); break; case SET_TARGETING: Object.assign(cache.targeting, args); break; case BID_WON: let auctionCache = cache.auctions[args.auctionId]; auctionCache.bidsWon[args.adUnitCode] = args.requestId; // check if this BID_WON missed the boat, if so send by itself if (auctionCache.sent === true) { sendMessage.call(this, args.auctionId, args.requestId, 'soloBidWon'); } else if (!rubiConf.waitForGamSlots && Object.keys(auctionCache.bidsWon).reduce((memo, adUnitCode) => { // only send if we've received bidWon events for all adUnits in auction memo = memo && auctionCache.bidsWon[adUnitCode]; return memo; }, true)) { clearTimeout(cache.timeouts[args.auctionId]); delete cache.timeouts[args.auctionId]; sendMessage.call(this, args.auctionId, undefined, 'allBidWons'); } break; case AUCTION_END: // see how long it takes for the payload to come fire cache.auctions[args.auctionId].endTs = Date.now(); const isOnlyInstreamAuction = args.adUnits && args.adUnits.every(adUnit => adUnitIsOnlyInstream(adUnit)); // If only instream, do not wait around, just send payload if (isOnlyInstreamAuction) { sendMessage.call(this, args.auctionId, undefined, 'instreamAuction'); } else { // start timer to send batched payload just in case we don't hear any BID_WON events cache.timeouts[args.auctionId] = setTimeout(() => { sendMessage.call(this, args.auctionId, undefined, 'auctionEnd'); }, rubiConf.analyticsBatchTimeout || SEND_TIMEOUT); } break; case BID_TIMEOUT: args.forEach(badBid => { let auctionCache = cache.auctions[badBid.auctionId]; let bid = auctionCache.bids[badBid.bidId || badBid.requestId]; // might be set already by bidder-done, so do not overwrite if (bid.status !== 'error') { bid.status = 'error'; bid.error = { code: 'timeout-error', message: 'marked by prebid.js as timeout' // will help us diff if timeout was set by PBS or PBJS }; } }); break; } } }); adapterManager.registerAnalyticsAdapter({ adapter: rubiconAdapter, code: 'rubicon', gvlid: RUBICON_GVL_ID }); export default rubiconAdapter;