@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.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,20 @@ 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
+ };
167
+ /** Application version string, e.g. "2.1.0" */
168
+ appVersion?: string;
169
+ /** Release/deploy identifier, e.g. "v2.1.0-rc1" or a commit SHA */
170
+ release?: string;
131
171
  /** Returns an external session replay URL at report time (Datadog, FullStory, LogRocket, etc.) */
132
172
  externalReplayProvider?: () => string | undefined;
133
173
  /** @internal Inject platform-specific collectors (e.g. React Native) */
@@ -164,6 +204,8 @@ interface ReportPayload {
164
204
  label?: string;
165
205
  source?: "widget" | "auto_capture" | "text_issue" | "feature_request";
166
206
  type?: "BUG" | "FEATURE_REQUEST";
207
+ appVersion?: string;
208
+ release?: string;
167
209
  }
168
210
  interface ReportResult {
169
211
  id: string;
@@ -179,6 +221,33 @@ declare function submitReplay(serverUrl: string, projectKey: string, reportId: s
179
221
 
180
222
  declare function collectEnvironment(): EnvironmentInfo;
181
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
+
182
251
  interface FormErrorCollector {
183
252
  start(): void;
184
253
  stop(): void;
@@ -227,6 +296,7 @@ interface FlintInstance {
227
296
  network: NetworkCollector | null;
228
297
  formErrors: FormErrorCollector | null;
229
298
  frustration: FrustrationCollector | null;
299
+ errorCapture: ErrorCaptureCollector | null;
230
300
  replayEvents: unknown[];
231
301
  stopReplay: (() => void) | null;
232
302
  }
@@ -276,4 +346,4 @@ interface ResolvedTheme {
276
346
  }
277
347
  declare function resolveTheme(theme: Theme): ResolvedTheme;
278
348
 
279
- 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
@@ -39,6 +39,8 @@ async function submitReport(serverUrl, projectKey, payload, screenshot) {
39
39
  form.append("severity", payload.severity);
40
40
  if (payload.url) form.append("url", payload.url);
41
41
  if (payload.meta) form.append("meta", JSON.stringify(payload.meta));
42
+ if (payload.appVersion) form.append("appVersion", payload.appVersion);
43
+ if (payload.release) form.append("release", payload.release);
42
44
  form.append("screenshot", screenshot);
43
45
  body = form;
44
46
  } else {
@@ -67,8 +69,7 @@ async function submitReplay(serverUrl, projectKey, reportId, events) {
67
69
  });
68
70
  }
69
71
 
70
- // src/collectors/console.ts
71
- var MAX_ENTRIES = 50;
72
+ // src/sanitize.ts
72
73
  var SENSITIVE_PATTERNS = [
73
74
  /(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|authorization|bearer)\s*[:=]\s*["']?[^\s"',]{4,}/gi,
74
75
  /\b(sk-[a-zA-Z0-9_-]{20,})\b/g,
@@ -87,6 +88,9 @@ function sanitize(str) {
87
88
  }
88
89
  return result;
89
90
  }
91
+
92
+ // src/collectors/console.ts
93
+ var MAX_ENTRIES = 50;
90
94
  function createConsoleCollector() {
91
95
  const entries = [];
92
96
  let active = false;
@@ -206,6 +210,216 @@ function collectEnvironment() {
206
210
  };
207
211
  }
208
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
+
209
423
  // src/collectors/formErrors.ts
210
424
  var MAX_ENTRIES2 = 30;
211
425
  var POST_SUBMIT_CHECK_MS = 300;
@@ -900,6 +1114,8 @@ function init(config) {
900
1114
  enableFormErrors = true,
901
1115
  enableFrustration = false,
902
1116
  autoReportFrustration = false,
1117
+ enableErrorMonitoring = true,
1118
+ errorMonitoring: errorMonitoringOpts,
903
1119
  enableReplay = false,
904
1120
  replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
905
1121
  blockedHosts = [],
@@ -935,6 +1151,23 @@ function init(config) {
935
1151
  }
936
1152
  const frustrationCol = enableFrustration ? createFrustrationCollector(frustrationOpts) : null;
937
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();
938
1171
  if (config.user) {
939
1172
  flint.setUser(config.user);
940
1173
  }
@@ -944,7 +1177,8 @@ function init(config) {
944
1177
  console: !!consoleCol,
945
1178
  network: !!networkCol,
946
1179
  formErrors: !!formErrorsCol,
947
- frustration: !!frustrationCol
1180
+ frustration: !!frustrationCol,
1181
+ errorCapture: !!errorCaptureCol
948
1182
  });
949
1183
  instance = {
950
1184
  config,
@@ -952,6 +1186,7 @@ function init(config) {
952
1186
  network: networkCol,
953
1187
  formErrors: formErrorsCol,
954
1188
  frustration: frustrationCol,
1189
+ errorCapture: errorCaptureCol,
955
1190
  replayEvents,
956
1191
  stopReplay: null
957
1192
  };
@@ -981,6 +1216,8 @@ function init(config) {
981
1216
  severity: event.type === "error_loop" ? "P1" : event.type === "rage_click" ? "P2" : "P3",
982
1217
  url: event.url,
983
1218
  source: "auto_capture",
1219
+ appVersion: config.appVersion,
1220
+ release: config.release,
984
1221
  meta: {
985
1222
  ...config.meta,
986
1223
  environment: getEnvironment(),
@@ -1030,6 +1267,7 @@ function shutdown() {
1030
1267
  instance.formErrors?.stop();
1031
1268
  _setFormErrorCollector(null);
1032
1269
  instance.frustration?.stop();
1270
+ instance.errorCapture?.stop();
1033
1271
  instance.stopReplay?.();
1034
1272
  instance = null;
1035
1273
  }
@@ -1120,6 +1358,7 @@ export {
1120
1358
  collectEnvironment,
1121
1359
  createConsoleCollector,
1122
1360
  createDatadogReplayProvider,
1361
+ createErrorCaptureCollector,
1123
1362
  createFormErrorCollector,
1124
1363
  createFrustrationCollector,
1125
1364
  createNetworkCollector,