@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.
@@ -21,9 +21,22 @@ function createDatadogReplayProvider(site) {
21
21
  return void 0;
22
22
  };
23
23
  }
24
+ function trackDatadogBugReported(meta) {
25
+ try {
26
+ const ddRum = window.DD_RUM;
27
+ ddRum?.addAction?.("flint.bug_reported", {
28
+ bug_id: meta.bugId,
29
+ severity: meta.severity,
30
+ url: meta.url,
31
+ title: meta.title
32
+ });
33
+ } catch {
34
+ }
35
+ }
24
36
 
25
37
  export {
26
38
  DATADOG_BLOCKED_HOSTS,
27
- createDatadogReplayProvider
39
+ createDatadogReplayProvider,
40
+ trackDatadogBugReported
28
41
  };
29
- //# sourceMappingURL=chunk-HVSD45YR.js.map
42
+ //# sourceMappingURL=chunk-SO6WYKFF.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/integrations/datadog.ts"],"sourcesContent":["/**\n * Datadog RUM integration — opt-in helper.\n *\n * Usage:\n * import { Flint } from \"@diegotsi/flint-core\";\n * import { createDatadogReplayProvider, DATADOG_BLOCKED_HOSTS } from \"@diegotsi/flint-core\";\n *\n * Flint.init({\n * projectKey: \"...\",\n * serverUrl: \"...\",\n * externalReplayProvider: createDatadogReplayProvider(\"app.datadoghq.com\"),\n * blockedHosts: DATADOG_BLOCKED_HOSTS,\n * });\n */\n\n/** Datadog intake hosts to exclude from network capture. */\nexport const DATADOG_BLOCKED_HOSTS = [\n \"browser-intake-datadoghq.com\",\n \"rum.browser-intake-datadoghq.com\",\n \"logs.browser-intake-datadoghq.com\",\n \"session-replay.browser-intake-datadoghq.com\",\n];\n\n/**\n * Creates an `externalReplayProvider` that reads the current Datadog RUM\n * session and returns a deep link to the Session Replay viewer.\n */\nexport function createDatadogReplayProvider(site: string): () => string | undefined {\n return () => {\n try {\n const ddRum = (window as unknown as Record<string, unknown>).DD_RUM as\n | { getInternalContext?: () => { session_id?: string } | undefined }\n | undefined;\n const ctx = ddRum?.getInternalContext?.();\n if (ctx?.session_id) {\n const ts = Date.now();\n const fromTs = ts - 30_000;\n const toTs = ts + 5_000;\n return `https://${site}/rum/replay/sessions/${ctx.session_id}?from_ts=${fromTs}&to_ts=${toTs}&tab=replay&live=false`;\n }\n } catch {\n // DD_RUM not available — silently skip\n }\n return undefined;\n };\n}\n"],"mappings":";AAgBO,IAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,SAAS,4BAA4B,MAAwC;AAClF,SAAO,MAAM;AACX,QAAI;AACF,YAAM,QAAS,OAA8C;AAG7D,YAAM,MAAM,OAAO,qBAAqB;AACxC,UAAI,KAAK,YAAY;AACnB,cAAM,KAAK,KAAK,IAAI;AACpB,cAAM,SAAS,KAAK;AACpB,cAAM,OAAO,KAAK;AAClB,eAAO,WAAW,IAAI,wBAAwB,IAAI,UAAU,YAAY,MAAM,UAAU,IAAI;AAAA,MAC9F;AAAA,IACF,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/integrations/datadog.ts"],"sourcesContent":["/**\n * Datadog RUM integration — opt-in helper.\n *\n * Usage:\n * import { Flint } from \"@diegotsi/flint-core\";\n * import { createDatadogReplayProvider, DATADOG_BLOCKED_HOSTS } from \"@diegotsi/flint-core\";\n *\n * Flint.init({\n * projectKey: \"...\",\n * serverUrl: \"...\",\n * externalReplayProvider: createDatadogReplayProvider(\"app.datadoghq.com\"),\n * blockedHosts: DATADOG_BLOCKED_HOSTS,\n * });\n */\n\n/** Datadog intake hosts to exclude from network capture. */\nexport const DATADOG_BLOCKED_HOSTS = [\n \"browser-intake-datadoghq.com\",\n \"rum.browser-intake-datadoghq.com\",\n \"logs.browser-intake-datadoghq.com\",\n \"session-replay.browser-intake-datadoghq.com\",\n];\n\n/**\n * Creates an `externalReplayProvider` that reads the current Datadog RUM\n * session and returns a deep link to the Session Replay viewer.\n */\nexport function createDatadogReplayProvider(site: string): () => string | undefined {\n return () => {\n try {\n const ddRum = (window as unknown as Record<string, unknown>).DD_RUM as\n | { getInternalContext?: () => { session_id?: string } | undefined }\n | undefined;\n const ctx = ddRum?.getInternalContext?.();\n if (ctx?.session_id) {\n const ts = Date.now();\n const fromTs = ts - 30_000;\n const toTs = ts + 5_000;\n return `https://${site}/rum/replay/sessions/${ctx.session_id}?from_ts=${fromTs}&to_ts=${toTs}&tab=replay&live=false`;\n }\n } catch {\n // DD_RUM not available — silently skip\n }\n return undefined;\n };\n}\n\n/**\n * Emits a custom Datadog RUM action at the moment a bug is reported, so it\n * shows up as a clickable marker on the Session Replay timeline — making the\n * report moment easy to find in long sessions.\n *\n * No-op when Datadog RUM (`window.DD_RUM`) is not present on the page.\n */\nexport function trackDatadogBugReported(meta: {\n bugId: string;\n severity?: string;\n url?: string;\n title?: string;\n}): void {\n try {\n const ddRum = (window as unknown as Record<string, unknown>).DD_RUM as\n | { addAction?: (name: string, context?: Record<string, unknown>) => void }\n | undefined;\n ddRum?.addAction?.(\"flint.bug_reported\", {\n bug_id: meta.bugId,\n severity: meta.severity,\n url: meta.url,\n title: meta.title,\n });\n } catch {\n // DD_RUM not available — silently skip\n }\n}\n"],"mappings":";AAgBO,IAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,SAAS,4BAA4B,MAAwC;AAClF,SAAO,MAAM;AACX,QAAI;AACF,YAAM,QAAS,OAA8C;AAG7D,YAAM,MAAM,OAAO,qBAAqB;AACxC,UAAI,KAAK,YAAY;AACnB,cAAM,KAAK,KAAK,IAAI;AACpB,cAAM,SAAS,KAAK;AACpB,cAAM,OAAO,KAAK;AAClB,eAAO,WAAW,IAAI,wBAAwB,IAAI,UAAU,YAAY,MAAM,UAAU,IAAI;AAAA,MAC9F;AAAA,IACF,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT;AACF;AASO,SAAS,wBAAwB,MAK/B;AACP,MAAI;AACF,UAAM,QAAS,OAA8C;AAG7D,WAAO,YAAY,sBAAsB;AAAA,MACvC,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,MACf,KAAK,KAAK;AAAA,MACV,OAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;","names":[]}
@@ -0,0 +1,11 @@
1
+ import {
2
+ DATADOG_BLOCKED_HOSTS,
3
+ createDatadogReplayProvider,
4
+ trackDatadogBugReported
5
+ } from "./chunk-SO6WYKFF.js";
6
+ export {
7
+ DATADOG_BLOCKED_HOSTS,
8
+ createDatadogReplayProvider,
9
+ trackDatadogBugReported
10
+ };
11
+ //# sourceMappingURL=datadog-FLEAFTUB.js.map
package/dist/index.cjs CHANGED
@@ -24,7 +24,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
24
24
  var datadog_exports = {};
