@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.cjs CHANGED
@@ -180,8 +180,10 @@ function sanitize(str) {
180
180
 
181
181
  // src/collectors/console.ts
182
182
  var MAX_ENTRIES = 50;
183
+ var MAX_ERRORS = 10;
183
184
  function createConsoleCollector() {
184
185
  const entries = [];
186
+ const errors = [];
185
187
  let active = false;
186
188
  const originals = {
187
189
  log: console.log.bind(console),
@@ -202,6 +204,15 @@ function createConsoleCollector() {
202
204
  entries.push({ level, args: str, timestamp: Date.now() });
203
205
  if (entries.length > MAX_ENTRIES) entries.shift();
204
206
  }
207
+ function pushError(message, stack, errorClass) {
208
+ errors.push({
209
+ message: sanitize(message),
210
+ stack: stack ? sanitize(stack) : void 0,
211
+ errorClass,
212
+ timestamp: Date.now()
213
+ });
214
+ if (errors.length > MAX_ERRORS) errors.shift();
215
+ }
205
216
  return {
206
217
  start() {
207
218
  if (active) return;
@@ -220,14 +231,22 @@ function createConsoleCollector() {
220
231
  };
221
232
  origOnerror = window.onerror;
222
233
  window.onerror = (msg, src, line, col, err) => {
223
- push("error", [err?.message ?? String(msg), `${src}:${line}:${col}`]);
234
+ const message = err?.message ?? String(msg);
235
+ push("error", [message, `${src}:${line}:${col}`]);
236
+ pushError(message, err?.stack, err?.name);
224
237
  if (typeof origOnerror === "function") return origOnerror(msg, src, line, col, err);
225
238
  return false;
226
239
  };
227
240
  origUnhandled = window.onunhandledrejection;
228
241
  window.onunhandledrejection = (event) => {
229
- const reason = event.reason instanceof Error ? event.reason.message : JSON.stringify(event.reason);
242
+ const isError = event.reason instanceof Error;
243
+ const reason = isError ? event.reason.message : JSON.stringify(event.reason);
230
244
  push("error", ["UnhandledRejection:", reason]);
245
+ pushError(
246
+ reason,
247
+ isError ? event.reason.stack : void 0,
248
+ isError ? event.reason.name : void 0
249
+ );
231
250
  if (typeof origUnhandled === "function") origUnhandled.call(window, event);
232
251
  };
233
252
  },
@@ -242,6 +261,9 @@ function createConsoleCollector() {
242
261
  },
243
262
  getEntries() {
244
263
  return [...entries];
264
+ },
265
+ getErrors() {
266
+ return [...errors];
245
267
  }
246
268
  };
247
269
  }
@@ -851,6 +873,100 @@ function createFrustrationCollector(opts) {
851
873
  };
852
874
  }
853
875
 
876
+ // src/collectors/sanitize.ts
877
+ var REDACTED = "[REDACTED]";
878
+ var DEFAULT_REDACT_KEYS = [
879
+ "password",
880
+ "passwd",
881
+ "pwd",
882
+ "secret",
883
+ "token",
884
+ "auth",
885
+ "credential",
886
+ "apikey",
887
+ "api_key",
888
+ "session",
889
+ "cookie",
890
+ "ssn",
891
+ "creditcard",
892
+ "credit_card",
893
+ "card_number",
894
+ "cardnumber",
895
+ "cvv",
896
+ "cvc",
897
+ "pin"
898
+ ];
899
+ function createKeyMatcher(extra = []) {
900
+ const needles = [...DEFAULT_REDACT_KEYS, ...extra.map((k) => k.toLowerCase())];
901
+ return (key) => {
902
+ const lower = key.toLowerCase();
903
+ return needles.some((n) => lower.includes(n));
904
+ };
905
+ }
906
+ function redactValue(value, match) {
907
+ if (Array.isArray(value)) return value.map((v) => redactValue(v, match));
908
+ if (value && typeof value === "object") {
909
+ const out = {};
910
+ for (const [k, v] of Object.entries(value)) {
911
+ out[k] = match(k) ? REDACTED : redactValue(v, match);
912
+ }
913
+ return out;
914
+ }
915
+ return value;
916
+ }
917
+ function redactQueryString(qs, match) {
918
+ if (!qs) return qs;
919
+ return qs.split("&").map((pair) => {
920
+ const eq = pair.indexOf("=");
921
+ if (eq === -1) return pair;
922
+ const rawKey = pair.slice(0, eq);
923
+ let key = rawKey;
924
+ try {
925
+ key = decodeURIComponent(rawKey.replace(/\+/g, " "));
926
+ } catch {
927
+ }
928
+ return match(key) ? `${rawKey}=${REDACTED}` : pair;
929
+ }).join("&");
930
+ }
931
+ function sanitizeBody(body, match) {
932
+ if (!body) return body;
933
+ const trimmed = body.trimStart();
934
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
935
+ try {
936
+ const parsed = JSON.parse(body);
937
+ return JSON.stringify(redactValue(parsed, match));
938
+ } catch {
939
+ }
940
+ }
941
+ if (body.includes("=") && !/\s/.test(body)) {
942
+ const redacted = redactQueryString(body, match);
943
+ if (redacted !== body) return redacted;
944
+ }
945
+ return body.replace(
946
+ /("?)([\w.-]+)\1\s*:\s*"(?:[^"\\]|\\.)*"/gi,
947
+ (full, q, key) => match(key) ? `${q}${key}${q}:"${REDACTED}"` : full
948
+ ).replace(/([\w.-]+)=([^&\s]+)/gi, (full, key) => match(key) ? `${key}=${REDACTED}` : full);
949
+ }
950
+ function sanitizeUrl(url, match) {
951
+ const qIdx = url.indexOf("?");
952
+ if (qIdx === -1) return url;
953
+ const base = url.slice(0, qIdx);
954
+ const query = url.slice(qIdx + 1);
955
+ const hashIdx = query.indexOf("#");
956
+ const qs = hashIdx === -1 ? query : query.slice(0, hashIdx);
957
+ const hash = hashIdx === -1 ? "" : query.slice(hashIdx);
958
+ const redacted = redactQueryString(qs, match);
959
+ return `${base}?${redacted}${hash}`;
960
+ }
961
+ function sanitizeHeaders(headers, match) {
962
+ if (!headers) return headers;
963
+ const out = {};
964
+ for (const [k, v] of Object.entries(headers)) {
965
+ out[k] = match(k) ? REDACTED : v;
966
+ }
967
+ return out;
968
+ }
969
+
854
970
  // src/collectors/network.ts
855
971
  var MAX_ENTRIES3 = 50;
856
972
  var MAX_FULL_URL = 2e3;
@@ -1026,6 +1142,7 @@ function extractContentType(headers) {
1026
1142
  function createNetworkCollector(extraBlockedHosts = [], options) {
1027
1143
  const maxEntries = options?.maxEntries ?? MAX_ENTRIES3;
1028
1144
  const maxResponseBody = options?.maxResponseBody ?? MAX_RESPONSE_BODY;
1145
+ const matchSecret = createKeyMatcher(options?.redactKeys);
1029
1146
  const entries = [];
1030
1147
  const blocked = /* @__PURE__ */ new Set([...DEFAULT_BLOCKED_HOSTS, ...extraBlockedHosts]);
1031
1148
  let origFetch = null;
@@ -1037,6 +1154,12 @@ function createNetworkCollector(extraBlockedHosts = [], options) {
1037
1154
  const seenUrls = /* @__PURE__ */ new Set();
1038
1155
  function push(entry) {
1039
1156
  seenUrls.add(entry.fullUrl ?? entry.url);
1157
+ if (entry.fullUrl) entry.fullUrl = sanitizeUrl(entry.fullUrl, matchSecret);
1158
+ entry.url = sanitizeUrl(entry.url, matchSecret);
1159
+ entry.requestBody = sanitizeBody(entry.requestBody, matchSecret);
1160
+ entry.responseBody = sanitizeBody(entry.responseBody, matchSecret);
1161
+ entry.requestHeaders = sanitizeHeaders(entry.requestHeaders, matchSecret);
1162
+ entry.responseHeaders = sanitizeHeaders(entry.responseHeaders, matchSecret);
1040
1163
  entries.push(entry);
1041
1164
  if (entries.length > maxEntries) entries.shift();
1042
1165
  }
@@ -1102,7 +1225,8 @@ function createNetworkCollector(extraBlockedHosts = [], options) {
1102
1225
  try {
1103
1226
  const clone = res.clone();
1104
1227
  clone.text().then((text) => {
1105
- entry.responseBody = text.length > maxResponseBody ? text.slice(0, maxResponseBody) + "\u2026" : text;
1228
+ const truncated = text.length > maxResponseBody ? text.slice(0, maxResponseBody) + "\u2026" : text;
1229
+ entry.responseBody = sanitizeBody(truncated, matchSecret);
1106
1230
  if (!entry.responseSize && text.length) entry.responseSize = text.length;
1107
1231
  }).catch(() => {
1108
1232
  });
@@ -1335,6 +1459,7 @@ function init(config) {
1335
1459
  enableReplay = false,
1336
1460
  replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
1337
1461
  blockedHosts = [],
1462
+ redactKeys,
1338
1463
  frustration: frustrationOpts,
1339
1464
  onFrustration,
1340
1465
  _replayRecorder,
@@ -1358,7 +1483,7 @@ function init(config) {
1358
1483
  const allBlockedHosts = [...blockedHosts, ...flintHost ? [flintHost] : []];
1359
1484
  const consoleCol = enableConsole ? _collectors?.console?.() ?? createConsoleCollector() : null;
1360
1485
  consoleCol?.start();
1361
- const networkCol = enableNetwork ? _collectors?.network?.(allBlockedHosts) ?? createNetworkCollector(allBlockedHosts) : null;
1486
+ const networkCol = enableNetwork ? _collectors?.network?.(allBlockedHosts, { redactKeys }) ?? createNetworkCollector(allBlockedHosts, { redactKeys }) : null;
1362
1487
  networkCol?.start();
1363
1488
  const formErrorsCol = enableFormErrors ? createFormErrorCollector() : null;
1364
1489
  if (formErrorsCol) {