diff --git a/modules/djaxAnalyticsAdapter.js b/modules/djaxAnalyticsAdapter.js new file mode 100644 index 00000000000..3f93b6a3863 --- /dev/null +++ b/modules/djaxAnalyticsAdapter.js @@ -0,0 +1,152 @@ +import AnalyticsAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import {prefixLog, isPlainObject} from '../src/utils.js'; +import {has as hasEvent} from '../src/events.js'; +import adapterManager from '../src/adapterManager.js'; +import {ajaxBuilder} from '../src/ajax.js'; + +const DEFAULTS = { + batchSize: 1, + batchDelay: 100, + method: 'POST' +} + +const TYPES = { + handler: 'function', + batchSize: 'number', + batchDelay: 'number', + gvlid: 'number', +} + +const MAX_CALL_DEPTH = 20; + +export function DjaxAnalytics() { + const parent = AnalyticsAdapter({analyticsType: 'endpoint'}); + const {logError, logWarn} = prefixLog('Djax analytics:'); + let batch = []; + let callDepth = 0; + let options, handler, timer, translate; + + function optionsAreValid(options) { + if (!options.url && !options.handler) { + logError('options must specify either `url` or `handler`') + return false; + } + if (options.hasOwnProperty('method') && !['GET', 'POST'].includes(options.method)) { + logError('options.method must be GET or POST'); + return false; + } + for (const [field, type] of Object.entries(TYPES)) { + // eslint-disable-next-line valid-typeof + if (options.hasOwnProperty(field) && typeof options[field] !== type) { + logError(`options.${field} must be a ${type}`); + return false; + } + } + if (options.hasOwnProperty('events')) { + if (!isPlainObject(options.events)) { + logError('options.events must be an object'); + return false; + } + for (const [event, handler] of Object.entries(options.events)) { + if (!hasEvent(event)) { + logWarn(`options.events.${event} does not match any known Prebid event`); + } + if (typeof handler !== 'function') { + logError(`options.events.${event} must be a function`); + return false; + } + } + } + return true; + } + + function processBatch() { + const currentBatch = batch; + batch = []; + callDepth++; + try { + // the pub-provided handler may inadvertently cause an infinite chain of events; + // even just logging an exception from it may cause an AUCTION_DEBUG event, that + // gets back to the handler, that throws another exception etc. + // to avoid the issue, put a cap on recursion + if (callDepth === MAX_CALL_DEPTH) { + logError('detected probable infinite recursion, discarding events', currentBatch); + } + if (callDepth >= MAX_CALL_DEPTH) { + return; + } + try { + handler(currentBatch); + } catch (e) { + logError('error executing options.handler', e); + } + } finally { + callDepth--; + } + } + + function translator(eventHandlers) { + if (!eventHandlers) { + return (data) => data; + } + return function ({eventType, args}) { + if (eventHandlers.hasOwnProperty(eventType)) { + try { + return eventHandlers[eventType](args); + } catch (e) { + logError(`error executing options.events.${eventType}`, e); + } + } + } + } + + return Object.assign( + Object.create(parent), + { + gvlid(config) { + return config?.options?.gvlid + }, + enableAnalytics(config) { + if (optionsAreValid(config?.options || {})) { + options = Object.assign({}, DEFAULTS, config.options); + handler = options.handler || defaultHandler(options); + translate = translator(options.events); + parent.enableAnalytics.call(this, config); + } + }, + track(event) { + const datum = translate(event); + if (datum != null) { + batch.push(datum); + if (timer != null) { + clearTimeout(timer); + timer = null; + } + if (batch.length >= options.batchSize) { + processBatch(); + } else { + timer = setTimeout(processBatch, options.batchDelay); + } + } + } + } + ) +} + +export function defaultHandler({url, method, batchSize, ajax = ajaxBuilder()}) { + const callbacks = { + success() {}, + error() {} + } + const extract = batchSize > 1 ? (events) => events : (events) => events[0]; + const serialize = method === 'GET' ? (data) => ({data: JSON.stringify(data)}) : (data) => JSON.stringify(data); + + return function (events) { + ajax(url, callbacks, serialize(extract(events)), {method, keepalive: true}) + } +} + +adapterManager.registerAnalyticsAdapter({ + adapter: DjaxAnalytics(), + code: 'djax', +}); diff --git a/modules/djaxAnalyticsAdapter.md b/modules/djaxAnalyticsAdapter.md new file mode 100644 index 00000000000..70ed8c1d704 --- /dev/null +++ b/modules/djaxAnalyticsAdapter.md @@ -0,0 +1,48 @@ +# Overview +``` +Module Name: djax Analytics Adapter +Module Type: Analytics Adapter +Maintainer: support@djaxtech.com +``` + +### Usage + +The djax analytics adapter can be used by all clients . + +### Example Configuration + +```javascript +pbjs.enableAnalytics({ + provider: 'djax', + options: { + options: { + url: 'https://example.com', // change your end point url to fetch the tracked information + } +}); + + +// Based on events +pbjs.enableAnalytics({ + provider: 'djax', + options: { + url: 'https://example.com', + batchSize: 10, + events: { + bidRequested(request) { + return { + type: 'REQUEST', + auctionId: request.auctionId, + bidder: request.bidderCode + } + }, + bidResponse(response) { + return { + type: 'RESPONSE', + auctionId: response.auctionId, + bidder: response.bidderCode + } + } + } + } +}) +```