@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.
- package/README.md +251 -0
- package/dist/error-explorer.cjs.js +2818 -0
- package/dist/error-explorer.cjs.js.map +1 -0
- package/dist/error-explorer.esm.js +2813 -0
- package/dist/error-explorer.esm.js.map +1 -0
- package/dist/error-explorer.min.js +2 -0
- package/dist/error-explorer.min.js.map +1 -0
- package/dist/index.d.ts +288 -0
- package/package.json +67 -0
|
@@ -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
|