@diegotsi/flint-core 1.7.0 → 1.8.0

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/dist/index.cjs CHANGED
@@ -64,6 +64,7 @@ __export(index_exports, {
64
64
  collectEnvironment: () => collectEnvironment,
65
65
  createConsoleCollector: () => createConsoleCollector,
66
66
  createDatadogReplayProvider: () => createDatadogReplayProvider,
67
+ createErrorCaptureCollector: () => createErrorCaptureCollector,
67
68
  createFormErrorCollector: () => createFormErrorCollector,
68
69
  createFrustrationCollector: () => createFrustrationCollector,
69
70
  createNetworkCollector: () => createNetworkCollector,
@@ -141,8 +142,7 @@ async function submitReplay(serverUrl, projectKey, reportId, events) {
141
142
  });
142
143
  }
143
144
 
144
- // src/collectors/console.ts
145
- var MAX_ENTRIES = 50;
145
+ // src/sanitize.ts
146
146
  var SENSITIVE_PATTERNS = [
147
147
  /(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|authorization|bearer)\s*[:=]\s*["']?[^\s"',]{4,}/gi,
148
148
  /\b(sk-[a-zA-Z0-9_-]{20,})\b/g,
@@ -161,6 +161,9 @@ function sanitize(str) {
161
161
  }
162
162
  return result;
163
163
  }
164
+
165
+ // src/collectors/console.ts
166
+ var MAX_ENTRIES = 50;
164
167
  function createConsoleCollector() {
165
168
  const entries = [];
166
169
  let active = false;
@@ -280,6 +283,216 @@ function collectEnvironment() {
280
283
  };
281
284
  }
282
285
 
286
+ // src/collectors/errorCapture.ts
287
+ var MAX_MESSAGE = 1e3;
288
+ var MAX_STACK = 8e3;
289
+ var MAX_BATCH = 20;
290
+ var MAX_PAYLOAD_BYTES = 6e4;
291
+ var PER_KEY_PER_MINUTE = 10;
292
+ var GLOBAL_PAGE_CAP = 100;
293
+ var FLUSH_INTERVAL_MS = 5e3;
294
+ var THROTTLE_WINDOW_MS = 6e4;
295
+ var BREADCRUMB_CONSOLE = 10;
296
+ var BREADCRUMB_NETWORK = 10;
297
+ var DEFAULT_IGNORE = [/ResizeObserver loop/i, /^Script error\.?$/];
298
+ var EXTENSION_URL = /(chrome|moz|safari|safari-web)-extension:\/\//;
299
+ function normalizeForKey(message) {
300
+ return message.toLowerCase().replace(/\d+/g, "#").replace(/\s+/g, " ").trim().slice(0, 200);
301
+ }
302
+ function topFrame(stack) {
303
+ if (!stack) return "";
304
+ const lines = stack.split("\n").map((l) => l.trim());
305
+ return lines.find((l) => l.startsWith("at ") || /@/.test(l)) ?? "";
306
+ }
307
+ function createErrorCaptureCollector(options) {
308
+ const sampleRate = options.sampleRate ?? 1;
309
+ const ignoreList = [...DEFAULT_IGNORE, ...options.ignoreErrors ?? []];
310
+ const sessionId = `s_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
311
+ let active = false;
312
+ let queue = [];
313
+ let interval = null;
314
+ let sentCount = 0;
315
+ let environment;
316
+ const keyTimestamps = /* @__PURE__ */ new Map();
317
+ function debugLog2(...args) {
318
+ if (options.debug) console.log("[Flint]", ...args);
319
+ }
320
+ function isIgnored(message, stack, frameUrl) {
321
+ for (const entry of ignoreList) {
322
+ if (typeof entry === "string") {
323
+ if (message.includes(entry)) return true;
324
+ } else if (entry.test(message)) {
325
+ return true;
326
+ }
327
+ }
328
+ if (EXTENSION_URL.test(frameUrl ?? "") || EXTENSION_URL.test(stack ?? "")) return true;
329
+ return false;
330
+ }
331
+ function isThrottled(localKey) {
332
+ const now = Date.now();
333
+ const stamps = (keyTimestamps.get(localKey) ?? []).filter((t) => now - t < THROTTLE_WINDOW_MS);
334
+ if (stamps.length >= PER_KEY_PER_MINUTE) {
335
+ keyTimestamps.set(localKey, stamps);
336
+ return true;
337
+ }
338
+ stamps.push(now);
339
+ keyTimestamps.set(localKey, stamps);
340
+ return false;
341
+ }
342
+ function capture(input) {
343
+ try {
344
+ if (sentCount + queue.length >= GLOBAL_PAGE_CAP) return;
345
+ let message = input.message.slice(0, MAX_MESSAGE);
346
+ if (isIgnored(message, input.stack, input.frameUrl)) return;
347
+ if (sampleRate < 1 && Math.random() >= sampleRate) return;
348
+ const localKey = `${input.type}|${normalizeForKey(message)}|${topFrame(input.stack)}`;
349
+ if (isThrottled(localKey)) return;
350
+ const pending = queue.find((e) => e._localKey === localKey);
351
+ if (pending) {
352
+ pending.count += 1;
353
+ return;
354
+ }
355
+ message = sanitize(message);
356
+ const stack = input.stack ? sanitize(input.stack.slice(0, MAX_STACK)) : void 0;
357
+ if (!environment && options.getEnvironment) {
358
+ try {
359
+ environment = options.getEnvironment();
360
+ } catch {
361
+ }
362
+ }
363
+ const user = options.getUser?.();
364
+ let event = {
365
+ type: input.type,
366
+ message,
367
+ errorClass: input.errorClass,
368
+ stack,
369
+ url: typeof location !== "undefined" ? location.href : "",
370
+ timestamp: Date.now(),
371
+ release: options.release,
372
+ appVersion: options.appVersion,
373
+ userId: user?.id,
374
+ sessionId,
375
+ browser: environment?.browser,
376
+ os: environment?.os,
377
+ count: 1,
378
+ breadcrumbs: options.getBreadcrumbs ? {
379
+ consoleLogs: options.getBreadcrumbs().consoleLogs.slice(-BREADCRUMB_CONSOLE),
380
+ networkErrors: options.getBreadcrumbs().networkErrors.slice(-BREADCRUMB_NETWORK)
381
+ } : void 0,
382
+ _localKey: localKey
383
+ };
384
+ if (options.beforeSend) {
385
+ try {
386
+ const result = options.beforeSend(event);
387
+ if (!result) return;
388
+ event = { ...result, _localKey: localKey };
389
+ } catch {
390
+ }
391
+ }
392
+ queue.push(event);
393
+ debugLog2("Error captured", input.errorClass, message);
394
+ if (queue.length >= MAX_BATCH) flush(false);
395
+ } catch {
396
+ }
397
+ }
398
+ function serialize(events) {
399
+ const wire = events.map(({ _localKey, ...e }) => e);
400
+ let body = JSON.stringify(wire);
401
+ if (body.length > MAX_PAYLOAD_BYTES) {
402
+ body = JSON.stringify(wire.map((e) => ({ ...e, breadcrumbs: void 0 })));
403
+ if (body.length > MAX_PAYLOAD_BYTES) {
404
+ body = JSON.stringify(wire.map((e) => ({ ...e, breadcrumbs: void 0, stack: e.stack?.slice(0, 2e3) })));
405
+ }
406
+ }
407
+ return body;
408
+ }
409
+ function flush(unloading) {
410
+ if (queue.length === 0) return;
411
+ const events = queue;
412
+ queue = [];
413
+ sentCount += events.length;
414
+ const base = options.serverUrl.replace(/\/$/, "");
415
+ const url = `${base}/api/v1/error-events?project_key=${encodeURIComponent(options.projectKey)}`;
416
+ const body = serialize(events);
417
+ if (unloading && typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
418
+ const ok = navigator.sendBeacon(url, body);
419
+ debugLog2("Flushed via beacon", events.length, ok);
420
+ if (ok) return;
421
+ }
422
+ fetch(url, {
423
+ method: "POST",
424
+ headers: { "Content-Type": "text/plain;charset=UTF-8" },
425
+ body,
426
+ keepalive: unloading
427
+ }).catch(() => {
428
+ });
429
+ debugLog2("Flushed via fetch", events.length);
430
+ }
431
+ function onError(event) {
432
+ const e = event;
433
+ if (typeof e.message !== "string" && !(e.error instanceof Error)) return;
434
+ const err = e.error instanceof Error ? e.error : void 0;
435
+ capture({
436
+ type: "error",
437
+ message: err?.message ?? (e.message || "Unknown error"),
438
+ errorClass: err?.name ?? "Error",
439
+ stack: err?.stack,
440
+ frameUrl: e.filename
441
+ });
442
+ }
443
+ function onRejection(event) {
444
+ const reason = event.reason;
445
+ const err = reason instanceof Error ? reason : void 0;
446
+ let message;
447
+ if (err) {
448
+ message = err.message;
449
+ } else {
450
+ try {
451
+ message = typeof reason === "string" ? reason : JSON.stringify(reason);
452
+ } catch {
453
+ message = String(reason);
454
+ }
455
+ }
456
+ capture({
457
+ type: "unhandledrejection",
458
+ message: message || "Unhandled promise rejection",
459
+ errorClass: err?.name ?? "UnhandledRejection",
460
+ stack: err?.stack
461
+ });
462
+ }
463
+ function onPageHide() {
464
+ flush(true);
465
+ }
466
+ function onVisibilityChange() {
467
+ if (document.visibilityState === "hidden") flush(true);
468
+ }
469
+ return {
470
+ start() {
471
+ if (active) return;
472
+ active = true;
473
+ window.addEventListener("error", onError, { capture: true });
474
+ window.addEventListener("unhandledrejection", onRejection, { capture: true });
475
+ window.addEventListener("pagehide", onPageHide);
476
+ document.addEventListener("visibilitychange", onVisibilityChange);
477
+ interval = setInterval(() => flush(false), FLUSH_INTERVAL_MS);
478
+ },
479
+ stop() {
480
+ if (!active) return;
481
+ active = false;
482
+ window.removeEventListener("error", onError, { capture: true });
483
+ window.removeEventListener("unhandledrejection", onRejection, { capture: true });
484
+ window.removeEventListener("pagehide", onPageHide);
485
+ document.removeEventListener("visibilitychange", onVisibilityChange);
486
+ if (interval) clearInterval(interval);
487
+ interval = null;
488
+ flush(false);
489
+ },
490
+ flush() {
491
+ flush(false);
492
+ }
493
+ };
494
+ }
495
+
283
496
  // src/collectors/formErrors.ts
284
497
  var MAX_ENTRIES2 = 30;
285
498
  var POST_SUBMIT_CHECK_MS = 300;
@@ -977,6 +1190,8 @@ function init(config) {
977
1190
  enableFormErrors = true,
978
1191
  enableFrustration = false,
979
1192
  autoReportFrustration = false,
1193
+ enableErrorMonitoring = true,
1194
+ errorMonitoring: errorMonitoringOpts,
980
1195
  enableReplay = false,
981
1196
  replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
982
1197
  blockedHosts = [],
@@ -1012,6 +1227,23 @@ function init(config) {
1012
1227
  }
1013
1228
  const frustrationCol = enableFrustration ? createFrustrationCollector(frustrationOpts) : null;
1014
1229
  frustrationCol?.start();
1230
+ const errorCaptureCol = enableErrorMonitoring ? createErrorCaptureCollector({
1231
+ serverUrl: config.serverUrl,
1232
+ projectKey: config.projectKey,
1233
+ release: config.release,
1234
+ appVersion: config.appVersion,
1235
+ debug: config.debug,
1236
+ sampleRate: errorMonitoringOpts?.sampleRate,
1237
+ ignoreErrors: errorMonitoringOpts?.ignoreErrors,
1238
+ beforeSend: errorMonitoringOpts?.beforeSend,
1239
+ getUser: () => getSnapshot().user ?? config.user,
1240
+ getBreadcrumbs: () => ({
1241
+ consoleLogs: consoleCol?.getEntries() ?? [],
1242
+ networkErrors: networkCol?.getEntries() ?? []
1243
+ }),
1244
+ getEnvironment: _collectors?.environment ?? collectEnvironment
1245
+ }) : null;
1246
+ errorCaptureCol?.start();
1015
1247
  if (config.user) {
1016
1248
  flint.setUser(config.user);
1017
1249
  }
@@ -1021,7 +1253,8 @@ function init(config) {
1021
1253
  console: !!consoleCol,
1022
1254
  network: !!networkCol,
1023
1255
  formErrors: !!formErrorsCol,
1024
- frustration: !!frustrationCol
1256
+ frustration: !!frustrationCol,
1257
+ errorCapture: !!errorCaptureCol
1025
1258
  });
1026
1259
  instance = {
1027
1260
  config,
@@ -1029,6 +1262,7 @@ function init(config) {
1029
1262
  network: networkCol,
1030
1263
  formErrors: formErrorsCol,
1031
1264
  frustration: frustrationCol,
1265
+ errorCapture: errorCaptureCol,
1032
1266
  replayEvents,
1033
1267
  stopReplay: null
1034
1268
  };
@@ -1109,6 +1343,7 @@ function shutdown() {
1109
1343
  instance.formErrors?.stop();
1110
1344
  _setFormErrorCollector(null);
1111
1345
  instance.frustration?.stop();
1346
+ instance.errorCapture?.stop();
1112
1347
  instance.stopReplay?.();
1113
1348
  instance = null;
1114
1349
  }
@@ -1200,6 +1435,7 @@ function resolveTheme(theme) {
1200
1435
  collectEnvironment,
1201
1436
  createConsoleCollector,
1202
1437
  createDatadogReplayProvider,
1438
+ createErrorCaptureCollector,
1203
1439
  createFormErrorCollector,
1204
1440
  createFrustrationCollector,
1205
1441
  createNetworkCollector,