import { debugLog, gatherEventData, getSafeUrl, getSanitizedPageUrl, getSessionId, getTargetNode, shouldLogEvent, } from "./utils"; import { StatsigClient } from "statsig-js"; export default class AutoCapture { _started: boolean = false; _statsigInstance: StatsigClient | null = null; _queuedEvents: Array = []; _maxQueueSize: number = 1000; _startTime: number = Date.now(); _deepestScroll: number = 0; constructor(autoStart: boolean = true) { this._started = autoStart; this.initialize(); } private addEventHandlers(): boolean { if (!document || !window) { return false; } const eventHandler = (event: Event) => { this.autoLogEvent(event || window.event); }; this.registerEventHandler(document, 'click', eventHandler); this.registerEventHandler(document, 'submit', eventHandler); this.registerEventHandler(window, 'error', eventHandler); this.registerEventHandler(window, 'beforeunload', () => this.pageUnloadHandler()); this.registerEventHandler(window, 'scroll', () => this.scrollEventHandler()); return true; } private autoLogEvent(event: Event) { let eventType = event.type?.toLowerCase(); if (eventType === 'error' && event instanceof ErrorEvent) { this.logError(event); return; } const target = getTargetNode(event); if (!target) { return; } if (!shouldLogEvent(event, target)) { return; } if (eventType === 'submit') { eventType = 'form_submit'; } const { value, metadata } = gatherEventData(target); this.queueAutoCapture( eventType, value, Object.fromEntries(metadata.entries()), ); } private flush() { if (!this._statsigInstance) { return; } this._queuedEvents.forEach(([eventName, value, metadata]) => { this._statsigInstance?.logEvent(eventName, value, metadata); }); this._queuedEvents = []; } private initialize() { if (!document || !window || !document.addEventListener || !window.addEventListener) { return; } const docInitFunc = () => { this.addEventHandlers(); this.logPageView(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', docInitFunc); } else { docInitFunc(); } const windowLoadFunc = () => { this.logPerformance(); } if (document.readyState !== 'complete') { window.addEventListener('load', windowLoadFunc); } else { windowLoadFunc(); } } private logError(event: ErrorEvent) { const error = (event as ErrorEvent)?.error || {}; let errorStr = error; if (typeof error === 'object') { try { errorStr = JSON.stringify(error); } catch (e) { errorStr = error.toString(); } } this.queueAutoCapture( 'error', event.message, { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, error_str: errorStr, }, ); } private logPageView() { const func = () => { const url = getSafeUrl(); this.queueAutoCapture('page_view', getSanitizedPageUrl(), { title: document?.title, referrer: document?.referrer || '', queryParams: Object.fromEntries(url.searchParams), }, true); }; setTimeout(func, 1); } private logPerformance() { if (!window || !window.performance) { return; } const func = () => { if ( typeof window.performance.getEntriesByType !== 'function' || typeof window.performance.getEntriesByName !== 'function' ) { return; } const metadata = new Map(); const navEntries = window.performance.getEntriesByType('navigation'); if (navEntries && navEntries.length > 0) { const nav = navEntries[0]; if (nav instanceof PerformanceNavigationTiming) { metadata.set('load_time_ms', nav.duration?.toFixed(2)); metadata.set('dom_interactive_time_ms', (nav.domInteractive - nav.startTime).toFixed(2)); metadata.set('redirect_count', nav.redirectCount); metadata.set('transfer_bytes', nav.transferSize); } } const fpEntries = window.performance.getEntriesByName('first-contentful-paint'); if (fpEntries && fpEntries.length > 0) { const fp = fpEntries[0]; if (fp instanceof PerformancePaintTiming) { metadata.set('first_contentful_paint_time_ms', fp.startTime?.toFixed(2)); } } this.queueAutoCapture('performance', getSanitizedPageUrl(), { ...Object.fromEntries(metadata.entries()), }); }; setTimeout(func, 1); } private pageUnloadHandler() { this.queueAutoCapture( 'page_view_end', getSanitizedPageUrl(), { scrollDepth: this._deepestScroll, pageViewLength: Date.now() - this._startTime, }, true, ); } private queueAutoCapture( name: string, value: string, metadata: Record, immediate: boolean = false, ) { const href = window?.location?.href || ''; this.queue( `auto_capture::${name}`, value, { sessionID: getSessionId(), page_url: href, ...metadata, }, immediate, ); } private queue( eventName: string, value: string, metadata: Record, immediate: boolean = false, ) { debugLog(`Queuing: ${eventName}, ${value}, ${JSON.stringify(metadata, null, 2)}`); const shouldQueue = !this._started || !this._statsigInstance; if (shouldQueue) { if (this._queuedEvents.length < this._maxQueueSize) { this._queuedEvents.push([eventName, value, metadata]); } } else { if (this._queuedEvents.length > 0) { this.flush(); } this._statsigInstance?.logEvent(eventName, value, metadata); if (immediate) { this._statsigInstance?.flushEvents(); } } } private registerEventHandler( element: Document | Window, eventType: string, handler: (event: Event) => void ) { if (!element || !element.addEventListener) { return; } element.addEventListener(eventType, handler, true); } private scrollEventHandler() { const scrollHeight = document?.body?.scrollHeight || 1; this._deepestScroll = Math.max( this._deepestScroll, Math.min( 100, Math.round((window.scrollY + window.innerHeight) / scrollHeight * 100), ) ); } public setStatsigInstance(statsigInstance: StatsigClient) { this._statsigInstance = statsigInstance; } public start() { this._started = true; this.flush(); } public stop() { this._started = false; } }