From 7fb4d9a4fe3473c1a124b4d3ce34161d8768bfa5 Mon Sep 17 00:00:00 2001 From: Nathan Oliver Date: Wed, 20 Mar 2024 21:33:27 +0000 Subject: [PATCH 1/4] PTOW-2; updates to the pubx analytics adapter --- modules/pubxaiAnalyticsAdapter.js | 447 ++++-- .../modules/pubxaiAnalyticsAdapter_spec.js | 1385 ++++++++++------- 2 files changed, 1084 insertions(+), 748 deletions(-) diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index e97e5505768..c069a52cb47 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -1,10 +1,20 @@ -import { deepAccess, parseSizesInput, getWindowLocation, buildUrl } from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; +import { + deepAccess, + parseSizesInput, + getWindowLocation, + buildUrl, + cyrb53Hash, +} from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import {getGlobal} from '../src/prebidGlobal.js'; -import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { + getGptSlotInfoForAdUnitCode, + getGptSlotForAdUnitCode, +} from '../libraries/gptUtils/gptUtils.js'; + +let initOptions; const emptyUrl = ''; const analyticsType = 'endpoint'; @@ -13,123 +23,229 @@ const defaultHost = 'api.pbxai.com'; const auctionPath = '/analytics/auction'; const winningBidPath = '/analytics/bidwon'; -let initOptions; -let auctionTimestamp; -let auctionCache = []; -let events = { - bids: [], - floorDetail: {}, - pageDetail: {}, - deviceDetail: {} +/** + * auctionCache is a global cache object which stores all auction histories + * for the session. When getting a key from the auction cache, any + * information already known about the auction or associated data (floor + * data configured by prebid, browser data, user data etc) is added to + * the cache automatically. + */ +export const auctionCache = new Proxy( + {}, + { + get: (target, name) => { + if (!Object.hasOwn(target, name)) { + target[name] = { + bids: [], + auctionDetail: { + refreshRank: Object.keys(target).length, + auctionId: name, + }, + floorDetail: {}, + pageDetail: { + host: getWindowLocation().host, + path: getWindowLocation().pathname, + search: getWindowLocation().search, + }, + deviceDetail: { + platform: navigator.platform, + deviceType: getDeviceType(), + deviceOS: getOS(), + browser: getBrowser(), + }, + userDetail: { + userIdTypes: Object.keys(getGlobal().getUserIds?.() || {}), + }, + pmacDetail: JSON.parse(getStorage()?.getItem('pubx:pmac')) || {}, // {auction_1: {floor:0.23,maxBid:0.34,bidCount:3},auction_2:{floor:0.13,maxBid:0.14,bidCount:2} + initOptions: { + ...initOptions, + auctionId: name, // back-compat + }, + sentAs: [], + }; + } + return target[name]; + }, + } +); + +/** + * Fetch extra ad server data for a specific ad slot (bid) + * @param {object} bid an output from extractBid + * @returns {object} key value pairs from the adserver + */ +const getAdServerDataForBid = (bid) => { + const gptSlot = getGptSlotForAdUnitCode(bid); + if (gptSlot) { + return Object.fromEntires( + gptSlot.getTargetingKeys().map((key) => [key, gptSlot.getTargeting(key)]) + ); + } + return {}; // TODO: support more ad servers }; -function getStorage() { +/** + * Access sessionStorage + * @returns {Storage} + */ +const getStorage = () => { try { return window.top['sessionStorage']; } catch (e) { return null; } -} +}; -var pubxaiAnalyticsAdapter = Object.assign(adapter( - { - emptyUrl, - analyticsType - }), { - track({ eventType, args }) { - if (typeof args !== 'undefined') { - if (eventType === CONSTANTS.EVENTS.BID_TIMEOUT) { - args.forEach(item => { mapBidResponse(item, 'timeout'); }); - } else if (eventType === CONSTANTS.EVENTS.AUCTION_INIT) { - events.auctionInit = args; - events.floorDetail = {}; - events.bids = []; - const floorData = deepAccess(args, 'bidderRequests.0.bids.0.floorData'); - if (typeof floorData !== 'undefined') { - Object.assign(events.floorDetail, floorData); - } - auctionTimestamp = args.timestamp; - } else if (eventType === CONSTANTS.EVENTS.BID_RESPONSE) { - mapBidResponse(args, 'response'); - } else if (eventType === CONSTANTS.EVENTS.BID_WON) { - send({ - winningBid: mapBidResponse(args, 'bidwon') - }, 'bidwon'); - } - } - if (eventType === CONSTANTS.EVENTS.AUCTION_END) { - send(events, 'auctionEnd'); - } - } -}); +/** + * extracts and derives valuable data from a prebid bidder bidResponse object + * @param {object} bidResponse a prebid bidder bidResponse (see + * https://docs.prebid.org/dev-docs/publisher-api-reference/getBidResponses.html) + * @returns {object} + */ +const extractBid = (bidResponse) => { + return { + adUnitCode: bidResponse.adUnitCode, + gptSlotCode: + getGptSlotInfoForAdUnitCode(bidResponse.adUnitCode).gptSlot || null, + auctionId: bidResponse.auctionId, + bidderCode: bidResponse.bidder, + cpm: bidResponse.cpm, + creativeId: bidResponse.creativeId, + dealId: bidResponse.dealId, + currency: bidResponse.currency, + floorData: bidResponse.floorData, + mediaType: bidResponse.mediaType, + netRevenue: bidResponse.netRevenue, + requestTimestamp: bidResponse.requestTimestamp, + responseTimestamp: bidResponse.responseTimestamp, + status: bidResponse.status, + sizes: parseSizesInput(bidResponse.size).toString(), + statusMessage: bidResponse.statusMessage, + timeToRespond: bidResponse.timeToRespond, + transactionId: bidResponse.transactionId, + bidId: bidResponse.bidId || bidResponse.requestId, + placementId: bidResponse.params + ? deepAccess(bidResponse, 'params.0.placementId') + : null, + }; +}; -function mapBidResponse(bidResponse, status) { - if (typeof bidResponse !== 'undefined') { - let bid = { - adUnitCode: bidResponse.adUnitCode, - gptSlotCode: getGptSlotInfoForAdUnitCode(bidResponse.adUnitCode).gptSlot || null, - auctionId: bidResponse.auctionId, - bidderCode: bidResponse.bidder, - cpm: bidResponse.cpm, - creativeId: bidResponse.creativeId, - currency: bidResponse.currency, - floorData: bidResponse.floorData, - mediaType: bidResponse.mediaType, - netRevenue: bidResponse.netRevenue, - requestTimestamp: bidResponse.requestTimestamp, - responseTimestamp: bidResponse.responseTimestamp, - status: bidResponse.status, - statusMessage: bidResponse.statusMessage, - timeToRespond: bidResponse.timeToRespond, - transactionId: bidResponse.transactionId - }; - if (status !== 'bidwon') { - Object.assign(bid, { - bidId: status === 'timeout' ? bidResponse.bidId : bidResponse.requestId, - renderStatus: status === 'timeout' ? 3 : 2, - sizes: parseSizesInput(bidResponse.size).toString(), +/** + * Track the events emitted by prebid and handle each case. See https://docs.prebid.org/dev-docs/publisher-api-reference/getEvents.html for more info + * @param {object} event the prebid event emmitted + * @param {string} event.eventType the type of the event + * @param {object} event.args the arguments of the emitted event + */ +const track = ({ eventType, args }) => { + switch (eventType) { + // handle invalid bids, and remove them from the adUnit cache + case CONSTANTS.EVENTS.BID_TIMEOUT: + args.map(extractBid).forEach((bid) => { + bid.renderStatus = 3; + auctionCache[bid.auctionId].bids.push(bid); + }); + break; + // handle valid bid responses and record them as part of an auction + case CONSTANTS.EVENTS.BID_RESPONSE: + const bid = Object.assign(extractBid(args), { renderStatus: 2 }); + auctionCache[bid.auctionId].bids.push(bid); + break; + // capture extra information from the auction, and if there were no bids + // (and so no chance of a win) send the auction + case CONSTANTS.EVENTS.AUCTION_END: + Object.assign( + auctionCache[args.auctionId].floorDetail, + deepAccess(args, 'adUnits.0.bids.0.floorData') + ); + auctionCache[args.auctionId].pageDetail.adUnits = args.adUnitCodes; + Object.assign(auctionCache[args.auctionId].auctionDetail, { + adUnitCodes: args.adUnits.map((i) => i.code), + timestamp: args.timestamp, }); - events.bids.push(bid); - } else { - Object.assign(bid, { - bidId: bidResponse.requestId, - floorProvider: events.floorDetail?.floorProvider || null, - floorFetchStatus: events.floorDetail?.fetchStatus || null, - floorLocation: events.floorDetail?.location || null, - floorModelVersion: events.floorDetail?.modelVersion || null, - floorSkipRate: events.floorDetail?.skipRate || 0, - isFloorSkipped: events.floorDetail?.skipped || false, + if ( + auctionCache[args.auctionId].bids.every((bid) => bid.renderStatus === 3) + ) { + send(args.auctionId); + } + break; + // send the prebid winning bid back to pubx + case CONSTANTS.EVENTS.BID_WON: + const winningBid = extractBid(args); + const floorDetail = auctionCache[winningBid.auctionId].floorDetail; + Object.assign(winningBid, { + floorProvider: floorDetail?.floorProvider || null, + floorFetchStatus: floorDetail?.fetchStatus || null, + floorLocation: floorDetail?.location || null, + floorModelVersion: floorDetail?.modelVersion || null, + floorSkipRate: floorDetail?.skipRate || 0, + isFloorSkipped: floorDetail?.skipped || false, isWinningBid: true, - placementId: bidResponse.params ? deepAccess(bidResponse, 'params.0.placementId') : null, - renderedSize: bidResponse.size, - renderStatus: 4 + renderedSize: args.size, + renderStatus: 4, }); - return bid; - } + winningBid.adServerData = getAdServerDataForBid(winningBid); + auctionCache[winningBid.auctionId].winningBid = winningBid; + send(winningBid.auctionId); + break; + // do nothing + default: + break; } -} +}; -export function getDeviceType() { - if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { +/** + * Get the approximate device type from the user agent + * @returns {string} + */ +export const getDeviceType = () => { + if ( + /ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test( + navigator.userAgent.toLowerCase() + ) + ) { return 'tablet'; } - if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { + if ( + /iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test( + navigator.userAgent.toLowerCase() + ) + ) { return 'mobile'; } return 'desktop'; -} +}; -export function getBrowser() { - if (/Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor)) return 'Chrome'; - else if (navigator.userAgent.match('CriOS')) return 'Chrome'; +/** + * Get the approximate browser type from the user agent (or vendor if available) + * @returns {string} + */ +export const getBrowser = () => { + if ( + /Chrome/.test(navigator.userAgent) && + /Google Inc/.test(navigator.vendor) + ) { + return 'Chrome'; + } else if (navigator.userAgent.match('CriOS')) return 'Chrome'; else if (/Firefox/.test(navigator.userAgent)) return 'Firefox'; else if (/Edg/.test(navigator.userAgent)) return 'Microsoft Edge'; - else if (/Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor)) return 'Safari'; - else if (/Trident/.test(navigator.userAgent) || /MSIE/.test(navigator.userAgent)) return 'Internet Explorer'; - else return 'Others'; -} + else if ( + /Safari/.test(navigator.userAgent) && + /Apple Computer/.test(navigator.vendor) + ) { + return 'Safari'; + } else if ( + /Trident/.test(navigator.userAgent) || + /MSIE/.test(navigator.userAgent) + ) { + return 'Internet Explorer'; + } else return 'Others'; +}; -export function getOS() { +/** + * Get the approximate OS from the user agent (or app version, if available) + * @returns {string} + */ +export const getOS = () => { if (navigator.userAgent.indexOf('Android') != -1) return 'Android'; if (navigator.userAgent.indexOf('like Mac') != -1) return 'iOS'; if (navigator.userAgent.indexOf('Win') != -1) return 'Windows'; @@ -137,72 +253,105 @@ export function getOS() { if (navigator.userAgent.indexOf('Linux') != -1) return 'Linux'; if (navigator.appVersion.indexOf('X11') != -1) return 'Unix'; return 'Others'; -} +}; -// add sampling rate -pubxaiAnalyticsAdapter.shouldFireEventRequest = function (samplingRate = 1) { - return (Math.floor((Math.random() * samplingRate + 1)) === parseInt(samplingRate)); +/** + * If true, send data back to pubxai + * @param {string} auctionId + * @param {number} samplingRate + * @returns {boolean} + */ +const shouldFireEventRequest = (auctionId, samplingRate = 1) => { + return parseInt(cyrb53Hash(auctionId)) % samplingRate === 0; }; -function send(data, status) { - if (pubxaiAnalyticsAdapter.shouldFireEventRequest(initOptions.samplingRate)) { - let location = getWindowLocation(); - const storage = getStorage(); - data.initOptions = initOptions; - data.pageDetail = {}; - Object.assign(data.pageDetail, { - host: location.host, - path: location.pathname, - search: location.search - }); - if (typeof data !== 'undefined' && typeof data.auctionInit !== 'undefined') { - data.pageDetail.adUnits = data.auctionInit.adUnitCodes; - data.initOptions.auctionId = data.auctionInit.auctionId; - delete data.auctionInit; - - data.pmcDetail = {}; - Object.assign(data.pmcDetail, { - bidDensity: storage ? storage.getItem('pbx:dpbid') : null, - maxBid: storage ? storage.getItem('pbx:mxbid') : null, - auctionId: storage ? storage.getItem('pbx:aucid') : null, - }); +/** + * Send auction data back to pubx.ai + * @param {string} auctionId the auction to send + */ +const send = (auctionId) => { + const auctionData = Object.assign({}, auctionCache[auctionId]); + if (!shouldFireEventRequest(auctionId, initOptions.samplingRate)) { + return; + } + [ + { + path: winningBidPath, + requiredKeys: [ + 'winningBid', + 'pageDetail', + 'deviceDetail', + 'floorDetail', + 'auctionDetail', + 'userDetail', + 'pmacDetail', + 'initOptions', + ], + eventType: 'win', + }, + { + path: auctionPath, + requiredKeys: [ + 'bids', + 'pageDetail', + 'deviceDetail', + 'floorDetail', + 'auctionDetail', + 'userDetail', + 'pmacDetail', + 'initOptions', + ], + eventType: 'auction', + }, + ].forEach(({ path, requiredKeys, eventType }) => { + const data = Object.fromEntries( + requiredKeys.map((key) => [key, auctionData[key]]) + ); + if ( + auctionCache[auctionId].sentAs.includes(eventType) || + !requiredKeys.every((key) => !!auctionData[key]) + ) { + return; } - data.deviceDetail = {}; - Object.assign(data.deviceDetail, { - platform: navigator.platform, - deviceType: getDeviceType(), - deviceOS: getOS(), - browser: getBrowser() - }); - - let pubxaiAnalyticsRequestUrl = buildUrl({ + const pubxaiAnalyticsRequestUrl = buildUrl({ protocol: 'https', - hostname: (initOptions && initOptions.hostName) || defaultHost, - pathname: status == 'bidwon' ? winningBidPath : auctionPath, + hostname: + (auctionData.initOptions && auctionData.initOptions.hostName) || + defaultHost, + pathname: path, search: { - auctionTimestamp: auctionTimestamp, + auctionTimestamp: auctionData.auctionDetail.timestamp, pubxaiAnalyticsVersion: pubxaiAnalyticsVersion, - prebidVersion: getGlobal().version - } + prebidVersion: getGlobal().version, + }, }); - if (status == 'bidwon') { - ajax(pubxaiAnalyticsRequestUrl, undefined, JSON.stringify(data), { method: 'POST', contentType: 'text/json' }); - } else if (status == 'auctionEnd' && auctionCache.indexOf(data.initOptions.auctionId) === -1) { - ajax(pubxaiAnalyticsRequestUrl, undefined, JSON.stringify(data), { method: 'POST', contentType: 'text/json' }); - auctionCache.push(data.initOptions.auctionId); - } - } -} + const payload = new Blob([JSON.stringify(data)], { + type: 'text/json', + }); + navigator.sendBeacon(pubxaiAnalyticsRequestUrl, payload); + auctionCache[auctionId].sentAs.push(eventType); + }); +}; + +var pubxaiAnalyticsAdapter = Object.assign( + adapter({ + emptyUrl, + analyticsType, + }), + { track } +); +pubxaiAnalyticsAdapter.track = track; -pubxaiAnalyticsAdapter.originEnableAnalytics = pubxaiAnalyticsAdapter.enableAnalytics; -pubxaiAnalyticsAdapter.enableAnalytics = function (config) { +pubxaiAnalyticsAdapter.originEnableAnalytics = + pubxaiAnalyticsAdapter.enableAnalytics; +pubxaiAnalyticsAdapter.enableAnalytics = (config) => { initOptions = config.options; pubxaiAnalyticsAdapter.originEnableAnalytics(config); }; adapterManager.registerAnalyticsAdapter({ adapter: pubxaiAnalyticsAdapter, - code: 'pubxai' + code: 'pubxai', }); export default pubxaiAnalyticsAdapter; diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index e0f4497a8c8..478360c741f 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -1,675 +1,694 @@ -import pubxaiAnalyticsAdapter, {getBrowser, getDeviceType, getOS} from 'modules/pubxaiAnalyticsAdapter.js'; -import {expect} from 'chai'; +import pubxaiAnalyticsAdapter, { + getDeviceType, + getOS, + getBrowser, + auctionCache, +} from 'modules/pubxaiAnalyticsAdapter.js'; +import { expect } from 'chai'; import adapterManager from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import {server} from 'test/mocks/xhr.js'; -import {getGptSlotInfoForAdUnitCode} from '../../../libraries/gptUtils/gptUtils.js'; +import { getGptSlotInfoForAdUnitCode } from '../../../libraries/gptUtils/gptUtils.js'; let events = require('src/events'); let constants = require('src/constants.json'); -describe('pubxai analytics adapter', function() { - beforeEach(function() { +describe('pubxai analytics adapter', () => { + beforeEach(() => { sinon.stub(events, 'getEvents').returns([]); }); - afterEach(function() { + afterEach(() => { events.getEvents.restore(); }); - describe('track', function() { + describe('track', () => { let initOptions = { samplingRate: '1', - pubxId: '6c415fc0-8b0e-4cf5-be73-01526a4db625' + pubxId: '6c415fc0-8b0e-4cf5-be73-01526a4db625', }; let location = utils.getWindowLocation(); let storage = window.top['sessionStorage']; + const replaceProperty = (obj, params) => { + let strObj = JSON.stringify(obj); + params.forEach(({ field, updated, replaced }) => { + strObj = strObj.replace( + new RegExp('"' + field + '":' + replaced, 'g'), + '"' + field + '":' + updated + ); + }); + return JSON.parse(strObj); + }; + let prebidEvent = { - 'auctionInit': { - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'timestamp': 1603865707180, - 'auctionStatus': 'inProgress', - 'adUnits': [{ - 'code': '/19968336/header-bid-tag-1', - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } + auctionInit: { + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + timestamp: 1603865707180, + auctionStatus: 'inProgress', + adUnits: [ + { + code: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + }, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + floorData: { + skipped: false, + skipRate: 0, + modelVersion: 'test model 1.0', + location: 'fetch', + floorProvider: 'PubXFloorProvider', + fetchStatus: 'success', + }, + }, + ], + sizes: [[300, 250]], + transactionId: '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', }, - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 + ], + adUnitCodes: ['/19968336/header-bid-tag-1'], + bidderRequests: [ + { + bidderCode: 'appnexus', + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + bidderRequestId: '184cbc05bb90ba', + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + }, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + floorData: { + skipped: false, + skipRate: 0, + modelVersion: 'test model 1.0', + location: 'fetch', + floorProvider: 'PubXFloorProvider', + fetchStatus: 'success', + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + adUnitCode: '/19968336/header-bid-tag-1', + transactionId: '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + sizes: [[300, 250]], + bidId: '248f9a4489835e', + bidderRequestId: '184cbc05bb90ba', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + }, + ], + auctionStart: 1603865707180, + timeout: 1000, + refererInfo: { + referer: 'http://local-pnh.net:8080/stream/', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: ['http://local-pnh.net:8080/stream/'], + canonicalUrl: null, }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'test model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloorProvider', - 'fetchStatus': 'success' - } - }], - 'sizes': [ - [ - 300, - 250 - ] - ], - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' - }], - 'adUnitCodes': [ - '/19968336/header-bid-tag-1' + start: 1603865707182, + }, ], - 'bidderRequests': [{ - 'bidderCode': 'appnexus', - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'bidderRequestId': '184cbc05bb90ba', - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 + noBids: [], + bidsReceived: [], + winningBids: [], + timeout: 1000, + config: { + samplingRate: '1', + pubxId: '6c415fc0-8b0e-4cf5-be73-01526a4db625', + }, + }, + bidRequested: { + bidderCode: 'appnexus', + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + bidderRequestId: '184cbc05bb90ba', + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'test model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloorProvider', - 'fetchStatus': 'success' + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + floorData: { + skipped: false, + skipRate: 0, + modelVersion: 'test model 1.0', + location: 'fetch', + floorProvider: 'PubXFloorProvider', + fetchStatus: 'success', }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, }, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', - 'sizes': [ - [ - 300, - 250 - ] - ], - 'bidId': '248f9a4489835e', - 'bidderRequestId': '184cbc05bb90ba', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }], - 'auctionStart': 1603865707180, - 'timeout': 1000, - 'refererInfo': { - 'referer': 'http://local-pnh.net:8080/stream/', - 'reachedTop': true, - 'isAmp': false, - 'numIframes': 0, - 'stack': [ - 'http://local-pnh.net:8080/stream/' - ], - 'canonicalUrl': null + adUnitCode: '/19968336/header-bid-tag-1', + transactionId: '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + sizes: [[300, 250]], + bidId: '248f9a4489835e', + bidderRequestId: '184cbc05bb90ba', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, }, - 'start': 1603865707182 - }], - 'noBids': [], - 'bidsReceived': [], - 'winningBids': [], - 'timeout': 1000, - 'config': { - 'samplingRate': '1', - 'pubxId': '6c415fc0-8b0e-4cf5-be73-01526a4db625' - } - }, - 'bidRequested': { - 'bidderCode': 'appnexus', - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'bidderRequestId': '184cbc05bb90ba', - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 - }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'test model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloorProvider', - 'fetchStatus': 'success' - }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } - }, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', - 'sizes': [ - [ - 300, - 250 - ] - ], - 'bidId': '248f9a4489835e', - 'bidderRequestId': '184cbc05bb90ba', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }], - 'auctionStart': 1603865707180, - 'timeout': 1000, - 'refererInfo': { - 'referer': 'http://local-pnh.net:8080/stream/', - 'reachedTop': true, - 'isAmp': false, - 'numIframes': 0, - 'stack': [ - 'http://local-pnh.net:8080/stream/' - ], - 'canonicalUrl': null + ], + auctionStart: 1603865707180, + timeout: 1000, + refererInfo: { + referer: 'http://local-pnh.net:8080/stream/', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: ['http://local-pnh.net:8080/stream/'], + canonicalUrl: null, }, - 'start': 1603865707182 + start: 1603865707182, }, - 'bidTimeout': [], - 'bidResponse': { - 'bidderCode': 'appnexus', - 'width': 300, - 'height': 250, - 'statusMessage': 'Bid available', - 'adId': '32780c4bc382cb', - 'requestId': '248f9a4489835e', - 'mediaType': 'banner', - 'source': 'client', - 'cpm': 0.5, - 'creativeId': 96846035, - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 300, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'appnexus': { - 'buyerMemberId': 9325 + bidTimeout: [], + bidResponse: { + bidderCode: 'appnexus', + width: 300, + height: 250, + statusMessage: 'Bid available', + adId: '32780c4bc382cb', + requestId: '248f9a4489835e', + mediaType: 'banner', + source: 'client', + cpm: 0.5, + creativeId: 96846035, + currency: 'USD', + netRevenue: true, + ttl: 300, + adUnitCode: '/19968336/header-bid-tag-1', + appnexus: { + buyerMemberId: 9325, }, - 'meta': { - 'advertiserId': 2529885 + meta: { + advertiserId: 2529885, }, - 'ad': '', - 'originalCpm': 0.5, - 'originalCurrency': 'USD', - 'floorData': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false, - 'floorValue': 0.4, - 'floorRule': '/19968336/header-bid-tag-1|banner', - 'floorCurrency': 'USD', - 'cpmAfterAdjustments': 0.5, - 'enforcements': { - 'enforceJS': true, - 'enforcePBS': false, - 'floorDeals': true, - 'bidAdjustment': true + ad: '', + originalCpm: 0.5, + originalCurrency: 'USD', + floorData: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, + floorValue: 0.4, + floorRule: '/19968336/header-bid-tag-1|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 0.5, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: true, + bidAdjustment: true, + }, + matchedFields: { + gptSlot: '/19968336/header-bid-tag-1', + mediaType: 'banner', }, - 'matchedFields': { - 'gptSlot': '/19968336/header-bid-tag-1', - 'mediaType': 'banner' - } }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'responseTimestamp': 1616654313071, - 'requestTimestamp': 1616654312804, - 'bidder': 'appnexus', - 'timeToRespond': 267, - 'pbLg': '0.50', - 'pbMg': '0.50', - 'pbHg': '0.50', - 'pbAg': '0.50', - 'pbDg': '0.50', - 'pbCg': '0.50', - 'size': '300x250', - 'adserverTargeting': { - 'hb_bidder': 'appnexus', - 'hb_adid': '32780c4bc382cb', - 'hb_pb': '0.50', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner' + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + responseTimestamp: 1616654313071, + requestTimestamp: 1616654312804, + bidder: 'appnexus', + timeToRespond: 267, + pbLg: '0.50', + pbMg: '0.50', + pbHg: '0.50', + pbAg: '0.50', + pbDg: '0.50', + pbCg: '0.50', + size: '300x250', + adserverTargeting: { + hb_bidder: 'appnexus', + hb_adid: '32780c4bc382cb', + hb_pb: '0.50', + hb_size: '300x250', + hb_source: 'client', + hb_format: 'banner', }, }, - 'auctionEnd': { - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'timestamp': 1616654312804, - 'auctionEnd': 1616654313090, - 'auctionStatus': 'completed', - 'adUnits': [{ - 'code': '/19968336/header-bid-tag-1', - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } + auctionEnd: { + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + timestamp: 1616654312804, + auctionEnd: 1616654313090, + auctionStatus: 'completed', + adUnits: [ + { + code: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + }, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + floorData: { + skipped: false, + skipRate: 0, + modelVersion: 'test model 1.0', + location: 'fetch', + floorProvider: 'PubXFloorProvider', + fetchStatus: 'success', + }, + }, + ], + sizes: [[300, 250]], + transactionId: '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', }, - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 + ], + adUnitCodes: ['/19968336/header-bid-tag-1'], + bidderRequests: [ + { + bidderCode: 'appnexus', + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + bidderRequestId: '184cbc05bb90ba', + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + }, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + floorData: { + skipped: false, + skipRate: 0, + modelVersion: 'test model 1.0', + location: 'fetch', + floorProvider: 'PubXFloorProvider', + fetchStatus: 'success', + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + adUnitCode: '/19968336/header-bid-tag-1', + transactionId: '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + sizes: [[300, 250]], + bidId: '248f9a4489835e', + bidderRequestId: '184cbc05bb90ba', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + }, + ], + auctionStart: 1603865707180, + timeout: 1000, + refererInfo: { + referer: 'http://local-pnh.net:8080/stream/', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: ['http://local-pnh.net:8080/stream/'], + canonicalUrl: null, }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'test model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloorProvider', - 'fetchStatus': 'success' - } - }], - 'sizes': [ - [ - 300, - 250 - ] - ], - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' - }], - 'adUnitCodes': [ - '/19968336/header-bid-tag-1' + start: 1603865707182, + }, ], - 'bidderRequests': [{ - 'bidderCode': 'appnexus', - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'bidderRequestId': '184cbc05bb90ba', - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 + noBids: [], + bidsReceived: [ + { + bidderCode: 'appnexus', + width: 300, + height: 250, + statusMessage: 'Bid available', + adId: '32780c4bc382cb', + requestId: '248f9a4489835e', + mediaType: 'banner', + source: 'client', + cpm: 0.5, + creativeId: 96846035, + currency: 'USD', + netRevenue: true, + ttl: 300, + adUnitCode: '/19968336/header-bid-tag-1', + appnexus: { + buyerMemberId: 9325, }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'test model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloorProvider', - 'fetchStatus': 'success' + meta: { + advertiserId: 2529885, }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } + ad: '', + originalCpm: 0.5, + originalCurrency: 'USD', + floorData: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, + floorValue: 0.4, + floorRule: '/19968336/header-bid-tag-1|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 0.5, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: true, + bidAdjustment: true, + }, + matchedFields: { + gptSlot: '/19968336/header-bid-tag-1', + mediaType: 'banner', + }, }, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', - 'sizes': [ - [ - 300, - 250 - ] - ], - 'bidId': '248f9a4489835e', - 'bidderRequestId': '184cbc05bb90ba', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }], - 'auctionStart': 1603865707180, - 'timeout': 1000, - 'refererInfo': { - 'referer': 'http://local-pnh.net:8080/stream/', - 'reachedTop': true, - 'isAmp': false, - 'numIframes': 0, - 'stack': [ - 'http://local-pnh.net:8080/stream/' - ], - 'canonicalUrl': null - }, - 'start': 1603865707182 - }], - 'noBids': [], - 'bidsReceived': [{ - 'bidderCode': 'appnexus', - 'width': 300, - 'height': 250, - 'statusMessage': 'Bid available', - 'adId': '32780c4bc382cb', - 'requestId': '248f9a4489835e', - 'mediaType': 'banner', - 'source': 'client', - 'cpm': 0.5, - 'creativeId': 96846035, - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 300, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'appnexus': { - 'buyerMemberId': 9325 - }, - 'meta': { - 'advertiserId': 2529885 - }, - 'ad': '', - 'originalCpm': 0.5, - 'originalCurrency': 'USD', - 'floorData': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false, - 'floorValue': 0.4, - 'floorRule': '/19968336/header-bid-tag-1|banner', - 'floorCurrency': 'USD', - 'cpmAfterAdjustments': 0.5, - 'enforcements': { - 'enforceJS': true, - 'enforcePBS': false, - 'floorDeals': true, - 'bidAdjustment': true + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + responseTimestamp: 1616654313071, + requestTimestamp: 1616654312804, + bidder: 'appnexus', + timeToRespond: 267, + pbLg: '0.50', + pbMg: '0.50', + pbHg: '0.50', + pbAg: '0.50', + pbDg: '0.50', + pbCg: '0.50', + size: '300x250', + adserverTargeting: { + hb_bidder: 'appnexus', + hb_adid: '32780c4bc382cb', + hb_pb: '0.50', + hb_size: '300x250', + hb_source: 'client', + hb_format: 'banner', }, - 'matchedFields': { - 'gptSlot': '/19968336/header-bid-tag-1', - 'mediaType': 'banner' - } - }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'responseTimestamp': 1616654313071, - 'requestTimestamp': 1616654312804, - 'bidder': 'appnexus', - 'timeToRespond': 267, - 'pbLg': '0.50', - 'pbMg': '0.50', - 'pbHg': '0.50', - 'pbAg': '0.50', - 'pbDg': '0.50', - 'pbCg': '0.50', - 'size': '300x250', - 'adserverTargeting': { - 'hb_bidder': 'appnexus', - 'hb_adid': '32780c4bc382cb', - 'hb_pb': '0.50', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner' + status: 'rendered', + params: [ + { + placementId: 13144370, + }, + ], }, - 'status': 'rendered', - 'params': [{ - 'placementId': 13144370 - }] - }], - 'winningBids': [], - 'timeout': 1000 + ], + winningBids: [], + timeout: 1000, }, - 'bidWon': { - 'bidderCode': 'appnexus', - 'width': 300, - 'height': 250, - 'statusMessage': 'Bid available', - 'adId': '32780c4bc382cb', - 'requestId': '248f9a4489835e', - 'mediaType': 'banner', - 'source': 'client', - 'cpm': 0.5, - 'creativeId': 96846035, - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 300, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'appnexus': { - 'buyerMemberId': 9325 + bidWon: { + bidderCode: 'appnexus', + width: 300, + height: 250, + statusMessage: 'Bid available', + adId: '32780c4bc382cb', + requestId: '248f9a4489835e', + mediaType: 'banner', + source: 'client', + cpm: 0.5, + creativeId: 96846035, + currency: 'USD', + netRevenue: true, + ttl: 300, + adUnitCode: '/19968336/header-bid-tag-1', + appnexus: { + buyerMemberId: 9325, }, - 'meta': { - 'advertiserId': 2529885 + meta: { + advertiserId: 2529885, }, - 'ad': '', - 'originalCpm': 0.5, - 'originalCurrency': 'USD', - 'floorData': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false, - 'floorValue': 0.4, - 'floorRule': '/19968336/header-bid-tag-1|banner', - 'floorCurrency': 'USD', - 'cpmAfterAdjustments': 0.5, - 'enforcements': { - 'enforceJS': true, - 'enforcePBS': false, - 'floorDeals': true, - 'bidAdjustment': true + ad: '', + originalCpm: 0.5, + originalCurrency: 'USD', + floorData: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, + floorValue: 0.4, + floorRule: '/19968336/header-bid-tag-1|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 0.5, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: true, + bidAdjustment: true, + }, + matchedFields: { + gptSlot: '/19968336/header-bid-tag-1', + mediaType: 'banner', }, - 'matchedFields': { - 'gptSlot': '/19968336/header-bid-tag-1', - 'mediaType': 'banner' - } }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'responseTimestamp': 1616654313071, - 'requestTimestamp': 1616654312804, - 'bidder': 'appnexus', - 'timeToRespond': 267, - 'pbLg': '0.50', - 'pbMg': '0.50', - 'pbHg': '0.50', - 'pbAg': '0.50', - 'pbDg': '0.50', - 'pbCg': '0.50', - 'size': '300x250', - 'adserverTargeting': { - 'hb_bidder': 'appnexus', - 'hb_adid': '32780c4bc382cb', - 'hb_pb': '0.50', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner' + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + responseTimestamp: 1616654313071, + requestTimestamp: 1616654312804, + bidder: 'appnexus', + timeToRespond: 267, + pbLg: '0.50', + pbMg: '0.50', + pbHg: '0.50', + pbAg: '0.50', + pbDg: '0.50', + pbCg: '0.50', + size: '300x250', + adserverTargeting: { + hb_bidder: 'appnexus', + hb_adid: '32780c4bc382cb', + hb_pb: '0.50', + hb_size: '300x250', + hb_source: 'client', + hb_format: 'banner', }, - 'status': 'rendered', - 'params': [{ - 'placementId': 13144370 - }] - }, - 'pageDetail': { - 'host': location.host, - 'path': location.pathname, - 'search': location.search + status: 'rendered', + params: [ + { + placementId: 13144370, + }, + ], }, - 'pmcDetail': { - 'bidDensity': storage.getItem('pbx:dpbid'), - 'maxBid': storage.getItem('pbx:mxbid'), - 'auctionId': storage.getItem('pbx:aucid') - } + // pageDetail: { + // host: location.host, + // path: location.pathname, + // search: location.search, + // }, + // pmcDetail: { + // bidDensity: storage.getItem("pbx:dpbid"), + // maxBid: storage.getItem("pbx:mxbid"), + // auctionId: storage.getItem("pbx:aucid"), + // }, }; let expectedAfterBid = { - 'bids': [{ - 'bidderCode': 'appnexus', - 'bidId': '248f9a4489835e', - 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'sizes': '300x250', - 'renderStatus': 2, - 'requestTimestamp': 1616654312804, - 'creativeId': 96846035, - 'currency': 'USD', - 'cpm': 0.5, - 'netRevenue': true, - 'mediaType': 'banner', - 'statusMessage': 'Bid available', - 'floorData': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false, - 'floorValue': 0.4, - 'floorRule': '/19968336/header-bid-tag-1|banner', - 'floorCurrency': 'USD', - 'cpmAfterAdjustments': 0.5, - 'enforcements': { - 'enforceJS': true, - 'enforcePBS': false, - 'floorDeals': true, - 'bidAdjustment': true + bids: [ + { + bidderCode: 'appnexus', + bidId: '248f9a4489835e', + adUnitCode: '/19968336/header-bid-tag-1', + gptSlotCode: + getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || + null, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + sizes: '300x250', + renderStatus: 2, + requestTimestamp: 1616654312804, + creativeId: 96846035, + currency: 'USD', + cpm: 0.5, + netRevenue: true, + mediaType: 'banner', + statusMessage: 'Bid available', + floorData: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, + floorValue: 0.4, + floorRule: '/19968336/header-bid-tag-1|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 0.5, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: true, + bidAdjustment: true, + }, + matchedFields: { + gptSlot: '/19968336/header-bid-tag-1', + mediaType: 'banner', + }, }, - 'matchedFields': { - 'gptSlot': '/19968336/header-bid-tag-1', - 'mediaType': 'banner' - } + placementId: null, + timeToRespond: 267, + responseTimestamp: 1616654313071, }, - 'timeToRespond': 267, - 'responseTimestamp': 1616654313071 - }], - 'pageDetail': { - 'host': location.host, - 'path': location.pathname, - 'search': location.search, - 'adUnits': [ - '/19968336/header-bid-tag-1' - ] + ], + auctionDetail: { + adUnitCodes: ['/19968336/header-bid-tag-1'], + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + refreshRank: 0, + timestamp: 1616654312804, + }, + pageDetail: { + host: location.host, + path: location.pathname, + search: location.search, + adUnits: ['/19968336/header-bid-tag-1'], }, - 'floorDetail': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false + floorDetail: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, }, - 'deviceDetail': { - 'platform': navigator.platform, - 'deviceType': getDeviceType(), - 'deviceOS': getOS(), - 'browser': getBrowser() + deviceDetail: { + platform: navigator.platform, + deviceType: getDeviceType(), + deviceOS: getOS(), + browser: getBrowser(), }, - 'pmcDetail': { - 'bidDensity': storage.getItem('pbx:dpbid'), - 'maxBid': storage.getItem('pbx:mxbid'), - 'auctionId': storage.getItem('pbx:aucid') + userDetail: { + userIdTypes: [], + }, + pmacDetail: JSON.parse(storage.getItem('pbx:pmac')) || {}, + initOptions: { + ...initOptions, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', }, - 'initOptions': initOptions }; let expectedAfterBidWon = { - 'winningBid': { - 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'bidderCode': 'appnexus', - 'bidId': '248f9a4489835e', - 'cpm': 0.5, - 'creativeId': 96846035, - 'currency': 'USD', - 'floorData': { - 'fetchStatus': 'success', - 'floorProvider': 'PubXFloorProvider', - 'location': 'fetch', - 'modelVersion': 'test model 1.0', - 'skipRate': 0, - 'skipped': false, - 'floorValue': 0.4, - 'floorRule': '/19968336/header-bid-tag-1|banner', - 'floorCurrency': 'USD', - 'cpmAfterAdjustments': 0.5, - 'enforcements': { - 'enforceJS': true, - 'enforcePBS': false, - 'floorDeals': true, - 'bidAdjustment': true + winningBid: { + adUnitCode: '/19968336/header-bid-tag-1', + gptSlotCode: + getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || + null, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + bidderCode: 'appnexus', + bidId: '248f9a4489835e', + cpm: 0.5, + creativeId: 96846035, + currency: 'USD', + floorData: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, + floorValue: 0.4, + floorRule: '/19968336/header-bid-tag-1|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 0.5, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: true, + bidAdjustment: true, + }, + matchedFields: { + gptSlot: '/19968336/header-bid-tag-1', + mediaType: 'banner', }, - 'matchedFields': { - 'gptSlot': '/19968336/header-bid-tag-1', - 'mediaType': 'banner' - } }, - 'floorProvider': 'PubXFloorProvider', - 'floorFetchStatus': 'success', - 'floorLocation': 'fetch', - 'floorModelVersion': 'test model 1.0', - 'floorSkipRate': 0, - 'isFloorSkipped': false, - 'isWinningBid': true, - 'mediaType': 'banner', - 'netRevenue': true, - 'placementId': 13144370, - 'renderedSize': '300x250', - 'renderStatus': 4, - 'responseTimestamp': 1616654313071, - 'requestTimestamp': 1616654312804, - 'status': 'rendered', - 'statusMessage': 'Bid available', - 'timeToRespond': 267 + adServerData: {}, + floorProvider: 'PubXFloorProvider', + floorFetchStatus: 'success', + floorLocation: 'fetch', + floorModelVersion: 'test model 1.0', + floorSkipRate: 0, + isFloorSkipped: false, + isWinningBid: true, + mediaType: 'banner', + netRevenue: true, + placementId: 13144370, + renderedSize: '300x250', + sizes: '300x250', + renderStatus: 4, + responseTimestamp: 1616654313071, + requestTimestamp: 1616654312804, + status: 'rendered', + statusMessage: 'Bid available', + timeToRespond: 267, + }, + auctionDetail: { + adUnitCodes: ['/19968336/header-bid-tag-1'], + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + refreshRank: 0, + timestamp: 1616654312804, }, - 'pageDetail': { - 'host': location.host, - 'path': location.pathname, - 'search': location.search + pageDetail: { + host: location.host, + path: location.pathname, + search: location.search, + adUnits: ['/19968336/header-bid-tag-1'], }, - 'deviceDetail': { - 'platform': navigator.platform, - 'deviceType': getDeviceType(), - 'deviceOS': getOS(), - 'browser': getBrowser() + floorDetail: { + fetchStatus: 'success', + floorProvider: 'PubXFloorProvider', + location: 'fetch', + modelVersion: 'test model 1.0', + skipRate: 0, + skipped: false, }, - 'initOptions': initOptions - } + deviceDetail: { + platform: navigator.platform, + deviceType: getDeviceType(), + deviceOS: getOS(), + browser: getBrowser(), + }, + userDetail: { + userIdTypes: [], + }, + pmacDetail: JSON.parse(storage.getItem('pbx:pmac')) || {}, + initOptions: { + ...initOptions, + auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', + }, + }; adapterManager.registerAnalyticsAdapter({ code: 'pubxai', - adapter: pubxaiAnalyticsAdapter + adapter: pubxaiAnalyticsAdapter, }); - beforeEach(function() { + beforeEach(() => { adapterManager.enableAnalytics({ provider: 'pubxai', - options: initOptions + options: initOptions, }); + sinon.stub(navigator, 'sendBeacon').returns(true); }); - afterEach(function() { + afterEach(() => { pubxaiAnalyticsAdapter.disableAnalytics(); + navigator.sendBeacon.restore(); + delete auctionCache['bc3806e4-873e-453c-8ae5-204f35e923b4']; }); - it('builds and sends auction data', function() { + it('builds and sends auction data', async () => { // Step 1: Send auction init event events.emit(constants.EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); @@ -685,20 +704,188 @@ describe('pubxai analytics adapter', function() { // Step 5: Send auction end event events.emit(constants.EVENTS.AUCTION_END, prebidEvent['auctionEnd']); - expect(server.requests.length).to.equal(1); + expect(navigator.sendBeacon.callCount).to.equal(0); - let realAfterBid = JSON.parse(server.requests[0].requestBody); + // Step 6: Send auction bid won event + events.emit(constants.EVENTS.BID_WON, prebidEvent['bidWon']); - expect(realAfterBid).to.deep.equal(expectedAfterBid); + expect(navigator.sendBeacon.callCount).to.equal(2); - // Step 6: Send auction bid won event + for (const [index, arg] of navigator.sendBeacon.args.entries()) { + const [expectedUrl, expectedData] = arg; + const parsedUrl = new URL(expectedUrl); + expect(parsedUrl.pathname).to.equal( + ['/analytics/bidwon', '/analytics/auction'][index] + ); + expect(Object.fromEntries(parsedUrl.searchParams)).to.deep.equal({ + auctionTimestamp: '1616654312804', + pubxaiAnalyticsVersion: 'v1.2.0', + prebidVersion: 'undefined', // not configured for test case + }); + expect(expectedData.type).to.equal('text/json'); + expect(JSON.parse(await expectedData.text())).to.deep.equal( + [expectedAfterBidWon, expectedAfterBid][index] + ); + } + }); + + it('auction with no bids', async () => { + // Step 1: Send auction init event + events.emit(constants.EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + // Step 2: Send bid requested event + events.emit(constants.EVENTS.BID_REQUESTED, prebidEvent['bidRequested']); + + // Step 3: Send bid time out event + events.emit(constants.EVENTS.BID_TIMEOUT, prebidEvent['bidTimeout']); + + // Step 4: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(0); + + // Step 5: Send auction end event + events.emit(constants.EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + + // Step 6: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(1); + + // Step 7: check the pathname of the calls is correct (sent only to the auction endpoint) + const [expectedUrl, expectedData] = navigator.sendBeacon.args[0]; + const parsedUrl = new URL(expectedUrl); + expect(parsedUrl.pathname).to.equal('/analytics/auction'); + + // Step 8: check that the meta information in the call is correct + expect(Object.fromEntries(parsedUrl.searchParams)).to.deep.equal({ + auctionTimestamp: '1616654312804', + pubxaiAnalyticsVersion: 'v1.2.0', + prebidVersion: 'undefined', // not configured for test case + }); + + // Step 9: check that the data sent in the request is correct + expect(expectedData.type).to.equal('text/json'); + expect(JSON.parse(await expectedData.text())).to.deep.equal({ + ...expectedAfterBid, + bids: [], + }); + }); + + it('2 concurrent auctions', async () => { + // Step 1: Send auction init event for auction 1 + events.emit(constants.EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + // Step 2: Send bid requested event for auction 1 + events.emit(constants.EVENTS.BID_REQUESTED, prebidEvent['bidRequested']); + + // Step 3: Send auction init event for auction 2 + events.emit( + constants.EVENTS.AUCTION_INIT, + replaceProperty(prebidEvent['auctionInit'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 4: Send bid requested event for auction 2 + events.emit( + constants.EVENTS.BID_REQUESTED, + replaceProperty(prebidEvent['bidRequested'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 5: Send bid response event for auction 1 + events.emit(constants.EVENTS.BID_RESPONSE, prebidEvent['bidResponse']); + + // Step 6: Send bid time out event for auction 1 + events.emit(constants.EVENTS.BID_TIMEOUT, prebidEvent['bidTimeout']); + + // Step 7: Send bid response event for auction 2 + events.emit( + constants.EVENTS.BID_RESPONSE, + replaceProperty(prebidEvent['bidResponse'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 8: Send auction end event for auction 1 + events.emit(constants.EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + + // Step 9: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(0); + + // Step 10: Send auction bid won event for auction 1 events.emit(constants.EVENTS.BID_WON, prebidEvent['bidWon']); - expect(server.requests.length).to.equal(2); + // Step 11: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(2); + + // Step 12: Send auction end event for auction 2 + events.emit( + constants.EVENTS.AUCTION_END, + replaceProperty(prebidEvent['auctionEnd'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); - let winEventData = JSON.parse(server.requests[1].requestBody); + // Step 13: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(2); - expect(winEventData).to.deep.equal(expectedAfterBidWon); + // Step 14: Send auction bid won event for auction 2 + events.emit( + constants.EVENTS.BID_WON, + replaceProperty(prebidEvent['bidWon'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 15: check the calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(4); + for (const [index, arg] of navigator.sendBeacon.args.entries()) { + const [expectedUrl, expectedData] = arg; + const parsedUrl = new URL(expectedUrl); + const auctionIdMapFn = index < 2 ? (i, _) => i : replaceProperty; + expect(parsedUrl.pathname).to.equal( + ['/analytics/bidwon', '/analytics/auction'][index % 2] + ); + expect(Object.fromEntries(parsedUrl.searchParams)).to.deep.equal({ + auctionTimestamp: '1616654312804', + pubxaiAnalyticsVersion: 'v1.2.0', + prebidVersion: 'undefined', // not configured for test case + }); + expect(expectedData.type).to.equal('text/json'); + expect(JSON.parse(await expectedData.text())).to.deep.equal( + auctionIdMapFn([expectedAfterBidWon, expectedAfterBid][index % 2], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + { + field: 'refreshRank', + updated: '1', + replaced: '0', + }, + ]) + ); + } }); }); }); From 741a8a096b94eada4bedf3c7b3d123ad8697fea1 Mon Sep 17 00:00:00 2001 From: Nathan Oliver Date: Tue, 2 Apr 2024 23:47:33 +0100 Subject: [PATCH 2/4] PTOW-2 review actions --- modules/pubxaiAnalyticsAdapter.js | 22 ++++++++-- .../modules/pubxaiAnalyticsAdapter_spec.js | 41 +++++++++++++------ 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index c069a52cb47..625428fc6ff 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -56,6 +56,9 @@ export const auctionCache = new Proxy( userDetail: { userIdTypes: Object.keys(getGlobal().getUserIds?.() || {}), }, + consentDetail: { + consentTypes: Object.keys(getGlobal().getConsentMetadata?.() || {}), + }, pmacDetail: JSON.parse(getStorage()?.getItem('pubx:pmac')) || {}, // {auction_1: {floor:0.23,maxBid:0.34,bidCount:3},auction_2:{floor:0.13,maxBid:0.14,bidCount:2} initOptions: { ...initOptions, @@ -78,7 +81,14 @@ const getAdServerDataForBid = (bid) => { const gptSlot = getGptSlotForAdUnitCode(bid); if (gptSlot) { return Object.fromEntires( - gptSlot.getTargetingKeys().map((key) => [key, gptSlot.getTargeting(key)]) + gptSlot + .getTargetingKeys() + .filter( + (key) => + key.startsWith('pubx-') || + (key.startsWith('hb_') && (key.match(/_/g) || []).length === 1) + ) + .map((key) => [key, gptSlot.getTargeting(key)]) ); } return {}; // TODO: support more ad servers @@ -155,9 +165,13 @@ const track = ({ eventType, args }) => { case CONSTANTS.EVENTS.AUCTION_END: Object.assign( auctionCache[args.auctionId].floorDetail, - deepAccess(args, 'adUnits.0.bids.0.floorData') + args.adUnits + .map((i) => i?.bids.length && i.bids[0]?.floorData) + .find((i) => i) || {} ); - auctionCache[args.auctionId].pageDetail.adUnits = args.adUnitCodes; + auctionCache[args.auctionId].deviceDetail.cdep = args.bidderRequests + .map((bidRequest) => bidRequest.ortb2?.device?.ext?.cdep) + .find((i) => i); Object.assign(auctionCache[args.auctionId].auctionDetail, { adUnitCodes: args.adUnits.map((i) => i.code), timestamp: args.timestamp, @@ -284,6 +298,7 @@ const send = (auctionId) => { 'floorDetail', 'auctionDetail', 'userDetail', + 'consentDetail', 'pmacDetail', 'initOptions', ], @@ -298,6 +313,7 @@ const send = (auctionId) => { 'floorDetail', 'auctionDetail', 'userDetail', + 'consentDetail', 'pmacDetail', 'initOptions', ], diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index 478360c741f..ce5c944fe54 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -112,6 +112,13 @@ describe('pubxai analytics adapter', () => { bidderWinsCount: 0, }, ], + ortb2: { + device: { + ext: { + cdep: true, + }, + }, + }, auctionStart: 1603865707180, timeout: 1000, refererInfo: { @@ -169,6 +176,13 @@ describe('pubxai analytics adapter', () => { bidderWinsCount: 0, }, ], + ortb2: { + device: { + ext: { + cdep: true, + }, + }, + }, auctionStart: 1603865707180, timeout: 1000, refererInfo: { @@ -320,6 +334,13 @@ describe('pubxai analytics adapter', () => { bidderWinsCount: 0, }, ], + ortb2: { + device: { + ext: { + cdep: true, + }, + }, + }, auctionStart: 1603865707180, timeout: 1000, refererInfo: { @@ -485,16 +506,6 @@ describe('pubxai analytics adapter', () => { }, ], }, - // pageDetail: { - // host: location.host, - // path: location.pathname, - // search: location.search, - // }, - // pmcDetail: { - // bidDensity: storage.getItem("pbx:dpbid"), - // maxBid: storage.getItem("pbx:mxbid"), - // auctionId: storage.getItem("pbx:aucid"), - // }, }; let expectedAfterBid = { @@ -553,7 +564,6 @@ describe('pubxai analytics adapter', () => { host: location.host, path: location.pathname, search: location.search, - adUnits: ['/19968336/header-bid-tag-1'], }, floorDetail: { fetchStatus: 'success', @@ -568,10 +578,14 @@ describe('pubxai analytics adapter', () => { deviceType: getDeviceType(), deviceOS: getOS(), browser: getBrowser(), + cdep: true, }, userDetail: { userIdTypes: [], }, + consentDetail: { + consentTypes: [], + }, pmacDetail: JSON.parse(storage.getItem('pbx:pmac')) || {}, initOptions: { ...initOptions, @@ -643,7 +657,6 @@ describe('pubxai analytics adapter', () => { host: location.host, path: location.pathname, search: location.search, - adUnits: ['/19968336/header-bid-tag-1'], }, floorDetail: { fetchStatus: 'success', @@ -658,10 +671,14 @@ describe('pubxai analytics adapter', () => { deviceType: getDeviceType(), deviceOS: getOS(), browser: getBrowser(), + cdep: true, }, userDetail: { userIdTypes: [], }, + consentDetail: { + consentTypes: [], + }, pmacDetail: JSON.parse(storage.getItem('pbx:pmac')) || {}, initOptions: { ...initOptions, From 96b0f361b5bf2eb1df341b2ddddc5014923028ac Mon Sep 17 00:00:00 2001 From: Nathan Oliver Date: Tue, 30 Apr 2024 07:40:30 +0100 Subject: [PATCH 3/4] PTOW-2 Review actions --- modules/pubxaiAnalyticsAdapter.js | 190 ++++++++++++------ .../modules/pubxaiAnalyticsAdapter_spec.js | 167 ++++++++++++++- 2 files changed, 281 insertions(+), 76 deletions(-) diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index 625428fc6ff..b3541a664d1 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -4,24 +4,40 @@ import { getWindowLocation, buildUrl, cyrb53Hash, -} from '../src/utils.js'; -import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; -import adapterManager from '../src/adapterManager.js'; -import CONSTANTS from '../src/constants.json'; -import { getGlobal } from '../src/prebidGlobal.js'; +} from "../src/utils.js"; +import adapter from "../libraries/analyticsAdapter/AnalyticsAdapter.js"; +import adapterManager from "../src/adapterManager.js"; +import CONSTANTS from "../src/constants.json"; +import { getGlobal } from "../src/prebidGlobal.js"; import { getGptSlotInfoForAdUnitCode, getGptSlotForAdUnitCode, -} from '../libraries/gptUtils/gptUtils.js'; +} from "../libraries/gptUtils/gptUtils.js"; let initOptions; -const emptyUrl = ''; -const analyticsType = 'endpoint'; -const pubxaiAnalyticsVersion = 'v1.2.0'; -const defaultHost = 'api.pbxai.com'; -const auctionPath = '/analytics/auction'; -const winningBidPath = '/analytics/bidwon'; +const emptyUrl = ""; +const analyticsType = "endpoint"; +const pubxaiAnalyticsVersion = "v1.2.0"; +const defaultHost = "api.pbxai.com"; +const auctionPath = "/analytics/auction"; +const winningBidPath = "/analytics/bidwon"; + +/** + * The sendCache is a global cache object which tracks the pending sends + * back to pubx.ai. The data may be removed from this cache, post send. + */ +export const sendCache = new Proxy( + {}, + { + get: (target, name) => { + if (!Object.hasOwn(target, name)) { + target[name] = []; + } + return target[name]; + }, + } +); /** * auctionCache is a global cache object which stores all auction histories @@ -59,12 +75,12 @@ export const auctionCache = new Proxy( consentDetail: { consentTypes: Object.keys(getGlobal().getConsentMetadata?.() || {}), }, - pmacDetail: JSON.parse(getStorage()?.getItem('pubx:pmac')) || {}, // {auction_1: {floor:0.23,maxBid:0.34,bidCount:3},auction_2:{floor:0.13,maxBid:0.14,bidCount:2} + pmacDetail: JSON.parse(getStorage()?.getItem("pubx:pmac")) || {}, // {auction_1: {floor:0.23,maxBid:0.34,bidCount:3},auction_2:{floor:0.13,maxBid:0.14,bidCount:2} initOptions: { ...initOptions, auctionId: name, // back-compat }, - sentAs: [], + sendAs: [], }; } return target[name]; @@ -72,6 +88,17 @@ export const auctionCache = new Proxy( } ); +/** + * + * @returns {boolean} whether or not the browser session supports sendBeacon + */ +const hasSendBeaconSupport = () => { + if (!navigator.sendBeacon || !document.visibilityState) { + return false; + } + return true; +}; + /** * Fetch extra ad server data for a specific ad slot (bid) * @param {object} bid an output from extractBid @@ -85,8 +112,8 @@ const getAdServerDataForBid = (bid) => { .getTargetingKeys() .filter( (key) => - key.startsWith('pubx-') || - (key.startsWith('hb_') && (key.match(/_/g) || []).length === 1) + key.startsWith("pubx-") || + (key.startsWith("hb_") && (key.match(/_/g) || []).length === 1) ) .map((key) => [key, gptSlot.getTargeting(key)]) ); @@ -100,7 +127,7 @@ const getAdServerDataForBid = (bid) => { */ const getStorage = () => { try { - return window.top['sessionStorage']; + return window.top["sessionStorage"]; } catch (e) { return null; } @@ -135,7 +162,7 @@ const extractBid = (bidResponse) => { transactionId: bidResponse.transactionId, bidId: bidResponse.bidId || bidResponse.requestId, placementId: bidResponse.params - ? deepAccess(bidResponse, 'params.0.placementId') + ? deepAccess(bidResponse, "params.0.placementId") : null, }; }; @@ -179,7 +206,7 @@ const track = ({ eventType, args }) => { if ( auctionCache[args.auctionId].bids.every((bid) => bid.renderStatus === 3) ) { - send(args.auctionId); + prepareSend(args.auctionId); } break; // send the prebid winning bid back to pubx @@ -199,7 +226,7 @@ const track = ({ eventType, args }) => { }); winningBid.adServerData = getAdServerDataForBid(winningBid); auctionCache[winningBid.auctionId].winningBid = winningBid; - send(winningBid.auctionId); + prepareSend(winningBid.auctionId); break; // do nothing default: @@ -217,16 +244,16 @@ export const getDeviceType = () => { navigator.userAgent.toLowerCase() ) ) { - return 'tablet'; + return "tablet"; } if ( /iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test( navigator.userAgent.toLowerCase() ) ) { - return 'mobile'; + return "mobile"; } - return 'desktop'; + return "desktop"; }; /** @@ -238,21 +265,21 @@ export const getBrowser = () => { /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor) ) { - return 'Chrome'; - } else if (navigator.userAgent.match('CriOS')) return 'Chrome'; - else if (/Firefox/.test(navigator.userAgent)) return 'Firefox'; - else if (/Edg/.test(navigator.userAgent)) return 'Microsoft Edge'; + return "Chrome"; + } else if (navigator.userAgent.match("CriOS")) return "Chrome"; + else if (/Firefox/.test(navigator.userAgent)) return "Firefox"; + else if (/Edg/.test(navigator.userAgent)) return "Microsoft Edge"; else if ( /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor) ) { - return 'Safari'; + return "Safari"; } else if ( /Trident/.test(navigator.userAgent) || /MSIE/.test(navigator.userAgent) ) { - return 'Internet Explorer'; - } else return 'Others'; + return "Internet Explorer"; + } else return "Others"; }; /** @@ -260,13 +287,13 @@ export const getBrowser = () => { * @returns {string} */ export const getOS = () => { - if (navigator.userAgent.indexOf('Android') != -1) return 'Android'; - if (navigator.userAgent.indexOf('like Mac') != -1) return 'iOS'; - if (navigator.userAgent.indexOf('Win') != -1) return 'Windows'; - if (navigator.userAgent.indexOf('Mac') != -1) return 'Macintosh'; - if (navigator.userAgent.indexOf('Linux') != -1) return 'Linux'; - if (navigator.appVersion.indexOf('X11') != -1) return 'Unix'; - return 'Others'; + if (navigator.userAgent.indexOf("Android") != -1) return "Android"; + if (navigator.userAgent.indexOf("like Mac") != -1) return "iOS"; + if (navigator.userAgent.indexOf("Win") != -1) return "Windows"; + if (navigator.userAgent.indexOf("Mac") != -1) return "Macintosh"; + if (navigator.userAgent.indexOf("Linux") != -1) return "Linux"; + if (navigator.appVersion.indexOf("X11") != -1) return "Unix"; + return "Others"; }; /** @@ -280,10 +307,10 @@ const shouldFireEventRequest = (auctionId, samplingRate = 1) => { }; /** - * Send auction data back to pubx.ai + * prepare the payload for sending auction data back to pubx.ai * @param {string} auctionId the auction to send */ -const send = (auctionId) => { +const prepareSend = (auctionId) => { const auctionData = Object.assign({}, auctionCache[auctionId]); if (!shouldFireEventRequest(auctionId, initOptions.samplingRate)) { return; @@ -292,45 +319,45 @@ const send = (auctionId) => { { path: winningBidPath, requiredKeys: [ - 'winningBid', - 'pageDetail', - 'deviceDetail', - 'floorDetail', - 'auctionDetail', - 'userDetail', - 'consentDetail', - 'pmacDetail', - 'initOptions', + "winningBid", + "pageDetail", + "deviceDetail", + "floorDetail", + "auctionDetail", + "userDetail", + "consentDetail", + "pmacDetail", + "initOptions", ], - eventType: 'win', + eventType: "win", }, { path: auctionPath, requiredKeys: [ - 'bids', - 'pageDetail', - 'deviceDetail', - 'floorDetail', - 'auctionDetail', - 'userDetail', - 'consentDetail', - 'pmacDetail', - 'initOptions', + "bids", + "pageDetail", + "deviceDetail", + "floorDetail", + "auctionDetail", + "userDetail", + "consentDetail", + "pmacDetail", + "initOptions", ], - eventType: 'auction', + eventType: "auction", }, ].forEach(({ path, requiredKeys, eventType }) => { const data = Object.fromEntries( requiredKeys.map((key) => [key, auctionData[key]]) ); if ( - auctionCache[auctionId].sentAs.includes(eventType) || + auctionCache[auctionId].sendAs.includes(eventType) || !requiredKeys.every((key) => !!auctionData[key]) ) { return; } const pubxaiAnalyticsRequestUrl = buildUrl({ - protocol: 'https', + protocol: "https", hostname: (auctionData.initOptions && auctionData.initOptions.hostName) || defaultHost, @@ -341,14 +368,44 @@ const send = (auctionId) => { prebidVersion: getGlobal().version, }, }); - const payload = new Blob([JSON.stringify(data)], { - type: 'text/json', + sendCache[pubxaiAnalyticsRequestUrl].push(data); + auctionCache[auctionId].sendAs.push(eventType); + }); +}; + +const send = () => { + const toBlob = (d) => new Blob([JSON.stringify(d)], { type: "text/json" }); + + Object.entries(sendCache).forEach(([requestUrl, events]) => { + let payloadStart = 0; + + events.forEach((event, index, arr) => { + const payload = arr.slice(payloadStart, index + 2); + const payloadTooLarge = toBlob(payload).size > 65536; + + if (payloadTooLarge || index + 1 === arr.length) { + navigator.sendBeacon( + requestUrl, + toBlob(payloadTooLarge ? payload.slice(0, -1) : payload) + ); + payloadStart = index; + } }); - navigator.sendBeacon(pubxaiAnalyticsRequestUrl, payload); - auctionCache[auctionId].sentAs.push(eventType); + + events.splice(0); }); }; +// register event listener to send logs when user leaves page +if (hasSendBeaconSupport()) { + document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "hidden") { + send(); + } + }); +} + +// declare the analytics adapter var pubxaiAnalyticsAdapter = Object.assign( adapter({ emptyUrl, @@ -356,7 +413,6 @@ var pubxaiAnalyticsAdapter = Object.assign( }), { track } ); -pubxaiAnalyticsAdapter.track = track; pubxaiAnalyticsAdapter.originEnableAnalytics = pubxaiAnalyticsAdapter.enableAnalytics; @@ -367,7 +423,7 @@ pubxaiAnalyticsAdapter.enableAnalytics = (config) => { adapterManager.registerAnalyticsAdapter({ adapter: pubxaiAnalyticsAdapter, - code: 'pubxai', + code: "pubxai", }); export default pubxaiAnalyticsAdapter; diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index ce5c944fe54..d126ed21715 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -692,17 +692,23 @@ describe('pubxai analytics adapter', () => { }); beforeEach(() => { + Object.defineProperty(document, 'visibilityState', { + value: 'hidden', + writable: true, + }); // prep for the document visibility state change adapterManager.enableAnalytics({ provider: 'pubxai', options: initOptions, }); sinon.stub(navigator, 'sendBeacon').returns(true); + sinon.stub(); }); afterEach(() => { pubxaiAnalyticsAdapter.disableAnalytics(); navigator.sendBeacon.restore(); delete auctionCache['bc3806e4-873e-453c-8ae5-204f35e923b4']; + delete auctionCache['auction2']; }); it('builds and sends auction data', async () => { @@ -721,11 +727,17 @@ describe('pubxai analytics adapter', () => { // Step 5: Send auction end event events.emit(constants.EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); + expect(navigator.sendBeacon.callCount).to.equal(0); // Step 6: Send auction bid won event events.emit(constants.EVENTS.BID_WON, prebidEvent['bidWon']); + // Simulate end of session + document.dispatchEvent(new Event('visibilitychange')); + expect(navigator.sendBeacon.callCount).to.equal(2); for (const [index, arg] of navigator.sendBeacon.args.entries()) { @@ -740,9 +752,9 @@ describe('pubxai analytics adapter', () => { prebidVersion: 'undefined', // not configured for test case }); expect(expectedData.type).to.equal('text/json'); - expect(JSON.parse(await expectedData.text())).to.deep.equal( - [expectedAfterBidWon, expectedAfterBid][index] - ); + expect(JSON.parse(await expectedData.text())).to.deep.equal([ + [expectedAfterBidWon, expectedAfterBid][index], + ]); } }); @@ -756,12 +768,18 @@ describe('pubxai analytics adapter', () => { // Step 3: Send bid time out event events.emit(constants.EVENTS.BID_TIMEOUT, prebidEvent['bidTimeout']); + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); + // Step 4: check the number of calls made to pubx.ai expect(navigator.sendBeacon.callCount).to.equal(0); // Step 5: Send auction end event events.emit(constants.EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + // Simulate end of session + document.dispatchEvent(new Event('visibilitychange')); + // Step 6: check the number of calls made to pubx.ai expect(navigator.sendBeacon.callCount).to.equal(1); @@ -779,10 +797,12 @@ describe('pubxai analytics adapter', () => { // Step 9: check that the data sent in the request is correct expect(expectedData.type).to.equal('text/json'); - expect(JSON.parse(await expectedData.text())).to.deep.equal({ - ...expectedAfterBid, - bids: [], - }); + expect(JSON.parse(await expectedData.text())).to.deep.equal([ + { + ...expectedAfterBid, + bids: [], + }, + ]); }); it('2 concurrent auctions', async () => { @@ -837,12 +857,18 @@ describe('pubxai analytics adapter', () => { // Step 8: Send auction end event for auction 1 events.emit(constants.EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); + // Step 9: check the number of calls made to pubx.ai expect(navigator.sendBeacon.callCount).to.equal(0); // Step 10: Send auction bid won event for auction 1 events.emit(constants.EVENTS.BID_WON, prebidEvent['bidWon']); + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); + // Step 11: check the number of calls made to pubx.ai expect(navigator.sendBeacon.callCount).to.equal(2); @@ -858,6 +884,9 @@ describe('pubxai analytics adapter', () => { ]) ); + // Simulate "navigate away" behaviour + document.dispatchEvent(new Event('visibilitychange')); + // Step 13: check the number of calls made to pubx.ai expect(navigator.sendBeacon.callCount).to.equal(2); @@ -873,6 +902,9 @@ describe('pubxai analytics adapter', () => { ]) ); + // Simulate end of session + document.dispatchEvent(new Event('visibilitychange')); + // Step 15: check the calls made to pubx.ai expect(navigator.sendBeacon.callCount).to.equal(4); for (const [index, arg] of navigator.sendBeacon.args.entries()) { @@ -888,7 +920,7 @@ describe('pubxai analytics adapter', () => { prebidVersion: 'undefined', // not configured for test case }); expect(expectedData.type).to.equal('text/json'); - expect(JSON.parse(await expectedData.text())).to.deep.equal( + expect(JSON.parse(await expectedData.text())).to.deep.equal([ auctionIdMapFn([expectedAfterBidWon, expectedAfterBid][index % 2], [ { field: 'auctionId', @@ -900,8 +932,125 @@ describe('pubxai analytics adapter', () => { updated: '1', replaced: '0', }, - ]) + ]), + ]); + } + }); + + it('2 concurrent auctions with batch sending', async () => { + // Step 1: Send auction init event for auction 1 + events.emit(constants.EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + // Step 2: Send bid requested event for auction 1 + events.emit(constants.EVENTS.BID_REQUESTED, prebidEvent['bidRequested']); + + // Step 3: Send auction init event for auction 2 + events.emit( + constants.EVENTS.AUCTION_INIT, + replaceProperty(prebidEvent['auctionInit'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 4: Send bid requested event for auction 2 + events.emit( + constants.EVENTS.BID_REQUESTED, + replaceProperty(prebidEvent['bidRequested'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 5: Send bid response event for auction 1 + events.emit(constants.EVENTS.BID_RESPONSE, prebidEvent['bidResponse']); + + // Step 6: Send bid time out event for auction 1 + events.emit(constants.EVENTS.BID_TIMEOUT, prebidEvent['bidTimeout']); + + // Step 7: Send bid response event for auction 2 + events.emit( + constants.EVENTS.BID_RESPONSE, + replaceProperty(prebidEvent['bidResponse'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 8: Send auction end event for auction 1 + events.emit(constants.EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + + // Step 9: Send auction bid won event for auction 1 + events.emit(constants.EVENTS.BID_WON, prebidEvent['bidWon']); + + // Step 10: Send auction end event for auction 2 + events.emit( + constants.EVENTS.AUCTION_END, + replaceProperty(prebidEvent['auctionEnd'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 11: Send auction bid won event for auction 2 + events.emit( + constants.EVENTS.BID_WON, + replaceProperty(prebidEvent['bidWon'], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + ]) + ); + + // Step 12: check the number of calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(0); + + // Simulate end of session + document.dispatchEvent(new Event('visibilitychange')); + + // Step 13: check the calls made to pubx.ai + expect(navigator.sendBeacon.callCount).to.equal(2); + for (const [index, arg] of navigator.sendBeacon.args.entries()) { + const [expectedUrl, expectedData] = arg; + const parsedUrl = new URL(expectedUrl); + expect(parsedUrl.pathname).to.equal( + ['/analytics/bidwon', '/analytics/auction'][index] ); + expect(Object.fromEntries(parsedUrl.searchParams)).to.deep.equal({ + auctionTimestamp: '1616654312804', + pubxaiAnalyticsVersion: 'v1.2.0', + prebidVersion: 'undefined', // not configured for test case + }); + expect(expectedData.type).to.equal('text/json'); + expect(JSON.parse(await expectedData.text())).to.deep.equal([ + [expectedAfterBidWon, expectedAfterBid][index], + replaceProperty([expectedAfterBidWon, expectedAfterBid][index], [ + { + field: 'auctionId', + updated: '"auction2"', + replaced: '"bc3806e4-873e-453c-8ae5-204f35e923b4"', + }, + { + field: 'refreshRank', + updated: '1', + replaced: '0', + }, + ]), + ]); } }); }); From a5c3f4658d8b6e8b2517b6907cdf6219f1ba171b Mon Sep 17 00:00:00 2001 From: tej656 Date: Mon, 22 Apr 2024 11:20:31 +0530 Subject: [PATCH 4/4] PTOW-2 updating pubx.ai analytics version --- modules/pubxaiAnalyticsAdapter.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index b3541a664d1..f6093022787 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -18,7 +18,7 @@ let initOptions; const emptyUrl = ""; const analyticsType = "endpoint"; -const pubxaiAnalyticsVersion = "v1.2.0"; +const pubxaiAnalyticsVersion = "v2.0.0"; const defaultHost = "api.pbxai.com"; const auctionPath = "/analytics/auction"; const winningBidPath = "/analytics/bidwon"; @@ -261,25 +261,25 @@ export const getDeviceType = () => { * @returns {string} */ export const getBrowser = () => { - if ( + if (/Edg/.test(navigator.userAgent)) return "Microsoft Edge"; + else if ( /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor) - ) { + ) return "Chrome"; - } else if (navigator.userAgent.match("CriOS")) return "Chrome"; + else if (navigator.userAgent.match("CriOS")) return "Chrome"; else if (/Firefox/.test(navigator.userAgent)) return "Firefox"; - else if (/Edg/.test(navigator.userAgent)) return "Microsoft Edge"; else if ( /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor) - ) { + ) return "Safari"; - } else if ( + else if ( /Trident/.test(navigator.userAgent) || /MSIE/.test(navigator.userAgent) - ) { + ) return "Internet Explorer"; - } else return "Others"; + else return "Others"; }; /**