@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.d.ts CHANGED
@@ -75,6 +75,32 @@ interface FormErrorEntry {
75
75
  url: string;
76
76
  timestamp: number;
77
77
  }
78
+ interface ErrorBreadcrumbs {
79
+ consoleLogs: ConsoleEntry[];
80
+ networkErrors: NetworkEntry[];
81
+ }
82
+ interface ErrorEventPayload {
83
+ type: "error" | "unhandledrejection";
84
+ message: string;
85
+ errorClass: string;
86
+ stack?: string;
87
+ /** Page URL where the error occurred */
88
+ url: string;
89
+ /** Client time — orders breadcrumbs only; the server buckets by receive time */
90
+ timestamp: number;
91
+ release?: string;
92
+ appVersion?: string;
93
+ userId?: string;
94
+ /** Anonymous per-page-load session hash when no user is set */
95
+ sessionId?: string;
96
+ browser?: string;
97
+ os?: string;
98
+ /** In-batch dedup count — identical errors merged client-side before flush */
99
+ count: number;
100
+ breadcrumbs?: ErrorBreadcrumbs;
101
+ /** @internal client-side throttle/dedup key — stripped before sending */
102
+ _localKey?: string;
103
+ }
78
104
  type Locale = "pt-BR" | "en-US";
79
105
  type Theme = "light" | "dark" | ThemeOverride;
