@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/{chunk-SO6WYKFF.js → chunk-FQ6TBPEG.js} +3 -1
- package/dist/chunk-FQ6TBPEG.js.map +1 -0
- package/dist/{datadog-FLEAFTUB.js → datadog-F3R66MH7.js} +2 -2
- package/dist/index.cjs +107 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +107 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-SO6WYKFF.js.map +0 -1
- /package/dist/{datadog-FLEAFTUB.js.map → datadog-F3R66MH7.js.map} +0 -0
|
@@ -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-
|
|
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-
|
|
5
|
+
} from "./chunk-FQ6TBPEG.js";
|
|
6
6
|
export {
|
|
7
7
|
DATADOG_BLOCKED_HOSTS,
|
|
8
8
|
createDatadogReplayProvider,
|
|
9
9
|
trackDatadogBugReported
|
|
10
10
|
};
|
|
11
|
-
//# sourceMappingURL=datadog-
|
|
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
|
-
|
|
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) {
|