@diegotsi/flint-core 1.10.1 → 1.11.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.cts CHANGED
@@ -6,6 +6,8 @@ interface NetworkCollector {
6
6
  interface NetworkCollectorOptions {
7
7
  maxEntries?: number;
8
8
  maxResponseBody?: number;
9
+ /** Extra key names (substring, case-insensitive) to redact from URLs, bodies and headers. */
10
+ redactKeys?: string[];
9
11
  }
10
12
  declare function createNetworkCollector(extraBlockedHosts?: string[], options?: NetworkCollectorOptions): NetworkCollector;
11
13
 
@@ -13,6 +15,7 @@ interface ConsoleCollector {
13
15
  start(): void;
14
16
  stop(): void;
15
17
  getEntries(): ConsoleEntry[];
18
+ getErrors(): CapturedError[];
16
19
  }
17
20
  declare function createConsoleCollector(): ConsoleCollector;
18
21
 
@@ -52,6 +55,15 @@ interface ConsoleEntry {
52
55
  args: string;
53
56
  timestamp: number;
54
57
  }
58
+ /** A structured uncaught error / unhandled rejection, captured with its stack. */
59
+ interface CapturedError {
60
+ message: string;
61
+ /** Raw (possibly minified) stack; symbolicated server-side against uploaded source maps. */
62
+ stack?: string;
63
+ /** Error constructor name, e.g. "TypeError". */
64
+ errorClass?: string;
65
+ timestamp: number;
66
+ }
55
67
  interface NetworkEntry {
56
68
  method: string;
57
69
  url: string;
@@ -157,6 +169,8 @@ interface FlintConfig {
157
169
  enableReplay?: boolean;
158
170
  replayBufferMs?: number;
159
171
  blockedHosts?: string[];
172
+ /** Extra key names (substring, case-insensitive) to redact from captured network URLs, bodies and headers, on top of the built-in defaults (password, token, secret, …). */
173
+ redactKeys?: string[];
160
174
  frustration?: {
161
175
  rageClickThreshold?: number;
162
176
  rageClickWindow?: number;
@@ -184,7 +198,7 @@ interface FlintConfig {
184
198
  /** @internal Inject platform-specific collectors (e.g. React Native) */
185
199
  _collectors?: {
186
200
  console?: () => ConsoleCollector;
187
- network?: (blockedHosts: string[]) => NetworkCollector;
201
+ network?: (blockedHosts: string[], options?: NetworkCollectorOptions) => NetworkCollector;
188
202
  environment?: () => EnvironmentInfo;
189
203
  };
190
204
  /** @internal Injected by framework packages to start replay without rrweb in core */
@@ -195,6 +209,14 @@ interface CollectedMeta {
195
209
  consoleLogs: ConsoleEntry[];
196
210
  networkErrors: NetworkEntry[];
197
211
  formErrors: FormErrorEntry[];
212
+ /** Structured uncaught errors captured during the session (newest last). */
213
+ errors?: CapturedError[];
214
+ /** Epoch ms when the user opened the report — anchors the bug moment. */
215
+ reportedAt?: number;
216
+ /** Epoch ms of the most recent captured error before the report, if any. */
217
+ errorAt?: number;
218
+ /** Raw stack of the most recent captured error, if any (symbolicated server-side). */
219
+ errorStack?: string;
198
220
  }
199
221
  interface ReportPayload {
200
222
  reporterId: string;
@@ -392,4 +414,4 @@ interface ResolvedTheme {
392
414
  }
393
415
  declare function resolveTheme(theme: Theme): ResolvedTheme;
394
416
 
395
- 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 };
417
+ export { type CapturedError, 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.d.ts CHANGED
@@ -6,6 +6,8 @@ interface NetworkCollector {
6
6
  interface NetworkCollectorOptions {
7
7
  maxEntries?: number;
8
8
  maxResponseBody?: number;
9
+ /** Extra key names (substring, case-insensitive) to redact from URLs, bodies and headers. */
10
+ redactKeys?: string[];
9
11
  }
10
12
  declare function createNetworkCollector(extraBlockedHosts?: string[], options?: NetworkCollectorOptions): NetworkCollector;
11
13
 
@@ -13,6 +15,7 @@ interface ConsoleCollector {
13
15
  start(): void;
14
16
  stop(): void;
15
17
  getEntries(): ConsoleEntry[];
18
+ getErrors(): CapturedError[];
16
19
  }
17
20
  declare function createConsoleCollector(): ConsoleCollector;
18
21
 
@@ -52,6 +55,15 @@ interface ConsoleEntry {
52
55
  args: string;
53
56
  timestamp: number;
54
57
  }
58
+ /** A structured uncaught error / unhandled rejection, captured with its stack. */
59
+ interface CapturedError {
60
+ message: string;
61
+ /** Raw (possibly minified) stack; symbolicated server-side against uploaded source maps. */
62
+ stack?: string;
63
+ /** Error constructor name, e.g. "TypeError". */
64
+ errorClass?: string;
65
+ timestamp: number;
66
+ }
55
67
  interface NetworkEntry {
56
68
  method: string;
57
69
  url: string;
@@ -157,6 +169,8 @@ interface FlintConfig {
157
169
  enableReplay?: boolean;
158
170
  replayBufferMs?: number;
159
171
  blockedHosts?: string[];
172
+ /** Extra key names (substring, case-insensitive) to redact from captured network URLs, bodies and headers, on top of the built-in defaults (password, token, secret, …). */
173
+ redactKeys?: string[];
160
174
  frustration?: {
161
175
  rageClickThreshold?: number;
162
176
  rageClickWindow?: number;
@@ -184,7 +198,7 @@ interface FlintConfig {
184
198
  /** @internal Inject platform-specific collectors (e.g. React Native) */
185
199
  _collectors?: {
186
200
  console?: () => ConsoleCollector;
187
- network?: (blockedHosts: string[]) => NetworkCollector;
201
+ network?: (blockedHosts: string[], options?: NetworkCollectorOptions) => NetworkCollector;
188
202
  environment?: () => EnvironmentInfo;
189
203
  };
190
204
  /** @internal Injected by framework packages to start replay without rrweb in core */
@@ -195,6 +209,14 @@ interface CollectedMeta {
195
209
  consoleLogs: ConsoleEntry[];
196
210
  networkErrors: NetworkEntry[];
197
211
  formErrors: FormErrorEntry[];
212
+ /** Structured uncaught errors captured during the session (newest last). */
213
+ errors?: CapturedError[];
214
+ /** Epoch ms when the user opened the report — anchors the bug moment. */
215
+ reportedAt?: number;
216
+ /** Epoch ms of the most recent captured error before the report, if any. */
217
+ errorAt?: number;
218
+ /** Raw stack of the most recent captured error, if any (symbolicated server-side). */
219
+ errorStack?: string;
198
220
  }
199
221
  interface ReportPayload {
200
222
  reporterId: string;
@@ -392,4 +414,4 @@ interface ResolvedTheme {
392
414
  }
393
415
  declare function resolveTheme(theme: Theme): ResolvedTheme;
394
416
 
395
- 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 };
417
+ export { type CapturedError, 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
@@ -92,8 +92,10 @@ function sanitize(str) {
92
92
 
93
93
  // src/collectors/console.ts
94
94
  var MAX_ENTRIES = 50;
95
+ var MAX_ERRORS = 10;
95
96
  function createConsoleCollector() {
96
97
  const entries = [];
98
+ const errors = [];
97
99
  let active = false;
98
100
  const originals = {
99
101
  log: console.log.bind(console),
@@ -114,6 +116,15 @@ function createConsoleCollector() {
114
116
  entries.push({ level, args: str, timestamp: Date.now() });
115
117
  if (entries.length > MAX_ENTRIES) entries.shift();
116
118
  }
119
+ function pushError(message, stack, errorClass) {
120
+ errors.push({
121
+ message: sanitize(message),
122
+ stack: stack ? sanitize(stack) : void 0,
123
+ errorClass,
124
+ timestamp: Date.now()
125
+ });
126
+ if (errors.length > MAX_ERRORS) errors.shift();
127
+ }
117
128
  return {
118
129
  start() {
119
130
  if (active) return;
@@ -132,14 +143,22 @@ function createConsoleCollector() {
132
143
  };
133
144
  origOnerror = window.onerror;
134
145
  window.onerror = (msg, src, line, col, err) => {
135
- push("error", [err?.message ?? String(msg), `${src}:${line}:${col}`]);
146
+ const message = err?.message ?? String(msg);
147
+ push("error", [message, `${src}:${line}:${col}`]);
148
+ pushError(message, err?.stack, err?.name);
136
149
  if (typeof origOnerror === "function") return origOnerror(msg, src, line, col, err);
137
150
  return false;
138
151
  };
139
152
  origUnhandled = window.onunhandledrejection;
140
153
  window.onunhandledrejection = (event) => {
141
- const reason = event.reason instanceof Error ? event.reason.message : JSON.stringify(event.reason);
154
+ const isError = event.reason instanceof Error;
155
+ const reason = isError ? event.reason.message : JSON.stringify(event.reason);
142
156
  push("error", ["UnhandledRejection:", reason]);
157
+ pushError(
158
+ reason,
159
+ isError ? event.reason.stack : void 0,
160
+ isError ? event.reason.name : void 0
161
+ );
143
162
  if (typeof origUnhandled === "function") origUnhandled.call(window, event);
144
163
  };
145
164
  },
@@ -154,6 +173,9 @@ function createConsoleCollector() {
154
173
  },
155
174
  getEntries() {
156
175
  return [...entries];
176
+ },
177
+ getErrors() {
178
+ return [...errors];
157
179
  }
158
180
  };
159
181
  }
@@ -763,6 +785,100 @@ function createFrustrationCollector(opts) {
763
785
  };
764
786
  }
765
787
 
788
+ // src/collectors/sanitize.ts
789
+ var REDACTED = "[REDACTED]";
790
+ var DEFAULT_REDACT_KEYS = [
791
+ "password",
792
+ "passwd",
793
+ "pwd",
794
+ "secret",
795
+ "token",
796
+ "auth",
797
+ "credential",
798
+ "apikey",
799
+ "api_key",
800
+ "session",
801
+ "cookie",
802
+ "ssn",
803
+ "creditcard",
804
+ "credit_card",
805
+ "card_number",
806
+ "cardnumber",
807
+ "cvv",
808
+ "cvc",
809
+ "pin"
810
+ ];
811
+ function createKeyMatcher(extra = []) {
812
+ const needles = [...DEFAULT_REDACT_KEYS, ...extra.map((k) => k.toLowerCase())];
813
+ return (key) => {
814
+ const lower = key.toLowerCase();
815
+ return needles.some((n) => lower.includes(n));
816
+ };
817
+ }
818
+ function redactValue(value, match) {
819
+ if (Array.isArray(value)) return value.map((v) => redactValue(v, match));
820
+ if (value && typeof value === "object") {
821
+ const out = {};
822
+ for (const [k, v] of Object.entries(value)) {
823
+ out[k] = match(k) ? REDACTED : redactValue(v, match);
824
+ }
825
+ return out;
826
+ }
827
+ return value;
828
+ }
829
+ function redactQueryString(qs, match) {
830
+ if (!qs) return qs;
831
+ return qs.split("&").map((pair) => {
832
+ const eq = pair.indexOf("=");
833
+ if (eq === -1) return pair;
834
+ const rawKey = pair.slice(0, eq);
835
+ let key = rawKey;
836
+ try {
837
+ key = decodeURIComponent(rawKey.replace(/\+/g, " "));
838
+ } catch {
839
+ }
840
+ return match(key) ? `${rawKey}=${REDACTED}` : pair;
841
+ }).join("&");
842
+ }
843
+ function sanitizeBody(body, match) {
844
+ if (!body) return body;
845
+ const trimmed = body.trimStart();
846
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
847
+ try {
848
+ const parsed = JSON.parse(body);
849
+ return JSON.stringify(redactValue(parsed, match));
850
+ } catch {
851
+ }
852
+ }
853
+ if (body.includes("=") && !/\s/.test(body)) {
854
+ const redacted = redactQueryString(body, match);
855
+ if (redacted !== body) return redacted;
856
+ }
857
+ return body.replace(
858
+ /("?)([\w.-]+)\1\s*:\s*"(?:[^"\\]|\\.)*"/gi,
859
+ (full, q, key) => match(key) ? `${q}${key}${q}:"${REDACTED}"` : full
860
+ ).replace(/([\w.-]+)=([^&\s]+)/gi, (full, key) => match(key) ? `${key}=${REDACTED}` : full);
861
+ }
862
+ function sanitizeUrl(url, match) {
863
+ const qIdx = url.indexOf("?");
864
+ if (qIdx === -1) return url;
865
+ const base = url.slice(0, qIdx);
866
+ const query = url.slice(qIdx + 1);
867
+ const hashIdx = query.indexOf("#");
868
+ const qs = hashIdx === -1 ? query : query.slice(0, hashIdx);
869
+ const hash = hashIdx === -1 ? "" : query.slice(hashIdx);
870
+ const redacted = redactQueryString(qs, match);
871
+ return `${base}?${redacted}${hash}`;
872
+ }
873
+ function sanitizeHeaders(headers, match) {
874
+ if (!headers) return headers;
875
+ const out = {};
876
+ for (const [k, v] of Object.entries(headers)) {
877
+ out[k] = match(k) ? REDACTED : v;
878
+ }
879
+ return out;
880
+ }
881
+
766
882
  // src/collectors/network.ts
767
883
  var MAX_ENTRIES3 = 50;
768
884
  var MAX_FULL_URL = 2e3;
@@ -938,6 +1054,7 @@ function extractContentType(headers) {
938
1054
  function createNetworkCollector(extraBlockedHosts = [], options) {
939
1055
  const maxEntries = options?.maxEntries ?? MAX_ENTRIES3;
940
1056
  const maxResponseBody = options?.maxResponseBody ?? MAX_RESPONSE_BODY;
1057
+ const matchSecret = createKeyMatcher(options?.redactKeys);
941
1058
  const entries = [];
942
1059
  const blocked = /* @__PURE__ */ new Set([...DEFAULT_BLOCKED_HOSTS, ...extraBlockedHosts]);
943
1060
  let origFetch = null;
@@ -949,6 +1066,12 @@ function createNetworkCollector(extraBlockedHosts = [], options) {
949
1066
  const seenUrls = /* @__PURE__ */ new Set();
950
1067
  function push(entry) {
951
1068
  seenUrls.add(entry.fullUrl ?? entry.url);
1069
+ if (entry.fullUrl) entry.fullUrl = sanitizeUrl(entry.fullUrl, matchSecret);
1070
+ entry.url = sanitizeUrl(entry.url, matchSecret);
1071
+ entry.requestBody = sanitizeBody(entry.requestBody, matchSecret);
1072
+ entry.responseBody = sanitizeBody(entry.responseBody, matchSecret);
1073
+ entry.requestHeaders = sanitizeHeaders(entry.requestHeaders, matchSecret);
1074
+ entry.responseHeaders = sanitizeHeaders(entry.responseHeaders, matchSecret);
952
1075
  entries.push(entry);
953
1076
  if (entries.length > maxEntries) entries.shift();
954
1077
  }
@@ -1014,7 +1137,8 @@ function createNetworkCollector(extraBlockedHosts = [], options) {
1014
1137
  try {
1015
1138
  const clone = res.clone();
1016
1139
  clone.text().then((text) => {
1017
- entry.responseBody = text.length > maxResponseBody ? text.slice(0, maxResponseBody) + "\u2026" : text;
1140
+ const truncated = text.length > maxResponseBody ? text.slice(0, maxResponseBody) + "\u2026" : text;
1141
+ entry.responseBody = sanitizeBody(truncated, matchSecret);
1018
1142
  if (!entry.responseSize && text.length) entry.responseSize = text.length;
1019
1143
  }).catch(() => {
1020
1144
  });
@@ -1244,6 +1368,7 @@ function init(config) {
1244
1368
  enableReplay = false,
1245
1369
  replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
1246
1370
  blockedHosts = [],
1371
+ redactKeys,
1247
1372
  frustration: frustrationOpts,
1248
1373
  onFrustration,
1249
1374
  _replayRecorder,
@@ -1267,7 +1392,7 @@ function init(config) {
1267
1392
  const allBlockedHosts = [...blockedHosts, ...flintHost ? [flintHost] : []];
1268
1393
  const consoleCol = enableConsole ? _collectors?.console?.() ?? createConsoleCollector() : null;
1269
1394
  consoleCol?.start();
1270
- const networkCol = enableNetwork ? _collectors?.network?.(allBlockedHosts) ?? createNetworkCollector(allBlockedHosts) : null;
1395
+ const networkCol = enableNetwork ? _collectors?.network?.(allBlockedHosts, { redactKeys }) ?? createNetworkCollector(allBlockedHosts, { redactKeys }) : null;
1271
1396
  networkCol?.start();
1272
1397
  const formErrorsCol = enableFormErrors ? createFormErrorCollector() : null;
1273
1398
  if (formErrorsCol) {