@diegotsi/flint-core 1.10.0 → 1.10.2

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
@@ -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
 
@@ -157,6 +159,8 @@ interface FlintConfig {
157
159
  enableReplay?: boolean;
158
160
  replayBufferMs?: number;
159
161
  blockedHosts?: string[];
162
+ /** 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, …). */
163
+ redactKeys?: string[];
160
164
  frustration?: {
161
165
  rageClickThreshold?: number;
162
166
  rageClickWindow?: number;
@@ -184,7 +188,7 @@ interface FlintConfig {
184
188
  /** @internal Inject platform-specific collectors (e.g. React Native) */
185
189
  _collectors?: {
186
190
  console?: () => ConsoleCollector;
187
- network?: (blockedHosts: string[]) => NetworkCollector;
191
+ network?: (blockedHosts: string[], options?: NetworkCollectorOptions) => NetworkCollector;
188
192
  environment?: () => EnvironmentInfo;
189
193
  };
190
194
  /** @internal Injected by framework packages to start replay without rrweb in core */
@@ -296,8 +300,13 @@ declare function createFormErrorCollector(): FormErrorCollector;
296
300
  /** Datadog intake hosts to exclude from network capture. */
297
301
  declare const DATADOG_BLOCKED_HOSTS: string[];
298
302
  /**
299
- * Creates an `externalReplayProvider` that reads the current Datadog RUM
300
- * session and returns a deep link to the Session Replay viewer.
303
+ * Creates an `externalReplayProvider` that returns a deep link to the current
304
+ * Datadog RUM Session Replay.
305
+ *
306
+ * Prefers the SDK's own `getSessionReplayLink()` — Datadog builds it with the
307
+ * current view + timestamp, so the player opens at the reported moment rather
308
+ * than the session start. Falls back to a hand-built session URL (opens the
309
+ * session, without a precise seek) only when that method isn't available.
301
310
  */
302
311
  declare function createDatadogReplayProvider(site: string): () => string | undefined;
303
312
  /**
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  DATADOG_BLOCKED_HOSTS,
3
3
  createDatadogReplayProvider,
4
4
  trackDatadogBugReported
5
- } from "./chunk-SO6WYKFF.js";
5
+ } from "./chunk-FQ6TBPEG.js";
6
6
 
7
7
  // src/api.ts
8
8
  import { gzipSync } from "fflate";
@@ -763,6 +763,100 @@ function createFrustrationCollector(opts) {
763
763
  };
764
764
  }
765
765
 
766
+ // src/collectors/sanitize.ts
767
+ var REDACTED = "[REDACTED]";
768
+ var DEFAULT_REDACT_KEYS = [
769
+ "password",
770
+ "passwd",
771
+ "pwd",
772
+ "secret",
773
+ "token",
774
+ "auth",
775
+ "credential",
776
+ "apikey",
777
+ "api_key",
778
+ "session",
779
+ "cookie",
780
+ "ssn",
781
+ "creditcard",
782
+ "credit_card",
783
+ "card_number",
784
+ "cardnumber",
785
+ "cvv",
786
+ "cvc",
787
+ "pin"
788
+ ];
789
+ function createKeyMatcher(extra = []) {
790
+ const needles = [...DEFAULT_REDACT_KEYS, ...extra.map((k) => k.toLowerCase())];
791
+ return (key) => {
792
+ const lower = key.toLowerCase();
793
+ return needles.some((n) => lower.includes(n));
794
+ };
795
+ }
796
+ function redactValue(value, match) {
797
+ if (Array.isArray(value)) return value.map((v) => redactValue(v, match));
798
+ if (value && typeof value === "object") {
799
+ const out = {};
800
+ for (const [k, v] of Object.entries(value)) {
801
+ out[k] = match(k) ? REDACTED : redactValue(v, match);
802
+ }
803
+ return out;
804
+ }
805
+ return value;
806
+ }
807
+ function redactQueryString(qs, match) {
808
+ if (!qs) return qs;
809
+ return qs.split("&").map((pair) => {
810
+ const eq = pair.indexOf("=");
811
+ if (eq === -1) return pair;
812
+ const rawKey = pair.slice(0, eq);
813
+ let key = rawKey;
814
+ try {
815
+ key = decodeURIComponent(rawKey.replace(/\+/g, " "));
816
+ } catch {
817
+ }
818
+ return match(key) ? `${rawKey}=${REDACTED}` : pair;
819
+ }).join("&");
820
+ }
821
+ function sanitizeBody(body, match) {
822
+ if (!body) return body;
823
+ const trimmed = body.trimStart();
824
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
825
+ try {
826
+ const parsed = JSON.parse(body);
827
+ return JSON.stringify(redactValue(parsed, match));
828
+ } catch {
829
+ }
830
+ }
831
+ if (body.includes("=") && !/\s/.test(body)) {
832
+ const redacted = redactQueryString(body, match);
833
+ if (redacted !== body) return redacted;
834
+ }
835
+ return body.replace(
836
+ /("?)([\w.-]+)\1\s*:\s*"(?:[^"\\]|\\.)*"/gi,
837
+ (full, q, key) => match(key) ? `${q}${key}${q}:"${REDACTED}"` : full
838
+ ).replace(/([\w.-]+)=([^&\s]+)/gi, (full, key) => match(key) ? `${key}=${REDACTED}` : full);
839
+ }
840
+ function sanitizeUrl(url, match) {
841
+ const qIdx = url.indexOf("?");
842
+ if (qIdx === -1) return url;
843
+ const base = url.slice(0, qIdx);
844
+ const query = url.slice(qIdx + 1);
845
+ const hashIdx = query.indexOf("#");
846
+ const qs = hashIdx === -1 ? query : query.slice(0, hashIdx);
847
+ const hash = hashIdx === -1 ? "" : query.slice(hashIdx);
848
+ const redacted = redactQueryString(qs, match);
849
+ return `${base}?${redacted}${hash}`;
850
+ }
851
+ function sanitizeHeaders(headers, match) {
852
+ if (!headers) return headers;
853
+ const out = {};
854
+ for (const [k, v] of Object.entries(headers)) {
855
+ out[k] = match(k) ? REDACTED : v;
856
+ }
857
+ return out;
858
+ }
859
+
766
860
  // src/collectors/network.ts
767
861
  var MAX_ENTRIES3 = 50;
768
862
  var MAX_FULL_URL = 2e3;
@@ -938,6 +1032,7 @@ function extractContentType(headers) {
938
1032
  function createNetworkCollector(extraBlockedHosts = [], options) {
939
1033
  const maxEntries = options?.maxEntries ?? MAX_ENTRIES3;
940
1034
  const maxResponseBody = options?.maxResponseBody ?? MAX_RESPONSE_BODY;
1035
+ const matchSecret = createKeyMatcher(options?.redactKeys);
941
1036
  const entries = [];
942
1037
  const blocked = /* @__PURE__ */ new Set([...DEFAULT_BLOCKED_HOSTS, ...extraBlockedHosts]);
943
1038
  let origFetch = null;
@@ -949,6 +1044,12 @@ function createNetworkCollector(extraBlockedHosts = [], options) {
949
1044
  const seenUrls = /* @__PURE__ */ new Set();
950
1045
  function push(entry) {
951
1046
  seenUrls.add(entry.fullUrl ?? entry.url);
1047
+ if (entry.fullUrl) entry.fullUrl = sanitizeUrl(entry.fullUrl, matchSecret);
1048
+ entry.url = sanitizeUrl(entry.url, matchSecret);
1049
+ entry.requestBody = sanitizeBody(entry.requestBody, matchSecret);
1050
+ entry.responseBody = sanitizeBody(entry.responseBody, matchSecret);
1051
+ entry.requestHeaders = sanitizeHeaders(entry.requestHeaders, matchSecret);
1052
+ entry.responseHeaders = sanitizeHeaders(entry.responseHeaders, matchSecret);
952
1053
  entries.push(entry);
953
1054
  if (entries.length > maxEntries) entries.shift();
954
1055
  }
@@ -1014,7 +1115,8 @@ function createNetworkCollector(extraBlockedHosts = [], options) {
1014
1115
  try {
1015
1116
  const clone = res.clone();
1016
1117
  clone.text().then((text) => {
1017
- entry.responseBody = text.length > maxResponseBody ? text.slice(0, maxResponseBody) + "\u2026" : text;
1118
+ const truncated = text.length > maxResponseBody ? text.slice(0, maxResponseBody) + "\u2026" : text;
1119
+ entry.responseBody = sanitizeBody(truncated, matchSecret);
1018
1120
  if (!entry.responseSize && text.length) entry.responseSize = text.length;
1019
1121
  }).catch(() => {
1020
1122
  });
@@ -1244,6 +1346,7 @@ function init(config) {
1244
1346
  enableReplay = false,
1245
1347
  replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
1246
1348
  blockedHosts = [],
1349
+ redactKeys,
1247
1350
  frustration: frustrationOpts,
1248
1351
  onFrustration,
1249
1352
  _replayRecorder,
@@ -1267,7 +1370,7 @@ function init(config) {
1267
1370
  const allBlockedHosts = [...blockedHosts, ...flintHost ? [flintHost] : []];
1268
1371
  const consoleCol = enableConsole ? _collectors?.console?.() ?? createConsoleCollector() : null;
1269
1372
  consoleCol?.start();
1270
- const networkCol = enableNetwork ? _collectors?.network?.(allBlockedHosts) ?? createNetworkCollector(allBlockedHosts) : null;
1373
+ const networkCol = enableNetwork ? _collectors?.network?.(allBlockedHosts, { redactKeys }) ?? createNetworkCollector(allBlockedHosts, { redactKeys }) : null;
1271
1374
  networkCol?.start();
1272
1375
  const formErrorsCol = enableFormErrors ? createFormErrorCollector() : null;
1273
1376
  if (formErrorsCol) {
@@ -1358,7 +1461,7 @@ function init(config) {
1358
1461
  if (!instance || !remote?.integrations) return;
1359
1462
  const dd = remote.integrations.datadog;
1360
1463
  if (dd?.enabled && dd.site) {
1361
- import("./datadog-FLEAFTUB.js").then(({ createDatadogReplayProvider: createDatadogReplayProvider2 }) => {
1464
+ import("./datadog-F3R66MH7.js").then(({ createDatadogReplayProvider: createDatadogReplayProvider2 }) => {
1362
1465
  if (!instance) return;
1363
1466
  instance.config = {
1364
1467
  ...instance.config,