@diegotsi/flint-core 1.6.1 → 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,
@@ -111,6 +112,8 @@ async function submitReport(serverUrl, projectKey, payload, screenshot) {
111
112
  form.append("severity", payload.severity);
112
113
  if (payload.url) form.append("url", payload.url);
113
114
  if (payload.meta) form.append("meta", JSON.stringify(payload.meta));
115
+ if (payload.appVersion) form.append("appVersion", payload.appVersion);
116
+ if (payload.release) form.append("release", payload.release);
114
117
  form.append("screenshot", screenshot);
115
118
  body = form;
116
119
  } else {
@@ -139,8 +142,7 @@ async function submitReplay(serverUrl, projectKey, reportId, events) {
139
142
  });
140
143
  }
141
144
 
142
- // src/collectors/console.ts
143
- var MAX_ENTRIES = 50;
145
+ // src/sanitize.ts
144
146
  var SENSITIVE_PATTERNS = [
145
147
  /(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|authorization|bearer)\s*[:=]\s*["']?[^\s"',]{4,}/gi,
146
148
  /\b(sk-[a-zA-Z0-9_-]{20,})\b/g,
@@ -159,6 +161,9 @@ function sanitize(str) {
159
161
  }
160
162
  return result;
161
163
  }
164
+
165
+ // src/collectors/console.ts
166
+ var MAX_ENTRIES = 50;
162
167
  function createConsoleCollector() {
163
168
  const entries = [];
164
169
  let active = false;
@@ -278,6 +283,216 @@ function collectEnvironment() {
278
283
  };
279
284
  }
280
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
+
281
496
  // src/collectors/formErrors.ts
282
497
  var MAX_ENTRIES2 = 30;
283
498
  var POST_SUBMIT_CHECK_MS = 300;
@@ -975,6 +1190,8 @@ function init(config) {
975
1190
  enableFormErrors = true,
976
1191
  enableFrustration = false,
977
1192
  autoReportFrustration = false,
1193
+ enableErrorMonitoring = true,
1194
+ errorMonitoring: errorMonitoringOpts,
978
1195
  enableReplay = false,
979
1196
  replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
980
1197
  blockedHosts = [],
@@ -1010,6 +1227,23 @@ function init(config) {
1010
1227
  }
1011
1228
  const frustrationCol = enableFrustration ? createFrustrationCollector(frustrationOpts) : null;
1012
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();
1013
1247
  if (config.user) {
1014
1248
  flint.setUser(config.user);
1015
1249
  }
@@ -1019,7 +1253,8 @@ function init(config) {
1019
1253
  console: !!consoleCol,
1020
1254
  network: !!networkCol,
1021
1255
  formErrors: !!formErrorsCol,
1022
- frustration: !!frustrationCol
1256
+ frustration: !!frustrationCol,
1257
+ errorCapture: !!errorCaptureCol
1023
1258
  });
1024
1259
  instance = {
1025
1260
  config,
@@ -1027,6 +1262,7 @@ function init(config) {
1027
1262
  network: networkCol,
1028
1263
  formErrors: formErrorsCol,
1029
1264
  frustration: frustrationCol,
1265
+ errorCapture: errorCaptureCol,
1030
1266
  replayEvents,
1031
1267
  stopReplay: null
1032
1268
  };
@@ -1056,6 +1292,8 @@ function init(config) {
1056
1292
  severity: event.type === "error_loop" ? "P1" : event.type === "rage_click" ? "P2" : "P3",
1057
1293
  url: event.url,
1058
1294
  source: "auto_capture",
1295
+ appVersion: config.appVersion,
1296
+ release: config.release,
1059
1297
  meta: {
1060
1298
  ...config.meta,
1061
1299
  environment: getEnvironment(),
@@ -1105,6 +1343,7 @@ function shutdown() {
1105
1343
  instance.formErrors?.stop();
1106
1344
  _setFormErrorCollector(null);
1107
1345
  instance.frustration?.stop();
1346
+ instance.errorCapture?.stop();
1108
1347
  instance.stopReplay?.();
1109
1348
  instance = null;
1110
1349
  }
@@ -1196,6 +1435,7 @@ function resolveTheme(theme) {
1196
1435
  collectEnvironment,
1197
1436
  createConsoleCollector,
1198
1437
  createDatadogReplayProvider,
1438
+ createErrorCaptureCollector,
1199
1439
  createFormErrorCollector,
1200
1440
  createFrustrationCollector,
1201
1441
  createNetworkCollector,