@nexussdk/tracker 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +26 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +355 -0
- package/dist/index.d.ts +355 -0
- package/dist/index.global.js +9 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +34 -0
- package/src/breadcrumbs.ts +156 -0
- package/src/client.ts +289 -0
- package/src/fingerprint.ts +107 -0
- package/src/index.ts +16 -0
- package/src/listeners.ts +49 -0
- package/src/sanitizer.ts +107 -0
- package/src/transport.ts +137 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +19 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sanitizer.ts","../src/fingerprint.ts","../src/breadcrumbs.ts","../src/transport.ts","../src/listeners.ts","../src/client.ts"],"names":["SENSITIVE_KEY_PATTERN","CREDIT_CARD_PATTERN","EMAIL_PATTERN","SENSITIVE_QUERY_PARAM_PATTERN","sanitizeString","value","sanitizeObject","depth","item","sanitized","key","val","sanitizeUrl","url","parseStackTrace","stack","frames","lines","line","trimmed","v8Match","geckoMatch","computeFingerprint","errorType","errorMessage","topFrame","fileName","lineNumber","signature","hash","i","BreadcrumbManager","maxBreadcrumbs","RingBuffer","breadcrumb","sanitizedData","evt","target","description","el","parts","original","args","Transport","options","payload","body","safeStringify","blob","attempt","response","delay","computeBackoffMs","r","attachGlobalListeners","client","errorHandler","event","error","rejectionHandler","NexusTrackerClient","resolveApiKey","baseUrl","resolveBaseUrl","extra","normalized","stackFrames","fingerprint","existing","finalPayload","timer","entry","aggregated","user","err","isBrowser"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @fileoverview PII sanitization engine for safe error payload transmission.\n * Recursively redacts sensitive fields before any data leaves the client device.\n * @module @nexus/sdk-tracker/sanitizer\n */\n\n/** Maximum object recursion depth to prevent stack overflows on deep structures. */\nconst MAX_DEPTH = 5;\n\n/**\n * Regex matching sensitive key names that should be redacted.\n * Matches exact key names (case-insensitive) for password, token, secret, etc.\n */\nconst SENSITIVE_KEY_PATTERN =\n /^(password|passwd|token|secret|authorization|bearer|auth|credit_?card|cvv|cvc|ssn|api_?key|access_?token|refresh_?token)$/i;\n\n/**\n * Regex matching credit card number patterns (13-16 digit sequences).\n * Covers common formats with spaces or dashes between groups.\n */\nconst CREDIT_CARD_PATTERN = /\\b(?:\\d[ -]*?){13,16}\\b/g;\n\n/**\n * Regex matching email addresses in string values.\n * Only the domain portion is retained for limited diagnostic context.\n */\nconst EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g;\n\n/**\n * Regex matching URL query parameters containing sensitive tokens.\n * Strips values for: token, auth, key, secret, password, api_key.\n */\nconst SENSITIVE_QUERY_PARAM_PATTERN =\n /([?&](token|auth|key|secret|password|api_key|access_token|refresh_token)=)[^&]*/gi;\n\n/**\n * Sanitizes a string value by removing credit card numbers and email addresses.\n *\n * @param value - The string to sanitize.\n * @returns Sanitized string with sensitive patterns replaced.\n */\nfunction sanitizeString(value: string): string {\n return value\n .replace(CREDIT_CARD_PATTERN, '[CARD_REDACTED]')\n .replace(EMAIL_PATTERN, '[EMAIL_REDACTED]')\n .replace(SENSITIVE_QUERY_PARAM_PATTERN, '$1[REDACTED]');\n}\n\n/**\n * Recursively sanitizes an object, redacting sensitive key-value pairs.\n * Handles nested objects, arrays, and string values.\n *\n * @param value - The value to sanitize (any type).\n * @param depth - Current recursion depth (internal, starts at 0).\n * @returns A sanitized deep copy of the input.\n *\n * @example\n * const payload = {\n * user: { email: 'john@example.com', password: 'secret123' },\n * token: 'Bearer abc123',\n * creditCard: '4111 1111 1111 1111',\n * };\n * const safe = sanitizeObject(payload);\n * // { user: { email: '[EMAIL_REDACTED]', password: '[REDACTED]' },\n * // token: '[REDACTED]', creditCard: '[REDACTED]' }\n */\nexport function sanitizeObject(value: unknown, depth = 0): unknown {\n if (depth > MAX_DEPTH) return '[MaxDepthExceeded]';\n\n if (value === null || value === undefined) return value;\n\n if (typeof value === 'string') {\n return sanitizeString(value);\n }\n\n if (typeof value !== 'object') {\n return value; // number, boolean, etc.\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => sanitizeObject(item, depth + 1));\n }\n\n const sanitized: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n if (SENSITIVE_KEY_PATTERN.test(key)) {\n sanitized[key] = '[REDACTED]';\n } else {\n sanitized[key] = sanitizeObject(val, depth + 1);\n }\n }\n return sanitized;\n}\n\n/**\n * Sanitizes a URL by stripping sensitive query parameters.\n *\n * @param url - URL string to sanitize.\n * @returns URL with sensitive query values replaced with [REDACTED].\n *\n * @example\n * sanitizeUrl('https://app.com/auth?token=abc123&redirect=/home');\n * // 'https://app.com/auth?token=[REDACTED]&redirect=/home'\n */\nexport function sanitizeUrl(url: string): string {\n return url.replace(SENSITIVE_QUERY_PARAM_PATTERN, '$1[REDACTED]');\n}\n","/**\n * @fileoverview Error fingerprinting and stack trace parsing utilities.\n * Generates deterministic SHA-256-based fingerprints for error deduplication.\n * @module @nexus/sdk-tracker/fingerprint\n */\n\nimport type { StackFrame } from '@nexussdk/contracts';\n\n/**\n * Parses a JavaScript error stack string into structured StackFrame objects.\n * Supports V8 (Chrome/Node), SpiderMonkey (Firefox), and JavaScriptCore (Safari) formats.\n *\n * @param stack - Raw stack trace string from an Error object.\n * @returns Array of parsed {@link StackFrame} objects (innermost first).\n *\n * @example\n * const frames = parseStackTrace(new TypeError('test').stack);\n * // [{ functionName: 'processPayment', fileName: 'chunk.js', lineNumber: 1, columnNumber: 400 }]\n */\nexport function parseStackTrace(stack?: string): StackFrame[] {\n if (!stack) return [];\n\n const frames: StackFrame[] = [];\n const lines = stack.split('\\n');\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // V8 format: \" at FunctionName (file.js:line:col)\"\n // V8 anonymous: \" at file.js:line:col\"\n const v8Match =\n trimmed.match(/^at\\s+(.+?)\\s+\\((.+?):(\\d+):(\\d+)\\)$/) ||\n trimmed.match(/^at\\s+(.+?):(\\d+):(\\d+)$/) ||\n trimmed.match(/^at\\s+\\((.+?):(\\d+):(\\d+)\\)$/);\n\n if (v8Match) {\n if (v8Match.length === 5) {\n // Named function\n frames.push({\n functionName: v8Match[1] ?? '<anonymous>',\n fileName: v8Match[2] ?? '<unknown>',\n lineNumber: parseInt(v8Match[3] ?? '0', 10),\n columnNumber: parseInt(v8Match[4] ?? '0', 10),\n });\n } else if (v8Match.length === 4) {\n // Anonymous or \"at (file:line:col)\"\n frames.push({\n functionName: '<anonymous>',\n fileName: v8Match[1] ?? '<unknown>',\n lineNumber: parseInt(v8Match[2] ?? '0', 10),\n columnNumber: parseInt(v8Match[3] ?? '0', 10),\n });\n }\n continue;\n }\n\n // Firefox/Safari format: \"functionName@file.js:line:col\"\n const geckoMatch = trimmed.match(/^(.+?)@(.+?):(\\d+):(\\d+)$/);\n if (geckoMatch) {\n frames.push({\n functionName: geckoMatch[1] ?? '<anonymous>',\n fileName: geckoMatch[2] ?? '<unknown>',\n lineNumber: parseInt(geckoMatch[3] ?? '0', 10),\n columnNumber: parseInt(geckoMatch[4] ?? '0', 10),\n });\n }\n }\n\n return frames;\n}\n\n/**\n * Computes a fast non-cryptographic fingerprint string for error deduplication.\n * Uses a djb2-style hash over the signature components to avoid SubtleCrypto async API.\n *\n * Format: hash(errorType + \":\" + errorMessage + \":\" + topFileName + \":\" + topLineNumber)\n *\n * @param errorType - JavaScript error type (e.g. \"TypeError\").\n * @param errorMessage - Primary error message.\n * @param topFrame - Innermost (first) stack frame, or undefined if stack is empty.\n * @returns Hex-like fingerprint string for deduplication grouping.\n *\n * @example\n * const fp = computeFingerprint(\n * 'TypeError',\n * \"Cannot read properties of undefined (reading 'map')\",\n * { functionName: 'render', fileName: 'chunk.js', lineNumber: 1, columnNumber: 400 }\n * );\n * // 'fp_a3f9b2c1d4...'\n */\nexport function computeFingerprint(\n errorType: string,\n errorMessage: string,\n topFrame?: StackFrame,\n): string {\n const fileName = topFrame?.fileName ?? 'unknown';\n const lineNumber = topFrame?.lineNumber ?? 0;\n const signature = `${errorType}:${errorMessage}:${fileName}:${lineNumber}`;\n\n // djb2 hash algorithm — fast, no async, no external deps\n let hash = 5381;\n for (let i = 0; i < signature.length; i++) {\n hash = ((hash << 5) + hash + signature.charCodeAt(i)) >>> 0;\n }\n\n return `fp_${hash.toString(16).padStart(8, '0')}`;\n}\n","/**\n * @fileoverview Breadcrumb ring buffer manager for recording user activity trails.\n * Automatically captures DOM clicks, navigation events, and console calls.\n * @module @nexus/sdk-tracker/breadcrumbs\n */\n\nimport type { Breadcrumb, BreadcrumbCategory } from '@nexussdk/contracts';\nimport { RingBuffer } from '@nexussdk/core';\nimport { sanitizeObject, sanitizeUrl } from './sanitizer.js';\n\n/**\n * Manages the breadcrumb ring buffer and attaches automatic DOM/navigation listeners.\n *\n * @example\n * const manager = new BreadcrumbManager(20);\n * manager.attachListeners();\n * manager.push({ category: 'custom', message: 'User entered checkout flow', level: 'info' });\n * const trail = manager.getAll(); // Array of last 20 breadcrumbs\n */\nexport class BreadcrumbManager {\n private readonly buffer: RingBuffer<Breadcrumb>;\n private listenersAttached = false;\n private readonly clickHandler: (evt: MouseEvent) => void;\n private readonly popStateHandler: () => void;\n\n constructor(maxBreadcrumbs: number) {\n this.buffer = new RingBuffer<Breadcrumb>(maxBreadcrumbs);\n\n // Bind handlers once to enable proper removeEventListener\n this.clickHandler = this.handleClick.bind(this);\n this.popStateHandler = this.handleNavigation.bind(this);\n }\n\n /**\n * Adds a breadcrumb to the ring buffer, sanitizing any PII in the data.\n *\n * @param breadcrumb - Breadcrumb data (timestamp will be auto-injected).\n *\n * @example\n * manager.push({ category: 'http', message: 'POST /api/checkout', level: 'info', data: { status: 200 } });\n */\n push(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void {\n const sanitizedData = breadcrumb.data\n ? (sanitizeObject(breadcrumb.data) as Record<string, unknown>)\n : undefined;\n\n this.buffer.push({\n ...breadcrumb,\n data: sanitizedData,\n timestamp: Date.now(),\n });\n }\n\n /**\n * Returns all breadcrumbs in chronological order (oldest first).\n *\n * @returns Ordered array of breadcrumbs for inclusion in error payloads.\n */\n getAll(): Breadcrumb[] {\n return this.buffer.toArray();\n }\n\n /**\n * Clears all stored breadcrumbs.\n */\n clear(): void {\n this.buffer.clear();\n }\n\n /**\n * Attaches automatic event listeners for DOM clicks and navigation changes.\n * Safe to call multiple times — idempotent.\n *\n * @example\n * manager.attachListeners(); // called during SDK init\n */\n attachListeners(): void {\n if (this.listenersAttached || typeof window === 'undefined') return;\n this.listenersAttached = true;\n\n // DOM click breadcrumbs\n document.addEventListener('click', this.clickHandler, { capture: true, passive: true });\n\n // Browser navigation breadcrumbs (popstate = back/forward)\n window.addEventListener('popstate', this.popStateHandler, { passive: true });\n\n // Intercept console.error for breadcrumb recording\n this.interceptConsoleError();\n }\n\n /**\n * Removes all attached event listeners.\n *\n * @example\n * manager.detachListeners(); // called during SDK destroy\n */\n detachListeners(): void {\n if (!this.listenersAttached || typeof window === 'undefined') return;\n document.removeEventListener('click', this.clickHandler, { capture: true });\n window.removeEventListener('popstate', this.popStateHandler);\n this.listenersAttached = false;\n }\n\n private handleClick(evt: MouseEvent): void {\n const target = evt.target as HTMLElement | null;\n if (!target) return;\n\n // Mask password fields and data-nexus-mask elements\n if (\n target instanceof HTMLInputElement &&\n (target.type === 'password' || target.hasAttribute('data-nexus-mask'))\n ) {\n return; // Skip masked fields entirely\n }\n\n const description = this.describeElement(target);\n this.push({\n category: 'ui.click' as BreadcrumbCategory,\n message: `Clicked ${description}`,\n level: 'info',\n data: {\n elementTag: target.tagName.toLowerCase(),\n elementId: target.id || undefined,\n elementClass: target.className || undefined,\n },\n });\n }\n\n private handleNavigation(): void {\n this.push({\n category: 'navigation' as BreadcrumbCategory,\n message: `Navigated to ${sanitizeUrl(window.location.href)}`,\n level: 'info',\n data: { url: sanitizeUrl(window.location.href) },\n });\n }\n\n private describeElement(el: HTMLElement): string {\n const parts: string[] = [el.tagName.toLowerCase()];\n if (el.id) parts.push(`#${el.id}`);\n if (el.getAttribute('aria-label')) parts.push(`[aria-label=\"${el.getAttribute('aria-label')}\"]`);\n return parts.join('');\n }\n\n private interceptConsoleError(): void {\n const original = console.error.bind(console);\n console.error = (...args: unknown[]) => {\n this.push({\n category: 'console' as BreadcrumbCategory,\n message: args.map(String).join(' ').substring(0, 500),\n level: 'error',\n });\n original(...args);\n };\n }\n}\n","/**\n * @fileoverview Hybrid transport dispatcher for error payload delivery.\n * Prefers fetch with keepalive; falls back to navigator.sendBeacon during page unload.\n * @module @nexus/sdk-tracker/transport\n */\n\nimport type { ErrorEventPayload } from '@nexussdk/contracts';\nimport { safeStringify, computeBackoffMs } from '@nexussdk/core';\n\n/**\n * Options for configuring the transport dispatcher.\n */\nexport interface TransportOptions {\n /** Go-Gin ingestion endpoint URL. */\n endpoint: string;\n /** Public API key for Authorization header. */\n apiKey: string;\n /** Maximum retry attempts on network failures. Defaults to 2. */\n maxRetries?: number;\n}\n\n/**\n * Hybrid transport dispatcher that intelligently selects the delivery mechanism:\n * - **Normal execution**: `fetch(url, { keepalive: true })` with retry\n * - **Page teardown** (`visibilityState === 'hidden'` or `pagehide`): `navigator.sendBeacon`\n *\n * @example\n * const transport = new Transport({\n * endpoint: 'http://localhost:8080/api/v1/telemetry/errors',\n * apiKey: 'pk_live_...',\n * });\n * transport.send(errorPayload);\n */\nexport class Transport {\n private readonly options: TransportOptions;\n private readonly maxRetries: number;\n private isPageHiding = false;\n\n constructor(options: TransportOptions) {\n this.options = options;\n this.maxRetries = options.maxRetries ?? 2;\n\n // Detect page teardown events to switch to sendBeacon\n if (typeof document !== 'undefined') {\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') {\n this.isPageHiding = true;\n }\n });\n }\n if (typeof window !== 'undefined') {\n window.addEventListener('pagehide', () => {\n this.isPageHiding = true;\n });\n }\n }\n\n /**\n * Dispatches an error payload to the Go-Gin ingestion endpoint.\n * Automatically selects fetch or sendBeacon based on page lifecycle state.\n *\n * @param payload - The sanitized {@link ErrorEventPayload} to transmit.\n *\n * @example\n * transport.send({\n * fingerprint: 'fp_abc123',\n * errorType: 'TypeError',\n * errorMessage: \"Cannot read property 'map' of undefined\",\n * // ...\n * });\n */\n send(payload: ErrorEventPayload): void {\n const body = safeStringify(payload);\n\n // Use sendBeacon during page teardown — prevents cancelled fetch requests\n if (\n this.isPageHiding &&\n typeof navigator !== 'undefined' &&\n typeof navigator.sendBeacon === 'function'\n ) {\n const blob = new Blob([body], { type: 'application/json' });\n navigator.sendBeacon(this.options.endpoint, blob);\n return;\n }\n\n // Normal execution: fetch with keepalive and exponential backoff retry\n void this.sendWithRetry(body, 0);\n }\n\n /**\n * Forces immediate flush of all pending events using sendBeacon.\n * Called during manual flush or SDK destroy lifecycle.\n *\n * @param payload - The error payload to flush.\n * @returns Promise that resolves when the beacon is dispatched.\n */\n async flush(payload: ErrorEventPayload): Promise<void> {\n const body = safeStringify(payload);\n if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {\n const blob = new Blob([body], { type: 'application/json' });\n navigator.sendBeacon(this.options.endpoint, blob);\n return;\n }\n await this.sendWithRetry(body, 0);\n }\n\n private async sendWithRetry(body: string, attempt: number): Promise<void> {\n try {\n const response = await fetch(this.options.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.options.apiKey}`,\n },\n body,\n keepalive: true,\n });\n\n if (response.ok) return;\n\n // Retry on 5xx\n if (response.status >= 500 && attempt < this.maxRetries) {\n const delay = computeBackoffMs(attempt, 1000, 30_000);\n await new Promise((r) => setTimeout(r, delay));\n await this.sendWithRetry(body, attempt + 1);\n }\n } catch {\n // Network failure — retry with backoff\n if (attempt < this.maxRetries) {\n const delay = computeBackoffMs(attempt, 1000, 30_000);\n await new Promise((r) => setTimeout(r, delay));\n await this.sendWithRetry(body, attempt + 1);\n }\n // All retries exhausted — silently drop to prevent host app instability\n }\n }\n}\n","/**\n * @fileoverview Global error and unhandled rejection listeners.\n * Attaches window-level hooks without interfering with native browser behavior.\n * @module @nexus/sdk-tracker/listeners\n */\n\nimport type { NexusTrackerClient } from './client.js';\n\n/**\n * Attaches global error event listeners to the browser window.\n * Captures:\n * - `window.onerror` — synchronous uncaught exceptions\n * - `window.onunhandledrejection` — unhandled Promise rejections\n *\n * CRITICAL: Neither handler calls `event.preventDefault()`.\n * Native browser behavior (console.error, DevTools display) is preserved.\n *\n * @param client - The NexusTrackerClient instance to forward errors to.\n * @returns Cleanup function that removes all attached listeners.\n *\n * @example\n * const cleanup = attachGlobalListeners(trackerClient);\n * // On SDK destroy:\n * cleanup();\n */\nexport function attachGlobalListeners(client: NexusTrackerClient): () => void {\n if (typeof window === 'undefined') {\n return () => void 0; // No-op in SSR contexts\n }\n\n const errorHandler = (event: ErrorEvent): void => {\n // Do NOT call event.preventDefault() — preserve native browser behavior\n const error = event.error instanceof Error ? event.error : new Error(event.message);\n client.captureError(error);\n };\n\n const rejectionHandler = (event: PromiseRejectionEvent): void => {\n // Do NOT call event.preventDefault()\n client.captureError(event.reason);\n };\n\n window.addEventListener('error', errorHandler);\n window.addEventListener('unhandledrejection', rejectionHandler);\n\n return () => {\n window.removeEventListener('error', errorHandler);\n window.removeEventListener('unhandledrejection', rejectionHandler);\n };\n}\n","/**\n * @fileoverview NexusTrackerClient — Full-featured crash ingestion and telemetry SDK.\n * Automated global error capture, PII sanitization, deduplication, and hybrid transport.\n * @module @nexus/sdk-tracker/client\n */\n\nimport type { Breadcrumb, DeviceContext, ErrorEventPayload, UserContext } from '@nexussdk/contracts';\nimport { resolveApiKey, resolveBaseUrl } from '@nexussdk/core';\nimport { sanitizeObject } from './sanitizer.js';\nimport { computeFingerprint, parseStackTrace } from './fingerprint.js';\nimport { BreadcrumbManager } from './breadcrumbs.js';\nimport { Transport } from './transport.js';\nimport { attachGlobalListeners } from './listeners.js';\n\n/**\n * Options for initializing the NexusTrackerClient.\n *\n * @example\n * const tracker = new NexusTrackerClient({\n * apiKey: 'pk_live_...',\n * environment: 'production',\n * autoCapture: true,\n * maxBreadcrumbs: 20,\n * });\n */\nexport interface NexusTrackerOptions {\n /**\n * Public API Key ('pk_live_...' or 'pk_test_...').\n * If omitted, resolved automatically via env variables.\n */\n apiKey?: string;\n /**\n * Centralized Go-Gin Ingestion URL. Defaults to 'https://api.nexus.dev'.\n */\n baseUrl?: string;\n /**\n * Target deployment environment ('production' | 'staging' | 'development').\n */\n environment?: string;\n /**\n * Max breadcrumbs retained in ring buffer. Defaults to 20 (max 50).\n */\n maxBreadcrumbs?: number;\n /**\n * Global tags attached to every captured telemetry event.\n */\n tags?: Record<string, string>;\n /**\n * Toggle automated capturing of uncaught exceptions. Defaults to true.\n */\n autoCapture?: boolean;\n /**\n * Callback hook to inspect, mutate, or drop an event before dispatch.\n * Return null to drop the event completely.\n */\n beforeSend?: (event: ErrorEventPayload) => ErrorEventPayload | null;\n}\n\n/** Internal deduplication entry. */\ninterface DedupeEntry {\n timer: ReturnType<typeof setTimeout>;\n count: number;\n lastPayload: ErrorEventPayload;\n}\n\n/**\n * Public interface contract for NexusTrackerClient.\n */\nexport interface INexusTrackerClient {\n /**\n * Manually captures an exception or custom error instance.\n *\n * @param error - Error object, string message, or unknown rejection value.\n * @param extra - Optional custom metadata tags.\n *\n * @example\n * tracker.captureError(new TypeError('Payment failed'), { checkoutStep: 'payment' });\n */\n captureError(error: unknown, extra?: Record<string, unknown>): void;\n\n /**\n * Records a user activity step into the chronological breadcrumb ring buffer.\n *\n * @param breadcrumb - Breadcrumb data without timestamp (auto-injected).\n *\n * @example\n * tracker.addBreadcrumb({ category: 'navigation', message: 'Navigated to /checkout', level: 'info' });\n */\n addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void;\n\n /**\n * Attaches end-user context to subsequent error payloads.\n *\n * @param user - User context or null to clear.\n *\n * @example\n * tracker.setUser({ id: 'usr_12345', email: 'john@example.com' });\n */\n setUser(user: UserContext | null): void;\n\n /**\n * Dynamically sets or updates a persistent search tag.\n *\n * @param key - Tag key name.\n * @param value - Tag value string.\n *\n * @example\n * tracker.setTag('app_version', '2.4.1');\n */\n setTag(key: string, value: string): void;\n\n /**\n * Flushes any buffered events immediately via navigator.sendBeacon or fetch.\n *\n * @returns Promise that resolves when all events are dispatched.\n *\n * @example\n * await tracker.flush();\n */\n flush(): Promise<void>;\n\n /**\n * Detaches global window listeners and clears in-memory ring buffers.\n *\n * @example\n * tracker.destroy();\n */\n destroy(): void;\n}\n\n/**\n * NexusTrackerClient — resilient browser crash ingestion agent.\n *\n * @implements {INexusTrackerClient}\n *\n * @example\n * const tracker = new NexusTrackerClient({ apiKey: 'pk_live_...' });\n * tracker.captureError(new Error('Checkout failed'));\n */\nexport class NexusTrackerClient implements INexusTrackerClient {\n private readonly apiKey: string;\n private readonly environment: string;\n private readonly tags: Record<string, string>;\n private readonly beforeSend?: (event: ErrorEventPayload) => ErrorEventPayload | null;\n private readonly breadcrumbManager: BreadcrumbManager;\n private readonly transport: Transport;\n private userContext?: UserContext;\n private readonly dedupeMap = new Map<string, DedupeEntry>();\n private cleanupListeners?: () => void;\n\n constructor(options: NexusTrackerOptions = {}) {\n this.apiKey = resolveApiKey(options.apiKey);\n const baseUrl = resolveBaseUrl(options.baseUrl);\n this.environment = options.environment ?? 'production';\n this.tags = { ...options.tags };\n this.beforeSend = options.beforeSend;\n\n this.breadcrumbManager = new BreadcrumbManager(Math.min(options.maxBreadcrumbs ?? 20, 50));\n this.transport = new Transport({\n endpoint: `${baseUrl}/api/v1/telemetry/errors`,\n apiKey: this.apiKey,\n });\n\n if (options.autoCapture !== false && typeof window !== 'undefined') {\n this.breadcrumbManager.attachListeners();\n this.cleanupListeners = attachGlobalListeners(this);\n }\n }\n\n /**\n * Captures an error exception, generates fingerprint, scrubs PII, and enqueues transmission.\n */\n public captureError(error: unknown, extra?: Record<string, unknown>): void {\n const normalized = this.normalizeError(error);\n const stackFrames = parseStackTrace(normalized.stack);\n const fingerprint = computeFingerprint(normalized.type, normalized.message, stackFrames[0]);\n\n // Client-side 10-second sliding window deduplication\n const existing = this.dedupeMap.get(fingerprint);\n if (existing) {\n existing.count += 1;\n return;\n }\n\n const payload: ErrorEventPayload = {\n fingerprint,\n errorType: normalized.type,\n errorMessage: normalized.message,\n stackTrace: stackFrames,\n breadcrumbs: this.breadcrumbManager.getAll(),\n userContext: this.userContext,\n deviceContext: this.getDeviceContext(),\n tags: { environment: this.environment, ...this.tags, ...(extra as Record<string, string> | undefined) },\n occurrenceCount: 1,\n clientTimestamp: Date.now(),\n };\n\n // Apply PII sanitization before transmission\n const sanitized = sanitizeObject(payload) as ErrorEventPayload;\n const finalPayload = this.beforeSend ? this.beforeSend(sanitized) : sanitized;\n if (!finalPayload) return;\n\n this.transport.send(finalPayload);\n\n // Track duplicate window — send aggregated event after 10 seconds\n const timer = setTimeout(() => {\n const entry = this.dedupeMap.get(fingerprint);\n if (entry && entry.count > 1) {\n const aggregated = { ...entry.lastPayload, occurrenceCount: entry.count, clientTimestamp: Date.now() };\n this.transport.send(aggregated);\n }\n this.dedupeMap.delete(fingerprint);\n }, 10_000);\n\n this.dedupeMap.set(fingerprint, { timer, count: 1, lastPayload: finalPayload });\n }\n\n /**\n * Records a contextual breadcrumb in the ring buffer.\n */\n public addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void {\n this.breadcrumbManager.push(breadcrumb);\n }\n\n /**\n * Sets or clears the active user context for error telemetry.\n */\n public setUser(user: UserContext | null): void {\n this.userContext = user ?? undefined;\n }\n\n /**\n * Sets a custom tag associated with error events.\n */\n public setTag(key: string, value: string): void {\n this.tags[key] = value;\n }\n\n /**\n * Flushes all pending deduplication queues and dispatches queued payloads immediately.\n */\n public async flush(): Promise<void> {\n // Flush all pending deduplication timers immediately\n for (const [fingerprint, entry] of this.dedupeMap.entries()) {\n clearTimeout(entry.timer);\n if (entry.count > 0) {\n const aggregated = { ...entry.lastPayload, occurrenceCount: entry.count, clientTimestamp: Date.now() };\n await this.transport.flush(aggregated);\n }\n this.dedupeMap.delete(fingerprint);\n }\n }\n\n /**\n * Tears down global event listeners, flushes queues, and releases resources.\n */\n public destroy(): void {\n this.cleanupListeners?.();\n this.breadcrumbManager.detachListeners();\n this.breadcrumbManager.clear();\n this.dedupeMap.forEach((entry) => clearTimeout(entry.timer));\n this.dedupeMap.clear();\n }\n\n private normalizeError(err: unknown): { type: string; message: string; stack?: string } {\n if (err instanceof Error) {\n return { type: err.name || 'Error', message: err.message, stack: err.stack };\n }\n if (typeof err === 'string') {\n return { type: 'UnhandledException', message: err };\n }\n // Unknown rejection value (non-Error thrown)\n return { type: 'NonErrorRejection', message: String(err) };\n }\n\n private getDeviceContext(): DeviceContext {\n const isBrowser = typeof window !== 'undefined' && typeof navigator !== 'undefined';\n return {\n userAgent: isBrowser ? navigator.userAgent : 'Node/SSR',\n currentUrl: isBrowser ? window.location.href : '',\n viewport: isBrowser ? `${window.innerWidth}x${window.innerHeight}` : undefined,\n timezone: Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone,\n networkStatus:\n isBrowser && 'connection' in navigator\n ? ((navigator as { connection?: { effectiveType?: string } }).connection?.effectiveType ?? 'unknown')\n : undefined,\n };\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexussdk/tracker",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"description": "Ultra-resilient client crash ingestion and telemetry SDK with automated PII sanitization",
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@nexussdk/contracts": "0.0.1",
|
|
21
|
+
"@nexussdk/core": "0.0.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.0.2",
|
|
25
|
+
"typescript": "^5.4.5",
|
|
26
|
+
"rimraf": "^5.0.5"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"dev": "tsup --watch",
|
|
31
|
+
"lint": "tsc --noEmit",
|
|
32
|
+
"clean": "rimraf dist"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Breadcrumb ring buffer manager for recording user activity trails.
|
|
3
|
+
* Automatically captures DOM clicks, navigation events, and console calls.
|
|
4
|
+
* @module @nexus/sdk-tracker/breadcrumbs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Breadcrumb, BreadcrumbCategory } from '@nexussdk/contracts';
|
|
8
|
+
import { RingBuffer } from '@nexussdk/core';
|
|
9
|
+
import { sanitizeObject, sanitizeUrl } from './sanitizer.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Manages the breadcrumb ring buffer and attaches automatic DOM/navigation listeners.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const manager = new BreadcrumbManager(20);
|
|
16
|
+
* manager.attachListeners();
|
|
17
|
+
* manager.push({ category: 'custom', message: 'User entered checkout flow', level: 'info' });
|
|
18
|
+
* const trail = manager.getAll(); // Array of last 20 breadcrumbs
|
|
19
|
+
*/
|
|
20
|
+
export class BreadcrumbManager {
|
|
21
|
+
private readonly buffer: RingBuffer<Breadcrumb>;
|
|
22
|
+
private listenersAttached = false;
|
|
23
|
+
private readonly clickHandler: (evt: MouseEvent) => void;
|
|
24
|
+
private readonly popStateHandler: () => void;
|
|
25
|
+
|
|
26
|
+
constructor(maxBreadcrumbs: number) {
|
|
27
|
+
this.buffer = new RingBuffer<Breadcrumb>(maxBreadcrumbs);
|
|
28
|
+
|
|
29
|
+
// Bind handlers once to enable proper removeEventListener
|
|
30
|
+
this.clickHandler = this.handleClick.bind(this);
|
|
31
|
+
this.popStateHandler = this.handleNavigation.bind(this);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Adds a breadcrumb to the ring buffer, sanitizing any PII in the data.
|
|
36
|
+
*
|
|
37
|
+
* @param breadcrumb - Breadcrumb data (timestamp will be auto-injected).
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* manager.push({ category: 'http', message: 'POST /api/checkout', level: 'info', data: { status: 200 } });
|
|
41
|
+
*/
|
|
42
|
+
push(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void {
|
|
43
|
+
const sanitizedData = breadcrumb.data
|
|
44
|
+
? (sanitizeObject(breadcrumb.data) as Record<string, unknown>)
|
|
45
|
+
: undefined;
|
|
46
|
+
|
|
47
|
+
this.buffer.push({
|
|
48
|
+
...breadcrumb,
|
|
49
|
+
data: sanitizedData,
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns all breadcrumbs in chronological order (oldest first).
|
|
56
|
+
*
|
|
57
|
+
* @returns Ordered array of breadcrumbs for inclusion in error payloads.
|
|
58
|
+
*/
|
|
59
|
+
getAll(): Breadcrumb[] {
|
|
60
|
+
return this.buffer.toArray();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Clears all stored breadcrumbs.
|
|
65
|
+
*/
|
|
66
|
+
clear(): void {
|
|
67
|
+
this.buffer.clear();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Attaches automatic event listeners for DOM clicks and navigation changes.
|
|
72
|
+
* Safe to call multiple times — idempotent.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* manager.attachListeners(); // called during SDK init
|
|
76
|
+
*/
|
|
77
|
+
attachListeners(): void {
|
|
78
|
+
if (this.listenersAttached || typeof window === 'undefined') return;
|
|
79
|
+
this.listenersAttached = true;
|
|
80
|
+
|
|
81
|
+
// DOM click breadcrumbs
|
|
82
|
+
document.addEventListener('click', this.clickHandler, { capture: true, passive: true });
|
|
83
|
+
|
|
84
|
+
// Browser navigation breadcrumbs (popstate = back/forward)
|
|
85
|
+
window.addEventListener('popstate', this.popStateHandler, { passive: true });
|
|
86
|
+
|
|
87
|
+
// Intercept console.error for breadcrumb recording
|
|
88
|
+
this.interceptConsoleError();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Removes all attached event listeners.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* manager.detachListeners(); // called during SDK destroy
|
|
96
|
+
*/
|
|
97
|
+
detachListeners(): void {
|
|
98
|
+
if (!this.listenersAttached || typeof window === 'undefined') return;
|
|
99
|
+
document.removeEventListener('click', this.clickHandler, { capture: true });
|
|
100
|
+
window.removeEventListener('popstate', this.popStateHandler);
|
|
101
|
+
this.listenersAttached = false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private handleClick(evt: MouseEvent): void {
|
|
105
|
+
const target = evt.target as HTMLElement | null;
|
|
106
|
+
if (!target) return;
|
|
107
|
+
|
|
108
|
+
// Mask password fields and data-nexus-mask elements
|
|
109
|
+
if (
|
|
110
|
+
target instanceof HTMLInputElement &&
|
|
111
|
+
(target.type === 'password' || target.hasAttribute('data-nexus-mask'))
|
|
112
|
+
) {
|
|
113
|
+
return; // Skip masked fields entirely
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const description = this.describeElement(target);
|
|
117
|
+
this.push({
|
|
118
|
+
category: 'ui.click' as BreadcrumbCategory,
|
|
119
|
+
message: `Clicked ${description}`,
|
|
120
|
+
level: 'info',
|
|
121
|
+
data: {
|
|
122
|
+
elementTag: target.tagName.toLowerCase(),
|
|
123
|
+
elementId: target.id || undefined,
|
|
124
|
+
elementClass: target.className || undefined,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private handleNavigation(): void {
|
|
130
|
+
this.push({
|
|
131
|
+
category: 'navigation' as BreadcrumbCategory,
|
|
132
|
+
message: `Navigated to ${sanitizeUrl(window.location.href)}`,
|
|
133
|
+
level: 'info',
|
|
134
|
+
data: { url: sanitizeUrl(window.location.href) },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private describeElement(el: HTMLElement): string {
|
|
139
|
+
const parts: string[] = [el.tagName.toLowerCase()];
|
|
140
|
+
if (el.id) parts.push(`#${el.id}`);
|
|
141
|
+
if (el.getAttribute('aria-label')) parts.push(`[aria-label="${el.getAttribute('aria-label')}"]`);
|
|
142
|
+
return parts.join('');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private interceptConsoleError(): void {
|
|
146
|
+
const original = console.error.bind(console);
|
|
147
|
+
console.error = (...args: unknown[]) => {
|
|
148
|
+
this.push({
|
|
149
|
+
category: 'console' as BreadcrumbCategory,
|
|
150
|
+
message: args.map(String).join(' ').substring(0, 500),
|
|
151
|
+
level: 'error',
|
|
152
|
+
});
|
|
153
|
+
original(...args);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview NexusTrackerClient — Full-featured crash ingestion and telemetry SDK.
|
|
3
|
+
* Automated global error capture, PII sanitization, deduplication, and hybrid transport.
|
|
4
|
+
* @module @nexus/sdk-tracker/client
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Breadcrumb, DeviceContext, ErrorEventPayload, UserContext } from '@nexussdk/contracts';
|
|
8
|
+
import { resolveApiKey, resolveBaseUrl } from '@nexussdk/core';
|
|
9
|
+
import { sanitizeObject } from './sanitizer.js';
|
|
10
|
+
import { computeFingerprint, parseStackTrace } from './fingerprint.js';
|
|
11
|
+
import { BreadcrumbManager } from './breadcrumbs.js';
|
|
12
|
+
import { Transport } from './transport.js';
|
|
13
|
+
import { attachGlobalListeners } from './listeners.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Options for initializing the NexusTrackerClient.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const tracker = new NexusTrackerClient({
|
|
20
|
+
* apiKey: 'pk_live_...',
|
|
21
|
+
* environment: 'production',
|
|
22
|
+
* autoCapture: true,
|
|
23
|
+
* maxBreadcrumbs: 20,
|
|
24
|
+
* });
|
|
25
|
+
*/
|
|
26
|
+
export interface NexusTrackerOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Public API Key ('pk_live_...' or 'pk_test_...').
|
|
29
|
+
* If omitted, resolved automatically via env variables.
|
|
30
|
+
*/
|
|
31
|
+
apiKey?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Centralized Go-Gin Ingestion URL. Defaults to 'https://api.nexus.dev'.
|
|
34
|
+
*/
|
|
35
|
+
baseUrl?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Target deployment environment ('production' | 'staging' | 'development').
|
|
38
|
+
*/
|
|
39
|
+
environment?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Max breadcrumbs retained in ring buffer. Defaults to 20 (max 50).
|
|
42
|
+
*/
|
|
43
|
+
maxBreadcrumbs?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Global tags attached to every captured telemetry event.
|
|
46
|
+
*/
|
|
47
|
+
tags?: Record<string, string>;
|
|
48
|
+
/**
|
|
49
|
+
* Toggle automated capturing of uncaught exceptions. Defaults to true.
|
|
50
|
+
*/
|
|
51
|
+
autoCapture?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Callback hook to inspect, mutate, or drop an event before dispatch.
|
|
54
|
+
* Return null to drop the event completely.
|
|
55
|
+
*/
|
|
56
|
+
beforeSend?: (event: ErrorEventPayload) => ErrorEventPayload | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Internal deduplication entry. */
|
|
60
|
+
interface DedupeEntry {
|
|
61
|
+
timer: ReturnType<typeof setTimeout>;
|
|
62
|
+
count: number;
|
|
63
|
+
lastPayload: ErrorEventPayload;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Public interface contract for NexusTrackerClient.
|
|
68
|
+
*/
|
|
69
|
+
export interface INexusTrackerClient {
|
|
70
|
+
/**
|
|
71
|
+
* Manually captures an exception or custom error instance.
|
|
72
|
+
*
|
|
73
|
+
* @param error - Error object, string message, or unknown rejection value.
|
|
74
|
+
* @param extra - Optional custom metadata tags.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* tracker.captureError(new TypeError('Payment failed'), { checkoutStep: 'payment' });
|
|
78
|
+
*/
|
|
79
|
+
captureError(error: unknown, extra?: Record<string, unknown>): void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Records a user activity step into the chronological breadcrumb ring buffer.
|
|
83
|
+
*
|
|
84
|
+
* @param breadcrumb - Breadcrumb data without timestamp (auto-injected).
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* tracker.addBreadcrumb({ category: 'navigation', message: 'Navigated to /checkout', level: 'info' });
|
|
88
|
+
*/
|
|
89
|
+
addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Attaches end-user context to subsequent error payloads.
|
|
93
|
+
*
|
|
94
|
+
* @param user - User context or null to clear.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* tracker.setUser({ id: 'usr_12345', email: 'john@example.com' });
|
|
98
|
+
*/
|
|
99
|
+
setUser(user: UserContext | null): void;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Dynamically sets or updates a persistent search tag.
|
|
103
|
+
*
|
|
104
|
+
* @param key - Tag key name.
|
|
105
|
+
* @param value - Tag value string.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* tracker.setTag('app_version', '2.4.1');
|
|
109
|
+
*/
|
|
110
|
+
setTag(key: string, value: string): void;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Flushes any buffered events immediately via navigator.sendBeacon or fetch.
|
|
114
|
+
*
|
|
115
|
+
* @returns Promise that resolves when all events are dispatched.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* await tracker.flush();
|
|
119
|
+
*/
|
|
120
|
+
flush(): Promise<void>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detaches global window listeners and clears in-memory ring buffers.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* tracker.destroy();
|
|
127
|
+
*/
|
|
128
|
+
destroy(): void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* NexusTrackerClient — resilient browser crash ingestion agent.
|
|
133
|
+
*
|
|
134
|
+
* @implements {INexusTrackerClient}
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* const tracker = new NexusTrackerClient({ apiKey: 'pk_live_...' });
|
|
138
|
+
* tracker.captureError(new Error('Checkout failed'));
|
|
139
|
+
*/
|
|
140
|
+
export class NexusTrackerClient implements INexusTrackerClient {
|
|
141
|
+
private readonly apiKey: string;
|
|
142
|
+
private readonly environment: string;
|
|
143
|
+
private readonly tags: Record<string, string>;
|
|
144
|
+
private readonly beforeSend?: (event: ErrorEventPayload) => ErrorEventPayload | null;
|
|
145
|
+
private readonly breadcrumbManager: BreadcrumbManager;
|
|
146
|
+
private readonly transport: Transport;
|
|
147
|
+
private userContext?: UserContext;
|
|
148
|
+
private readonly dedupeMap = new Map<string, DedupeEntry>();
|
|
149
|
+
private cleanupListeners?: () => void;
|
|
150
|
+
|
|
151
|
+
constructor(options: NexusTrackerOptions = {}) {
|
|
152
|
+
this.apiKey = resolveApiKey(options.apiKey);
|
|
153
|
+
const baseUrl = resolveBaseUrl(options.baseUrl);
|
|
154
|
+
this.environment = options.environment ?? 'production';
|
|
155
|
+
this.tags = { ...options.tags };
|
|
156
|
+
this.beforeSend = options.beforeSend;
|
|
157
|
+
|
|
158
|
+
this.breadcrumbManager = new BreadcrumbManager(Math.min(options.maxBreadcrumbs ?? 20, 50));
|
|
159
|
+
this.transport = new Transport({
|
|
160
|
+
endpoint: `${baseUrl}/api/v1/telemetry/errors`,
|
|
161
|
+
apiKey: this.apiKey,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (options.autoCapture !== false && typeof window !== 'undefined') {
|
|
165
|
+
this.breadcrumbManager.attachListeners();
|
|
166
|
+
this.cleanupListeners = attachGlobalListeners(this);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Captures an error exception, generates fingerprint, scrubs PII, and enqueues transmission.
|
|
172
|
+
*/
|
|
173
|
+
public captureError(error: unknown, extra?: Record<string, unknown>): void {
|
|
174
|
+
const normalized = this.normalizeError(error);
|
|
175
|
+
const stackFrames = parseStackTrace(normalized.stack);
|
|
176
|
+
const fingerprint = computeFingerprint(normalized.type, normalized.message, stackFrames[0]);
|
|
177
|
+
|
|
178
|
+
// Client-side 10-second sliding window deduplication
|
|
179
|
+
const existing = this.dedupeMap.get(fingerprint);
|
|
180
|
+
if (existing) {
|
|
181
|
+
existing.count += 1;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const payload: ErrorEventPayload = {
|
|
186
|
+
fingerprint,
|
|
187
|
+
errorType: normalized.type,
|
|
188
|
+
errorMessage: normalized.message,
|
|
189
|
+
stackTrace: stackFrames,
|
|
190
|
+
breadcrumbs: this.breadcrumbManager.getAll(),
|
|
191
|
+
userContext: this.userContext,
|
|
192
|
+
deviceContext: this.getDeviceContext(),
|
|
193
|
+
tags: { environment: this.environment, ...this.tags, ...(extra as Record<string, string> | undefined) },
|
|
194
|
+
occurrenceCount: 1,
|
|
195
|
+
clientTimestamp: Date.now(),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Apply PII sanitization before transmission
|
|
199
|
+
const sanitized = sanitizeObject(payload) as ErrorEventPayload;
|
|
200
|
+
const finalPayload = this.beforeSend ? this.beforeSend(sanitized) : sanitized;
|
|
201
|
+
if (!finalPayload) return;
|
|
202
|
+
|
|
203
|
+
this.transport.send(finalPayload);
|
|
204
|
+
|
|
205
|
+
// Track duplicate window — send aggregated event after 10 seconds
|
|
206
|
+
const timer = setTimeout(() => {
|
|
207
|
+
const entry = this.dedupeMap.get(fingerprint);
|
|
208
|
+
if (entry && entry.count > 1) {
|
|
209
|
+
const aggregated = { ...entry.lastPayload, occurrenceCount: entry.count, clientTimestamp: Date.now() };
|
|
210
|
+
this.transport.send(aggregated);
|
|
211
|
+
}
|
|
212
|
+
this.dedupeMap.delete(fingerprint);
|
|
213
|
+
}, 10_000);
|
|
214
|
+
|
|
215
|
+
this.dedupeMap.set(fingerprint, { timer, count: 1, lastPayload: finalPayload });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Records a contextual breadcrumb in the ring buffer.
|
|
220
|
+
*/
|
|
221
|
+
public addBreadcrumb(breadcrumb: Omit<Breadcrumb, 'timestamp'>): void {
|
|
222
|
+
this.breadcrumbManager.push(breadcrumb);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Sets or clears the active user context for error telemetry.
|
|
227
|
+
*/
|
|
228
|
+
public setUser(user: UserContext | null): void {
|
|
229
|
+
this.userContext = user ?? undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Sets a custom tag associated with error events.
|
|
234
|
+
*/
|
|
235
|
+
public setTag(key: string, value: string): void {
|
|
236
|
+
this.tags[key] = value;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Flushes all pending deduplication queues and dispatches queued payloads immediately.
|
|
241
|
+
*/
|
|
242
|
+
public async flush(): Promise<void> {
|
|
243
|
+
// Flush all pending deduplication timers immediately
|
|
244
|
+
for (const [fingerprint, entry] of this.dedupeMap.entries()) {
|
|
245
|
+
clearTimeout(entry.timer);
|
|
246
|
+
if (entry.count > 0) {
|
|
247
|
+
const aggregated = { ...entry.lastPayload, occurrenceCount: entry.count, clientTimestamp: Date.now() };
|
|
248
|
+
await this.transport.flush(aggregated);
|
|
249
|
+
}
|
|
250
|
+
this.dedupeMap.delete(fingerprint);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Tears down global event listeners, flushes queues, and releases resources.
|
|
256
|
+
*/
|
|
257
|
+
public destroy(): void {
|
|
258
|
+
this.cleanupListeners?.();
|
|
259
|
+
this.breadcrumbManager.detachListeners();
|
|
260
|
+
this.breadcrumbManager.clear();
|
|
261
|
+
this.dedupeMap.forEach((entry) => clearTimeout(entry.timer));
|
|
262
|
+
this.dedupeMap.clear();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private normalizeError(err: unknown): { type: string; message: string; stack?: string } {
|
|
266
|
+
if (err instanceof Error) {
|
|
267
|
+
return { type: err.name || 'Error', message: err.message, stack: err.stack };
|
|
268
|
+
}
|
|
269
|
+
if (typeof err === 'string') {
|
|
270
|
+
return { type: 'UnhandledException', message: err };
|
|
271
|
+
}
|
|
272
|
+
// Unknown rejection value (non-Error thrown)
|
|
273
|
+
return { type: 'NonErrorRejection', message: String(err) };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private getDeviceContext(): DeviceContext {
|
|
277
|
+
const isBrowser = typeof window !== 'undefined' && typeof navigator !== 'undefined';
|
|
278
|
+
return {
|
|
279
|
+
userAgent: isBrowser ? navigator.userAgent : 'Node/SSR',
|
|
280
|
+
currentUrl: isBrowser ? window.location.href : '',
|
|
281
|
+
viewport: isBrowser ? `${window.innerWidth}x${window.innerHeight}` : undefined,
|
|
282
|
+
timezone: Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone,
|
|
283
|
+
networkStatus:
|
|
284
|
+
isBrowser && 'connection' in navigator
|
|
285
|
+
? ((navigator as { connection?: { effectiveType?: string } }).connection?.effectiveType ?? 'unknown')
|
|
286
|
+
: undefined,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|