@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.
@@ -9,6 +9,8 @@ function createDatadogReplayProvider(site) {
9
9
  return () => {
10
10
  try {
11
11
  const ddRum = window.DD_RUM;
12
+ const link = ddRum?.getSessionReplayLink?.();
13
+ if (link) return link;
12
14
  const ctx = ddRum?.getInternalContext?.();
13
15
  if (ctx?.session_id) {
14
16
  const ts = Date.now();
@@ -39,4 +41,4 @@ export {
39
41
  createDatadogReplayProvider,
40
42
  trackDatadogBugReported
41
43
  };
42
- //# sourceMappingURL=chunk-SO6WYKFF.js.map
44
+ //# sourceMappingURL=chunk-FQ6TBPEG.js.map
@@ -0,0 +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 returns a deep link to the current\n * Datadog RUM Session Replay.\n *\n * Prefers the SDK's own `getSessionReplayLink()` — Datadog builds it with the\n * current view + timestamp, so the player opens at the reported moment rather\n * than the session start. Falls back to a hand-built session URL (opens the\n * session, without a precise seek) only when that method isn't available.\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 | {\n getSessionReplayLink?: () => string | undefined;\n getInternalContext?: () => { session_id?: string } | undefined;\n }\n | undefined;\n\n // Official link generator — lands on the current moment/view.\n const link = ddRum?.getSessionReplayLink?.();\n if (link) return link;\n\n // Fallback: build from the session id (opens the session, no exact seek).\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;AAWO,SAAS,4BAA4B,MAAwC;AAClF,SAAO,MAAM;AACX,QAAI;AACF,YAAM,QAAS,OAA8C;AAQ7D,YAAM,OAAO,OAAO,uBAAuB;AAC3C,UAAI,KAAM,QAAO;AAGjB,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":[]}
@@ -2,10 +2,10 @@ import {
2
2
  DATADOG_BLOCKED_HOSTS,
3
3
  createDatadogReplayProvider,
4
4
  trackDatadogBugReported
5
- } from "./chunk-SO6WYKFF.js";
5
+ } from "./chunk-FQ6TBPEG.js";
6
6
  export {
7
7
  DATADOG_BLOCKED_HOSTS,
8
8
  createDatadogReplayProvider,
9
9
  trackDatadogBugReported
10
10
  };
11
- //# sourceMappingURL=datadog-FLEAFTUB.js.map
11
+ //# sourceMappingURL=datadog-F3R66MH7.js.map
package/dist/index.cjs CHANGED
@@ -31,6 +31,8 @@ function createDatadogReplayProvider(site) {
31
31
  return () => {
32
32
  try {
33
33
  const ddRum = window.DD_RUM;
34
+ const link = ddRum?.getSessionReplayLink?.();
35
+ if (link) return link;
34
36
  const ctx = ddRum?.getInternalContext?.();
35
37
  if (ctx?.session_id) {
36
38
  const ts = Date.now();
@@ -849,6 +851,100 @@ function createFrustrationCollector(opts) {
849
851
  };
850
852
  }
851
853
 
854
+ // src/collectors/sanitize.ts
855
+ var REDACTED = "[REDACTED]";
856
+ var DEFAULT_REDACT_KEYS = [
857
+ "password",
858
+ "passwd",
859
+ "pwd",
860
+ "secret",
861
+ "token",
862
+ "auth",
863
+ "credential",
864
+ "apikey",
865
+ "api_key",
866
+ "session",
867
+ "cookie",
868
+ "ssn",
869
+ "creditcard",
870
+ "credit_card",
871
+ "card_number",
872
+ "cardnumber",
873
+ "cvv",
874
+ "cvc",
875
+ "pin"
876
+ ];
877
+ function createKeyMatcher(extra = []) {
878
+ const needles = [...DEFAULT_REDACT_KEYS, ...extra.map((k) => k.toLowerCase())];
879
+ return (key) => {
880
+ const lower = key.toLowerCase();
881
+ return needles.some((n) => lower.includes(n));
882
+ };
883
+ }
884
+ function redactValue(value, match) {
885
+ if (Array.isArray(value)) return value.map((v) => redactValue(v, match));
886
+ if (value && typeof value === "object") {
887
+ const out = {};
888
+ for (const [k, v] of Object.entries(value)) {
889
+ out[k] = match(k) ? REDACTED : redactValue(v, match);
890
+ }
891
+ return out;
892
+ }
893
+ return value;
894
+ }
895
+ function redactQueryString(qs, match) {
896
+ if (!qs) return qs;
897
+ return qs.split("&").map((pair) => {
898
+ const eq = pair.indexOf("=");
899
+ if (eq === -1) return pair;
900
+ const rawKey = pair.slice(0, eq);
901
+ let key = rawKey;
902
+ try {
903
+ key = decodeURIComponent(rawKey.replace(/\+/g, " "));
904
+ } catch {
905
+ }
906
+ return match(key) ? `${rawKey}=${REDACTED}` : pair;
907
+ }).join("&");
908
+ }
909
+ function sanitizeBody(body, match) {
910
+ if (!body) return body;
911
+ const trimmed = body.trimStart();
912
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
913
+ try {
914
+ const parsed = JSON.parse(body);
915
+ return JSON.stringify(redactValue(parsed, match));
916
+ } catch {
917
+ }
918
+ }
919
+ if (body.includes("=") && !/\s/.test(body)) {
920
+ const redacted = redactQueryString(body, match);
921
+ if (redacted !== body) return redacted;
922
+ }
923
+ return body.replace(
924
+ /("?)([\w.-]+)\1\s*:\s*"(?:[^"\\]|\\.)*"/gi,
925
+ (full, q, key) => match(key) ? `${q}${key}${q}:"${REDACTED}"` : full
926
+ ).replace(/([\w.-]+)=([^&\s]+)/gi, (full, key) => match(key) ? `${key}=${REDACTED}` : full);
927
+ }
928
+ function sanitizeUrl(url, match) {
929
+ const qIdx = url.indexOf("?");
930
+ if (qIdx === -1) return url;
931
+ const base = url.slice(0, qIdx);
932
+ const query = url.slice(qIdx + 1);
933
+ const hashIdx = query.indexOf("#");
934
+ const qs = hashIdx === -1 ? query : query.slice(0, hashIdx);
935
+ const hash = hashIdx === -1 ? "" : query.slice(hashIdx);
936
+ const redacted = redactQueryString(qs, match);
937
+ return `${base}?${redacted}${hash}`;
938
+ }
939
+ function sanitizeHeaders(headers, match) {
940
+ if (!headers) return headers;
941
+ const out = {};
942
+ for (const [k, v] of Object.entries(headers)) {
943
+ out[k] = match(k) ? REDACTED : v;
944
+ }
945
+ return out;
946
+ }
947
+
852
948
  // src/collectors/network.ts
853
949
  var MAX_ENTRIES3 = 50;
854
950
  var MAX_FULL_URL = 2e3;
@@ -1024,6 +1120,7 @@ function extractContentType(headers) {
1024
1120
  function createNetworkCollector(extraBlockedHosts = [], options) {
1025
1121
  const maxEntries = options?.maxEntries ?? MAX_ENTRIES3;
1026
1122
  const maxResponseBody = options?.maxResponseBody ?? MAX_RESPONSE_BODY;
1123
+ const matchSecret = createKeyMatcher(options?.redactKeys);
1027
1124
  const entries = [];
1028
1125
  const blocked = /* @__PURE__ */ new Set([...DEFAULT_BLOCKED_HOSTS, ...extraBlockedHosts]);
1029
1126
  let origFetch = null;
@@ -1035,6 +1132,12 @@ function createNetworkCollector(extraBlockedHosts = [], options) {
1035
1132
  const seenUrls = /* @__PURE__ */ new Set();
1036
1133
  function push(entry) {
1037
1134
  seenUrls.add(entry.fullUrl ?? entry.url);
1135
+ if (entry.fullUrl) entry.fullUrl = sanitizeUrl(entry.fullUrl, matchSecret);
1136
+ entry.url = sanitizeUrl(entry.url, matchSecret);
1137
+ entry.requestBody = sanitizeBody(entry.requestBody, matchSecret);
1138
+ entry.responseBody = sanitizeBody(entry.responseBody, matchSecret);
1139
+ entry.requestHeaders = sanitizeHeaders(entry.requestHeaders, matchSecret);
1140
+ entry.responseHeaders = sanitizeHeaders(entry.responseHeaders, matchSecret);
1038
1141
  entries.push(entry);
1039
1142
  if (entries.length > maxEntries) entries.shift();
1040
1143
  }
@@ -1100,7 +1203,8 @@ function createNetworkCollector(extraBlockedHosts = [], options) {
1100
1203
  try {
1101
1204
  const clone = res.clone();
1102
1205
  clone.text().then((text) => {
1103
- entry.responseBody = text.length > maxResponseBody ? text.slice(0, maxResponseBody) + "\u2026" : text;
1206
+ const truncated = text.length > maxResponseBody ? text.slice(0, maxResponseBody) + "\u2026" : text;
1207
+ entry.responseBody = sanitizeBody(truncated, matchSecret);
1104
1208
  if (!entry.responseSize && text.length) entry.responseSize = text.length;
1105
1209
  }).catch(() => {
1106
1210
  });
@@ -1333,6 +1437,7 @@ function init(config) {
1333
1437
  enableReplay = false,
1334
1438
  replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
1335
1439
  blockedHosts = [],
1440
+ redactKeys,
1336
1441
  frustration: frustrationOpts,
1337
1442
  onFrustration,
1338
1443
  _replayRecorder,
@@ -1356,7 +1461,7 @@ function init(config) {
1356
1461
  const allBlockedHosts = [...blockedHosts, ...flintHost ? [flintHost] : []];
1357
1462
  const consoleCol = enableConsole ? _collectors?.console?.() ?? createConsoleCollector() : null;
1358
1463
  consoleCol?.start();
1359
- const networkCol = enableNetwork ? _collectors?.network?.(allBlockedHosts) ?? createNetworkCollector(allBlockedHosts) : null;
1464
+ const networkCol = enableNetwork ? _collectors?.network?.(allBlockedHosts, { redactKeys }) ?? createNetworkCollector(allBlockedHosts, { redactKeys }) : null;
1360
1465
  networkCol?.start();
1361
1466
  const formErrorsCol = enableFormErrors ? createFormErrorCollector() : null;
1362
1467
  if (formErrorsCol) {