/** * Browser capture hooks. Each installer returns its cleanup function. * Everything funnels into `devtoolsStore.capture()` — dedupe, panel entry, * and ingest buffering all happen there, once. */ import { devtoolsStore } from './store' import { INGEST_PATTERN, getSessionId, truncate } from './internal' import { EventLevel, EventType } from './types' import type { DevtoolsEvent } from './types' const MSG_MAX = 2000 const ARG_MAX = 500 // Hydration mismatches caused by browser extensions (Grammarly, translators…). // Not application bugs — filtering them prevents alert noise. const HYDRATION_NOISE: RegExp[] = [ /hydration failed/i, /there was an error while hydrating/i, /minified react error #418/i, /minified react error #423/i, /minified react error #425/i, /text content does not match server-rendered html/i, ] function isNoise(msg: string, stack?: string): boolean { if (HYDRATION_NOISE.some((p) => p.test(msg))) return true // Our own transport failing must never loop back into capture. return INGEST_PATTERN.test(msg) || INGEST_PATTERN.test(stack ?? '') } function browserContext(): Pick { return { url: typeof window !== 'undefined' ? window.location.href : '', session_id: getSessionId(), user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : '', } } // ─── window.onerror + unhandledrejection ──────────────────────────────────── export function installJsErrorCapture(): () => void { if (typeof window === 'undefined') return () => {} const report = (msg: string, stack: string) => { try { if (isNoise(msg, stack)) return devtoolsStore.getState().capture({ event_type: EventType.JS_ERROR, level: EventLevel.ERROR, message: truncate(msg, MSG_MAX), stack_trace: stack, ...browserContext(), }) } catch { /* never crash the host app */ } } const onError = (e: ErrorEvent) => { const msg = e.message || String(e.error ?? 'Unknown error') report(msg, e.error?.stack ?? `at ${e.filename}:${e.lineno}:${e.colno}`) } const onRejection = (e: PromiseRejectionEvent) => { const reason = e.reason const msg = reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : 'Unhandled promise rejection' report(msg, reason instanceof Error ? (reason.stack ?? '') : '') } window.addEventListener('error', onError) window.addEventListener('unhandledrejection', onRejection) return () => { window.removeEventListener('error', onError) window.removeEventListener('unhandledrejection', onRejection) } } // ─── console.warn / console.error ──────────────────────────────────────────── function stringifyArgs(args: unknown[]): string { return args .map((a) => { let s: string if (typeof a === 'string') s = a else if (a instanceof Error) s = a.message else { try { s = JSON.stringify(a) } catch { s = String(a) } } return truncate(s, ARG_MAX) }) .join(' ') } export function installConsoleCapture(): () => void { if (typeof window === 'undefined') return () => {} const report = (level: 'warn' | 'error', args: unknown[]) => { try { const message = stringifyArgs(args) const stack = args.find((a): a is Error => a instanceof Error)?.stack if (isNoise(message, stack)) return devtoolsStore.getState().capture({ event_type: level === 'error' ? EventType.ERROR : EventType.WARNING, level: level === 'error' ? EventLevel.ERROR : EventLevel.WARNING, message, stack_trace: stack, ...browserContext(), }) } catch { /* never crash */ } } const origWarn = console.warn.bind(console) const origError = console.error.bind(console) console.warn = (...args: unknown[]) => { origWarn(...args) report('warn', args) } console.error = (...args: unknown[]) => { origError(...args) report('error', args) } return () => { console.warn = origWarn console.error = origError } } // ─── zod-validation-error events (fired by @djangocfg/api clients) ────────── interface ValidationErrorDetail { operation: string path: string method: string error: { message: string } } export function installValidationCapture(): () => void { if (typeof window === 'undefined') return () => {} const handler = (event: Event) => { if (!(event instanceof CustomEvent)) return try { const detail = event.detail as ValidationErrorDetail devtoolsStore.getState().capture({ event_type: EventType.WARNING, level: EventLevel.WARNING, message: truncate( `Zod validation error in ${detail.operation}: ${detail.error?.message ?? 'unknown'}`, MSG_MAX, ), http_method: detail.method, http_url: detail.path, extra: { operation: detail.operation, path: detail.path, method: detail.method }, ...browserContext(), }) } catch { /* never crash */ } } window.addEventListener('zod-validation-error', handler) return () => window.removeEventListener('zod-validation-error', handler) }