80
106
  interface ThemeOverride {
@@ -128,6 +154,16 @@ interface FlintConfig {
128
154
  enableDeadClicks?: boolean;
129
155
  };
130
156
  onFrustration?: (event: FrustrationEvent) => void;
157
+ /** Capture uncaught errors & unhandled rejections automatically. Default: true */
158
+ enableErrorMonitoring?: boolean;
159
+ errorMonitoring?: {
160
+ /** Fraction of captured errors to report, 0..1. Default: 1.0 */
161
+ sampleRate?: number;
162
+ /** Errors whose message matches any entry are dropped (string = substring match) */
163
+ ignoreErrors?: (string | RegExp)[];
164
+ /** Mutate or drop (return null) an event before it is sent */
165
+ beforeSend?: (event: ErrorEventPayload) => ErrorEventPayload | null;
166
+ };
131
167
  /** Application version string, e.g. "2.1.0" */
132
168
  appVersion?: string;
133
169
  /** Release/deploy identifier, e.g. "v2.1.0-rc1" or a commit SHA */
@@ -185,6 +221,33 @@ declare function submitReplay(serverUrl: string, projectKey: string, reportId: s
185
221
 
186
222
  declare function collectEnvironment(): EnvironmentInfo;
187
223
 
224
+ interface ErrorCaptureCollector {
225
+ start(): void;
226
+ stop(): void;
227
+ /** Flush pending events immediately (also called on pagehide/interval). */
228
+ flush(): void;
229
+ }
230
+ interface ErrorCaptureOptions {
231
+ serverUrl: string;
232
+ projectKey: string;
233
+ release?: string;
234
+ appVersion?: string;
235
+ /** Fraction of captured errors to report, 0..1. Default 1.0 */
236
+ sampleRate?: number;
237
+ /** Errors whose message matches any entry are dropped (string = substring match) */
238
+ ignoreErrors?: (string | RegExp)[];
239
+ /** Mutate or drop (return null) an event before it is queued for sending */
240
+ beforeSend?: (event: ErrorEventPayload) => ErrorEventPayload | null;
241
+ getUser?: () => FlintUser | undefined;
242
+ getBreadcrumbs?: () => {
243
+ consoleLogs: ConsoleEntry[];
244
+ networkErrors: NetworkEntry[];
245
+ };
246
+ getEnvironment?: () => EnvironmentInfo;
247
+ debug?: boolean;
248
+ }
249
+ declare function createErrorCaptureCollector(options: ErrorCaptureOptions): ErrorCaptureCollector;
250
+
188
251
  interface FormErrorCollector {
189
252
  start(): void;
190
253
  stop(): void;
@@ -233,6 +296,7 @@ interface FlintInstance {
233
296
  network: NetworkCollector | null;
234
297
  formErrors: FormErrorCollector | null;
235
298
  frustration: FrustrationCollector | null;
299
+ errorCapture: ErrorCaptureCollector | null;
236
300
  replayEvents: unknown[];
237
301
  stopReplay: (() => void) | null;
238
302
  }
@@ -282,4 +346,4 @@ interface ResolvedTheme {
282
346
  }
283
347
  declare function resolveTheme(theme: Theme): ResolvedTheme;
284
348
 
285
- export { type CollectedMeta, type ConsoleCollector, type ConsoleEntry, DATADOG_BLOCKED_HOSTS, type EnvironmentInfo, Flint, type FlintConfig, type FlintState, type FlintUser, type FlintWidgetProps, type FormErrorCollector, type FormErrorEntry, type FrustrationCollector, type FrustrationEvent, type Locale, type NetworkCollector, type NetworkEntry, type ReportPayload, type ReportResult, type ResolvedTheme, type Severity, type Theme, type ThemeOverride, _setFormErrorCollector, collectEnvironment, createConsoleCollector, createDatadogReplayProvider, createFormErrorCollector, createFrustrationCollector, createNetworkCollector, getSnapshot, resolveTheme, submitReplay, submitReport, subscribe };
349
+ export { type CollectedMeta, type ConsoleCollector, type ConsoleEntry, DATADOG_BLOCKED_HOSTS, type EnvironmentInfo, type ErrorBreadcrumbs, type ErrorCaptureCollector, type ErrorCaptureOptions, type ErrorEventPayload, Flint, type FlintConfig, type FlintState, type FlintUser, type FlintWidgetProps, type FormErrorCollector, type FormErrorEntry, type FrustrationCollector, type FrustrationEvent, type Locale, type NetworkCollector, type NetworkEntry, type ReportPayload, type ReportResult, type ResolvedTheme, type Severity, type Theme, type ThemeOverride, _setFormErrorCollector, collectEnvironment, createConsoleCollector, createDatadogReplayProvider, createErrorCaptureCollector, createFormErrorCollector, createFrustrationCollector, createNetworkCollector, getSnapshot, resolveTheme, submitReplay, submitReport, subscribe };
package/dist/index.js CHANGED
@@ -69,8 +69,7 @@ async function submitReplay(serverUrl, projectKey, reportId, events) {
69
69
  });
70
70
  }
71
71
 
72
- // src/collectors/console.ts
73
- var MAX_ENTRIES = 50;
72
+ // src/sanitize.ts
74
73
  var SENSITIVE_PATTERNS = [
75
74
  /(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|authorization|bearer)\s*[:=]\s*["']?[^\s"',]{4,}/gi,
76
75
  /\b(sk-[a-zA-Z0-9_-]{20,})\b/g,
@@ -89,6 +88,9 @@ function sanitize(str) {
89
88
  }
90
89
  return result;
91
90
  }
91
+
92
+ // src/collectors/console.ts
93
+ var MAX_ENTRIES = 50;
92
94
  function createConsoleCollector() {
93
95
  const entries = [];
94
96
  let active = false;
@@ -208,6 +210,216 @@ function collectEnvironment() {
208
210
  };
209
211
  }
210
212
 
213
+ // src/collectors/errorCapture.ts
214
+ var MAX_MESSAGE = 1e3;
215
+ var MAX_STACK = 8e3;
216
+ var MAX_BATCH = 20;
217
+ var MAX_PAYLOAD_BYTES = 6e4;
218
+ var PER_KEY_PER_MINUTE = 10;
219
+ var GLOBAL_PAGE_CAP = 100;
220
+ var FLUSH_INTERVAL_MS = 5e3;
221
+ var THROTTLE_WINDOW_MS = 6e4;
222
+ var BREADCRUMB_CONSOLE = 10;
223
+ var BREADCRUMB_NETWORK = 10;
224
+ var DEFAULT_IGNORE = [/ResizeObserver loop/i, /^Script error\.?$/];
225
+ var EXTENSION_URL = /(chrome|moz|safari|safari-web)-extension:\/\//;
226
+ function normalizeForKey(message) {
227
+ return message.toLowerCase().replace(/\d+/g, "#").replace(/\s+/g, " ").trim().slice(0, 200);
228
+ }
229
+ function topFrame(stack) {
230
+ if (!stack) return "";
231
+ const lines = stack.split("\n").map((l) => l.trim());
232
+ return lines.find((l) => l.startsWith("at ") || /@/.test(l)) ?? "";
233
+ }
234
+ function createErrorCaptureCollector(options) {
235
+ const sampleRate = options.sampleRate ?? 1;
236
+ const ignoreList = [...DEFAULT_IGNORE, ...options.ignoreErrors ?? []];
237
+ const sessionId = `s_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
238
+ let active = false;
239
+ let queue = [];
240
+ let interval = null;
241
+ let sentCount = 0;
242
+ let environment;
243
+ const keyTimestamps = /* @__PURE__ */ new Map();
244
+ function debugLog2(...args) {
245
+ if (options.debug) console.log("[Flint]", ...args);
246
+ }
247
+ function isIgnored(message, stack, frameUrl) {
248
+ for (const entry of ignoreList) {
249
+ if (typeof entry === "string") {
250
+ if (message.includes(entry)) return true;
251
+ } else if (entry.test(message)) {
252
+ return true;
253
+ }
254
+ }
255
+ if (EXTENSION_URL.test(frameUrl ?? "") || EXTENSION_URL.test(stack ?? "")) return true;
256
+ return false;
257
+ }
258
+ function isThrottled(localKey) {
259
+ const now = Date.now();
260
+ const stamps = (keyTimestamps.get(localKey) ?? []).filter((t) => now - t < THROTTLE_WINDOW_MS);
261
+ if (stamps.length >= PER_KEY_PER_MINUTE) {
262
+ keyTimestamps.set(localKey, stamps);
263
+ return true;
264
+ }
265
+ stamps.push(now);
266
+ keyTimestamps.set(localKey, stamps);
267
+ return false;
268
+ }
269
+ function capture(input) {
270
+ try {
271
+ if (sentCount + queue.length >= GLOBAL_PAGE_CAP) return;
272
+ let message = input.message.slice(0, MAX_MESSAGE);
273
+ if (isIgnored(message, input.stack, input.frameUrl)) return;
274
+ if (sampleRate < 1 && Math.random() >= sampleRate) return;
275
+ const localKey = `${input.type}|${normalizeForKey(message)}|${topFrame(input.stack)}`;
276
+ if (isThrottled(localKey)) return;
277
+ const pending = queue.find((e) => e._localKey === localKey);
278
+ if (pending) {
279
+ pending.count += 1;
280
+ return;
281
+ }
282
+ message = sanitize(message);
283
+ const stack = input.stack ? sanitize(input.stack.slice(0, MAX_STACK)) : void 0;
284
+ if (!environment && options.getEnvironment) {
285
+ try {
286
+ environment = options.getEnvironment();
287
+ } catch {
288
+ }
289
+ }
290
+ const user = options.getUser?.();
291
+ let event = {
292
+ type: input.type,
293
+ message,
294
+ errorClass: input.errorClass,
295
+ stack,
296
+ url: typeof location !== "undefined" ? location.href : "",
297
+ timestamp: Date.now(),
298
+ release: options.release,
299
+ appVersion: options.appVersion,
300
+ userId: user?.id,
301
+ sessionId,
302
+ browser: environment?.browser,
303
+ os: environment?.os,
304
+ count: 1,
305
+ breadcrumbs: options.getBreadcrumbs ? {
306
+ consoleLogs: options.getBreadcrumbs().consoleLogs.slice(-BREADCRUMB_CONSOLE),
307
+ networkErrors: options.getBreadcrumbs().networkErrors.slice(-BREADCRUMB_NETWORK)
308
+ } : void 0,
309
+ _localKey: localKey
310
+ };
311
+ if (options.beforeSend) {
312
+ try {
313
+ const result = options.beforeSend(event);
314
+ if (!result) return;
315
+ event = { ...result, _localKey: localKey };
316
+ } catch {
317
+ }
318
+ }
319
+ queue.push(event);
320
+ debugLog2("Error captured", input.errorClass, message);
321
+ if (queue.length >= MAX_BATCH) flush(false);
322
+ } catch {
323
+ }
324
+ }
325
+ function serialize(events) {
326
+ const wire = events.map(({ _localKey, ...e }) => e);
327
+ let body = JSON.stringify(wire);
328
+ if (body.length > MAX_PAYLOAD_BYTES) {
329
+ body = JSON.stringify(wire.map((e) => ({ ...e, breadcrumbs: void 0 })));
330
+ if (body.length > MAX_PAYLOAD_BYTES) {
331
+ body = JSON.stringify(wire.map((e) => ({ ...e, breadcrumbs: void 0, stack: e.stack?.slice(0, 2e3) })));
332
+ }
333
+ }
334
+ return body;
335
+ }
336
+ function flush(unloading) {
337
+ if (queue.length === 0) return;
338
+ const events = queue;
339
+ queue = [];
340
+ sentCount += events.length;
341
+ const base = options.serverUrl.replace(/\/$/, "");
342
+ const url = `${base}/api/v1/error-events?project_key=${encodeURIComponent(options.projectKey)}`;
343
+ const body = serialize(events);
344
+ if (unloading && typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
345
+ const ok = navigator.sendBeacon(url, body);
346
+ debugLog2("Flushed via beacon", events.length, ok);
347
+ if (ok) return;
348
+ }
349
+ fetch(url, {
350
+ method: "POST",
351
+ headers: { "Content-Type": "text/plain;charset=UTF-8" },
352
+ body,
353
+ keepalive: unloading
354
+ }).catch(() => {
355
+ });
356
+ debugLog2("Flushed via fetch", events.length);
357
+ }
358
+ function onError(event) {
359
+ const e = event;
360
+ if (typeof e.message !== "string" && !(e.error instanceof Error)) return;
361
+ const err = e.error instanceof Error ? e.error : void 0;
362
+ capture({
363
+ type: "error",
364
+ message: err?.message ?? (e.message || "Unknown error"),
365
+ errorClass: err?.name ?? "Error",
366
+ stack: err?.stack,
367
+ frameUrl: e.filename
368
+ });
369
+ }
370
+ function onRejection(event) {
371
+ const reason = event.reason;
372
+ const err = reason instanceof Error ? reason : void 0;
373
+ let message;
374
+ if (err) {
375
+ message = err.message;
376
+ } else {
377
+ try {
378
+ message = typeof reason === "string" ? reason : JSON.stringify(reason);
379
+ } catch {
380
+ message = String(reason);
381
+ }
382
+ }
383
+ capture({
384
+ type: "unhandledrejection",
385
+ message: message || "Unhandled promise rejection",
386
+ errorClass: err?.name ?? "UnhandledRejection",
387
+ stack: err?.stack
388
+ });
389
+ }
390
+ function onPageHide() {
391
+ flush(true);
392
+ }
393
+ function onVisibilityChange() {
394
+ if (document.visibilityState === "hidden") flush(true);
395
+ }
396
+ return {
397
+ start() {
398
+ if (active) return;
399
+ active = true;
400
+ window.addEventListener("error", onError, { capture: true });
401
+ window.addEventListener("unhandledrejection", onRejection, { capture: true });
402
+ window.addEventListener("pagehide", onPageHide);
403
+ document.addEventListener("visibilitychange", onVisibilityChange);
404
+ interval = setInterval(() => flush(false), FLUSH_INTERVAL_MS);
405
+ },
406
+ stop() {
407
+ if (!active) return;
408
+ active = false;
409
+ window.removeEventListener("error", onError, { capture: true });
410
+ window.removeEventListener("unhandledrejection", onRejection, { capture: true });
411
+ window.removeEventListener("pagehide", onPageHide);
412
+ document.removeEventListener("visibilitychange", onVisibilityChange);
413
+ if (interval) clearInterval(interval);
414
+ interval = null;
415
+ flush(false);
416
+ },
417
+ flush() {
418
+ flush(false);
419
+ }
420
+ };
421
+ }
422
+
211
423
  // src/collectors/formErrors.ts
212
424
  var MAX_ENTRIES2 = 30;
213
425
  var POST_SUBMIT_CHECK_MS = 300;
@@ -902,6 +1114,8 @@ function init(config) {
902
1114
  enableFormErrors = true,
903
1115
  enableFrustration = false,
904
1116
  autoReportFrustration = false,
1117
+ enableErrorMonitoring = true,
1118
+ errorMonitoring: errorMonitoringOpts,
905
1119
  enableReplay = false,
906
1120
  replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
907
1121
  blockedHosts = [],
@@ -937,6 +1151,23 @@ function init(config) {
937
1151
  }
938
1152
  const frustrationCol = enableFrustration ? createFrustrationCollector(frustrationOpts) : null;
939
1153
  frustrationCol?.start();
1154
+ const errorCaptureCol = enableErrorMonitoring ? createErrorCaptureCollector({
1155
+ serverUrl: config.serverUrl,
1156
+ projectKey: config.projectKey,
1157
+ release: config.release,
1158
+ appVersion: config.appVersion,
1159
+ debug: config.debug,
1160
+ sampleRate: errorMonitoringOpts?.sampleRate,
1161
+ ignoreErrors: errorMonitoringOpts?.ignoreErrors,
1162
+ beforeSend: errorMonitoringOpts?.beforeSend,
1163
+ getUser: () => getSnapshot().user ?? config.user,
1164
+ getBreadcrumbs: () => ({
1165
+ consoleLogs: consoleCol?.getEntries() ?? [],
1166
+ networkErrors: networkCol?.getEntries() ?? []
1167
+ }),
1168
+ getEnvironment: _collectors?.environment ?? collectEnvironment
1169
+ }) : null;
1170
+ errorCaptureCol?.start();
940
1171
  if (config.user) {
941
1172
  flint.setUser(config.user);
942
1173
  }
@@ -946,7 +1177,8 @@ function init(config) {
946
1177
  console: !!consoleCol,
947
1178
  network: !!networkCol,
948
1179
  formErrors: !!formErrorsCol,
949
- frustration: !!frustrationCol
1180
+ frustration: !!frustrationCol,
1181
+ errorCapture: !!errorCaptureCol
950
1182
  });
951
1183
  instance = {
952
1184
  config,
@@ -954,6 +1186,7 @@ function init(config) {
954
1186
  network: networkCol,
955
1187
  formErrors: formErrorsCol,
956
1188
  frustration: frustrationCol,
1189
+ errorCapture: errorCaptureCol,
957
1190
  replayEvents,
958
1191
  stopReplay: null
959
1192
  };
@@ -1034,6 +1267,7 @@ function shutdown() {
1034
1267
  instance.formErrors?.stop();
1035
1268
  _setFormErrorCollector(null);
1036
1269
  instance.frustration?.stop();
1270
+ instance.errorCapture?.stop();
1037
1271
  instance.stopReplay?.();
1038
1272
  instance = null;
1039
1273
  }
@@ -1124,6 +1358,7 @@ export {
1124
1358
  collectEnvironment,
1125
1359
  createConsoleCollector,
1126
1360
  createDatadogReplayProvider,
1361
+ createErrorCaptureCollector,
1127
1362
  createFormErrorCollector,
1128
1363
  createFrustrationCollector,
1129
1364
  createNetworkCollector,