import { EVENTS } from '../../src/constants.js'; import {ajax} from '../../src/ajax.js'; import {logError, logMessage} from '../../src/utils.js'; import * as events from '../../src/events.js'; import {config} from '../../src/config.js'; export const _internal = { ajax }; const ENDPOINT = 'endpoint'; const BUNDLE = 'bundle'; const LABELS_KEY = 'analyticsLabels'; type AnalyticsType = typeof ENDPOINT | typeof BUNDLE; const labels = { internal: {}, publisher: {}, }; let allLabels = {}; config.getConfig(LABELS_KEY, (cfg) => { labels.publisher = cfg[LABELS_KEY]; allLabels = combineLabels(); ; }); export function setLabels(internalLabels) { labels.internal = internalLabels; allLabels = combineLabels(); }; const combineLabels = () => Object.values(labels).reduce((acc, curr) => ({...acc, ...curr}), {}); export const DEFAULT_INCLUDE_EVENTS = Object.values(EVENTS) .filter(ev => ev !== EVENTS.AUCTION_DEBUG); let debounceDelay = 100; export function setDebounceDelay(delay) { debounceDelay = delay; } export type AnalyticsProvider = string; // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface AnalyticsProviderConfig { /** * Adapter-specific config types - to be extended in the adapters */ } export type DefaultOptions = { /** * Sampling rate, expressed as a number between 0 and 1. Data is collected only on this ratio of browser sessions. * Defaults to 1 */ sampling?: number; } export type AnalyticsConfig

= ( P extends keyof AnalyticsProviderConfig ? AnalyticsProviderConfig[P] : { [key: string]: unknown } ) & { /** * Analytics adapter code */ provider: P; /** * Event whitelist; if provided, only these events will be forwarded to the adapter */ includeEvents?: (keyof events.Events)[]; /** * Event blacklist; if provided, these events will not be forwarded to the adapter */ excludeEvents?: (keyof events.Events)[]; /** * Adapter specific options */ options?: P extends keyof AnalyticsProviderConfig ? AnalyticsProviderConfig[P] : Record } export default function AnalyticsAdapter({ url, analyticsType, global, handler }: { analyticsType?: AnalyticsType; url?: string; global?: string; handler?: any; }) { const queue = []; let handlers; let enabled = false; let sampled = true; let provider: PROVIDER; const emptyQueue = (() => { let running = false; let timer; const clearQueue = () => { if (!running) { running = true; // needed to avoid recursive re-processing when analytics event handlers trigger other events try { let i = 0; let notDecreasing = 0; while (queue.length > 0) { i++; const len = queue.length; queue.shift()(); if (queue.length >= len) { notDecreasing++; } else { notDecreasing = 0 } if (notDecreasing >= 10) { logError('Detected probable infinite loop, discarding events', queue) queue.length = 0; return; } } logMessage(`${provider} analytics: processed ${i} events`); } finally { running = false; } } }; return function () { if (timer != null) { clearTimeout(timer); timer = null; } debounceDelay === 0 ? clearQueue() : timer = setTimeout(clearQueue, debounceDelay); } })(); return Object.defineProperties({ track: _track, enqueue: _enqueue, enableAnalytics: _enable, disableAnalytics: _disable, getAdapterType: () => analyticsType, getGlobal: () => global, getHandler: () => handler, getUrl: () => url }, { enabled: { get: () => enabled } }); function _track(arg) { const {eventType, args} = arg; if (this.getAdapterType() === BUNDLE) { (window[global] as any)(handler, eventType, args); } if (this.getAdapterType() === ENDPOINT) { _callEndpoint(arg); } } function _callEndpoint({ eventType, args, callback }) { _internal.ajax(url, callback, JSON.stringify({ eventType, args, labels: allLabels })); } function _enqueue({eventType, args}) { queue.push(() => { if (Object.keys(allLabels || []).length > 0) { args = { [LABELS_KEY]: allLabels, ...args, } } this.track({eventType, labels: allLabels, args}); }); emptyQueue(); } function _enable(config: AnalyticsConfig) { provider = config?.provider; if (typeof config === 'object' && typeof config.options === 'object') { sampled = typeof (config.options as any).sampling === 'undefined' || Math.random() < parseFloat((config.options as any).sampling); } else { sampled = true; } if (sampled) { const trackedEvents: Set = (() => { const {includeEvents = DEFAULT_INCLUDE_EVENTS, excludeEvents = []} = (config || {}); return new Set( Object.values(EVENTS) .filter(ev => includeEvents.includes(ev)) .filter(ev => !excludeEvents.includes(ev)) ); })(); // first send all events fired before enableAnalytics called events.getEvents().forEach(event => { if (!event || !trackedEvents.has(event.eventType)) { return; } const { eventType, args } = event; _enqueue.call(this, { eventType, args }); }); // Next register event listeners to send data immediately handlers = Object.fromEntries( Array.from(trackedEvents) .map((ev) => { const handler = (args) => this.enqueue({eventType: ev, args}); events.on(ev, handler); return [ev, handler]; }) ) } else { logMessage(`Analytics adapter for "${global}" disabled by sampling`); } // finally set this function to return log message, prevents multiple adapter listeners this._oldEnable = this.enableAnalytics; this.enableAnalytics = function _enable() { return logMessage(`Analytics adapter for "${global}" already enabled, unnecessary call to \`enableAnalytics\`.`); }; enabled = true; } function _disable() { Object.entries(handlers || {}).forEach(([event, handler]: any) => { events.off(event, handler); }) this.enableAnalytics = this._oldEnable ? this._oldEnable : _enable; enabled = false; } }