@diegotsi/flint-core 1.7.0 → 1.9.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;
@@ -226,6 +289,19 @@ declare const DATADOG_BLOCKED_HOSTS: string[];
226
289
  * session and returns a deep link to the Session Replay viewer.
227
290
  */
228
291
  declare function createDatadogReplayProvider(site: string): () => string | undefined;
292
+ /**
293
+ * Emits a custom Datadog RUM action at the moment a bug is reported, so it
294
+ * shows up as a clickable marker on the Session Replay timeline — making the
295
+ * report moment easy to find in long sessions.
296
+ *
297
+ * No-op when Datadog RUM (`window.DD_RUM`) is not present on the page.
298
+ */
299
+ declare function trackDatadogBugReported(meta: {
300
+ bugId: string;
301
+ severity?: string;
302
+ url?: string;
303
+ title?: string;
304
+ }): void;
229
305
 
230
306
  interface FlintInstance {
231
307
  config: FlintConfig;
@@ -233,6 +309,7 @@ interface FlintInstance {
233
309
  network: NetworkCollector | null;
234
310
  formErrors: FormErrorCollector | null;
235
311
  frustration: FrustrationCollector | null;
312
+ errorCapture: ErrorCaptureCollector | null;
236
313
  replayEvents: unknown[];
237
314
  stopReplay: (() => void) | null;
238
315
  }
@@ -282,4 +359,4 @@ interface ResolvedTheme {
282
359
  }
283
360
  declare function resolveTheme(theme: Theme): ResolvedTheme;
284
361
 
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 };
362
+ 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, trackDatadogBugReported };
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  DATADOG_BLOCKED_HOSTS,
3
- createDatadogReplayProvider
4
- } from "./chunk-HVSD45YR.js";
3
+ createDatadogReplayProvider,
4
+ trackDatadogBugReported
5
+ } from "./chunk-SO6WYKFF.js";
5
6
 
6
7
  // src/api.ts
7
8
  import { gzipSync } from "fflate";
@@ -69,8 +70,7 @@ async function submitReplay(serverUrl, projectKey, reportId, events) {
69
70
  });
70
71
  }
71
72
 