25
25
  __export(datadog_exports, {
26
26
  DATADOG_BLOCKED_HOSTS: () => DATADOG_BLOCKED_HOSTS,
27
- createDatadogReplayProvider: () => createDatadogReplayProvider
27
+ createDatadogReplayProvider: () => createDatadogReplayProvider,
28
+ trackDatadogBugReported: () => trackDatadogBugReported
28
29
  });
29
30
  function createDatadogReplayProvider(site) {
30
31
  return () => {
@@ -42,6 +43,18 @@ function createDatadogReplayProvider(site) {
42
43
  return void 0;
43
44
  };
44
45
  }
46
+ function trackDatadogBugReported(meta) {
47
+ try {
48
+ const ddRum = window.DD_RUM;
49
+ ddRum?.addAction?.("flint.bug_reported", {
50
+ bug_id: meta.bugId,
51
+ severity: meta.severity,
52
+ url: meta.url,
53
+ title: meta.title
54
+ });
55
+ } catch {
56
+ }
57
+ }
45
58
  var DATADOG_BLOCKED_HOSTS;
46
59
  var init_datadog = __esm({
47
60
  "src/integrations/datadog.ts"() {
@@ -64,6 +77,7 @@ __export(index_exports, {
64
77
  collectEnvironment: () => collectEnvironment,
65
78
  createConsoleCollector: () => createConsoleCollector,
66
79
  createDatadogReplayProvider: () => createDatadogReplayProvider,
80
+ createErrorCaptureCollector: () => createErrorCaptureCollector,
67
81
  createFormErrorCollector: () => createFormErrorCollector,
68
82
  createFrustrationCollector: () => createFrustrationCollector,
69
83
  createNetworkCollector: () => createNetworkCollector,
@@ -71,7 +85,8 @@ __export(index_exports, {
71
85
  resolveTheme: () => resolveTheme,
72
86
  submitReplay: () => submitReplay,
73
87
  submitReport: () => submitReport,
74
- subscribe: () => subscribe
88
+ subscribe: () => subscribe,
89
+ trackDatadogBugReported: () => trackDatadogBugReported
75
90
  });
76
91
  module.exports = __toCommonJS(index_exports);
77
92
 
@@ -141,8 +156,7 @@ async function submitReplay(serverUrl, projectKey, reportId, events) {
141
156
  });
142
157
  }
143
158
 
144
- // src/collectors/console.ts
145
- var MAX_ENTRIES = 50;
159
+ // src/sanitize.ts
146
160
  var SENSITIVE_PATTERNS = [
147
161
  /(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|authorization|bearer)\s*[:=]\s*["']?[^\s"',]{4,}/gi,
148
162
  /\b(sk-[a-zA-Z0-9_-]{20,})\b/g,
@@ -161,6 +175,9 @@ function sanitize(str) {
161
175
  }
162
176
  return result;
163
177
  }
178
+
179
+ // src/collectors/console.ts
180
+ var MAX_ENTRIES = 50;
164
181
  function createConsoleCollector() {
165
182
  const entries = [];
166
183
  let active = false;
@@ -280,6 +297,216 @@ function collectEnvironment() {
280
297
  };
281
298
  }
282
299
 
300
+ // src/collectors/errorCapture.ts
301
+ var MAX_MESSAGE = 1e3;
302
+ var MAX_STACK = 8e3;
303
+ var MAX_BATCH = 20;
304
+ var MAX_PAYLOAD_BYTES = 6e4;
305
+ var PER_KEY_PER_MINUTE = 10;
306
+ var GLOBAL_PAGE_CAP = 100;
307
+ var FLUSH_INTERVAL_MS = 5e3;
308
+ var THROTTLE_WINDOW_MS = 6e4;
309
+ var BREADCRUMB_CONSOLE = 10;
310
+ var BREADCRUMB_NETWORK = 10;
311
+ var DEFAULT_IGNORE = [/ResizeObserver loop/i, /^Script error\.?$/];
312
+ var EXTENSION_URL = /(chrome|moz|safari|safari-web)-extension:\/\//;
313
+ function normalizeForKey(message) {
314
+ return message.toLowerCase().replace(/\d+/g, "#").replace(/\s+/g, " ").trim().slice(0, 200);
315
+ }
316
+ function topFrame(stack) {
317
+ if (!stack) return "";
318
+ const lines = stack.split("\n").map((l) => l.trim());
319
+ return lines.find((l) => l.startsWith("at ") || /@/.test(l)) ?? "";
320
+ }
321
+ function createErrorCaptureCollector(options) {
322
+ const sampleRate = options.sampleRate ?? 1;
323
+ const ignoreList = [...DEFAULT_IGNORE, ...options.ignoreErrors ?? []];
324
+ const sessionId = `s_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
325
+ let active = false;
326
+ let queue = [];
327
+ let interval = null;
328
+ let sentCount = 0;
329
+ let environment;
330
+ const keyTimestamps = /* @__PURE__ */ new Map();
331
+ function debugLog2(...args) {
332
+ if (options.debug) console.log("[Flint]", ...args);
333
+ }
334
+ function isIgnored(message, stack, frameUrl) {
335
+ for (const entry of ignoreList) {
336
+ if (typeof entry === "string") {
337
+ if (message.includes(entry)) return true;
338
+ } else if (entry.test(message)) {
339
+ return true;
340
+ }
341
+ }
342
+ if (EXTENSION_URL.test(frameUrl ?? "") || EXTENSION_URL.test(stack ?? "")) return true;
343
+ return false;
344
+ }
345
+ function isThrottled(localKey) {
346
+ const now = Date.now();
347
+ const stamps = (keyTimestamps.get(localKey) ?? []).filter((t) => now - t < THROTTLE_WINDOW_MS);
348
+ if (stamps.length >= PER_KEY_PER_MINUTE) {
349
+ keyTimestamps.set(localKey, stamps);
350
+ return true;
351
+ }
352
+ stamps.push(now);
353
+ keyTimestamps.set(localKey, stamps);
354
+ return false;
355
+ }
356
+ function capture(input) {
357
+ try {
358
+ if (sentCount + queue.length >= GLOBAL_PAGE_CAP) return;
359
+ let message = input.message.slice(0, MAX_MESSAGE);
360
+ if (isIgnored(message, input.stack, input.frameUrl)) return;
361
+ if (sampleRate < 1 && Math.random() >= sampleRate) return;
362
+ const localKey = `${input.type}|${normalizeForKey(message)}|${topFrame(input.stack)}`;
363
+ if (isThrottled(localKey)) return;
364
+ const pending = queue.find((e) => e._localKey === localKey);
365
+ if (pending) {
366
+ pending.count += 1;
367
+ return;
368
+ }
369
+ message = sanitize(message);
370
+ const stack = input.stack ? sanitize(input.stack.slice(0, MAX_STACK)) : void 0;
371
+ if (!environment && options.getEnvironment) {
372
+ try {
373
+ environment = options.getEnvironment();
374
+ } catch {
375
+ }
376
+ }
377
+ const user = options.getUser?.();
378
+ let event = {
379
+ type: input.type,
380
+ message,
381
+ errorClass: input.errorClass,
382
+ stack,
383
+ url: typeof location !== "undefined" ? location.href : "",
384
+ timestamp: Date.now(),
385
+ release: options.release,
386
+ appVersion: options.appVersion,
387
+ userId: user?.id,
388
+ sessionId,
389
+ browser: environment?.browser,
390
+ os: environment?.os,
391
+ count: 1,
392
+ breadcrumbs: options.getBreadcrumbs ? {
393
+ consoleLogs: options.getBreadcrumbs().consoleLogs.slice(-BREADCRUMB_CONSOLE),
394
+ networkErrors: options.getBreadcrumbs().networkErrors.slice(-BREADCRUMB_NETWORK)
395
+ } : void 0,
396
+ _localKey: localKey
397
+ };
398
+ if (options.beforeSend) {
399
+ try {
400
+ const result = options.beforeSend(event);
401
+ if (!result) return;
402
+ event = { ...result, _localKey: localKey };
403
+ } catch {
404
+ }
405
+ }
406
+ queue.push(event);
407
+ debugLog2("Error captured", input.errorClass, message);
408
+ if (queue.length >= MAX_BATCH) flush(false);
409
+ } catch {
410
+ }
411
+ }
412
+ function serialize(events) {
413
+ const wire = events.map(({ _localKey, ...e }) => e);
414
+ let body = JSON.stringify(wire);
415
+ if (body.length > MAX_PAYLOAD_BYTES) {
416
+ body = JSON.stringify(wire.map((e) => ({ ...e, breadcrumbs: void 0 })));
417
+ if (body.length > MAX_PAYLOAD_BYTES) {
418
+ body = JSON.stringify(wire.map((e) => ({ ...e, breadcrumbs: void 0, stack: e.stack?.slice(0, 2e3) })));
419
+ }
420
+ }
421
+ return body;
422
+ }
423
+ function flush(unloading) {
424
+ if (queue.length === 0) return;
425
+ const events = queue;
426
+ queue = [];
427
+ sentCount += events.length;
428
+ const base = options.serverUrl.replace(/\/$/, "");
429
+ const url = `${base}/api/v1/error-events?project_key=${encodeURIComponent(options.projectKey)}`;
430
+ const body = serialize(events);
431
+ if (unloading && typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
432
+ const ok = navigator.sendBeacon(url, body);
433
+ debugLog2("Flushed via beacon", events.length, ok);
434
+ if (ok) return;
435
+ }
436
+ fetch(url, {
437
+ method: "POST",
438
+ headers: { "Content-Type": "text/plain;charset=UTF-8" },
439
+ body,
440
+ keepalive: unloading
441
+ }).catch(() => {
442
+ });
443
+ debugLog2("Flushed via fetch", events.length);
444
+ }
445
+ function onError(event) {
446
+ const e = event;
447
+ if (typeof e.message !== "string" && !(e.error instanceof Error)) return;
448
+ const err = e.error instanceof Error ? e.error : void 0;
449
+ capture({
450
+ type: "error",
451
+ message: err?.message ?? (e.message || "Unknown error"),
452
+ errorClass: err?.name ?? "Error",
453
+ stack: err?.stack,
454
+ frameUrl: e.filename
455
+ });
456
+ }
457
+ function onRejection(event) {
458
+ const reason = event.reason;
459
+ const err = reason instanceof Error ? reason : void 0;
460
+ let message;
461
+ if (err) {
462
+ message = err.message;
463
+ } else {
464
+ try {
465
+ message = typeof reason === "string" ? reason : JSON.stringify(reason);
466
+ } catch {
467
+ message = String(reason);
468
+ }
469
+ }
470
+ capture({
471
+ type: "unhandledrejection",
472
+ message: message || "Unhandled promise rejection",
473
+ errorClass: err?.name ?? "UnhandledRejection",
474
+ stack: err?.stack
475
+ });
476
+ }
477
+ function onPageHide() {
478
+ flush(true);
479
+ }
480
+ function onVisibilityChange() {
481
+ if (document.visibilityState === "hidden") flush(true);
482
+ }
483
+ return {
484
+ start() {
485
+ if (active) return;
486
+ active = true;
487
+ window.addEventListener("error", onError, { capture: true });
488
+ window.addEventListener("unhandledrejection", onRejection, { capture: true });
489
+ window.addEventListener("pagehide", onPageHide);
490
+ document.addEventListener("visibilitychange", onVisibilityChange);
491
+ interval = setInterval(() => flush(false), FLUSH_INTERVAL_MS);
492
+ },
493
+ stop() {
494
+ if (!active) return;
495
+ active = false;
496
+ window.removeEventListener("error", onError, { capture: true });
497
+ window.removeEventListener("unhandledrejection", onRejection, { capture: true });
498
+ window.removeEventListener("pagehide", onPageHide);
499
+ document.removeEventListener("visibilitychange", onVisibilityChange);
500
+ if (interval) clearInterval(interval);
501
+ interval = null;
502
+ flush(false);
503
+ },
504
+ flush() {
505
+ flush(false);
506
+ }
507
+ };
508
+ }
509
+
283
510
  // src/collectors/formErrors.ts
284
511
  var MAX_ENTRIES2 = 30;
285
512
  var POST_SUBMIT_CHECK_MS = 300;
@@ -977,6 +1204,8 @@ function init(config) {
977
1204
  enableFormErrors = true,
978
1205
  enableFrustration = false,
979
1206
  autoReportFrustration = false,
1207
+ enableErrorMonitoring = true,
1208
+ errorMonitoring: errorMonitoringOpts,
980
1209
  enableReplay = false,
981
1210
  replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
982
1211
  blockedHosts = [],
@@ -1012,6 +1241,23 @@ function init(config) {
1012
1241
  }
1013
1242
  const frustrationCol = enableFrustration ? createFrustrationCollector(frustrationOpts) : null;
1014
1243
  frustrationCol?.start();
1244
+ const errorCaptureCol = enableErrorMonitoring ? createErrorCaptureCollector({
1245
+ serverUrl: config.serverUrl,
1246
+ projectKey: config.projectKey,
1247
+ release: config.release,
1248
+ appVersion: config.appVersion,
1249
+ debug: config.debug,
1250
+ sampleRate: errorMonitoringOpts?.sampleRate,
1251
+ ignoreErrors: errorMonitoringOpts?.ignoreErrors,
1252
+ beforeSend: errorMonitoringOpts?.beforeSend,
1253
+ getUser: () => getSnapshot().user ?? config.user,
1254
+ getBreadcrumbs: () => ({
1255
+ consoleLogs: consoleCol?.getEntries() ?? [],
1256
+ networkErrors: networkCol?.getEntries() ?? []
1257
+ }),
1258
+ getEnvironment: _collectors?.environment ?? collectEnvironment
1259
+ }) : null;
1260
+ errorCaptureCol?.start();
1015
1261
  if (config.user) {
1016
1262
  flint.setUser(config.user);
1017
1263
  }
@@ -1021,7 +1267,8 @@ function init(config) {
1021
1267
  console: !!consoleCol,
1022
1268
  network: !!networkCol,
1023
1269
  formErrors: !!formErrorsCol,
1024
- frustration: !!frustrationCol
1270
+ frustration: !!frustrationCol,
1271
+ errorCapture: !!errorCaptureCol
1025
1272
  });
1026
1273
  instance = {
1027
1274
  config,
@@ -1029,6 +1276,7 @@ function init(config) {
1029
1276
  network: networkCol,
1030
1277
  formErrors: formErrorsCol,
1031
1278
  frustration: frustrationCol,
1279
+ errorCapture: errorCaptureCol,
1032
1280
  replayEvents,
1033
1281
  stopReplay: null
1034
1282
  };
@@ -1109,6 +1357,7 @@ function shutdown() {
1109
1357
  instance.formErrors?.stop();
1110
1358
  _setFormErrorCollector(null);
1111
1359
  instance.frustration?.stop();
1360
+ instance.errorCapture?.stop();
1112
1361
  instance.stopReplay?.();
1113
1362
  instance = null;
1114
1363
  }
@@ -1200,6 +1449,7 @@ function resolveTheme(theme) {
1200
1449
  collectEnvironment,
1201
1450
  createConsoleCollector,
1202
1451
  createDatadogReplayProvider,
1452
+ createErrorCaptureCollector,
1203
1453
  createFormErrorCollector,
1204
1454
  createFrustrationCollector,
1205
1455
  createNetworkCollector,
@@ -1207,6 +1457,7 @@ function resolveTheme(theme) {
1207
1457
  resolveTheme,
1208
1458
  submitReplay,
1209
1459
  submitReport,
1210
- subscribe
1460
+ subscribe,
1461
+ trackDatadogBugReported
1211
1462
  });
1212
1463
  //# sourceMappingURL=index.cjs.map