@error-explorer/browser 1.1.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.
@@ -0,0 +1,2818 @@
1
+ /**
2
+ * @error-explorer/browser v1.0.0
3
+ * Error Explorer SDK for Browser
4
+ * https://github.com/error-explorer/error-explorer-sdks
5
+ * (c) 2026 Error Explorer
6
+ * Released under the MIT License
7
+ */
8
+ 'use strict';
9
+
10
+ Object.defineProperty(exports, '__esModule', { value: true });
11
+
12
+ const DEFAULT_ENDPOINT = 'https://error-explorer.com/api/v1/webhook';
13
+ const DEFAULT_CONFIG = {
14
+ environment: 'production',
15
+ release: '',
16
+ autoCapture: {
17
+ errors: true,
18
+ unhandledRejections: true,
19
+ console: true,
20
+ },
21
+ breadcrumbs: {
22
+ enabled: true,
23
+ maxBreadcrumbs: 20,
24
+ clicks: true,
25
+ navigation: true,
26
+ fetch: true,
27
+ xhr: true,
28
+ console: true,
29
+ inputs: false,
30
+ },
31
+ denyUrls: [],
32
+ allowUrls: [],
33
+ ignoreErrors: [],
34
+ maxRetries: 3,
35
+ timeout: 5000,
36
+ offline: true,
37
+ debug: false,
38
+ };
39
+ /**
40
+ * Parse DSN to extract token and endpoint
41
+ * DSN format: https://{token}@{host}/api/v1/webhook
42
+ */
43
+ function parseDsn(dsn) {
44
+ try {
45
+ const url = new URL(dsn);
46
+ const token = url.username;
47
+ if (!token) {
48
+ return null;
49
+ }
50
+ // Remove username from URL to get endpoint
51
+ url.username = '';
52
+ const endpoint = url.toString();
53
+ return { token, endpoint };
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ /**
60
+ * Build endpoint URL from token
61
+ */
62
+ function buildEndpoint(token, baseEndpoint) {
63
+ // If endpoint already contains token placeholder, replace it
64
+ if (baseEndpoint.includes('{token}')) {
65
+ return baseEndpoint.replace('{token}', token);
66
+ }
67
+ // Otherwise append token to endpoint
68
+ const separator = baseEndpoint.endsWith('/') ? '' : '/';
69
+ return `${baseEndpoint}${separator}${token}`;
70
+ }
71
+ /**
72
+ * Validate and resolve configuration
73
+ */
74
+ function resolveConfig(options) {
75
+ let token;
76
+ let endpoint;
77
+ // Parse DSN if provided
78
+ if (options.dsn) {
79
+ const parsed = parseDsn(options.dsn);
80
+ if (!parsed) {
81
+ throw new Error('[ErrorExplorer] Invalid DSN format');
82
+ }
83
+ token = parsed.token;
84
+ endpoint = parsed.endpoint;
85
+ }
86
+ else if (options.token) {
87
+ token = options.token;
88
+ endpoint = buildEndpoint(token, options.endpoint ?? DEFAULT_ENDPOINT);
89
+ }
90
+ else {
91
+ throw new Error('[ErrorExplorer] Either token or dsn is required');
92
+ }
93
+ // Validate token format
94
+ if (!token.startsWith('ee_')) {
95
+ console.warn('[ErrorExplorer] Token should start with "ee_"');
96
+ }
97
+ return {
98
+ token,
99
+ endpoint,
100
+ environment: options.environment ?? DEFAULT_CONFIG.environment,
101
+ release: options.release ?? DEFAULT_CONFIG.release,
102
+ project: options.project,
103
+ autoCapture: {
104
+ ...DEFAULT_CONFIG.autoCapture,
105
+ ...options.autoCapture,
106
+ },
107
+ breadcrumbs: {
108
+ ...DEFAULT_CONFIG.breadcrumbs,
109
+ ...options.breadcrumbs,
110
+ },
111
+ beforeSend: options.beforeSend,
112
+ denyUrls: options.denyUrls ?? DEFAULT_CONFIG.denyUrls,
113
+ allowUrls: options.allowUrls ?? DEFAULT_CONFIG.allowUrls,
114
+ ignoreErrors: options.ignoreErrors ?? DEFAULT_CONFIG.ignoreErrors,
115
+ maxRetries: options.maxRetries ?? DEFAULT_CONFIG.maxRetries,
116
+ timeout: options.timeout ?? DEFAULT_CONFIG.timeout,
117
+ offline: options.offline ?? DEFAULT_CONFIG.offline,
118
+ debug: options.debug ?? DEFAULT_CONFIG.debug,
119
+ hmacSecret: options.hmacSecret,
120
+ };
121
+ }
122
+ /**
123
+ * Check if URL matches any pattern in list
124
+ */
125
+ function matchesPattern(url, patterns) {
126
+ return patterns.some((pattern) => {
127
+ if (typeof pattern === 'string') {
128
+ return url.includes(pattern);
129
+ }
130
+ return pattern.test(url);
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Parse a stack trace string into structured frames
136
+ */
137
+ function parseStackTrace(stack) {
138
+ if (!stack) {
139
+ return [];
140
+ }
141
+ const lines = stack.split('\n');
142
+ const frames = [];
143
+ for (const line of lines) {
144
+ const frame = parseStackLine(line);
145
+ if (frame) {
146
+ frames.push(frame);
147
+ }
148
+ }
149
+ return frames;
150
+ }
151
+ /**
152
+ * Parse a single stack trace line
153
+ * Handles Chrome/V8, Firefox, and Safari formats
154
+ */
155
+ function parseStackLine(line) {
156
+ const trimmed = line.trim();
157
+ // Skip empty lines and "Error:" header
158
+ if (!trimmed || trimmed.startsWith('Error:') || trimmed === 'Error') {
159
+ return null;
160
+ }
161
+ // Chrome/V8 format: " at functionName (filename:line:col)"
162
+ // or " at filename:line:col"
163
+ const chromeMatch = trimmed.match(/^\s*at\s+(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+)|(.+?):(\d+)|(.+?))\)?$/);
164
+ if (chromeMatch) {
165
+ const [, func, file1, line1, col1, file2, line2, file3] = chromeMatch;
166
+ return {
167
+ function: func || '<anonymous>',
168
+ filename: file1 || file2 || file3,
169
+ lineno: line1 ? parseInt(line1, 10) : line2 ? parseInt(line2, 10) : undefined,
170
+ colno: col1 ? parseInt(col1, 10) : undefined,
171
+ in_app: isInApp(file1 || file2 || file3),
172
+ };
173
+ }
174
+ // Firefox format: "functionName@filename:line:col"
175
+ const firefoxMatch = trimmed.match(/^(.+?)@(.+?):(\d+):(\d+)$/);
176
+ if (firefoxMatch) {
177
+ const [, func, file, line, col] = firefoxMatch;
178
+ return {
179
+ function: func || '<anonymous>',
180
+ filename: file,
181
+ lineno: parseInt(line ?? '0', 10),
182
+ colno: parseInt(col ?? '0', 10),
183
+ in_app: isInApp(file),
184
+ };
185
+ }
186
+ // Safari format: "functionName@filename:line:col" or just "filename:line:col"
187
+ const safariMatch = trimmed.match(/^(?:(.+?)@)?(.+?):(\d+)(?::(\d+))?$/);
188
+ if (safariMatch) {
189
+ const [, func, file, line, col] = safariMatch;
190
+ return {
191
+ function: func || '<anonymous>',
192
+ filename: file,
193
+ lineno: parseInt(line ?? '0', 10),
194
+ colno: col ? parseInt(col, 10) : undefined,
195
+ in_app: isInApp(file),
196
+ };
197
+ }
198
+ // If no format matches, return a basic frame
199
+ if (trimmed.length > 0) {
200
+ return {
201
+ function: trimmed,
202
+ in_app: false,
203
+ };
204
+ }
205
+ return null;
206
+ }
207
+ /**
208
+ * Check if a filename is "in app" (not from node_modules, CDN, or browser internals)
209
+ */
210
+ function isInApp(filename) {
211
+ if (!filename) {
212
+ return false;
213
+ }
214
+ // Browser internal
215
+ if (filename.startsWith('<')) {
216
+ return false;
217
+ }
218
+ // Native code
219
+ if (filename === '[native code]' || filename.includes('native code')) {
220
+ return false;
221
+ }
222
+ // Node modules
223
+ if (filename.includes('node_modules')) {
224
+ return false;
225
+ }
226
+ // CDN or external
227
+ const externalPatterns = [
228
+ /cdn\./i,
229
+ /unpkg\.com/i,
230
+ /jsdelivr\.net/i,
231
+ /cdnjs\.cloudflare\.com/i,
232
+ /googleapis\.com/i,
233
+ /gstatic\.com/i,
234
+ ];
235
+ for (const pattern of externalPatterns) {
236
+ if (pattern.test(filename)) {
237
+ return false;
238
+ }
239
+ }
240
+ return true;
241
+ }
242
+ /**
243
+ * Get error name from an Error object or unknown value
244
+ */
245
+ function getErrorName(error) {
246
+ if (error instanceof Error) {
247
+ return error.name || 'Error';
248
+ }
249
+ if (typeof error === 'object' && error !== null) {
250
+ const obj = error;
251
+ if (typeof obj['name'] === 'string') {
252
+ return obj['name'];
253
+ }
254
+ }
255
+ return 'Error';
256
+ }
257
+ /**
258
+ * Get error message from an Error object or unknown value
259
+ */
260
+ function getErrorMessage(error) {
261
+ if (error instanceof Error) {
262
+ return error.message;
263
+ }
264
+ if (typeof error === 'string') {
265
+ return error;
266
+ }
267
+ if (typeof error === 'object' && error !== null) {
268
+ const obj = error;
269
+ if (typeof obj['message'] === 'string') {
270
+ return obj['message'];
271
+ }
272
+ }
273
+ try {
274
+ return String(error);
275
+ }
276
+ catch {
277
+ return 'Unknown error';
278
+ }
279
+ }
280
+ /**
281
+ * Get stack trace from an Error object or unknown value
282
+ */
283
+ function getErrorStack(error) {
284
+ if (error instanceof Error) {
285
+ return error.stack;
286
+ }
287
+ if (typeof error === 'object' && error !== null) {
288
+ const obj = error;
289
+ if (typeof obj['stack'] === 'string') {
290
+ return obj['stack'];
291
+ }
292
+ }
293
+ return undefined;
294
+ }
295
+
296
+ /**
297
+ * Generate a UUID v4
298
+ * Uses crypto.randomUUID() if available, falls back to custom implementation
299
+ */
300
+ function generateUuid() {
301
+ // Use native crypto.randomUUID if available (modern browsers)
302
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
303
+ return crypto.randomUUID();
304
+ }
305
+ // Fallback for older browsers
306
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
307
+ const r = (Math.random() * 16) | 0;
308
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
309
+ return v.toString(16);
310
+ });
311
+ }
312
+ /**
313
+ * Generate a short ID (for session, etc.)
314
+ */
315
+ function generateShortId() {
316
+ return generateUuid().replace(/-/g, '').substring(0, 16);
317
+ }
318
+
319
+ /**
320
+ * Manages breadcrumbs - a trail of events leading up to an error
321
+ */
322
+ class BreadcrumbManager {
323
+ constructor(config) {
324
+ this.breadcrumbs = [];
325
+ this.maxBreadcrumbs = config.breadcrumbs.maxBreadcrumbs;
326
+ }
327
+ /**
328
+ * Add a breadcrumb
329
+ */
330
+ add(breadcrumb) {
331
+ const internal = {
332
+ ...breadcrumb,
333
+ timestamp: breadcrumb.timestamp ?? Date.now(),
334
+ };
335
+ this.breadcrumbs.push(internal);
336
+ // FIFO: keep only the last N breadcrumbs
337
+ while (this.breadcrumbs.length > this.maxBreadcrumbs) {
338
+ this.breadcrumbs.shift();
339
+ }
340
+ }
341
+ /**
342
+ * Get all breadcrumbs
343
+ */
344
+ getAll() {
345
+ return [...this.breadcrumbs];
346
+ }
347
+ /**
348
+ * Clear all breadcrumbs
349
+ */
350
+ clear() {
351
+ this.breadcrumbs = [];
352
+ }
353
+ /**
354
+ * Get the number of breadcrumbs
355
+ */
356
+ get count() {
357
+ return this.breadcrumbs.length;
358
+ }
359
+ }
360
+ // Singleton instance
361
+ let instance$f = null;
362
+ /**
363
+ * Get the singleton BreadcrumbManager instance
364
+ */
365
+ function getBreadcrumbManager() {
366
+ return instance$f;
367
+ }
368
+ /**
369
+ * Initialize the singleton BreadcrumbManager
370
+ */
371
+ function initBreadcrumbManager(config) {
372
+ instance$f = new BreadcrumbManager(config);
373
+ return instance$f;
374
+ }
375
+ /**
376
+ * Reset the singleton (for testing)
377
+ */
378
+ function resetBreadcrumbManager() {
379
+ instance$f = null;
380
+ }
381
+
382
+ /**
383
+ * Maximum depth for object serialization
384
+ */
385
+ const MAX_DEPTH = 10;
386
+ /**
387
+ * Maximum string length
388
+ */
389
+ const MAX_STRING_LENGTH = 1000;
390
+ /**
391
+ * Maximum array length
392
+ */
393
+ const MAX_ARRAY_LENGTH = 100;
394
+ /**
395
+ * Safely serialize a value for JSON, handling circular references and depth limits
396
+ */
397
+ function safeSerialize(value, depth = 0) {
398
+ // Handle primitives
399
+ if (value === null || value === undefined) {
400
+ return value;
401
+ }
402
+ if (typeof value === 'boolean' || typeof value === 'number') {
403
+ return value;
404
+ }
405
+ if (typeof value === 'string') {
406
+ return truncateString(value, MAX_STRING_LENGTH);
407
+ }
408
+ if (typeof value === 'bigint') {
409
+ return value.toString();
410
+ }
411
+ if (typeof value === 'symbol') {
412
+ return value.toString();
413
+ }
414
+ if (typeof value === 'function') {
415
+ return `[Function: ${value.name || 'anonymous'}]`;
416
+ }
417
+ // Depth limit reached
418
+ if (depth >= MAX_DEPTH) {
419
+ return '[Max depth reached]';
420
+ }
421
+ // Handle Error objects
422
+ if (value instanceof Error) {
423
+ return {
424
+ name: value.name,
425
+ message: value.message,
426
+ stack: value.stack,
427
+ };
428
+ }
429
+ // Handle Date objects
430
+ if (value instanceof Date) {
431
+ return value.toISOString();
432
+ }
433
+ // Handle RegExp
434
+ if (value instanceof RegExp) {
435
+ return value.toString();
436
+ }
437
+ // Handle Arrays
438
+ if (Array.isArray(value)) {
439
+ const truncated = value.slice(0, MAX_ARRAY_LENGTH);
440
+ const serialized = truncated.map((item) => safeSerialize(item, depth + 1));
441
+ if (value.length > MAX_ARRAY_LENGTH) {
442
+ serialized.push(`[... ${value.length - MAX_ARRAY_LENGTH} more items]`);
443
+ }
444
+ return serialized;
445
+ }
446
+ // Handle DOM elements
447
+ if (typeof Element !== 'undefined' && value instanceof Element) {
448
+ return describeElement(value);
449
+ }
450
+ // Handle other objects
451
+ if (typeof value === 'object') {
452
+ const result = {};
453
+ const keys = Object.keys(value);
454
+ const truncatedKeys = keys.slice(0, MAX_ARRAY_LENGTH);
455
+ for (const key of truncatedKeys) {
456
+ try {
457
+ result[key] = safeSerialize(value[key], depth + 1);
458
+ }
459
+ catch {
460
+ result[key] = '[Unserializable]';
461
+ }
462
+ }
463
+ if (keys.length > MAX_ARRAY_LENGTH) {
464
+ result['...'] = `${keys.length - MAX_ARRAY_LENGTH} more keys`;
465
+ }
466
+ return result;
467
+ }
468
+ return '[Unknown type]';
469
+ }
470
+ /**
471
+ * Truncate a string to max length
472
+ */
473
+ function truncateString(str, maxLength) {
474
+ if (str.length <= maxLength) {
475
+ return str;
476
+ }
477
+ return str.substring(0, maxLength) + '...';
478
+ }
479
+ /**
480
+ * Describe a DOM element as a string
481
+ */
482
+ function describeElement(element) {
483
+ const tag = element.tagName.toLowerCase();
484
+ const id = element.id ? `#${element.id}` : '';
485
+ const classes = element.className
486
+ ? `.${element.className.split(' ').filter(Boolean).join('.')}`
487
+ : '';
488
+ let text = '';
489
+ if (element.textContent) {
490
+ text = truncateString(element.textContent.trim(), 50);
491
+ if (text) {
492
+ text = ` "${text}"`;
493
+ }
494
+ }
495
+ return `<${tag}${id}${classes}${text}>`;
496
+ }
497
+ /**
498
+ * Get element selector (best effort)
499
+ */
500
+ function getElementSelector(element) {
501
+ const parts = [];
502
+ let current = element;
503
+ let depth = 0;
504
+ const maxDepth = 5;
505
+ while (current && depth < maxDepth) {
506
+ let selector = current.tagName.toLowerCase();
507
+ if (current.id) {
508
+ selector += `#${current.id}`;
509
+ parts.unshift(selector);
510
+ break; // ID is unique, no need to go further
511
+ }
512
+ if (current.className) {
513
+ const classes = current.className.split(' ').filter(Boolean).slice(0, 2);
514
+ if (classes.length > 0) {
515
+ selector += `.${classes.join('.')}`;
516
+ }
517
+ }
518
+ parts.unshift(selector);
519
+ current = current.parentElement;
520
+ depth++;
521
+ }
522
+ return parts.join(' > ');
523
+ }
524
+
525
+ /**
526
+ * Track click events on the document
527
+ */
528
+ class ClickTracker {
529
+ constructor() {
530
+ this.enabled = false;
531
+ this.handler = null;
532
+ }
533
+ /**
534
+ * Start tracking clicks
535
+ */
536
+ start() {
537
+ if (this.enabled || typeof document === 'undefined') {
538
+ return;
539
+ }
540
+ this.handler = (event) => {
541
+ this.handleClick(event);
542
+ };
543
+ document.addEventListener('click', this.handler, {
544
+ capture: true,
545
+ passive: true,
546
+ });
547
+ this.enabled = true;
548
+ }
549
+ /**
550
+ * Stop tracking clicks
551
+ */
552
+ stop() {
553
+ if (!this.enabled || !this.handler) {
554
+ return;
555
+ }
556
+ document.removeEventListener('click', this.handler, { capture: true });
557
+ this.handler = null;
558
+ this.enabled = false;
559
+ }
560
+ /**
561
+ * Handle a click event
562
+ */
563
+ handleClick(event) {
564
+ const target = event.target;
565
+ if (!(target instanceof Element)) {
566
+ return;
567
+ }
568
+ const manager = getBreadcrumbManager();
569
+ if (!manager) {
570
+ return;
571
+ }
572
+ const breadcrumb = {
573
+ type: 'click',
574
+ category: 'ui',
575
+ level: 'info',
576
+ data: {
577
+ element: describeElement(target),
578
+ selector: getElementSelector(target),
579
+ },
580
+ };
581
+ // Add text content for buttons and links
582
+ if (target instanceof HTMLButtonElement || target instanceof HTMLAnchorElement) {
583
+ const text = target.textContent?.trim();
584
+ if (text) {
585
+ breadcrumb.message = `Clicked: "${truncateString(text, 50)}"`;
586
+ }
587
+ }
588
+ // Add href for links
589
+ if (target instanceof HTMLAnchorElement && target.href) {
590
+ breadcrumb.data = {
591
+ ...breadcrumb.data,
592
+ href: target.href,
593
+ };
594
+ }
595
+ manager.add(breadcrumb);
596
+ }
597
+ }
598
+ // Singleton instance
599
+ let instance$e = null;
600
+ function getClickTracker() {
601
+ if (!instance$e) {
602
+ instance$e = new ClickTracker();
603
+ }
604
+ return instance$e;
605
+ }
606
+ function resetClickTracker() {
607
+ if (instance$e) {
608
+ instance$e.stop();
609
+ instance$e = null;
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Track navigation events (History API, hash changes)
615
+ */
616
+ class NavigationTracker {
617
+ constructor() {
618
+ this.enabled = false;
619
+ this.lastUrl = '';
620
+ this.popstateHandler = null;
621
+ this.hashchangeHandler = null;
622
+ this.originalPushState = null;
623
+ this.originalReplaceState = null;
624
+ }
625
+ /**
626
+ * Start tracking navigation
627
+ */
628
+ start() {
629
+ if (this.enabled || typeof window === 'undefined') {
630
+ return;
631
+ }
632
+ this.lastUrl = window.location.href;
633
+ // Track popstate (back/forward)
634
+ this.popstateHandler = () => {
635
+ this.recordNavigation('popstate');
636
+ };
637
+ window.addEventListener('popstate', this.popstateHandler);
638
+ // Track hash changes
639
+ this.hashchangeHandler = () => {
640
+ this.recordNavigation('hashchange');
641
+ };
642
+ window.addEventListener('hashchange', this.hashchangeHandler);
643
+ // Wrap pushState
644
+ this.originalPushState = history.pushState.bind(history);
645
+ history.pushState = (...args) => {
646
+ this.originalPushState(...args);
647
+ this.recordNavigation('pushState');
648
+ };
649
+ // Wrap replaceState
650
+ this.originalReplaceState = history.replaceState.bind(history);
651
+ history.replaceState = (...args) => {
652
+ this.originalReplaceState(...args);
653
+ this.recordNavigation('replaceState');
654
+ };
655
+ this.enabled = true;
656
+ }
657
+ /**
658
+ * Stop tracking navigation
659
+ */
660
+ stop() {
661
+ if (!this.enabled) {
662
+ return;
663
+ }
664
+ if (this.popstateHandler) {
665
+ window.removeEventListener('popstate', this.popstateHandler);
666
+ this.popstateHandler = null;
667
+ }
668
+ if (this.hashchangeHandler) {
669
+ window.removeEventListener('hashchange', this.hashchangeHandler);
670
+ this.hashchangeHandler = null;
671
+ }
672
+ // Restore original history methods
673
+ if (this.originalPushState) {
674
+ history.pushState = this.originalPushState;
675
+ this.originalPushState = null;
676
+ }
677
+ if (this.originalReplaceState) {
678
+ history.replaceState = this.originalReplaceState;
679
+ this.originalReplaceState = null;
680
+ }
681
+ this.enabled = false;
682
+ }
683
+ /**
684
+ * Record a navigation event
685
+ */
686
+ recordNavigation(navigationType) {
687
+ const manager = getBreadcrumbManager();
688
+ if (!manager) {
689
+ return;
690
+ }
691
+ const currentUrl = window.location.href;
692
+ const from = this.lastUrl;
693
+ const to = currentUrl;
694
+ // Don't record if URL hasn't changed
695
+ if (from === to) {
696
+ return;
697
+ }
698
+ this.lastUrl = currentUrl;
699
+ const breadcrumb = {
700
+ type: 'navigation',
701
+ category: 'navigation',
702
+ level: 'info',
703
+ message: `Navigated to ${getPathname(to)}`,
704
+ data: {
705
+ from: stripOrigin(from),
706
+ to: stripOrigin(to),
707
+ type: navigationType,
708
+ },
709
+ };
710
+ manager.add(breadcrumb);
711
+ }
712
+ }
713
+ /**
714
+ * Get pathname from URL
715
+ */
716
+ function getPathname(url) {
717
+ try {
718
+ return new URL(url).pathname;
719
+ }
720
+ catch {
721
+ return url;
722
+ }
723
+ }
724
+ /**
725
+ * Strip origin from URL, keeping path + query + hash
726
+ */
727
+ function stripOrigin(url) {
728
+ try {
729
+ const parsed = new URL(url);
730
+ return parsed.pathname + parsed.search + parsed.hash;
731
+ }
732
+ catch {
733
+ return url;
734
+ }
735
+ }
736
+ // Singleton instance
737
+ let instance$d = null;
738
+ function getNavigationTracker() {
739
+ if (!instance$d) {
740
+ instance$d = new NavigationTracker();
741
+ }
742
+ return instance$d;
743
+ }
744
+ function resetNavigationTracker() {
745
+ if (instance$d) {
746
+ instance$d.stop();
747
+ instance$d = null;
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Track fetch() requests
753
+ */
754
+ class FetchTracker {
755
+ constructor() {
756
+ this.enabled = false;
757
+ this.originalFetch = null;
758
+ }
759
+ /**
760
+ * Start tracking fetch requests
761
+ */
762
+ start() {
763
+ if (this.enabled || typeof window === 'undefined' || typeof fetch === 'undefined') {
764
+ return;
765
+ }
766
+ this.originalFetch = window.fetch.bind(window);
767
+ window.fetch = async (input, init) => {
768
+ const startTime = Date.now();
769
+ const { method, url } = this.extractRequestInfo(input, init);
770
+ let response;
771
+ let error = null;
772
+ try {
773
+ response = await this.originalFetch(input, init);
774
+ }
775
+ catch (e) {
776
+ error = e instanceof Error ? e : new Error(String(e));
777
+ this.recordBreadcrumb(method, url, startTime, undefined, error);
778
+ throw e;
779
+ }
780
+ this.recordBreadcrumb(method, url, startTime, response.status);
781
+ return response;
782
+ };
783
+ this.enabled = true;
784
+ }
785
+ /**
786
+ * Stop tracking fetch requests
787
+ */
788
+ stop() {
789
+ if (!this.enabled || !this.originalFetch) {
790
+ return;
791
+ }
792
+ window.fetch = this.originalFetch;
793
+ this.originalFetch = null;
794
+ this.enabled = false;
795
+ }
796
+ /**
797
+ * Extract method and URL from fetch arguments
798
+ */
799
+ extractRequestInfo(input, init) {
800
+ let method = 'GET';
801
+ let url;
802
+ if (typeof input === 'string') {
803
+ url = input;
804
+ }
805
+ else if (input instanceof URL) {
806
+ url = input.toString();
807
+ }
808
+ else if (input instanceof Request) {
809
+ url = input.url;
810
+ method = input.method;
811
+ }
812
+ else {
813
+ url = String(input);
814
+ }
815
+ if (init?.method) {
816
+ method = init.method;
817
+ }
818
+ return { method: method.toUpperCase(), url };
819
+ }
820
+ /**
821
+ * Record a fetch breadcrumb
822
+ */
823
+ recordBreadcrumb(method, url, startTime, statusCode, error) {
824
+ const manager = getBreadcrumbManager();
825
+ if (!manager) {
826
+ return;
827
+ }
828
+ const duration = Date.now() - startTime;
829
+ const parsedUrl = parseUrl$1(url);
830
+ const breadcrumb = {
831
+ type: 'fetch',
832
+ category: 'http',
833
+ level: error || (statusCode && statusCode >= 400) ? 'error' : 'info',
834
+ message: `${method} ${parsedUrl.pathname}`,
835
+ data: {
836
+ method,
837
+ url: parsedUrl.full,
838
+ status_code: statusCode,
839
+ duration_ms: duration,
840
+ },
841
+ };
842
+ if (error) {
843
+ breadcrumb.data = {
844
+ ...breadcrumb.data,
845
+ error: error.message,
846
+ };
847
+ }
848
+ manager.add(breadcrumb);
849
+ }
850
+ }
851
+ /**
852
+ * Parse URL and extract components
853
+ */
854
+ function parseUrl$1(url) {
855
+ try {
856
+ // Handle relative URLs
857
+ const parsed = new URL(url, window.location.origin);
858
+ return {
859
+ full: parsed.href,
860
+ pathname: parsed.pathname,
861
+ };
862
+ }
863
+ catch {
864
+ return { full: url, pathname: url };
865
+ }
866
+ }
867
+ // Singleton instance
868
+ let instance$c = null;
869
+ function getFetchTracker() {
870
+ if (!instance$c) {
871
+ instance$c = new FetchTracker();
872
+ }
873
+ return instance$c;
874
+ }
875
+ function resetFetchTracker() {
876
+ if (instance$c) {
877
+ instance$c.stop();
878
+ instance$c = null;
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Track XMLHttpRequest requests
884
+ */
885
+ class XHRTracker {
886
+ constructor() {
887
+ this.enabled = false;
888
+ this.originalOpen = null;
889
+ this.originalSend = null;
890
+ this.xhrInfoMap = new WeakMap();
891
+ }
892
+ /**
893
+ * Start tracking XHR requests
894
+ */
895
+ start() {
896
+ if (this.enabled || typeof XMLHttpRequest === 'undefined') {
897
+ return;
898
+ }
899
+ const self = this;
900
+ // Wrap open() to capture method and URL
901
+ this.originalOpen = XMLHttpRequest.prototype.open;
902
+ XMLHttpRequest.prototype.open = function (method, url, async = true, username, password) {
903
+ self.xhrInfoMap.set(this, {
904
+ method: method.toUpperCase(),
905
+ url: url.toString(),
906
+ startTime: 0,
907
+ });
908
+ return self.originalOpen.call(this, method, url, async, username, password);
909
+ };
910
+ // Wrap send() to capture timing and response
911
+ this.originalSend = XMLHttpRequest.prototype.send;
912
+ XMLHttpRequest.prototype.send = function (body) {
913
+ const info = self.xhrInfoMap.get(this);
914
+ if (info) {
915
+ info.startTime = Date.now();
916
+ }
917
+ // Listen for completion
918
+ this.addEventListener('loadend', () => {
919
+ self.recordBreadcrumb(this);
920
+ });
921
+ return self.originalSend.call(this, body);
922
+ };
923
+ this.enabled = true;
924
+ }
925
+ /**
926
+ * Stop tracking XHR requests
927
+ */
928
+ stop() {
929
+ if (!this.enabled) {
930
+ return;
931
+ }
932
+ if (this.originalOpen) {
933
+ XMLHttpRequest.prototype.open = this.originalOpen;
934
+ this.originalOpen = null;
935
+ }
936
+ if (this.originalSend) {
937
+ XMLHttpRequest.prototype.send = this.originalSend;
938
+ this.originalSend = null;
939
+ }
940
+ this.enabled = false;
941
+ }
942
+ /**
943
+ * Record an XHR breadcrumb
944
+ */
945
+ recordBreadcrumb(xhr) {
946
+ const manager = getBreadcrumbManager();
947
+ if (!manager) {
948
+ return;
949
+ }
950
+ const info = this.xhrInfoMap.get(xhr);
951
+ if (!info) {
952
+ return;
953
+ }
954
+ const duration = info.startTime > 0 ? Date.now() - info.startTime : 0;
955
+ const parsedUrl = parseUrl(info.url);
956
+ const statusCode = xhr.status;
957
+ const isError = statusCode === 0 || statusCode >= 400;
958
+ const breadcrumb = {
959
+ type: 'xhr',
960
+ category: 'http',
961
+ level: isError ? 'error' : 'info',
962
+ message: `${info.method} ${parsedUrl.pathname}`,
963
+ data: {
964
+ method: info.method,
965
+ url: parsedUrl.full,
966
+ status_code: statusCode || undefined,
967
+ duration_ms: duration,
968
+ },
969
+ };
970
+ // Add error info if request failed
971
+ if (statusCode === 0) {
972
+ breadcrumb.data = {
973
+ ...breadcrumb.data,
974
+ error: 'Request failed (network error or CORS)',
975
+ };
976
+ }
977
+ manager.add(breadcrumb);
978
+ }
979
+ }
980
+ /**
981
+ * Parse URL and extract components
982
+ */
983
+ function parseUrl(url) {
984
+ try {
985
+ const parsed = new URL(url, window.location.origin);
986
+ return {
987
+ full: parsed.href,
988
+ pathname: parsed.pathname,
989
+ };
990
+ }
991
+ catch {
992
+ return { full: url, pathname: url };
993
+ }
994
+ }
995
+ // Singleton instance
996
+ let instance$b = null;
997
+ function getXHRTracker() {
998
+ if (!instance$b) {
999
+ instance$b = new XHRTracker();
1000
+ }
1001
+ return instance$b;
1002
+ }
1003
+ function resetXHRTracker() {
1004
+ if (instance$b) {
1005
+ instance$b.stop();
1006
+ instance$b = null;
1007
+ }
1008
+ }
1009
+
1010
+ const LEVEL_MAP = {
1011
+ log: 'info',
1012
+ info: 'info',
1013
+ warn: 'warning',
1014
+ error: 'error',
1015
+ debug: 'debug',
1016
+ };
1017
+ /**
1018
+ * Track console.log/info/warn/error/debug calls
1019
+ */
1020
+ class ConsoleTracker {
1021
+ constructor() {
1022
+ this.enabled = false;
1023
+ this.originalMethods = {};
1024
+ }
1025
+ /**
1026
+ * Start tracking console calls
1027
+ */
1028
+ start() {
1029
+ if (this.enabled || typeof console === 'undefined') {
1030
+ return;
1031
+ }
1032
+ const levels = ['log', 'info', 'warn', 'error', 'debug'];
1033
+ for (const level of levels) {
1034
+ this.wrapConsoleMethod(level);
1035
+ }
1036
+ this.enabled = true;
1037
+ }
1038
+ /**
1039
+ * Stop tracking console calls
1040
+ */
1041
+ stop() {
1042
+ if (!this.enabled) {
1043
+ return;
1044
+ }
1045
+ // Restore original methods
1046
+ for (const [level, original] of Object.entries(this.originalMethods)) {
1047
+ if (original) {
1048
+ console[level] = original;
1049
+ }
1050
+ }
1051
+ this.originalMethods = {};
1052
+ this.enabled = false;
1053
+ }
1054
+ /**
1055
+ * Wrap a console method
1056
+ */
1057
+ wrapConsoleMethod(level) {
1058
+ const original = console[level];
1059
+ if (!original) {
1060
+ return;
1061
+ }
1062
+ this.originalMethods[level] = original;
1063
+ console[level] = (...args) => {
1064
+ // Always call original first
1065
+ original.apply(console, args);
1066
+ // Then record breadcrumb
1067
+ this.recordBreadcrumb(level, args);
1068
+ };
1069
+ }
1070
+ /**
1071
+ * Record a console breadcrumb
1072
+ */
1073
+ recordBreadcrumb(level, args) {
1074
+ const manager = getBreadcrumbManager();
1075
+ if (!manager) {
1076
+ return;
1077
+ }
1078
+ // Format message
1079
+ const message = this.formatMessage(args);
1080
+ const breadcrumb = {
1081
+ type: 'console',
1082
+ category: 'console',
1083
+ level: LEVEL_MAP[level],
1084
+ message: truncateString(message, 200),
1085
+ data: {
1086
+ level,
1087
+ arguments: args.length > 1 ? args.map((arg) => safeSerialize(arg)) : undefined,
1088
+ },
1089
+ };
1090
+ manager.add(breadcrumb);
1091
+ }
1092
+ /**
1093
+ * Format console arguments into a message string
1094
+ */
1095
+ formatMessage(args) {
1096
+ if (args.length === 0) {
1097
+ return '';
1098
+ }
1099
+ return args
1100
+ .map((arg) => {
1101
+ if (typeof arg === 'string') {
1102
+ return arg;
1103
+ }
1104
+ if (arg instanceof Error) {
1105
+ return `${arg.name}: ${arg.message}`;
1106
+ }
1107
+ try {
1108
+ return JSON.stringify(arg);
1109
+ }
1110
+ catch {
1111
+ return String(arg);
1112
+ }
1113
+ })
1114
+ .join(' ');
1115
+ }
1116
+ }
1117
+ // Singleton instance
1118
+ let instance$a = null;
1119
+ function getConsoleTracker() {
1120
+ if (!instance$a) {
1121
+ instance$a = new ConsoleTracker();
1122
+ }
1123
+ return instance$a;
1124
+ }
1125
+ function resetConsoleTracker() {
1126
+ if (instance$a) {
1127
+ instance$a.stop();
1128
+ instance$a = null;
1129
+ }
1130
+ }
1131
+
1132
+ /**
1133
+ * Track input focus events (NOT values - privacy first!)
1134
+ */
1135
+ class InputTracker {
1136
+ constructor() {
1137
+ this.enabled = false;
1138
+ this.focusHandler = null;
1139
+ this.blurHandler = null;
1140
+ }
1141
+ /**
1142
+ * Start tracking input events
1143
+ */
1144
+ start() {
1145
+ if (this.enabled || typeof document === 'undefined') {
1146
+ return;
1147
+ }
1148
+ this.focusHandler = (event) => {
1149
+ this.handleFocus(event);
1150
+ };
1151
+ this.blurHandler = (event) => {
1152
+ this.handleBlur(event);
1153
+ };
1154
+ // Use capture phase to catch all focus/blur events
1155
+ document.addEventListener('focus', this.focusHandler, { capture: true, passive: true });
1156
+ document.addEventListener('blur', this.blurHandler, { capture: true, passive: true });
1157
+ this.enabled = true;
1158
+ }
1159
+ /**
1160
+ * Stop tracking input events
1161
+ */
1162
+ stop() {
1163
+ if (!this.enabled) {
1164
+ return;
1165
+ }
1166
+ if (this.focusHandler) {
1167
+ document.removeEventListener('focus', this.focusHandler, { capture: true });
1168
+ this.focusHandler = null;
1169
+ }
1170
+ if (this.blurHandler) {
1171
+ document.removeEventListener('blur', this.blurHandler, { capture: true });
1172
+ this.blurHandler = null;
1173
+ }
1174
+ this.enabled = false;
1175
+ }
1176
+ /**
1177
+ * Handle focus event
1178
+ */
1179
+ handleFocus(event) {
1180
+ const target = event.target;
1181
+ if (!this.isTrackedElement(target)) {
1182
+ return;
1183
+ }
1184
+ this.recordBreadcrumb(target, 'focus');
1185
+ }
1186
+ /**
1187
+ * Handle blur event
1188
+ */
1189
+ handleBlur(event) {
1190
+ const target = event.target;
1191
+ if (!this.isTrackedElement(target)) {
1192
+ return;
1193
+ }
1194
+ this.recordBreadcrumb(target, 'blur');
1195
+ }
1196
+ /**
1197
+ * Check if element should be tracked
1198
+ */
1199
+ isTrackedElement(target) {
1200
+ if (!target) {
1201
+ return false;
1202
+ }
1203
+ return (target instanceof HTMLInputElement ||
1204
+ target instanceof HTMLTextAreaElement ||
1205
+ target instanceof HTMLSelectElement);
1206
+ }
1207
+ /**
1208
+ * Record an input breadcrumb
1209
+ */
1210
+ recordBreadcrumb(element, action) {
1211
+ const manager = getBreadcrumbManager();
1212
+ if (!manager) {
1213
+ return;
1214
+ }
1215
+ const inputType = element instanceof HTMLInputElement ? element.type : element.tagName.toLowerCase();
1216
+ const name = element.name || element.id || undefined;
1217
+ const breadcrumb = {
1218
+ type: 'input',
1219
+ category: 'ui',
1220
+ level: 'info',
1221
+ message: `Input ${action}: ${inputType}${name ? ` (${name})` : ''}`,
1222
+ data: {
1223
+ action,
1224
+ element_type: inputType,
1225
+ name,
1226
+ selector: getElementSelector(element),
1227
+ // NEVER include the actual value for privacy!
1228
+ },
1229
+ };
1230
+ manager.add(breadcrumb);
1231
+ }
1232
+ }
1233
+ // Singleton instance
1234
+ let instance$9 = null;
1235
+ function getInputTracker() {
1236
+ if (!instance$9) {
1237
+ instance$9 = new InputTracker();
1238
+ }
1239
+ return instance$9;
1240
+ }
1241
+ function resetInputTracker() {
1242
+ if (instance$9) {
1243
+ instance$9.stop();
1244
+ instance$9 = null;
1245
+ }
1246
+ }
1247
+
1248
+ /**
1249
+ * Captures window.onerror errors
1250
+ */
1251
+ class ErrorCapture {
1252
+ constructor() {
1253
+ this.enabled = false;
1254
+ this.handler = null;
1255
+ this.originalOnError = null;
1256
+ }
1257
+ /**
1258
+ * Start capturing errors
1259
+ */
1260
+ start(handler) {
1261
+ if (this.enabled || typeof window === 'undefined') {
1262
+ return;
1263
+ }
1264
+ this.handler = handler;
1265
+ this.originalOnError = window.onerror;
1266
+ window.onerror = (message, filename, lineno, colno, error) => {
1267
+ // Call original handler first
1268
+ if (this.originalOnError) {
1269
+ this.originalOnError.call(window, message, filename, lineno, colno, error);
1270
+ }
1271
+ this.handleError(message, filename, lineno, colno, error);
1272
+ // Don't prevent default handling
1273
+ return false;
1274
+ };
1275
+ this.enabled = true;
1276
+ }
1277
+ /**
1278
+ * Stop capturing errors
1279
+ */
1280
+ stop() {
1281
+ if (!this.enabled) {
1282
+ return;
1283
+ }
1284
+ window.onerror = this.originalOnError;
1285
+ this.originalOnError = null;
1286
+ this.handler = null;
1287
+ this.enabled = false;
1288
+ }
1289
+ /**
1290
+ * Handle an error event
1291
+ */
1292
+ handleError(message, filename, lineno, colno, error) {
1293
+ if (!this.handler) {
1294
+ return;
1295
+ }
1296
+ const captured = {
1297
+ message: this.extractMessage(message, error),
1298
+ name: error ? getErrorName(error) : 'Error',
1299
+ stack: error ? getErrorStack(error) : undefined,
1300
+ filename,
1301
+ lineno,
1302
+ colno,
1303
+ severity: 'error',
1304
+ originalError: error,
1305
+ };
1306
+ this.handler(captured);
1307
+ }
1308
+ /**
1309
+ * Extract error message from various sources
1310
+ */
1311
+ extractMessage(message, error) {
1312
+ // If we have an Error object, prefer its message
1313
+ if (error) {
1314
+ return getErrorMessage(error);
1315
+ }
1316
+ // If message is a string, use it
1317
+ if (typeof message === 'string') {
1318
+ return message;
1319
+ }
1320
+ // If message is an Event, try to extract useful info
1321
+ if (message instanceof ErrorEvent) {
1322
+ return message.message || 'Unknown error';
1323
+ }
1324
+ return 'Unknown error';
1325
+ }
1326
+ }
1327
+ // Singleton instance
1328
+ let instance$8 = null;
1329
+ function getErrorCapture() {
1330
+ if (!instance$8) {
1331
+ instance$8 = new ErrorCapture();
1332
+ }
1333
+ return instance$8;
1334
+ }
1335
+ function resetErrorCapture() {
1336
+ if (instance$8) {
1337
+ instance$8.stop();
1338
+ instance$8 = null;
1339
+ }
1340
+ }
1341
+
1342
+ /**
1343
+ * Captures unhandled promise rejections
1344
+ */
1345
+ class PromiseCapture {
1346
+ constructor() {
1347
+ this.enabled = false;
1348
+ this.handler = null;
1349
+ this.rejectionHandler = null;
1350
+ }
1351
+ /**
1352
+ * Start capturing unhandled rejections
1353
+ */
1354
+ start(handler) {
1355
+ if (this.enabled || typeof window === 'undefined') {
1356
+ return;
1357
+ }
1358
+ this.handler = handler;
1359
+ this.rejectionHandler = (event) => {
1360
+ this.handleRejection(event);
1361
+ };
1362
+ window.addEventListener('unhandledrejection', this.rejectionHandler);
1363
+ this.enabled = true;
1364
+ }
1365
+ /**
1366
+ * Stop capturing unhandled rejections
1367
+ */
1368
+ stop() {
1369
+ if (!this.enabled || !this.rejectionHandler) {
1370
+ return;
1371
+ }
1372
+ window.removeEventListener('unhandledrejection', this.rejectionHandler);
1373
+ this.rejectionHandler = null;
1374
+ this.handler = null;
1375
+ this.enabled = false;
1376
+ }
1377
+ /**
1378
+ * Handle an unhandled rejection event
1379
+ */
1380
+ handleRejection(event) {
1381
+ if (!this.handler) {
1382
+ return;
1383
+ }
1384
+ const reason = event.reason;
1385
+ const captured = {
1386
+ message: this.extractMessage(reason),
1387
+ name: this.extractName(reason),
1388
+ stack: this.extractStack(reason),
1389
+ severity: 'error',
1390
+ originalError: reason instanceof Error ? reason : undefined,
1391
+ };
1392
+ this.handler(captured);
1393
+ }
1394
+ /**
1395
+ * Extract error message from rejection reason
1396
+ */
1397
+ extractMessage(reason) {
1398
+ if (reason instanceof Error) {
1399
+ return getErrorMessage(reason);
1400
+ }
1401
+ if (typeof reason === 'string') {
1402
+ return reason;
1403
+ }
1404
+ if (reason === undefined) {
1405
+ return 'Promise rejected with undefined';
1406
+ }
1407
+ if (reason === null) {
1408
+ return 'Promise rejected with null';
1409
+ }
1410
+ // Try to get message property
1411
+ if (typeof reason === 'object' && reason !== null) {
1412
+ const obj = reason;
1413
+ if (typeof obj['message'] === 'string') {
1414
+ return obj['message'];
1415
+ }
1416
+ }
1417
+ try {
1418
+ return `Promise rejected with: ${JSON.stringify(reason)}`;
1419
+ }
1420
+ catch {
1421
+ return 'Promise rejected with non-serializable value';
1422
+ }
1423
+ }
1424
+ /**
1425
+ * Extract error name from rejection reason
1426
+ */
1427
+ extractName(reason) {
1428
+ if (reason instanceof Error) {
1429
+ return getErrorName(reason);
1430
+ }
1431
+ if (typeof reason === 'object' && reason !== null) {
1432
+ const obj = reason;
1433
+ if (typeof obj['name'] === 'string') {
1434
+ return obj['name'];
1435
+ }
1436
+ }
1437
+ return 'UnhandledRejection';
1438
+ }
1439
+ /**
1440
+ * Extract stack trace from rejection reason
1441
+ */
1442
+ extractStack(reason) {
1443
+ if (reason instanceof Error) {
1444
+ return getErrorStack(reason);
1445
+ }
1446
+ if (typeof reason === 'object' && reason !== null) {
1447
+ const obj = reason;
1448
+ if (typeof obj['stack'] === 'string') {
1449
+ return obj['stack'];
1450
+ }
1451
+ }
1452
+ return undefined;
1453
+ }
1454
+ }
1455
+ // Singleton instance
1456
+ let instance$7 = null;
1457
+ function getPromiseCapture() {
1458
+ if (!instance$7) {
1459
+ instance$7 = new PromiseCapture();
1460
+ }
1461
+ return instance$7;
1462
+ }
1463
+ function resetPromiseCapture() {
1464
+ if (instance$7) {
1465
+ instance$7.stop();
1466
+ instance$7 = null;
1467
+ }
1468
+ }
1469
+
1470
+ /**
1471
+ * Captures console.error calls as errors (not just breadcrumbs)
1472
+ */
1473
+ class ConsoleCapture {
1474
+ constructor() {
1475
+ this.enabled = false;
1476
+ this.handler = null;
1477
+ this.originalConsoleError = null;
1478
+ }
1479
+ /**
1480
+ * Start capturing console.error
1481
+ */
1482
+ start(handler) {
1483
+ if (this.enabled || typeof console === 'undefined') {
1484
+ return;
1485
+ }
1486
+ this.handler = handler;
1487
+ this.originalConsoleError = console.error;
1488
+ console.error = (...args) => {
1489
+ // Always call original first
1490
+ this.originalConsoleError.apply(console, args);
1491
+ // Then capture as error
1492
+ this.handleConsoleError(args);
1493
+ };
1494
+ this.enabled = true;
1495
+ }
1496
+ /**
1497
+ * Stop capturing console.error
1498
+ */
1499
+ stop() {
1500
+ if (!this.enabled || !this.originalConsoleError) {
1501
+ return;
1502
+ }
1503
+ console.error = this.originalConsoleError;
1504
+ this.originalConsoleError = null;
1505
+ this.handler = null;
1506
+ this.enabled = false;
1507
+ }
1508
+ /**
1509
+ * Handle a console.error call
1510
+ */
1511
+ handleConsoleError(args) {
1512
+ if (!this.handler || args.length === 0) {
1513
+ return;
1514
+ }
1515
+ // Check if first argument is an Error
1516
+ const firstArg = args[0];
1517
+ let captured;
1518
+ if (firstArg instanceof Error) {
1519
+ captured = {
1520
+ message: getErrorMessage(firstArg),
1521
+ name: getErrorName(firstArg),
1522
+ stack: getErrorStack(firstArg),
1523
+ severity: 'error',
1524
+ originalError: firstArg,
1525
+ };
1526
+ }
1527
+ else {
1528
+ // Treat as a message
1529
+ const message = args
1530
+ .map((arg) => {
1531
+ if (typeof arg === 'string')
1532
+ return arg;
1533
+ if (arg instanceof Error)
1534
+ return arg.message;
1535
+ try {
1536
+ return JSON.stringify(arg);
1537
+ }
1538
+ catch {
1539
+ return String(arg);
1540
+ }
1541
+ })
1542
+ .join(' ');
1543
+ captured = {
1544
+ message,
1545
+ name: 'ConsoleError',
1546
+ severity: 'error',
1547
+ };
1548
+ // Try to find an Error in the args for stack trace
1549
+ for (const arg of args) {
1550
+ if (arg instanceof Error) {
1551
+ captured.stack = getErrorStack(arg);
1552
+ captured.originalError = arg;
1553
+ break;
1554
+ }
1555
+ }
1556
+ }
1557
+ this.handler(captured);
1558
+ }
1559
+ }
1560
+ // Singleton instance
1561
+ let instance$6 = null;
1562
+ function getConsoleCapture() {
1563
+ if (!instance$6) {
1564
+ instance$6 = new ConsoleCapture();
1565
+ }
1566
+ return instance$6;
1567
+ }
1568
+ function resetConsoleCapture() {
1569
+ if (instance$6) {
1570
+ instance$6.stop();
1571
+ instance$6 = null;
1572
+ }
1573
+ }
1574
+
1575
+ /**
1576
+ * Collect browser context information
1577
+ */
1578
+ function collectBrowserContext() {
1579
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
1580
+ return {};
1581
+ }
1582
+ const context = {
1583
+ user_agent: navigator.userAgent,
1584
+ language: navigator.language,
1585
+ online: navigator.onLine,
1586
+ };
1587
+ // Parse user agent for browser name/version
1588
+ const browserInfo = parseBrowserInfo(navigator.userAgent);
1589
+ if (browserInfo) {
1590
+ context.name = browserInfo.name;
1591
+ context.version = browserInfo.version;
1592
+ }
1593
+ // Viewport size
1594
+ context.viewport = {
1595
+ width: window.innerWidth,
1596
+ height: window.innerHeight,
1597
+ };
1598
+ // Memory info (Chrome only)
1599
+ if ('memory' in performance) {
1600
+ const memory = performance.memory;
1601
+ if (memory) {
1602
+ context.memory = {
1603
+ used_js_heap_size: memory.usedJSHeapSize,
1604
+ total_js_heap_size: memory.totalJSHeapSize,
1605
+ js_heap_size_limit: memory.jsHeapSizeLimit,
1606
+ };
1607
+ }
1608
+ }
1609
+ return context;
1610
+ }
1611
+ /**
1612
+ * Parse browser info from user agent string
1613
+ */
1614
+ function parseBrowserInfo(userAgent) {
1615
+ // Order matters - check more specific patterns first
1616
+ const browsers = [
1617
+ { name: 'Edge', pattern: /Edg(?:e|A|iOS)?\/(\d+(?:\.\d+)*)/ },
1618
+ { name: 'Opera', pattern: /(?:OPR|Opera)\/(\d+(?:\.\d+)*)/ },
1619
+ { name: 'Chrome', pattern: /Chrome\/(\d+(?:\.\d+)*)/ },
1620
+ { name: 'Safari', pattern: /Version\/(\d+(?:\.\d+)*).*Safari/ },
1621
+ { name: 'Firefox', pattern: /Firefox\/(\d+(?:\.\d+)*)/ },
1622
+ { name: 'IE', pattern: /(?:MSIE |rv:)(\d+(?:\.\d+)*)/ },
1623
+ ];
1624
+ for (const browser of browsers) {
1625
+ const match = userAgent.match(browser.pattern);
1626
+ if (match?.[1]) {
1627
+ return {
1628
+ name: browser.name,
1629
+ version: match[1],
1630
+ };
1631
+ }
1632
+ }
1633
+ return null;
1634
+ }
1635
+
1636
+ const SESSION_KEY = 'ee_session';
1637
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
1638
+ /**
1639
+ * Session manager - tracks user sessions across page views
1640
+ */
1641
+ class SessionManager {
1642
+ constructor() {
1643
+ this.session = null;
1644
+ this.loadOrCreateSession();
1645
+ }
1646
+ /**
1647
+ * Get current session context
1648
+ */
1649
+ getContext() {
1650
+ this.ensureActiveSession();
1651
+ return {
1652
+ id: this.session.id,
1653
+ started_at: this.session.started_at,
1654
+ page_views: this.session.page_views,
1655
+ };
1656
+ }
1657
+ /**
1658
+ * Record a page view
1659
+ */
1660
+ recordPageView() {
1661
+ this.ensureActiveSession();
1662
+ this.session.page_views++;
1663
+ this.session.last_activity = Date.now();
1664
+ this.saveSession();
1665
+ }
1666
+ /**
1667
+ * Get session ID
1668
+ */
1669
+ getSessionId() {
1670
+ this.ensureActiveSession();
1671
+ return this.session.id;
1672
+ }
1673
+ /**
1674
+ * Load existing session or create a new one
1675
+ */
1676
+ loadOrCreateSession() {
1677
+ const stored = this.loadSession();
1678
+ if (stored && this.isSessionValid(stored)) {
1679
+ this.session = stored;
1680
+ this.session.last_activity = Date.now();
1681
+ this.session.page_views++;
1682
+ this.saveSession();
1683
+ }
1684
+ else {
1685
+ this.createNewSession();
1686
+ }
1687
+ }
1688
+ /**
1689
+ * Ensure we have an active session
1690
+ */
1691
+ ensureActiveSession() {
1692
+ if (!this.session || !this.isSessionValid(this.session)) {
1693
+ this.createNewSession();
1694
+ }
1695
+ }
1696
+ /**
1697
+ * Create a new session
1698
+ */
1699
+ createNewSession() {
1700
+ this.session = {
1701
+ id: generateShortId(),
1702
+ started_at: new Date().toISOString(),
1703
+ last_activity: Date.now(),
1704
+ page_views: 1,
1705
+ };
1706
+ this.saveSession();
1707
+ }
1708
+ /**
1709
+ * Check if session is still valid (not timed out)
1710
+ */
1711
+ isSessionValid(session) {
1712
+ const elapsed = Date.now() - session.last_activity;
1713
+ return elapsed < SESSION_TIMEOUT_MS;
1714
+ }
1715
+ /**
1716
+ * Load session from storage
1717
+ */
1718
+ loadSession() {
1719
+ try {
1720
+ if (typeof sessionStorage === 'undefined') {
1721
+ return null;
1722
+ }
1723
+ const data = sessionStorage.getItem(SESSION_KEY);
1724
+ if (!data) {
1725
+ return null;
1726
+ }
1727
+ return JSON.parse(data);
1728
+ }
1729
+ catch {
1730
+ return null;
1731
+ }
1732
+ }
1733
+ /**
1734
+ * Save session to storage
1735
+ */
1736
+ saveSession() {
1737
+ try {
1738
+ if (typeof sessionStorage === 'undefined' || !this.session) {
1739
+ return;
1740
+ }
1741
+ sessionStorage.setItem(SESSION_KEY, JSON.stringify(this.session));
1742
+ }
1743
+ catch {
1744
+ // Storage might be full or disabled
1745
+ }
1746
+ }
1747
+ }
1748
+ // Singleton instance
1749
+ let instance$5 = null;
1750
+ function getSessionManager() {
1751
+ if (!instance$5) {
1752
+ instance$5 = new SessionManager();
1753
+ }
1754
+ return instance$5;
1755
+ }
1756
+ function resetSessionManager() {
1757
+ instance$5 = null;
1758
+ }
1759
+
1760
+ /**
1761
+ * Manages user context
1762
+ */
1763
+ class UserContextManager {
1764
+ constructor() {
1765
+ this.user = null;
1766
+ }
1767
+ /**
1768
+ * Set user context
1769
+ */
1770
+ setUser(user) {
1771
+ this.user = { ...user };
1772
+ }
1773
+ /**
1774
+ * Get current user context
1775
+ */
1776
+ getUser() {
1777
+ return this.user ? { ...this.user } : null;
1778
+ }
1779
+ /**
1780
+ * Clear user context
1781
+ */
1782
+ clearUser() {
1783
+ this.user = null;
1784
+ }
1785
+ /**
1786
+ * Check if user is set
1787
+ */
1788
+ hasUser() {
1789
+ return this.user !== null;
1790
+ }
1791
+ }
1792
+ // Singleton instance
1793
+ let instance$4 = null;
1794
+ function getUserContextManager() {
1795
+ if (!instance$4) {
1796
+ instance$4 = new UserContextManager();
1797
+ }
1798
+ return instance$4;
1799
+ }
1800
+ function resetUserContextManager() {
1801
+ instance$4 = null;
1802
+ }
1803
+
1804
+ /**
1805
+ * Collect current request/page context
1806
+ */
1807
+ function collectRequestContext() {
1808
+ if (typeof window === 'undefined') {
1809
+ return {};
1810
+ }
1811
+ const location = window.location;
1812
+ return {
1813
+ url: location.href,
1814
+ method: 'GET', // Browser is always GET for page loads
1815
+ query_string: location.search ? location.search.substring(1) : undefined,
1816
+ };
1817
+ }
1818
+
1819
+ /**
1820
+ * HMAC Signer for secure webhook transmission
1821
+ *
1822
+ * Uses timestamp-based signing to prevent replay attacks:
1823
+ * - Signature = HMAC-SHA256(timestamp.payload, secret)
1824
+ * - Headers: X-Webhook-Signature, X-Webhook-Timestamp
1825
+ */
1826
+ class HmacSigner {
1827
+ constructor(secret) {
1828
+ this.secret = secret;
1829
+ this.encoder = new TextEncoder();
1830
+ }
1831
+ /**
1832
+ * Sign a payload with timestamp
1833
+ */
1834
+ async sign(payload, timestamp) {
1835
+ const ts = timestamp ?? Math.floor(Date.now() / 1000);
1836
+ const signedPayload = `${ts}.${payload}`;
1837
+ const key = await this.getKey();
1838
+ const signature = await crypto.subtle.sign('HMAC', key, this.encoder.encode(signedPayload));
1839
+ return this.arrayBufferToHex(signature);
1840
+ }
1841
+ /**
1842
+ * Build headers for a signed request
1843
+ */
1844
+ async buildHeaders(payload) {
1845
+ const timestamp = Math.floor(Date.now() / 1000);
1846
+ const signature = await this.sign(payload, timestamp);
1847
+ return {
1848
+ 'X-Webhook-Signature': signature,
1849
+ 'X-Webhook-Timestamp': String(timestamp),
1850
+ };
1851
+ }
1852
+ /**
1853
+ * Get or create the HMAC key
1854
+ */
1855
+ async getKey() {
1856
+ return crypto.subtle.importKey('raw', this.encoder.encode(this.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
1857
+ }
1858
+ /**
1859
+ * Convert ArrayBuffer to hex string
1860
+ */
1861
+ arrayBufferToHex(buffer) {
1862
+ const bytes = new Uint8Array(buffer);
1863
+ return Array.from(bytes)
1864
+ .map((b) => b.toString(16).padStart(2, '0'))
1865
+ .join('');
1866
+ }
1867
+ }
1868
+
1869
+ /**
1870
+ * HTTP transport for sending error events to the API
1871
+ */
1872
+ class HttpTransport {
1873
+ constructor(options) {
1874
+ this.hmacSigner = null;
1875
+ this.endpoint = options.endpoint;
1876
+ this.token = options.token;
1877
+ this.timeout = options.timeout;
1878
+ if (options.hmacSecret) {
1879
+ this.hmacSigner = new HmacSigner(options.hmacSecret);
1880
+ }
1881
+ }
1882
+ /**
1883
+ * Send an error event to the API
1884
+ */
1885
+ async send(event) {
1886
+ try {
1887
+ const payload = JSON.stringify(event);
1888
+ // Try sendBeacon first (non-blocking, works during page unload)
1889
+ // Note: sendBeacon doesn't support custom headers, so no HMAC for beacon
1890
+ if (!this.hmacSigner && this.trySendBeacon(payload)) {
1891
+ return true;
1892
+ }
1893
+ // Fall back to fetch (supports HMAC headers)
1894
+ return await this.sendFetch(payload);
1895
+ }
1896
+ catch (error) {
1897
+ console.warn('[ErrorExplorer] Failed to send event:', error);
1898
+ return false;
1899
+ }
1900
+ }
1901
+ /**
1902
+ * Try to send using sendBeacon (best for page unload)
1903
+ * Note: sendBeacon doesn't support custom headers, so HMAC is not used
1904
+ */
1905
+ trySendBeacon(payload) {
1906
+ if (typeof navigator === 'undefined' || !navigator.sendBeacon) {
1907
+ return false;
1908
+ }
1909
+ try {
1910
+ const blob = new Blob([payload], { type: 'application/json' });
1911
+ return navigator.sendBeacon(this.endpoint, blob);
1912
+ }
1913
+ catch {
1914
+ return false;
1915
+ }
1916
+ }
1917
+ /**
1918
+ * Send using fetch with timeout and optional HMAC signing
1919
+ */
1920
+ async sendFetch(payload) {
1921
+ const controller = new AbortController();
1922
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1923
+ try {
1924
+ // Build headers
1925
+ const headers = {
1926
+ 'Content-Type': 'application/json',
1927
+ 'X-Webhook-Token': this.token,
1928
+ };
1929
+ // Add HMAC signature headers if configured
1930
+ if (this.hmacSigner) {
1931
+ const hmacHeaders = await this.hmacSigner.buildHeaders(payload);
1932
+ Object.assign(headers, hmacHeaders);
1933
+ }
1934
+ const response = await fetch(this.endpoint, {
1935
+ method: 'POST',
1936
+ headers,
1937
+ body: payload,
1938
+ signal: controller.signal,
1939
+ keepalive: true, // Allow request to outlive the page
1940
+ });
1941
+ clearTimeout(timeoutId);
1942
+ return response.ok;
1943
+ }
1944
+ catch (error) {
1945
+ clearTimeout(timeoutId);
1946
+ // Don't log abort errors (expected on timeout)
1947
+ if (error instanceof Error && error.name === 'AbortError') {
1948
+ console.warn('[ErrorExplorer] Request timed out');
1949
+ }
1950
+ return false;
1951
+ }
1952
+ }
1953
+ }
1954
+
1955
+ /**
1956
+ * Rate limiter to prevent flooding the API
1957
+ */
1958
+ class RateLimiter {
1959
+ /**
1960
+ * Create a rate limiter
1961
+ * @param maxRequests Maximum requests allowed in the time window
1962
+ * @param windowMs Time window in milliseconds
1963
+ */
1964
+ constructor(maxRequests = 10, windowMs = 60000) {
1965
+ this.timestamps = [];
1966
+ this.maxRequests = maxRequests;
1967
+ this.windowMs = windowMs;
1968
+ }
1969
+ /**
1970
+ * Check if a request is allowed
1971
+ */
1972
+ isAllowed() {
1973
+ const now = Date.now();
1974
+ // Remove timestamps outside the window
1975
+ this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs);
1976
+ // Check if under limit
1977
+ if (this.timestamps.length >= this.maxRequests) {
1978
+ return false;
1979
+ }
1980
+ // Record this request
1981
+ this.timestamps.push(now);
1982
+ return true;
1983
+ }
1984
+ /**
1985
+ * Get remaining requests in current window
1986
+ */
1987
+ getRemaining() {
1988
+ const now = Date.now();
1989
+ this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs);
1990
+ return Math.max(0, this.maxRequests - this.timestamps.length);
1991
+ }
1992
+ /**
1993
+ * Reset the rate limiter
1994
+ */
1995
+ reset() {
1996
+ this.timestamps = [];
1997
+ }
1998
+ }
1999
+ // Singleton instance
2000
+ let instance$3 = null;
2001
+ function getRateLimiter() {
2002
+ if (!instance$3) {
2003
+ instance$3 = new RateLimiter();
2004
+ }
2005
+ return instance$3;
2006
+ }
2007
+ function resetRateLimiter() {
2008
+ instance$3 = null;
2009
+ }
2010
+
2011
+ /**
2012
+ * Manages retry logic for failed requests
2013
+ */
2014
+ class RetryManager {
2015
+ constructor(maxRetries = 3) {
2016
+ this.queue = [];
2017
+ this.processing = false;
2018
+ this.sendFn = null;
2019
+ this.maxRetries = maxRetries;
2020
+ }
2021
+ /**
2022
+ * Set the send function
2023
+ */
2024
+ setSendFunction(fn) {
2025
+ this.sendFn = fn;
2026
+ }
2027
+ /**
2028
+ * Add an event to the retry queue
2029
+ */
2030
+ enqueue(event) {
2031
+ this.queue.push({
2032
+ event,
2033
+ retries: 0,
2034
+ timestamp: Date.now(),
2035
+ });
2036
+ this.processQueue();
2037
+ }
2038
+ /**
2039
+ * Process the retry queue
2040
+ */
2041
+ async processQueue() {
2042
+ if (this.processing || !this.sendFn || this.queue.length === 0) {
2043
+ return;
2044
+ }
2045
+ this.processing = true;
2046
+ while (this.queue.length > 0) {
2047
+ const item = this.queue[0];
2048
+ if (!item) {
2049
+ break;
2050
+ }
2051
+ // Check if max retries exceeded
2052
+ if (item.retries >= this.maxRetries) {
2053
+ this.queue.shift();
2054
+ continue;
2055
+ }
2056
+ // Exponential backoff
2057
+ const delay = this.calculateDelay(item.retries);
2058
+ await this.sleep(delay);
2059
+ // Try to send
2060
+ const success = await this.sendFn(item.event);
2061
+ if (success) {
2062
+ this.queue.shift();
2063
+ }
2064
+ else {
2065
+ item.retries++;
2066
+ // If max retries reached, remove from queue
2067
+ if (item.retries >= this.maxRetries) {
2068
+ this.queue.shift();
2069
+ console.warn('[ErrorExplorer] Max retries reached, dropping event');
2070
+ }
2071
+ }
2072
+ }
2073
+ this.processing = false;
2074
+ }
2075
+ /**
2076
+ * Calculate delay with exponential backoff
2077
+ */
2078
+ calculateDelay(retries) {
2079
+ // 1s, 2s, 4s, 8s, etc. with jitter
2080
+ const baseDelay = 1000 * Math.pow(2, retries);
2081
+ const jitter = Math.random() * 1000;
2082
+ return baseDelay + jitter;
2083
+ }
2084
+ /**
2085
+ * Sleep for a given duration
2086
+ */
2087
+ sleep(ms) {
2088
+ return new Promise((resolve) => setTimeout(resolve, ms));
2089
+ }
2090
+ /**
2091
+ * Get queue size
2092
+ */
2093
+ getQueueSize() {
2094
+ return this.queue.length;
2095
+ }
2096
+ /**
2097
+ * Clear the queue
2098
+ */
2099
+ clear() {
2100
+ this.queue = [];
2101
+ }
2102
+ }
2103
+ // Singleton instance
2104
+ let instance$2 = null;
2105
+ function getRetryManager() {
2106
+ if (!instance$2) {
2107
+ instance$2 = new RetryManager();
2108
+ }
2109
+ return instance$2;
2110
+ }
2111
+ function resetRetryManager() {
2112
+ if (instance$2) {
2113
+ instance$2.clear();
2114
+ }
2115
+ instance$2 = null;
2116
+ }
2117
+
2118
+ const STORAGE_KEY = 'ee_offline_queue';
2119
+ const MAX_QUEUE_SIZE = 50;
2120
+ /**
2121
+ * Offline queue using localStorage
2122
+ * Stores events when offline and sends them when back online
2123
+ */
2124
+ class OfflineQueue {
2125
+ constructor(enabled = true) {
2126
+ this.sendFn = null;
2127
+ this.onlineHandler = null;
2128
+ this.enabled = enabled && typeof localStorage !== 'undefined';
2129
+ if (this.enabled) {
2130
+ this.setupOnlineListener();
2131
+ }
2132
+ }
2133
+ /**
2134
+ * Set the send function
2135
+ */
2136
+ setSendFunction(fn) {
2137
+ this.sendFn = fn;
2138
+ }
2139
+ /**
2140
+ * Check if browser is online
2141
+ */
2142
+ isOnline() {
2143
+ if (typeof navigator === 'undefined') {
2144
+ return true;
2145
+ }
2146
+ return navigator.onLine;
2147
+ }
2148
+ /**
2149
+ * Add event to offline queue
2150
+ */
2151
+ enqueue(event) {
2152
+ if (!this.enabled) {
2153
+ return;
2154
+ }
2155
+ try {
2156
+ const queue = this.loadQueue();
2157
+ // Add to queue (FIFO)
2158
+ queue.push(event);
2159
+ // Trim if too large
2160
+ while (queue.length > MAX_QUEUE_SIZE) {
2161
+ queue.shift();
2162
+ }
2163
+ this.saveQueue(queue);
2164
+ }
2165
+ catch (error) {
2166
+ console.warn('[ErrorExplorer] Failed to save to offline queue:', error);
2167
+ }
2168
+ }
2169
+ /**
2170
+ * Process queued events
2171
+ */
2172
+ async flush() {
2173
+ if (!this.enabled || !this.sendFn || !this.isOnline()) {
2174
+ return;
2175
+ }
2176
+ const queue = this.loadQueue();
2177
+ if (queue.length === 0) {
2178
+ return;
2179
+ }
2180
+ // Clear queue first (to avoid duplicates if page closes during flush)
2181
+ this.saveQueue([]);
2182
+ // Try to send each event
2183
+ const failed = [];
2184
+ for (const event of queue) {
2185
+ try {
2186
+ const success = await this.sendFn(event);
2187
+ if (!success) {
2188
+ failed.push(event);
2189
+ }
2190
+ }
2191
+ catch {
2192
+ failed.push(event);
2193
+ }
2194
+ }
2195
+ // Re-queue failed events
2196
+ if (failed.length > 0) {
2197
+ const currentQueue = this.loadQueue();
2198
+ this.saveQueue([...failed, ...currentQueue].slice(0, MAX_QUEUE_SIZE));
2199
+ }
2200
+ }
2201
+ /**
2202
+ * Get queue size
2203
+ */
2204
+ getQueueSize() {
2205
+ return this.loadQueue().length;
2206
+ }
2207
+ /**
2208
+ * Clear the offline queue
2209
+ */
2210
+ clear() {
2211
+ this.saveQueue([]);
2212
+ }
2213
+ /**
2214
+ * Destroy the offline queue
2215
+ */
2216
+ destroy() {
2217
+ if (this.onlineHandler && typeof window !== 'undefined') {
2218
+ window.removeEventListener('online', this.onlineHandler);
2219
+ this.onlineHandler = null;
2220
+ }
2221
+ }
2222
+ /**
2223
+ * Setup listener for online event
2224
+ */
2225
+ setupOnlineListener() {
2226
+ if (typeof window === 'undefined') {
2227
+ return;
2228
+ }
2229
+ this.onlineHandler = () => {
2230
+ this.flush();
2231
+ };
2232
+ window.addEventListener('online', this.onlineHandler);
2233
+ }
2234
+ /**
2235
+ * Load queue from localStorage
2236
+ */
2237
+ loadQueue() {
2238
+ try {
2239
+ const data = localStorage.getItem(STORAGE_KEY);
2240
+ if (!data) {
2241
+ return [];
2242
+ }
2243
+ return JSON.parse(data);
2244
+ }
2245
+ catch {
2246
+ return [];
2247
+ }
2248
+ }
2249
+ /**
2250
+ * Save queue to localStorage
2251
+ */
2252
+ saveQueue(queue) {
2253
+ try {
2254
+ if (queue.length === 0) {
2255
+ localStorage.removeItem(STORAGE_KEY);
2256
+ }
2257
+ else {
2258
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(queue));
2259
+ }
2260
+ }
2261
+ catch {
2262
+ // Storage might be full
2263
+ }
2264
+ }
2265
+ }
2266
+ // Singleton instance
2267
+ let instance$1 = null;
2268
+ function getOfflineQueue(enabled = true) {
2269
+ if (!instance$1) {
2270
+ instance$1 = new OfflineQueue(enabled);
2271
+ }
2272
+ return instance$1;
2273
+ }
2274
+ function resetOfflineQueue() {
2275
+ if (instance$1) {
2276
+ instance$1.destroy();
2277
+ }
2278
+ instance$1 = null;
2279
+ }
2280
+
2281
+ /**
2282
+ * Default fields to scrub (case-insensitive)
2283
+ */
2284
+ const DEFAULT_SCRUB_FIELDS = [
2285
+ 'password',
2286
+ 'passwd',
2287
+ 'secret',
2288
+ 'token',
2289
+ 'api_key',
2290
+ 'apikey',
2291
+ 'access_token',
2292
+ 'accesstoken',
2293
+ 'refresh_token',
2294
+ 'refreshtoken',
2295
+ 'auth',
2296
+ 'authorization',
2297
+ 'credit_card',
2298
+ 'creditcard',
2299
+ 'card_number',
2300
+ 'cardnumber',
2301
+ 'cvv',
2302
+ 'cvc',
2303
+ 'ssn',
2304
+ 'social_security',
2305
+ 'private_key',
2306
+ 'privatekey',
2307
+ ];
2308
+ /**
2309
+ * Replacement string for scrubbed values
2310
+ */
2311
+ const SCRUBBED = '[FILTERED]';
2312
+ /**
2313
+ * Data scrubber to remove sensitive information before sending
2314
+ */
2315
+ class DataScrubber {
2316
+ constructor(additionalFields = []) {
2317
+ // Combine default and additional fields, all lowercase
2318
+ this.scrubFields = new Set([
2319
+ ...DEFAULT_SCRUB_FIELDS.map((f) => f.toLowerCase()),
2320
+ ...additionalFields.map((f) => f.toLowerCase()),
2321
+ ]);
2322
+ }
2323
+ /**
2324
+ * Scrub sensitive data from an object
2325
+ */
2326
+ scrub(data) {
2327
+ return this.scrubValue(data, 0);
2328
+ }
2329
+ /**
2330
+ * Recursively scrub a value
2331
+ */
2332
+ scrubValue(value, depth) {
2333
+ // Limit recursion depth
2334
+ if (depth > 10) {
2335
+ return value;
2336
+ }
2337
+ // Handle null/undefined
2338
+ if (value === null || value === undefined) {
2339
+ return value;
2340
+ }
2341
+ // Handle primitives
2342
+ if (typeof value !== 'object') {
2343
+ return value;
2344
+ }
2345
+ // Handle arrays
2346
+ if (Array.isArray(value)) {
2347
+ return value.map((item) => this.scrubValue(item, depth + 1));
2348
+ }
2349
+ // Handle objects
2350
+ const result = {};
2351
+ for (const [key, val] of Object.entries(value)) {
2352
+ if (this.shouldScrub(key)) {
2353
+ result[key] = SCRUBBED;
2354
+ }
2355
+ else if (typeof val === 'string') {
2356
+ result[key] = this.scrubString(val);
2357
+ }
2358
+ else {
2359
+ result[key] = this.scrubValue(val, depth + 1);
2360
+ }
2361
+ }
2362
+ return result;
2363
+ }
2364
+ /**
2365
+ * Check if a key should be scrubbed
2366
+ */
2367
+ shouldScrub(key) {
2368
+ const lowerKey = key.toLowerCase();
2369
+ return this.scrubFields.has(lowerKey);
2370
+ }
2371
+ /**
2372
+ * Scrub sensitive patterns from strings
2373
+ */
2374
+ scrubString(value) {
2375
+ let result = value;
2376
+ // Scrub credit card numbers (13-19 digits, possibly with spaces/dashes)
2377
+ result = result.replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{1,7}\b/g, SCRUBBED);
2378
+ // Scrub email addresses (optional, can be disabled if needed)
2379
+ // result = result.replace(/[\w.+-]+@[\w.-]+\.\w{2,}/g, SCRUBBED);
2380
+ // Scrub potential API keys (long alphanumeric strings)
2381
+ result = result.replace(/\b(sk|pk|api|key|token|secret)_[a-zA-Z0-9]{20,}\b/gi, SCRUBBED);
2382
+ // Scrub Bearer tokens
2383
+ result = result.replace(/Bearer\s+[a-zA-Z0-9._-]+/gi, `Bearer ${SCRUBBED}`);
2384
+ return result;
2385
+ }
2386
+ /**
2387
+ * Add fields to scrub
2388
+ */
2389
+ addFields(fields) {
2390
+ for (const field of fields) {
2391
+ this.scrubFields.add(field.toLowerCase());
2392
+ }
2393
+ }
2394
+ }
2395
+ // Singleton instance
2396
+ let instance = null;
2397
+ function initDataScrubber(additionalFields = []) {
2398
+ instance = new DataScrubber(additionalFields);
2399
+ return instance;
2400
+ }
2401
+ function resetDataScrubber() {
2402
+ instance = null;
2403
+ }
2404
+
2405
+ const SDK_NAME = '@error-explorer/browser';
2406
+ const SDK_VERSION = '1.0.0';
2407
+ /**
2408
+ * Main ErrorExplorer class - singleton pattern
2409
+ */
2410
+ class ErrorExplorerClient {
2411
+ constructor() {
2412
+ this.config = null;
2413
+ this.transport = null;
2414
+ this.initialized = false;
2415
+ this.tags = {};
2416
+ this.extra = {};
2417
+ this.contexts = {};
2418
+ }
2419
+ /**
2420
+ * Initialize the SDK
2421
+ */
2422
+ init(options) {
2423
+ if (this.initialized) {
2424
+ console.warn('[ErrorExplorer] Already initialized');
2425
+ return;
2426
+ }
2427
+ try {
2428
+ // Resolve configuration
2429
+ this.config = resolveConfig(options);
2430
+ // Initialize transport
2431
+ this.transport = new HttpTransport({
2432
+ endpoint: this.config.endpoint,
2433
+ token: this.config.token,
2434
+ timeout: this.config.timeout,
2435
+ maxRetries: this.config.maxRetries,
2436
+ hmacSecret: this.config.hmacSecret,
2437
+ });
2438
+ // Initialize data scrubber
2439
+ initDataScrubber();
2440
+ // Setup send function for retry/offline managers
2441
+ const sendFn = this.sendEvent.bind(this);
2442
+ getRetryManager().setSendFunction(sendFn);
2443
+ getOfflineQueue(this.config.offline).setSendFunction(sendFn);
2444
+ // Initialize breadcrumbs
2445
+ if (this.config.breadcrumbs.enabled) {
2446
+ initBreadcrumbManager(this.config);
2447
+ this.startBreadcrumbTrackers();
2448
+ }
2449
+ // Setup error captures
2450
+ if (this.config.autoCapture.errors) {
2451
+ getErrorCapture().start(this.handleCapturedError.bind(this));
2452
+ }
2453
+ if (this.config.autoCapture.unhandledRejections) {
2454
+ getPromiseCapture().start(this.handleCapturedError.bind(this));
2455
+ }
2456
+ if (this.config.autoCapture.console) {
2457
+ getConsoleCapture().start(this.handleCapturedError.bind(this));
2458
+ }
2459
+ // Record page view
2460
+ getSessionManager().recordPageView();
2461
+ // Flush offline queue on init
2462
+ getOfflineQueue(this.config.offline).flush();
2463
+ this.initialized = true;
2464
+ if (this.config.debug) {
2465
+ console.log('[ErrorExplorer] Initialized', {
2466
+ endpoint: this.config.endpoint,
2467
+ environment: this.config.environment,
2468
+ });
2469
+ }
2470
+ }
2471
+ catch (error) {
2472
+ console.error('[ErrorExplorer] Initialization failed:', error);
2473
+ }
2474
+ }
2475
+ /**
2476
+ * Check if SDK is initialized
2477
+ */
2478
+ isInitialized() {
2479
+ return this.initialized;
2480
+ }
2481
+ /**
2482
+ * Set user context
2483
+ */
2484
+ setUser(user) {
2485
+ getUserContextManager().setUser(user);
2486
+ }
2487
+ /**
2488
+ * Clear user context
2489
+ */
2490
+ clearUser() {
2491
+ getUserContextManager().clearUser();
2492
+ }
2493
+ /**
2494
+ * Set a single tag
2495
+ */
2496
+ setTag(key, value) {
2497
+ this.tags[key] = value;
2498
+ }
2499
+ /**
2500
+ * Set multiple tags
2501
+ */
2502
+ setTags(tags) {
2503
+ this.tags = { ...this.tags, ...tags };
2504
+ }
2505
+ /**
2506
+ * Set extra data
2507
+ */
2508
+ setExtra(extra) {
2509
+ this.extra = { ...this.extra, ...extra };
2510
+ }
2511
+ /**
2512
+ * Set a named context
2513
+ */
2514
+ setContext(name, context) {
2515
+ this.contexts[name] = context;
2516
+ }
2517
+ /**
2518
+ * Add a manual breadcrumb
2519
+ */
2520
+ addBreadcrumb(breadcrumb) {
2521
+ const manager = getBreadcrumbManager();
2522
+ if (manager) {
2523
+ manager.add(breadcrumb);
2524
+ }
2525
+ }
2526
+ /**
2527
+ * Capture an exception manually
2528
+ */
2529
+ captureException(error, context) {
2530
+ if (!this.initialized || !this.config) {
2531
+ console.warn('[ErrorExplorer] Not initialized');
2532
+ return '';
2533
+ }
2534
+ const eventId = generateUuid();
2535
+ const event = this.buildEvent(error, context, 'error');
2536
+ this.processAndSend(event);
2537
+ return eventId;
2538
+ }
2539
+ /**
2540
+ * Capture a message manually
2541
+ */
2542
+ captureMessage(message, level = 'info') {
2543
+ if (!this.initialized || !this.config) {
2544
+ console.warn('[ErrorExplorer] Not initialized');
2545
+ return '';
2546
+ }
2547
+ const eventId = generateUuid();
2548
+ const event = this.buildEvent(new Error(message), undefined, level);
2549
+ event.exception_class = 'Message';
2550
+ this.processAndSend(event);
2551
+ return eventId;
2552
+ }
2553
+ /**
2554
+ * Flush all pending events
2555
+ */
2556
+ async flush(timeout = 5000) {
2557
+ if (!this.initialized) {
2558
+ return false;
2559
+ }
2560
+ return new Promise((resolve) => {
2561
+ const timeoutId = setTimeout(() => resolve(false), timeout);
2562
+ Promise.all([getOfflineQueue().flush()])
2563
+ .then(() => {
2564
+ clearTimeout(timeoutId);
2565
+ resolve(true);
2566
+ })
2567
+ .catch(() => {
2568
+ clearTimeout(timeoutId);
2569
+ resolve(false);
2570
+ });
2571
+ });
2572
+ }
2573
+ /**
2574
+ * Close the SDK and cleanup
2575
+ */
2576
+ async close(timeout = 5000) {
2577
+ if (!this.initialized) {
2578
+ return true;
2579
+ }
2580
+ // Flush pending events
2581
+ await this.flush(timeout);
2582
+ // Stop all trackers and captures
2583
+ this.stopAllTrackers();
2584
+ // Reset all singletons
2585
+ resetBreadcrumbManager();
2586
+ resetErrorCapture();
2587
+ resetPromiseCapture();
2588
+ resetConsoleCapture();
2589
+ resetSessionManager();
2590
+ resetUserContextManager();
2591
+ resetRateLimiter();
2592
+ resetRetryManager();
2593
+ resetOfflineQueue();
2594
+ resetDataScrubber();
2595
+ this.config = null;
2596
+ this.transport = null;
2597
+ this.initialized = false;
2598
+ this.tags = {};
2599
+ this.extra = {};
2600
+ this.contexts = {};
2601
+ return true;
2602
+ }
2603
+ /**
2604
+ * Handle a captured error from auto-capture
2605
+ */
2606
+ handleCapturedError(captured) {
2607
+ if (!this.config) {
2608
+ return;
2609
+ }
2610
+ // Check if error should be ignored
2611
+ if (this.shouldIgnoreError(captured)) {
2612
+ if (this.config.debug) {
2613
+ console.log('[ErrorExplorer] Ignoring error:', captured.message);
2614
+ }
2615
+ return;
2616
+ }
2617
+ const event = this.buildEventFromCapture(captured);
2618
+ this.processAndSend(event);
2619
+ }
2620
+ /**
2621
+ * Check if an error should be ignored
2622
+ */
2623
+ shouldIgnoreError(captured) {
2624
+ if (!this.config) {
2625
+ return true;
2626
+ }
2627
+ // Check ignoreErrors patterns
2628
+ if (matchesPattern(captured.message, this.config.ignoreErrors)) {
2629
+ return true;
2630
+ }
2631
+ // Check denyUrls
2632
+ if (captured.filename && matchesPattern(captured.filename, this.config.denyUrls)) {
2633
+ return true;
2634
+ }
2635
+ // Check allowUrls (if specified, only allow these)
2636
+ if (this.config.allowUrls.length > 0 && captured.filename) {
2637
+ if (!matchesPattern(captured.filename, this.config.allowUrls)) {
2638
+ return true;
2639
+ }
2640
+ }
2641
+ return false;
2642
+ }
2643
+ /**
2644
+ * Build event from captured error
2645
+ */
2646
+ buildEventFromCapture(captured) {
2647
+ return this.buildEvent(captured.originalError || new Error(captured.message), undefined, captured.severity);
2648
+ }
2649
+ /**
2650
+ * Build a complete error event
2651
+ */
2652
+ buildEvent(error, context, severity = 'error') {
2653
+ const config = this.config;
2654
+ const message = getErrorMessage(error);
2655
+ const name = getErrorName(error);
2656
+ const stack = getErrorStack(error);
2657
+ // Parse stack trace
2658
+ const frames = parseStackTrace(stack);
2659
+ const topFrame = frames[0];
2660
+ // Get project name from config or derive from token
2661
+ const project = config.project || this.deriveProjectName(config.token);
2662
+ const event = {
2663
+ message,
2664
+ project,
2665
+ exception_class: name,
2666
+ file: topFrame?.filename || 'unknown',
2667
+ line: topFrame?.lineno || 0,
2668
+ column: topFrame?.colno,
2669
+ stack_trace: stack,
2670
+ frames,
2671
+ severity: context?.level ?? severity,
2672
+ environment: config.environment,
2673
+ release: config.release || undefined,
2674
+ timestamp: new Date().toISOString(),
2675
+ // User context
2676
+ user: context?.user ?? getUserContextManager().getUser() ?? undefined,
2677
+ // Request context
2678
+ request: collectRequestContext(),
2679
+ // Browser context
2680
+ browser: collectBrowserContext(),
2681
+ // Session context
2682
+ session: getSessionManager().getContext(),
2683
+ // Breadcrumbs
2684
+ breadcrumbs: getBreadcrumbManager()?.getAll(),
2685
+ // Tags (merge global + context)
2686
+ tags: { ...this.tags, ...context?.tags },
2687
+ // Extra data
2688
+ extra: { ...this.extra, ...context?.extra },
2689
+ // Custom contexts
2690
+ contexts: this.contexts,
2691
+ // SDK info
2692
+ sdk: {
2693
+ name: SDK_NAME,
2694
+ version: SDK_VERSION,
2695
+ },
2696
+ // Fingerprint for grouping
2697
+ fingerprint: context?.fingerprint,
2698
+ };
2699
+ return event;
2700
+ }
2701
+ /**
2702
+ * Process event through beforeSend and send
2703
+ */
2704
+ processAndSend(event) {
2705
+ if (!this.config) {
2706
+ return;
2707
+ }
2708
+ // Apply beforeSend hook
2709
+ let processedEvent = event;
2710
+ if (this.config.beforeSend) {
2711
+ try {
2712
+ processedEvent = this.config.beforeSend(event);
2713
+ }
2714
+ catch (e) {
2715
+ console.error('[ErrorExplorer] beforeSend threw an error:', e);
2716
+ processedEvent = event;
2717
+ }
2718
+ }
2719
+ // If beforeSend returned null, drop the event
2720
+ if (!processedEvent) {
2721
+ if (this.config.debug) {
2722
+ console.log('[ErrorExplorer] Event dropped by beforeSend');
2723
+ }
2724
+ return;
2725
+ }
2726
+ // Check rate limiter
2727
+ if (!getRateLimiter().isAllowed()) {
2728
+ if (this.config.debug) {
2729
+ console.log('[ErrorExplorer] Rate limit exceeded, queuing event');
2730
+ }
2731
+ getRetryManager().enqueue(processedEvent);
2732
+ return;
2733
+ }
2734
+ // Check if offline
2735
+ if (!getOfflineQueue(this.config.offline).isOnline()) {
2736
+ getOfflineQueue(this.config.offline).enqueue(processedEvent);
2737
+ return;
2738
+ }
2739
+ // Send event
2740
+ this.sendEvent(processedEvent);
2741
+ }
2742
+ /**
2743
+ * Send event to the API
2744
+ */
2745
+ async sendEvent(event) {
2746
+ if (!this.transport) {
2747
+ return false;
2748
+ }
2749
+ const success = await this.transport.send(event);
2750
+ if (!success && this.config?.offline) {
2751
+ // Queue for retry
2752
+ getRetryManager().enqueue(event);
2753
+ }
2754
+ return success;
2755
+ }
2756
+ /**
2757
+ * Start all breadcrumb trackers
2758
+ */
2759
+ startBreadcrumbTrackers() {
2760
+ if (!this.config) {
2761
+ return;
2762
+ }
2763
+ const bc = this.config.breadcrumbs;
2764
+ if (bc.clicks) {
2765
+ getClickTracker().start();
2766
+ }
2767
+ if (bc.navigation) {
2768
+ getNavigationTracker().start();
2769
+ }
2770
+ if (bc.fetch) {
2771
+ getFetchTracker().start();
2772
+ }
2773
+ if (bc.xhr) {
2774
+ getXHRTracker().start();
2775
+ }
2776
+ if (bc.console) {
2777
+ getConsoleTracker().start();
2778
+ }
2779
+ if (bc.inputs) {
2780
+ getInputTracker().start();
2781
+ }
2782
+ }
2783
+ /**
2784
+ * Stop all breadcrumb trackers
2785
+ */
2786
+ stopAllTrackers() {
2787
+ resetClickTracker();
2788
+ resetNavigationTracker();
2789
+ resetFetchTracker();
2790
+ resetXHRTracker();
2791
+ resetConsoleTracker();
2792
+ resetInputTracker();
2793
+ }
2794
+ /**
2795
+ * Derive project name from token or use default
2796
+ */
2797
+ deriveProjectName(token) {
2798
+ // If token starts with ee_, use a sanitized version as project name
2799
+ if (token.startsWith('ee_')) {
2800
+ // Take first 16 chars after ee_ for project identifier
2801
+ return `project_${token.slice(3, 19)}`;
2802
+ }
2803
+ // Fallback to generic name
2804
+ return 'browser-app';
2805
+ }
2806
+ }
2807
+ // Export singleton instance
2808
+ const ErrorExplorer = new ErrorExplorerClient();
2809
+
2810
+ /**
2811
+ * @error-explorer/browser
2812
+ * Error Explorer SDK for Browser - Automatic error tracking and breadcrumbs
2813
+ */
2814
+ // Main export
2815
+
2816
+ exports.ErrorExplorer = ErrorExplorer;
2817
+ exports.default = ErrorExplorer;
2818
+ //# sourceMappingURL=error-explorer.cjs.js.map