72
- // src/collectors/console.ts
73
- var MAX_ENTRIES = 50;
73
+ // src/sanitize.ts
74
74
  var SENSITIVE_PATTERNS = [
75
75
  /(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|authorization|bearer)\s*[:=]\s*["']?[^\s"',]{4,}/gi,
76
76
  /\b(sk-[a-zA-Z0-9_-]{20,})\b/g,
@@ -89,6 +89,9 @@ function sanitize(str) {
89
89
  }
90
90
  return result;
91
91
  }
92
+
93
+ // src/collectors/console.ts
94
+ var MAX_ENTRIES = 50;
92
95
  function createConsoleCollector() {
93
96
  const entries = [];
94
97
  let active = false;
@@ -208,6 +211,216 @@ function collectEnvironment() {
208
211
  };
209
212
  }
210
213
 
214
+ // src/collectors/errorCapture.ts
215
+ var MAX_MESSAGE = 1e3;
216
+ var MAX_STACK = 8e3;
217
+ var MAX_BATCH = 20;
218
+ var MAX_PAYLOAD_BYTES = 6e4;
219
+ var PER_KEY_PER_MINUTE = 10;
220
+ var GLOBAL_PAGE_CAP = 100;
221
+ var FLUSH_INTERVAL_MS = 5e3;
222
+ var THROTTLE_WINDOW_MS = 6e4;
223
+ var BREADCRUMB_CONSOLE = 10;
224
+ var BREADCRUMB_NETWORK = 10;
225
+ var DEFAULT_IGNORE = [/ResizeObserver loop/i, /^Script error\.?$/];
226
+ var EXTENSION_URL = /(chrome|moz|safari|safari-web)-extension:\/\//;
227
+ function normalizeForKey(message) {
228
+ return message.toLowerCase().replace(/\d+/g, "#").replace(/\s+/g, " ").trim().slice(0, 200);
229
+ }
230
+ function topFrame(stack) {
231
+ if (!stack) return "";
232
+ const lines = stack.split("\n").map((l) => l.trim());
233
+ return lines.find((l) => l.startsWith("at ") || /@/.test(l)) ?? "";
234
+ }
235
+ function createErrorCaptureCollector(options) {
236
+ const sampleRate = options.sampleRate ?? 1;
237
+ const ignoreList = [...DEFAULT_IGNORE, ...options.ignoreErrors ?? []];
238
+ const sessionId = `s_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
239
+ let active = false;
240
+ let queue = [];
241
+ let interval = null;
242
+ let sentCount = 0;
243
+ let environment;
244
+ const keyTimestamps = /* @__PURE__ */ new Map();
245
+ function debugLog2(...args) {
246
+ if (options.debug) console.log("[Flint]", ...args);
247
+ }
248
+ function isIgnored(message, stack, frameUrl) {
249
+ for (const entry of ignoreList) {
250
+ if (typeof entry === "string") {
251
+ if (message.includes(entry)) return true;
252
+ } else if (entry.test(message)) {
253
+ return true;
254
+ }
255
+ }
256
+ if (EXTENSION_URL.test(frameUrl ?? "") || EXTENSION_URL.test(stack ?? "")) return true;
257
+ return false;
258
+ }
259
+ function isThrottled(localKey) {
260
+ const now = Date.now();
261
+ const stamps = (keyTimestamps.get(localKey) ?? []).filter((t) => now - t < THROTTLE_WINDOW_MS);
262
+ if (stamps.length >= PER_KEY_PER_MINUTE) {
263
+ keyTimestamps.set(localKey, stamps);
264
+ return true;
265
+ }
266
+ stamps.push(now);
267
+ keyTimestamps.set(localKey, stamps);
268
+ return false;
269
+ }
270
+ function capture(input) {
271
+ try {
272
+ if (sentCount + queue.length >= GLOBAL_PAGE_CAP) return;
273
+ let message = input.message.slice(0, MAX_MESSAGE);
274
+ if (isIgnored(message, input.stack, input.frameUrl)) return;
275
+ if (sampleRate < 1 && Math.random() >= sampleRate) return;
276
+ const localKey = `${input.type}|${normalizeForKey(message)}|${topFrame(input.stack)}`;
277
+ if (isThrottled(localKey)) return;
278
+ const pending = queue.find((e) => e._localKey === localKey);
279
+ if (pending) {
280
+ pending.count += 1;
281
+ return;
282
+ }
283
+ message = sanitize(message);
284
+ const stack = input.stack ? sanitize(input.stack.slice(0, MAX_STACK)) : void 0;
285
+ if (!environment && options.getEnvironment) {
286
+ try {
287
+ environment = options.getEnvironment();
288
+ } catch {
289
+ }
290
+ }
291
+ const user = options.getUser?.();
292
+ let event = {
293
+ type: input.type,
294
+ message,
295
+ errorClass: input.errorClass,
296
+ stack,
297
+ url: typeof location !== "undefined" ? location.href : "",
298
+ timestamp: Date.now(),
299
+ release: options.release,
300
+ appVersion: options.appVersion,
301
+ userId: user?.id,
302
+ sessionId,
303
+ browser: environment?.browser,
304
+ os: environment?.os,
305
+ count: 1,
306
+ breadcrumbs: options.getBreadcrumbs ? {
307
+ consoleLogs: options.getBreadcrumbs().consoleLogs.slice(-BREADCRUMB_CONSOLE),
308
+ networkErrors: options.getBreadcrumbs().networkErrors.slice(-BREADCRUMB_NETWORK)
309
+ } : void 0,
310
+ _localKey: localKey
311
+ };
312
+ if (options.beforeSend) {
313
+ try {
314
+ const result = options.beforeSend(event);
315
+ if (!result) return;
316
+ event = { ...result, _localKey: localKey };
317
+ } catch {
318
+ }
319
+ }
320
+ queue.push(event);
321
+ debugLog2("Error captured", input.errorClass, message);
322
+ if (queue.length >= MAX_BATCH) flush(false);
323
+ } catch {
324
+ }
325
+ }
326
+ function serialize(events) {
327
+ const wire = events.map(({ _localKey, ...e }) => e);
328
+ let body = JSON.stringify(wire);
329
+ if (body.length > MAX_PAYLOAD_BYTES) {
330
+ body = JSON.stringify(wire.map((e) => ({ ...e, breadcrumbs: void 0 })));
331
+ if (body.length > MAX_PAYLOAD_BYTES) {
332
+ body = JSON.stringify(wire.map((e) => ({ ...e, breadcrumbs: void 0, stack: e.stack?.slice(0, 2e3) })));
333
+ }
334
+ }
335
+ return body;
336
+ }
337
+ function flush(unloading) {
338
+ if (queue.length === 0) return;
339
+ const events = queue;
340
+ queue = [];
341
+ sentCount += events.length;
342
+ const base = options.serverUrl.replace(/\/$/, "");
343
+ const url = `${base}/api/v1/error-events?project_key=${encodeURIComponent(options.projectKey)}`;
344
+ const body = serialize(events);
345
+ if (unloading && typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
346
+ const ok = navigator.sendBeacon(url, body);
347
+ debugLog2("Flushed via beacon", events.length, ok);
348
+ if (ok) return;
349
+ }
350
+ fetch(url, {
351
+ method: "POST",
352
+ headers: { "Content-Type": "text/plain;charset=UTF-8" },
353
+ body,
354
+ keepalive: unloading
355
+ }).catch(() => {
356
+ });
357
+ debugLog2("Flushed via fetch", events.length);
358
+ }
359
+ function onError(event) {
360
+ const e = event;
361
+ if (typeof e.message !== "string" && !(e.error instanceof Error)) return;
362
+ const err = e.error instanceof Error ? e.error : void 0;
363
+ capture({
364
+ type: "error",
365
+ message: err?.message ?? (e.message || "Unknown error"),
366
+ errorClass: err?.name ?? "Error",
367
+ stack: err?.stack,
368
+ frameUrl: e.filename
369
+ });
370
+ }
371
+ function onRejection(event) {
372
+ const reason = event.reason;
373
+ const err = reason instanceof Error ? reason : void 0;
374
+ let message;
375
+ if (err) {
376
+ message = err.message;
377
+ } else {
378
+ try {
379
+ message = typeof reason === "string" ? reason : JSON.stringify(reason);
380
+ } catch {
381
+ message = String(reason);
382
+ }
383
+ }
384
+ capture({
385
+ type: "unhandledrejection",
386
+ message: message || "Unhandled promise rejection",
387
+ errorClass: err?.name ?? "UnhandledRejection",
388
+ stack: err?.stack
389
+ });
390
+ }
391
+ function onPageHide() {
392
+ flush(true);
393
+ }
394
+ function onVisibilityChange() {
395
+ if (document.visibilityState === "hidden") flush(true);
396
+ }
397
+ return {
398
+ start() {
399
+ if (active) return;
400
+ active = true;
401
+ window.addEventListener("error", onError, { capture: true });
402
+ window.addEventListener("unhandledrejection", onRejection, { capture: true });
403
+ window.addEventListener("pagehide", onPageHide);
404
+ document.addEventListener("visibilitychange", onVisibilityChange);
405
+ interval = setInterval(() => flush(false), FLUSH_INTERVAL_MS);
406
+ },
407
+ stop() {
408
+ if (!active) return;
409
+ active = false;
410
+ window.removeEventListener("error", onError, { capture: true });
411
+ window.removeEventListener("unhandledrejection", onRejection, { capture: true });
412
+ window.removeEventListener("pagehide", onPageHide);
413
+ document.removeEventListener("visibilitychange", onVisibilityChange);
414
+ if (interval) clearInterval(interval);
415
+ interval = null;
416
+ flush(false);
417
+ },
418
+ flush() {
419
+ flush(false);
420
+ }
421
+ };
422
+ }
423
+
211
424
  // src/collectors/formErrors.ts
212
425
  var MAX_ENTRIES2 = 30;
213
426
  var POST_SUBMIT_CHECK_MS = 300;
@@ -902,6 +1115,8 @@ function init(config) {
902
1115
  enableFormErrors = true,
903
1116
  enableFrustration = false,
904
1117
  autoReportFrustration = false,
1118
+ enableErrorMonitoring = true,
1119
+ errorMonitoring: errorMonitoringOpts,
905
1120
  enableReplay = false,
906
1121
  replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
907
1122
  blockedHosts = [],
@@ -937,6 +1152,23 @@ function init(config) {
937
1152
  }
938
1153
  const frustrationCol = enableFrustration ? createFrustrationCollector(frustrationOpts) : null;
939
1154
  frustrationCol?.start();
1155
+ const errorCaptureCol = enableErrorMonitoring ? createErrorCaptureCollector({
1156
+ serverUrl: config.serverUrl,
1157
+ projectKey: config.projectKey,
1158
+ release: config.release,
1159
+ appVersion: config.appVersion,
1160
+ debug: config.debug,
1161
+ sampleRate: errorMonitoringOpts?.sampleRate,
1162
+ ignoreErrors: errorMonitoringOpts?.ignoreErrors,
1163
+ beforeSend: errorMonitoringOpts?.beforeSend,
1164
+ getUser: () => getSnapshot().user ?? config.user,
1165
+ getBreadcrumbs: () => ({
1166
+ consoleLogs: consoleCol?.getEntries() ?? [],
1167
+ networkErrors: networkCol?.getEntries() ?? []
1168
+ }),
1169
+ getEnvironment: _collectors?.environment ?? collectEnvironment
1170
+ }) : null;
1171
+ errorCaptureCol?.start();
940
1172
  if (config.user) {
941
1173
  flint.setUser(config.user);
942
1174
  }
@@ -946,7 +1178,8 @@ function init(config) {
946
1178
  console: !!consoleCol,
947
1179
  network: !!networkCol,
948
1180
  formErrors: !!formErrorsCol,
949
- frustration: !!frustrationCol
1181
+ frustration: !!frustrationCol,
1182
+ errorCapture: !!errorCaptureCol
950
1183
  });
951
1184
  instance = {
952
1185
  config,
@@ -954,6 +1187,7 @@ function init(config) {
954
1187
  network: networkCol,
955
1188
  formErrors: formErrorsCol,
956
1189
  frustration: frustrationCol,
1190
+ errorCapture: errorCaptureCol,
957
1191
  replayEvents,
958
1192
  stopReplay: null
959
1193
  };
@@ -1004,7 +1238,7 @@ function init(config) {
1004
1238
  if (!instance || !remote?.integrations) return;
1005
1239
  const dd = remote.integrations.datadog;
1006
1240
  if (dd?.enabled && dd.site) {
1007
- import("./datadog-I3QKI6Q3.js").then(({ createDatadogReplayProvider: createDatadogReplayProvider2 }) => {
1241
+ import("./datadog-FLEAFTUB.js").then(({ createDatadogReplayProvider: createDatadogReplayProvider2 }) => {
1008
1242
  if (!instance) return;
1009
1243
  instance.config = {
1010
1244
  ...instance.config,
@@ -1034,6 +1268,7 @@ function shutdown() {
1034
1268
  instance.formErrors?.stop();
1035
1269
  _setFormErrorCollector(null);
1036
1270
  instance.frustration?.stop();
1271
+ instance.errorCapture?.stop();
1037
1272
  instance.stopReplay?.();
1038
1273
  instance = null;
1039
1274
  }
@@ -1124,6 +1359,7 @@ export {
1124
1359
  collectEnvironment,
1125
1360
  createConsoleCollector,
1126
1361
  createDatadogReplayProvider,
1362
+ createErrorCaptureCollector,
1127
1363
  createFormErrorCollector,
1128
1364
  createFrustrationCollector,
1129
1365
  createNetworkCollector,
@@ -1131,6 +1367,7 @@ export {
1131
1367
  resolveTheme,
1132
1368
  submitReplay,
1133
1369
  submitReport,
1134
- subscribe
1370
+ subscribe,
1371
+ trackDatadogBugReported
1135
1372
  };
1136
1373
  //# sourceMappingURL=index.js.map