@diegotsi/flint-core 1.9.1 → 1.10.1

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();
@@ -989,7 +991,8 @@ function captureHeaders(raw, max) {
989
991
  count++;
990
992
  }
991
993
  } else {
992
- const entries = typeof Headers !== "undefined" && raw instanceof Headers ? raw.entries() : Array.isArray(raw) ? raw : Object.entries(raw);
994
+ const headersLike = raw;
995
+ const entries = typeof headersLike.entries === "function" && typeof headersLike.forEach === "function" ? headersLike.entries() : Array.isArray(raw) ? raw : Object.entries(raw);
993
996
  for (const [k, v] of entries) {
994
997
  total++;
995
998
  if (count >= max) continue;
@@ -1007,7 +1010,7 @@ function captureHeaders(raw, max) {
1007
1010
  function extractContentType(headers) {
1008
1011
  if (!headers) return void 0;
1009
1012
  try {
1010
- if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get("content-type") ?? void 0;
1013
+ if (typeof headers.get === "function") return headers.get("content-type") ?? void 0;
1011
1014
  if (Array.isArray(headers)) {
1012
1015
  const found = headers.find(([k]) => k.toLowerCase() === "content-type");
1013
1016
  return found?.[1];
@@ -1020,17 +1023,22 @@ function extractContentType(headers) {
1020
1023
  }
1021
1024
  return void 0;
1022
1025
  }
1023
- function createNetworkCollector(extraBlockedHosts = []) {
1026
+ function createNetworkCollector(extraBlockedHosts = [], options) {
1027
+ const maxEntries = options?.maxEntries ?? MAX_ENTRIES3;
1028
+ const maxResponseBody = options?.maxResponseBody ?? MAX_RESPONSE_BODY;
1024
1029
  const entries = [];
1025
1030
  const blocked = /* @__PURE__ */ new Set([...DEFAULT_BLOCKED_HOSTS, ...extraBlockedHosts]);
1026
1031
  let origFetch = null;
1027
1032
  let origXHROpen = null;
1028
1033
  let origXHRSend = null;
1029
1034
  let origXHRSetHeader = null;
1035
+ let perfObserver = null;
1030
1036
  let active = false;
1037
+ const seenUrls = /* @__PURE__ */ new Set();
1031
1038
  function push(entry) {
1039
+ seenUrls.add(entry.fullUrl ?? entry.url);
1032
1040
  entries.push(entry);
1033
- if (entries.length > MAX_ENTRIES3) entries.shift();
1041
+ if (entries.length > maxEntries) entries.shift();
1034
1042
  }
1035
1043
  return {
1036
1044
  start() {
@@ -1038,19 +1046,50 @@ function createNetworkCollector(extraBlockedHosts = []) {
1038
1046
  active = true;
1039
1047
  origFetch = window.fetch;
1040
1048
  window.fetch = async (input, init2) => {
1041
- const method = (init2?.method ?? "GET").toUpperCase();
1042
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1049
+ const reqObj = typeof Request !== "undefined" && input instanceof Request ? input : null;
1050
+ const method = (init2?.method ?? reqObj?.method ?? "GET").toUpperCase();
1051
+ const url = reqObj ? reqObj.url : typeof input === "string" ? input : input.href;
1043
1052
  const startTime = Date.now();
1044
- const reqBody = safeBodyPreview(init2?.body, MAX_REQUEST_BODY);
1045
- const reqHeaders = captureHeaders(init2?.headers, MAX_HEADERS);
1046
- const reqContentType = extractContentType(init2?.headers);
1047
- const res = await origFetch.call(window, input, init2);
1053
+ const reqHeaders = captureHeaders(init2?.headers ?? reqObj?.headers, MAX_HEADERS);
1054
+ const reqContentType = extractContentType(init2?.headers ?? reqObj?.headers);
1055
+ let reqBody = safeBodyPreview(init2?.body, MAX_REQUEST_BODY);
1056
+ if (reqBody === void 0 && reqObj && !shouldIgnore(url, blocked)) {
1057
+ try {
1058
+ const t = await reqObj.clone().text();
1059
+ if (t) reqBody = t.length > MAX_REQUEST_BODY ? t.slice(0, MAX_REQUEST_BODY) + "\u2026" : t;
1060
+ } catch {
1061
+ }
1062
+ }
1063
+ let res;
1064
+ try {
1065
+ res = await origFetch.call(window, input, init2);
1066
+ } catch (err) {
1067
+ if (!shouldIgnore(url, blocked)) {
1068
+ push({
1069
+ method,
1070
+ url: truncateUrl(url),
1071
+ fullUrl: captureFullUrl(url),
1072
+ status: 0,
1073
+ duration: Date.now() - startTime,
1074
+ timestamp: startTime,
1075
+ source: "fetch",
1076
+ failed: true,
1077
+ errorMessage: err instanceof Error ? err.message : String(err),
1078
+ requestHeaders: reqHeaders,
1079
+ requestBody: reqBody,
1080
+ requestContentType: reqContentType
1081
+ });
1082
+ }
1083
+ throw err;
1084
+ }
1048
1085
  if (!shouldIgnore(url, blocked)) {
1049
1086
  const entry = {
1050
1087
  method,
1051
1088
  url: truncateUrl(url),
1052
1089
  fullUrl: captureFullUrl(url),
1053
1090
  status: res.status,
1091
+ statusText: res.statusText || void 0,
1092
+ source: "fetch",
1054
1093
  duration: Date.now() - startTime,
1055
1094
  timestamp: startTime,
1056
1095
  requestHeaders: reqHeaders,
@@ -1063,7 +1102,7 @@ function createNetworkCollector(extraBlockedHosts = []) {
1063
1102
  try {
1064
1103
  const clone = res.clone();
1065
1104
  clone.text().then((text) => {
1066
- entry.responseBody = text.length > MAX_RESPONSE_BODY ? text.slice(0, MAX_RESPONSE_BODY) + "\u2026" : text;
1105
+ entry.responseBody = text.length > maxResponseBody ? text.slice(0, maxResponseBody) + "\u2026" : text;
1067
1106
  if (!entry.responseSize && text.length) entry.responseSize = text.length;
1068
1107
  }).catch(() => {
1069
1108
  });
@@ -1100,13 +1139,16 @@ function createNetworkCollector(extraBlockedHosts = []) {
1100
1139
  XMLHttpRequest.prototype.open = function(method, url, async, username, password) {
1101
1140
  const startTime = Date.now();
1102
1141
  const urlStr = typeof url === "string" ? url : url.href;
1142
+ let captured = false;
1103
1143
  this.addEventListener("load", () => {
1144
+ if (captured) return;
1145
+ captured = true;
1104
1146
  if (!shouldIgnore(urlStr, blocked)) {
1105
1147
  const meta = xhrMeta.get(this);
1106
1148
  let resBody;
1107
1149
  try {
1108
1150
  const text = this.responseText;
1109
- resBody = text && text.length > MAX_RESPONSE_BODY ? text.slice(0, MAX_RESPONSE_BODY) + "\u2026" : text || void 0;
1151
+ resBody = text && text.length > maxResponseBody ? text.slice(0, maxResponseBody) + "\u2026" : text || void 0;
1110
1152
  } catch {
1111
1153
  }
1112
1154
  push({
@@ -1114,6 +1156,8 @@ function createNetworkCollector(extraBlockedHosts = []) {
1114
1156
  url: truncateUrl(urlStr),
1115
1157
  fullUrl: captureFullUrl(urlStr),
1116
1158
  status: this.status,
1159
+ statusText: this.statusText || void 0,
1160
+ source: "xhr",
1117
1161
  duration: Date.now() - startTime,
1118
1162
  timestamp: startTime,
1119
1163
  requestHeaders: meta?.reqHeaders && Object.keys(meta.reqHeaders).length > 0 ? meta.reqHeaders : void 0,
@@ -1126,8 +1170,58 @@ function createNetworkCollector(extraBlockedHosts = []) {
1126
1170
  });
1127
1171
  }
1128
1172
  });
1173
+ const pushFailure = (errorMessage) => {
1174
+ if (captured) return;
1175
+ captured = true;
1176
+ if (shouldIgnore(urlStr, blocked)) return;
1177
+ const meta = xhrMeta.get(this);
1178
+ push({
1179
+ method: method.toUpperCase(),
1180
+ url: truncateUrl(urlStr),
1181
+ fullUrl: captureFullUrl(urlStr),
1182
+ status: 0,
1183
+ source: "xhr",
1184
+ failed: true,
1185
+ errorMessage,
1186
+ duration: Date.now() - startTime,
1187
+ timestamp: startTime,
1188
+ requestHeaders: meta?.reqHeaders && Object.keys(meta.reqHeaders).length > 0 ? meta.reqHeaders : void 0,
1189
+ requestBody: meta?.reqBody,
1190
+ requestContentType: meta?.reqContentType
1191
+ });
1192
+ };
1193
+ this.addEventListener("error", () => pushFailure("Network error"));
1194
+ this.addEventListener("timeout", () => pushFailure("Timeout"));
1195
+ this.addEventListener("abort", () => pushFailure("Aborted"));
1129
1196
  return origXHROpen.apply(this, [method, url, async ?? true, username, password]);
1130
1197
  };
1198
+ if (typeof PerformanceObserver !== "undefined") {
1199
+ try {
1200
+ perfObserver = new PerformanceObserver((list) => {
1201
+ for (const entry of list.getEntries()) {
1202
+ const res = entry;
1203
+ if (res.initiatorType !== "fetch" && res.initiatorType !== "xmlhttprequest") continue;
1204
+ const url = res.name;
1205
+ if (shouldIgnore(url, blocked)) continue;
1206
+ if (seenUrls.has(captureFullUrl(url)) || seenUrls.has(truncateUrl(url))) continue;
1207
+ push({
1208
+ // Resource timing does not expose the HTTP method; default to GET.
1209
+ method: "GET",
1210
+ url: truncateUrl(url),
1211
+ fullUrl: captureFullUrl(url),
1212
+ status: res.responseStatus ?? 0,
1213
+ source: "perf",
1214
+ duration: Math.round(res.duration),
1215
+ timestamp: Math.round(performance.timeOrigin + res.startTime),
1216
+ responseSize: res.transferSize || res.encodedBodySize || void 0
1217
+ });
1218
+ }
1219
+ });
1220
+ perfObserver.observe({ type: "resource", buffered: true });
1221
+ } catch {
1222
+ perfObserver = null;
1223
+ }
1224
+ }
1131
1225
  },
1132
1226
  stop() {
1133
1227
  if (!active) return;
@@ -1136,6 +1230,10 @@ function createNetworkCollector(extraBlockedHosts = []) {
1136
1230
  if (origXHROpen) XMLHttpRequest.prototype.open = origXHROpen;
1137
1231
  if (origXHRSend) XMLHttpRequest.prototype.send = origXHRSend;
1138
1232
  if (origXHRSetHeader) XMLHttpRequest.prototype.setRequestHeader = origXHRSetHeader;
1233
+ if (perfObserver) {
1234
+ perfObserver.disconnect();
1235
+ perfObserver = null;
1236
+ }
1139
1237
  },
1140
1238
  getEntries() {
1141
1239
  return [...entries];