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