@singhak/nodeui-core 0.3.0 → 0.5.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
@@ -34,15 +34,19 @@ __export(index_exports, {
34
34
  ConfirmationStore: () => ConfirmationStore,
35
35
  CpuProvider: () => CpuProvider,
36
36
  DEFAULT_LOG_SIZE: () => DEFAULT_LOG_SIZE,
37
+ DEFAULT_REQUEST_DETAIL: () => DEFAULT_REQUEST_DETAIL,
37
38
  EnvProvider: () => EnvProvider,
39
+ ErrorsProvider: () => ErrorsProvider,
38
40
  EventLoopLagProvider: () => EventLoopLagProvider,
39
41
  HealthProvider: () => HealthProvider,
40
42
  HeapSnapshotProvider: () => HeapSnapshotProvider,
41
43
  LogsProvider: () => LogsProvider,
42
44
  MemoryProvider: () => MemoryProvider,
43
45
  MetricsProvider: () => MetricsProvider,
46
+ N_PLUS_ONE_THRESHOLD: () => N_PLUS_ONE_THRESHOLD,
44
47
  OutgoingProvider: () => OutgoingProvider,
45
48
  ProviderRegistry: () => ProviderRegistry,
49
+ QueriesProvider: () => QueriesProvider,
46
50
  RequestsProvider: () => RequestsProvider,
47
51
  RingBuffer: () => RingBuffer,
48
52
  RoutesProvider: () => RoutesProvider,
@@ -52,19 +56,28 @@ __export(index_exports, {
52
56
  StartupTracker: () => StartupTracker,
53
57
  createGuard: () => createGuard,
54
58
  createNodeUI: () => createNodeUI,
59
+ currentRequestId: () => currentRequestId,
60
+ enterRequestContext: () => enterRequestContext,
55
61
  extractRoutes: () => extractRoutes,
62
+ fingerprintError: () => fingerprintError,
56
63
  hostnameFromHostHeader: () => hostnameFromHostHeader,
57
64
  interceptConsole: () => interceptConsole,
58
65
  interceptOutgoing: () => interceptOutgoing,
66
+ interceptQueries: () => interceptQueries,
59
67
  isLoopbackAddress: () => isLoopbackAddress,
60
68
  isLoopbackHostname: () => isLoopbackHostname,
69
+ isValidAddressEntry: () => isValidAddressEntry,
61
70
  maskSecretText: () => maskSecretText,
62
71
  maskSecrets: () => maskSecrets,
63
72
  matchesAddress: () => matchesAddress,
73
+ normalizeSql: () => normalizeSql,
64
74
  resolveActivation: () => resolveActivation,
65
75
  resolveStaticAsset: () => resolveStaticAsset,
76
+ runInRequestContext: () => runInRequestContext,
66
77
  serializeEnvelope: () => serializeEnvelope,
67
- startSse: () => startSse
78
+ startSse: () => startSse,
79
+ summarizeRequests: () => summarizeRequests,
80
+ uiAssetsDir: () => uiAssetsDir
68
81
  });
69
82
  module.exports = __toCommonJS(index_exports);
70
83
 
@@ -120,7 +133,10 @@ var BEARER_PATTERN = /\b(Bearer|Basic)\s+[A-Za-z0-9\-._~+/]+=*/gi;
120
133
  var JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g;
121
134
  function maskSecretText(text) {
122
135
  if (text.length < 6) return text;
123
- return text.replace(URL_CREDENTIALS_PATTERN, `$1${SECRET_MASKED}$2`).replace(BEARER_PATTERN, `$1 ${SECRET_MASKED}`).replace(JWT_PATTERN, SECRET_MASKED).replace(TEXT_PAIR_PATTERN, `$1$2${SECRET_MASKED}`);
136
+ return text.replace(URL_CREDENTIALS_PATTERN, `$1${SECRET_MASKED}$2`).replace(BEARER_PATTERN, `$1 ${SECRET_MASKED}`).replace(JWT_PATTERN, SECRET_MASKED).replace(TEXT_PAIR_PATTERN, (_m, name, sep2, value) => {
137
+ const quote = value.startsWith('"') || value.startsWith("'") ? value[0] : "";
138
+ return `${name}${sep2}${quote}${SECRET_MASKED}${quote}`;
139
+ });
124
140
  }
125
141
  function resolveActivation(env) {
126
142
  if (env.NODEUI_ENABLED === "true") {
@@ -158,21 +174,91 @@ function ipv4ToInt(ip) {
158
174
  }
159
175
  return out;
160
176
  }
177
+ function ipv6ToBigInt(address) {
178
+ const text = address.split("%")[0] ?? "";
179
+ if (!text.includes(":")) return null;
180
+ let head = text;
181
+ let tailGroups = [];
182
+ const doubleColon = text.indexOf("::");
183
+ if (doubleColon !== -1) {
184
+ if (text.indexOf("::", doubleColon + 1) !== -1) return null;
185
+ head = text.slice(0, doubleColon);
186
+ const tail = text.slice(doubleColon + 2);
187
+ tailGroups = tail === "" ? [] : tail.split(":");
188
+ }
189
+ const headGroups = doubleColon !== -1 ? head === "" ? [] : head.split(":") : text.split(":");
190
+ const groups = doubleColon !== -1 ? [...headGroups, ...tailGroups] : headGroups;
191
+ const last = groups[groups.length - 1];
192
+ const v4 = last?.includes(".") ? ipv4ToInt(last) : null;
193
+ if (last?.includes(".")) {
194
+ if (v4 === null) return null;
195
+ groups.splice(
196
+ groups.length - 1,
197
+ 1,
198
+ (v4 >>> 16 & 65535).toString(16),
199
+ (v4 & 65535).toString(16)
200
+ );
201
+ }
202
+ const expected = 8;
203
+ if (doubleColon === -1 ? groups.length !== expected : groups.length >= expected) return null;
204
+ const parseGroup = (g) => /^[0-9a-f]{1,4}$/i.test(g) ? BigInt(`0x${g}`) : null;
205
+ const headLen = doubleColon !== -1 ? headGroups.length : groups.length;
206
+ const fill = expected - groups.length;
207
+ const full = [
208
+ ...groups.slice(0, headLen),
209
+ ...Array(doubleColon !== -1 ? fill : 0).fill("0"),
210
+ ...groups.slice(headLen)
211
+ ];
212
+ let out = 0n;
213
+ for (const g of full) {
214
+ const n = parseGroup(g);
215
+ if (n === null) return null;
216
+ out = out << 16n | n;
217
+ }
218
+ return out;
219
+ }
220
+ function isValidAddressEntry(entry) {
221
+ const normalized = normalizeIp(entry);
222
+ if (!normalized.includes("/"))
223
+ return ipv4ToInt(normalized) !== null || ipv6ToBigInt(normalized) !== null;
224
+ const [base = "", bitsRaw = ""] = normalized.split("/");
225
+ const bits = Number(bitsRaw);
226
+ if (!/^\d{1,3}$/.test(bitsRaw) || !Number.isInteger(bits)) return false;
227
+ if (ipv4ToInt(base) !== null) return bits <= 32;
228
+ return ipv6ToBigInt(base) !== null && bits <= 128;
229
+ }
161
230
  function matchesAddress(address, allowed) {
162
231
  if (!address) return false;
163
232
  const ip = normalizeIp(address);
164
- for (const entry of allowed) {
233
+ const ip4 = ipv4ToInt(ip);
234
+ const ip6 = ip4 === null ? ipv6ToBigInt(ip) : null;
235
+ for (const raw of allowed) {
236
+ const entry = normalizeIp(raw);
165
237
  if (!entry.includes("/")) {
166
- if (normalizeIp(entry) === ip) return true;
238
+ if (entry === ip) return true;
239
+ const e4 = ipv4ToInt(entry);
240
+ if (ip4 !== null && e4 !== null) {
241
+ if (ip4 === e4) return true;
242
+ continue;
243
+ }
244
+ const e6 = ipv6ToBigInt(entry);
245
+ if (ip6 !== null && e6 !== null && ip6 === e6) return true;
167
246
  continue;
168
247
  }
169
- const [base, bitsRaw] = entry.split("/");
248
+ const [base = "", bitsRaw = ""] = entry.split("/");
249
+ if (!/^\d{1,3}$/.test(bitsRaw)) continue;
170
250
  const bits = Number(bitsRaw);
171
- const a = ipv4ToInt(ip);
172
- const b = ipv4ToInt(base ?? "");
173
- if (a === null || b === null || !Number.isInteger(bits) || bits < 0 || bits > 32) continue;
174
- const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
175
- if ((a & mask) >>> 0 === (b & mask) >>> 0) return true;
251
+ const b4 = ipv4ToInt(base);
252
+ if (b4 !== null) {
253
+ if (ip4 === null || bits > 32) continue;
254
+ const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
255
+ if ((ip4 & mask) >>> 0 === (b4 & mask) >>> 0) return true;
256
+ continue;
257
+ }
258
+ const b6 = ipv6ToBigInt(base);
259
+ if (b6 === null || ip6 === null || bits > 128) continue;
260
+ const shift = BigInt(128 - bits);
261
+ if (ip6 >> shift === b6 >> shift) return true;
176
262
  }
177
263
  return false;
178
264
  }
@@ -403,6 +489,9 @@ var MIME_TYPES = {
403
489
  ".woff2": "font/woff2",
404
490
  ".webmanifest": "application/manifest+json"
405
491
  };
492
+ function uiAssetsDir() {
493
+ return (0, import_node_path.resolve)(__dirname, "..", "static");
494
+ }
406
495
  function resolveStaticAsset(rootDir, urlPath) {
407
496
  const relative = urlPath === "" ? "index.html" : urlPath.replace(/^\/+/, "");
408
497
  const candidate = (0, import_node_path.resolve)(rootDir, relative);
@@ -657,12 +746,12 @@ var LAG_DEGRADED_MS = 200;
657
746
  var CHECK_TIMEOUT_MS = 3e3;
658
747
  var CHECK_CACHE_MS = 5e3;
659
748
  function withTimeout(promise, ms) {
660
- return new Promise((resolve3, reject) => {
749
+ return new Promise((resolve2, reject) => {
661
750
  const timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
662
751
  promise.then(
663
752
  (value) => {
664
753
  clearTimeout(timer);
665
- resolve3(value);
754
+ resolve2(value);
666
755
  },
667
756
  (err) => {
668
757
  clearTimeout(timer);
@@ -750,14 +839,355 @@ var HealthProvider = class {
750
839
  }
751
840
  };
752
841
 
842
+ // src/providers/queries.ts
843
+ var import_node_module = require("module");
844
+ var import_node_path2 = require("path");
845
+
846
+ // src/context.ts
847
+ var import_node_async_hooks = require("async_hooks");
848
+ var storage = new import_node_async_hooks.AsyncLocalStorage();
849
+ var ids = /* @__PURE__ */ new WeakMap();
850
+ var suppression = new import_node_async_hooks.AsyncLocalStorage();
851
+ function runSuppressed(fn) {
852
+ return suppression.run(true, fn);
853
+ }
854
+ function isSuppressed() {
855
+ return suppression.getStore() === true;
856
+ }
857
+ function currentRequestId() {
858
+ return storage.getStore()?.requestId;
859
+ }
860
+ function bindRequestId(req, requestId) {
861
+ ids.set(req, requestId);
862
+ }
863
+ function runWithRequestId(requestId, fn) {
864
+ return storage.run({ requestId }, fn);
865
+ }
866
+ function runInRequestContext(req, fn) {
867
+ const requestId = ids.get(req);
868
+ return requestId === void 0 ? fn() : storage.run({ requestId }, fn);
869
+ }
870
+ function enterRequestContext(req) {
871
+ const requestId = ids.get(req);
872
+ if (requestId !== void 0) storage.enterWith({ requestId });
873
+ }
874
+
875
+ // src/providers/queries.ts
876
+ var RESPONSE_PAGE_SIZE = 100;
877
+ var MAX_SQL_CHARS = 2e3;
878
+ var N_PLUS_ONE_THRESHOLD = 5;
879
+ var defaultLoader = (name) => {
880
+ try {
881
+ return (0, import_node_module.createRequire)((0, import_node_path2.join)(process.cwd(), "noop.js"))(name);
882
+ } catch {
883
+ return null;
884
+ }
885
+ };
886
+ var recorders = /* @__PURE__ */ new Set();
887
+ var restorers = [];
888
+ function normalizeSql(sql) {
889
+ return sql.replace(/'(?:[^']|'')*'/g, "?").replace(/\b\d+(?:\.\d+)?\b/g, "?").replace(/\(\s*\?(?:\s*,\s*\?)+\s*\)/g, "(?)").replace(/\s+/g, " ").trim().toLowerCase();
890
+ }
891
+ function emit(entry) {
892
+ for (const record of recorders) {
893
+ try {
894
+ record(entry);
895
+ } catch {
896
+ }
897
+ }
898
+ }
899
+ function sqlOf(arg) {
900
+ if (typeof arg === "string") return arg;
901
+ if (arg && typeof arg === "object") {
902
+ const candidate = arg.text ?? arg.sql;
903
+ if (typeof candidate === "string") return candidate;
904
+ }
905
+ return null;
906
+ }
907
+ function rowCountOf(result) {
908
+ if (Array.isArray(result)) {
909
+ const first = result[0];
910
+ return Array.isArray(first) ? first.length : result.length;
911
+ }
912
+ if (result && typeof result === "object") {
913
+ const r = result;
914
+ if (typeof r.rowCount === "number") return r.rowCount;
915
+ if (typeof r.affectedRows === "number") return r.affectedRows;
916
+ }
917
+ return void 0;
918
+ }
919
+ function wrapQueryMethod(proto, method, system, rowCount) {
920
+ const original = proto[method];
921
+ if (typeof original !== "function") return;
922
+ const orig = original;
923
+ const wrapped = function(...args) {
924
+ if (recorders.size === 0) return orig.apply(this, args);
925
+ const sql = sqlOf(args[0]);
926
+ if (sql === null) return orig.apply(this, args);
927
+ const started = process.hrtime.bigint();
928
+ const timestampMs = Date.now();
929
+ const requestId = currentRequestId();
930
+ let done = false;
931
+ const finish = (error, result) => {
932
+ if (done) return;
933
+ done = true;
934
+ emit({
935
+ system,
936
+ sql: sql.slice(0, MAX_SQL_CHARS),
937
+ durationMs: Number(process.hrtime.bigint() - started) / 1e6,
938
+ timestampMs,
939
+ ...requestId !== void 0 ? { requestId } : {},
940
+ ...error ? { error: error instanceof Error ? error.message : String(error) } : {},
941
+ ...!error && rowCount(result) !== void 0 ? { rowCount: rowCount(result) } : {}
942
+ });
943
+ };
944
+ const last = args.length - 1;
945
+ const hasCallback = typeof args[last] === "function";
946
+ if (hasCallback) {
947
+ const callback = args[last];
948
+ args[last] = function(err, result, ...rest) {
949
+ finish(err, result);
950
+ return callback.call(this, err, result, ...rest);
951
+ };
952
+ }
953
+ const ret = orig.apply(this, args);
954
+ if (!hasCallback && ret && typeof ret.then === "function") {
955
+ ret.then(
956
+ (result) => finish(void 0, result),
957
+ (error) => finish(error ?? new Error("query failed"))
958
+ );
959
+ } else if (!hasCallback && ret && typeof ret.once === "function") {
960
+ const emitter = ret;
961
+ emitter.once("end", (result) => finish(void 0, result));
962
+ emitter.once("error", (error) => finish(error ?? new Error("query failed")));
963
+ }
964
+ return ret;
965
+ };
966
+ proto[method] = wrapped;
967
+ restorers.push(() => {
968
+ if (proto[method] === wrapped) proto[method] = original;
969
+ });
970
+ }
971
+ function instrumentDrivers(load) {
972
+ const pg = load("pg");
973
+ if (pg?.Client?.prototype) wrapQueryMethod(pg.Client.prototype, "query", "pg", rowCountOf);
974
+ const mysql2 = load("mysql2");
975
+ const proto = mysql2?.Connection?.prototype;
976
+ if (proto) {
977
+ wrapQueryMethod(proto, "query", "mysql2", rowCountOf);
978
+ wrapQueryMethod(proto, "execute", "mysql2", rowCountOf);
979
+ }
980
+ }
981
+ function interceptQueries(record, load = defaultLoader) {
982
+ const first = recorders.size === 0;
983
+ recorders.add(record);
984
+ if (first) {
985
+ try {
986
+ instrumentDrivers(load);
987
+ } catch {
988
+ }
989
+ }
990
+ return () => {
991
+ if (!recorders.delete(record) || recorders.size > 0) return;
992
+ while (restorers.length > 0) restorers.pop()?.();
993
+ };
994
+ }
995
+ var QueriesProvider = class {
996
+ constructor(options) {
997
+ this.options = options;
998
+ this.buffer = new RingBuffer(options.size);
999
+ }
1000
+ options;
1001
+ id = "queries";
1002
+ buffer;
1003
+ nextId = 1;
1004
+ release = null;
1005
+ failed = 0;
1006
+ /** Called with every newly recorded entry (persistence hook). */
1007
+ onRecord;
1008
+ start() {
1009
+ if (this.release) return;
1010
+ this.release = interceptQueries((entry) => this.record(entry), this.options.loader);
1011
+ }
1012
+ stop() {
1013
+ this.release?.();
1014
+ this.release = null;
1015
+ }
1016
+ record(entry) {
1017
+ const full = { id: this.nextId, ...entry };
1018
+ this.buffer.push(full);
1019
+ this.nextId += 1;
1020
+ if (entry.error) this.failed += 1;
1021
+ this.onRecord?.(full);
1022
+ }
1023
+ /** Re-inserts journaled entries without re-emitting them. */
1024
+ restore(entries) {
1025
+ for (const entry of entries) {
1026
+ this.buffer.push(entry);
1027
+ this.nextId = Math.max(this.nextId, entry.id + 1);
1028
+ if (entry.error) this.failed += 1;
1029
+ }
1030
+ }
1031
+ get() {
1032
+ const all = this.buffer.toArray();
1033
+ const counts = /* @__PURE__ */ new Map();
1034
+ const keyOf = (e) => e.requestId === void 0 ? null : `${e.requestId}|${normalizeSql(e.sql)}`;
1035
+ for (const e of all) {
1036
+ const key = keyOf(e);
1037
+ if (key) counts.set(key, (counts.get(key) ?? 0) + 1);
1038
+ }
1039
+ const flagged = /* @__PURE__ */ new Set();
1040
+ const entries = all.map((e) => {
1041
+ const key = keyOf(e);
1042
+ const repeats = key ? counts.get(key) ?? 0 : 0;
1043
+ const nPlusOne = repeats >= N_PLUS_ONE_THRESHOLD;
1044
+ if (nPlusOne && key) flagged.add(key);
1045
+ return {
1046
+ ...e,
1047
+ slow: e.durationMs >= this.options.slowQueryMs,
1048
+ ...nPlusOne ? { nPlusOne, repeats } : {}
1049
+ };
1050
+ });
1051
+ return {
1052
+ ok: true,
1053
+ data: {
1054
+ total: this.nextId - 1,
1055
+ failed: this.failed,
1056
+ slow: entries.filter((e) => e.slow).length,
1057
+ nPlusOneGroups: flagged.size,
1058
+ slowQueryMs: this.options.slowQueryMs,
1059
+ entries: entries.slice(Math.max(0, entries.length - RESPONSE_PAGE_SIZE))
1060
+ }
1061
+ };
1062
+ }
1063
+ };
1064
+
1065
+ // src/providers/errors.ts
1066
+ var import_node_crypto3 = require("crypto");
1067
+ var MAX_GROUPS = 100;
1068
+ var MAX_STACK_CHARS = 4e3;
1069
+ var MAX_MESSAGE_CHARS = 500;
1070
+ var FINGERPRINT_FRAMES = 3;
1071
+ function toError(value) {
1072
+ if (value instanceof Error) {
1073
+ return { name: value.name, message: value.message, stack: value.stack ?? "" };
1074
+ }
1075
+ let message;
1076
+ try {
1077
+ message = typeof value === "string" ? value : JSON.stringify(value);
1078
+ } catch {
1079
+ message = String(value);
1080
+ }
1081
+ return { name: "NonError", message: message ?? String(value), stack: "" };
1082
+ }
1083
+ function topFrames(stack) {
1084
+ const frames = [];
1085
+ for (const line of stack.split("\n")) {
1086
+ const match = /^\s*at\s+(?:(.+?)\s+\()?(?:.*[\\/])?([^\\/:()]+?)(?::\d+){0,2}\)?\s*$/.exec(
1087
+ line
1088
+ );
1089
+ if (match) frames.push(`${match[1] ?? ""}@${match[2] ?? ""}`);
1090
+ if (frames.length >= FINGERPRINT_FRAMES) break;
1091
+ }
1092
+ return frames;
1093
+ }
1094
+ function fingerprintError(name, message, stack) {
1095
+ const shape = message.replace(/\b[0-9a-f]{8}-[0-9a-f-]{27}\b/gi, "#").replace(/\b[0-9a-f]{24}\b/gi, "#").replace(/\d+/g, "#");
1096
+ return (0, import_node_crypto3.createHash)("sha1").update([name, shape, ...topFrames(stack)].join("\n")).digest("hex").slice(0, 12);
1097
+ }
1098
+ var ErrorsProvider = class {
1099
+ id = "errors";
1100
+ groups = /* @__PURE__ */ new Map();
1101
+ total = 0;
1102
+ listener = null;
1103
+ /** Called with every newly recorded occurrence (persistence hook). */
1104
+ onRecord;
1105
+ /** Starts observing process-level failures. Idempotent. */
1106
+ attach() {
1107
+ if (this.listener) return;
1108
+ this.listener = (err, origin) => this.record(err, origin === "unhandledRejection" ? "rejection" : "uncaught");
1109
+ process.on("uncaughtExceptionMonitor", this.listener);
1110
+ }
1111
+ detach() {
1112
+ if (!this.listener) return;
1113
+ process.removeListener("uncaughtExceptionMonitor", this.listener);
1114
+ this.listener = null;
1115
+ }
1116
+ record(error, source, context = {}) {
1117
+ try {
1118
+ const { name, message, stack } = toError(error);
1119
+ const requestId = context.requestId ?? currentRequestId();
1120
+ const event = {
1121
+ name,
1122
+ message,
1123
+ stack: stack.slice(0, MAX_STACK_CHARS),
1124
+ source,
1125
+ at: Date.now(),
1126
+ ...requestId !== void 0 ? { requestId } : {},
1127
+ ...context.route ? { route: context.route } : {},
1128
+ ...context.status !== void 0 ? { status: context.status } : {}
1129
+ };
1130
+ this.apply(event);
1131
+ this.onRecord?.(event);
1132
+ } catch {
1133
+ }
1134
+ }
1135
+ /** Re-applies journaled occurrences without re-emitting them. */
1136
+ restore(events) {
1137
+ for (const event of events) this.apply(event);
1138
+ }
1139
+ apply(event) {
1140
+ const { name, message, stack, source, at: now, requestId } = event;
1141
+ const context = { route: event.route, status: event.status };
1142
+ const fingerprint = fingerprintError(name, message, stack);
1143
+ const existing = this.groups.get(fingerprint);
1144
+ this.total += 1;
1145
+ if (existing) {
1146
+ existing.count += 1;
1147
+ existing.lastSeenMs = now;
1148
+ existing.message = message.slice(0, MAX_MESSAGE_CHARS);
1149
+ existing.stack = stack.slice(0, MAX_STACK_CHARS);
1150
+ if (requestId !== void 0) existing.lastRequestId = requestId;
1151
+ if (context.route) existing.lastRoute = context.route;
1152
+ if (context.status !== void 0) existing.lastStatus = context.status;
1153
+ return;
1154
+ }
1155
+ if (this.groups.size >= MAX_GROUPS) {
1156
+ let oldest;
1157
+ for (const g of this.groups.values()) {
1158
+ if (!oldest || g.lastSeenMs < oldest.lastSeenMs) oldest = g;
1159
+ }
1160
+ if (oldest) this.groups.delete(oldest.id);
1161
+ }
1162
+ this.groups.set(fingerprint, {
1163
+ id: fingerprint,
1164
+ name,
1165
+ message: message.slice(0, MAX_MESSAGE_CHARS),
1166
+ stack: stack.slice(0, MAX_STACK_CHARS),
1167
+ source,
1168
+ count: 1,
1169
+ firstSeenMs: now,
1170
+ lastSeenMs: now,
1171
+ ...requestId !== void 0 ? { lastRequestId: requestId } : {},
1172
+ ...context.route ? { lastRoute: context.route } : {},
1173
+ ...context.status !== void 0 ? { lastStatus: context.status } : {}
1174
+ });
1175
+ }
1176
+ get() {
1177
+ const groups = [...this.groups.values()].sort((a, b) => b.lastSeenMs - a.lastSeenMs);
1178
+ return { ok: true, data: { total: this.total, groups } };
1179
+ }
1180
+ };
1181
+
753
1182
  // src/providers/outgoing.ts
754
1183
  var import_node_diagnostics_channel = __toESM(require("diagnostics_channel"));
755
1184
  var import_node_http = __toESM(require("http"));
756
1185
  var import_node_https = __toESM(require("https"));
757
- var import_node_module = require("module");
758
- var RESPONSE_PAGE_SIZE = 100;
759
- var recorders = /* @__PURE__ */ new Set();
1186
+ var import_node_module2 = require("module");
1187
+ var RESPONSE_PAGE_SIZE2 = 100;
1188
+ var recorders2 = /* @__PURE__ */ new Set();
760
1189
  var originals = /* @__PURE__ */ new Map();
1190
+ var installed = /* @__PURE__ */ new Map();
761
1191
  var undiciUnsubscribe = null;
762
1192
  function describeTarget(defaultProtocol, args) {
763
1193
  try {
@@ -786,8 +1216,8 @@ function describeTarget(defaultProtocol, args) {
786
1216
  return null;
787
1217
  }
788
1218
  }
789
- function emit(entry) {
790
- for (const record of recorders) {
1219
+ function emit2(entry) {
1220
+ for (const record of recorders2) {
791
1221
  try {
792
1222
  record(entry);
793
1223
  } catch {
@@ -799,21 +1229,24 @@ function wrapModule(mod, protocol) {
799
1229
  const key = `${protocol}${name}`;
800
1230
  const original = mod[name];
801
1231
  originals.set(key, original);
1232
+ const state = { live: true };
802
1233
  const wrapped = function(...args) {
803
1234
  const req = original.apply(this, args);
804
- const target = recorders.size > 0 ? describeTarget(protocol, args) : null;
1235
+ const target = state.live && recorders2.size > 0 && !isSuppressed() ? describeTarget(protocol, args) : null;
805
1236
  if (target && req && typeof req.once === "function") {
806
1237
  const started = process.hrtime.bigint();
807
1238
  const timestampMs = Date.now();
1239
+ const requestId = currentRequestId();
808
1240
  let done = false;
809
1241
  const finish = (status, error) => {
810
1242
  if (done) return;
811
1243
  done = true;
812
- emit({
1244
+ emit2({
813
1245
  ...target,
814
1246
  status,
815
1247
  durationMs: Number(process.hrtime.bigint() - started) / 1e6,
816
1248
  timestampMs,
1249
+ ...requestId !== void 0 ? { requestId } : {},
817
1250
  ...error ? { error } : {}
818
1251
  });
819
1252
  };
@@ -826,17 +1259,23 @@ function wrapModule(mod, protocol) {
826
1259
  }
827
1260
  return req;
828
1261
  };
1262
+ installed.set(key, { fn: wrapped, state });
829
1263
  mod[name] = wrapped;
830
1264
  }
831
- (0, import_node_module.syncBuiltinESMExports)();
1265
+ (0, import_node_module2.syncBuiltinESMExports)();
832
1266
  }
833
1267
  function unwrapModule(mod, protocol) {
834
1268
  for (const name of ["request", "get"]) {
835
- const original = originals.get(`${protocol}${name}`);
836
- if (original) mod[name] = original;
837
- originals.delete(`${protocol}${name}`);
838
- }
839
- (0, import_node_module.syncBuiltinESMExports)();
1269
+ const key = `${protocol}${name}`;
1270
+ const original = originals.get(key);
1271
+ const ours = installed.get(key);
1272
+ const target = mod;
1273
+ if (original && ours && target[name] === ours.fn) target[name] = original;
1274
+ if (ours) ours.state.live = false;
1275
+ originals.delete(key);
1276
+ installed.delete(key);
1277
+ }
1278
+ (0, import_node_module2.syncBuiltinESMExports)();
840
1279
  }
841
1280
  function subscribeUndici() {
842
1281
  const started = /* @__PURE__ */ new WeakMap();
@@ -847,7 +1286,13 @@ function subscribeUndici() {
847
1286
  });
848
1287
  const onCreate = (message) => {
849
1288
  const request = message.request;
850
- if (request) started.set(request, { at: process.hrtime.bigint(), ts: Date.now() });
1289
+ if (request && !isSuppressed()) {
1290
+ started.set(request, {
1291
+ at: process.hrtime.bigint(),
1292
+ ts: Date.now(),
1293
+ requestId: currentRequestId()
1294
+ });
1295
+ }
851
1296
  };
852
1297
  const onHeaders = (message) => {
853
1298
  const { request, response } = message;
@@ -860,11 +1305,12 @@ function subscribeUndici() {
860
1305
  const begin = started.get(request);
861
1306
  if (!begin) return;
862
1307
  started.delete(request);
863
- emit({
1308
+ emit2({
864
1309
  ...target(request),
865
1310
  status: statuses.get(request) ?? null,
866
1311
  durationMs: Number(process.hrtime.bigint() - begin.at) / 1e6,
867
1312
  timestampMs: begin.ts,
1313
+ ...begin.requestId !== void 0 ? { requestId: begin.requestId } : {},
868
1314
  ...error ? { error } : {}
869
1315
  });
870
1316
  };
@@ -885,8 +1331,8 @@ function subscribeUndici() {
885
1331
  };
886
1332
  }
887
1333
  function interceptOutgoing(record) {
888
- const first = recorders.size === 0;
889
- recorders.add(record);
1334
+ const first = recorders2.size === 0;
1335
+ recorders2.add(record);
890
1336
  if (first) {
891
1337
  wrapModule(import_node_http.default, "http:");
892
1338
  wrapModule(import_node_https.default, "https:");
@@ -897,7 +1343,7 @@ function interceptOutgoing(record) {
897
1343
  }
898
1344
  }
899
1345
  return () => {
900
- if (!recorders.delete(record) || recorders.size > 0) return;
1346
+ if (!recorders2.delete(record) || recorders2.size > 0) return;
901
1347
  unwrapModule(import_node_http.default, "http:");
902
1348
  unwrapModule(import_node_https.default, "https:");
903
1349
  undiciUnsubscribe?.();
@@ -910,6 +1356,8 @@ var OutgoingProvider = class {
910
1356
  nextId = 1;
911
1357
  failed = 0;
912
1358
  release = null;
1359
+ /** Called with every newly recorded entry (persistence hook). */
1360
+ onRecord;
913
1361
  constructor(size) {
914
1362
  this.buffer = new RingBuffer(size);
915
1363
  }
@@ -922,12 +1370,22 @@ var OutgoingProvider = class {
922
1370
  this.release = null;
923
1371
  }
924
1372
  record(entry) {
925
- this.buffer.push({ id: this.nextId, ...entry });
1373
+ const full = { id: this.nextId, ...entry };
1374
+ this.buffer.push(full);
926
1375
  this.nextId += 1;
927
1376
  if (entry.error || entry.status !== null && entry.status >= 500) this.failed += 1;
1377
+ this.onRecord?.(full);
1378
+ }
1379
+ /** Re-inserts journaled entries without re-emitting them. */
1380
+ restore(entries) {
1381
+ for (const entry of entries) {
1382
+ this.buffer.push(entry);
1383
+ this.nextId = Math.max(this.nextId, entry.id + 1);
1384
+ if (entry.error || entry.status !== null && entry.status >= 500) this.failed += 1;
1385
+ }
928
1386
  }
929
1387
  get() {
930
- const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE));
1388
+ const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE2));
931
1389
  return { ok: true, data: { total: this.nextId - 1, failed: this.failed, entries } };
932
1390
  }
933
1391
  };
@@ -935,10 +1393,10 @@ var OutgoingProvider = class {
935
1393
  // src/providers/heap-snapshot.ts
936
1394
  var import_promises = require("fs/promises");
937
1395
  var import_node_os2 = require("os");
938
- var import_node_path2 = require("path");
1396
+ var import_node_path3 = require("path");
939
1397
  var import_node_v8 = require("v8");
940
1398
  function defaultSnapshotDir() {
941
- return process.env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "nodeui-heap");
1399
+ return process.env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "nodeui-heap");
942
1400
  }
943
1401
  var HeapSnapshotProvider = class {
944
1402
  id = "heap-snapshot";
@@ -950,7 +1408,7 @@ var HeapSnapshotProvider = class {
950
1408
  const dir = ctx.config.heapSnapshotDir || defaultSnapshotDir();
951
1409
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
952
1410
  const fileName = `nodeui-heap-${process.pid}-${stamp}.heapsnapshot`;
953
- const filePath = (0, import_node_path2.join)(dir, fileName);
1411
+ const filePath = (0, import_node_path3.join)(dir, fileName);
954
1412
  try {
955
1413
  await (0, import_promises.mkdir)(dir, { recursive: true, mode: 448 });
956
1414
  (0, import_node_v8.writeHeapSnapshot)(filePath);
@@ -977,24 +1435,89 @@ var HeapSnapshotProvider = class {
977
1435
  };
978
1436
 
979
1437
  // src/providers/requests.ts
980
- var RESPONSE_PAGE_SIZE2 = 100;
1438
+ var RESPONSE_PAGE_SIZE3 = 100;
1439
+ var MAX_ROUTE_STATS = 8;
1440
+ function percentile(sorted, p) {
1441
+ if (sorted.length === 0) return 0;
1442
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1));
1443
+ return sorted[index] ?? 0;
1444
+ }
1445
+ function routeKey(path) {
1446
+ return path.split("/").map(
1447
+ (seg) => /^\d+$/.test(seg) || /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(seg) || /^[0-9a-f]{24}$/i.test(seg) ? ":id" : seg
1448
+ ).join("/");
1449
+ }
1450
+ function summarizeRequests(entries) {
1451
+ const durations = entries.map((e) => e.durationMs).sort((a, b) => a - b);
1452
+ const byStatus = { "2xx": 0, "3xx": 0, "4xx": 0, "5xx": 0 };
1453
+ const groups = /* @__PURE__ */ new Map();
1454
+ for (const e of entries) {
1455
+ const bucket = `${Math.floor(e.status / 100)}xx`;
1456
+ if (bucket in byStatus) byStatus[bucket] += 1;
1457
+ const path = e.route ?? routeKey(e.path);
1458
+ const key = `${e.method} ${path}`;
1459
+ const group = groups.get(key) ?? { method: e.method, path, ms: [], errors: 0 };
1460
+ group.ms.push(e.durationMs);
1461
+ if (e.status >= 500) group.errors += 1;
1462
+ groups.set(key, group);
1463
+ }
1464
+ const routes = [...groups.values()].map((g) => {
1465
+ const sorted = [...g.ms].sort((a, b) => a - b);
1466
+ return {
1467
+ method: g.method,
1468
+ path: g.path,
1469
+ count: g.ms.length,
1470
+ avgMs: g.ms.reduce((a, b) => a + b, 0) / g.ms.length,
1471
+ p95Ms: percentile(sorted, 0.95),
1472
+ errors: g.errors
1473
+ };
1474
+ }).sort((a, b) => b.p95Ms - a.p95Ms).slice(0, MAX_ROUTE_STATS);
1475
+ return {
1476
+ count: entries.length,
1477
+ errors: byStatus["5xx"],
1478
+ errorRate: entries.length === 0 ? 0 : byStatus["5xx"] / entries.length,
1479
+ avgMs: durations.length === 0 ? 0 : durations.reduce((a, b) => a + b, 0) / durations.length,
1480
+ p50Ms: percentile(durations, 0.5),
1481
+ p95Ms: percentile(durations, 0.95),
1482
+ p99Ms: percentile(durations, 0.99),
1483
+ byStatus,
1484
+ routes
1485
+ };
1486
+ }
981
1487
  var RequestsProvider = class {
982
1488
  id = "requests";
983
1489
  buffer;
984
1490
  nextId = 1;
1491
+ /** Called with every newly recorded entry (persistence hook). */
1492
+ onRecord;
985
1493
  constructor(size) {
986
1494
  this.buffer = new RingBuffer(size);
987
1495
  }
988
- record(entry) {
989
- this.buffer.push({ id: this.nextId, ...entry });
1496
+ /** Reserves an id up front so in-flight work can be attributed to the request. */
1497
+ reserveId() {
1498
+ const id = this.nextId;
990
1499
  this.nextId += 1;
1500
+ return id;
1501
+ }
1502
+ record(entry, id = this.reserveId()) {
1503
+ const full = { id, ...entry };
1504
+ this.buffer.push(full);
1505
+ this.onRecord?.(full);
1506
+ }
1507
+ /** Re-inserts journaled entries without re-emitting them. */
1508
+ restore(entries) {
1509
+ for (const entry of entries) {
1510
+ this.buffer.push(entry);
1511
+ this.nextId = Math.max(this.nextId, entry.id + 1);
1512
+ }
991
1513
  }
992
1514
  get length() {
993
1515
  return this.buffer.length;
994
1516
  }
995
1517
  get() {
996
- const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE2));
997
- return { ok: true, data: { total: this.buffer.length, entries } };
1518
+ const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE3));
1519
+ const summary = summarizeRequests(this.buffer.toArray());
1520
+ return { ok: true, data: { total: this.buffer.length, entries, summary } };
998
1521
  }
999
1522
  };
1000
1523
 
@@ -1172,7 +1695,7 @@ function installWrappers() {
1172
1695
  const wrapper = (...args) => {
1173
1696
  for (const push of pushes) {
1174
1697
  try {
1175
- push({ level, message: args.map(formatArg).join(" ") });
1698
+ push({ level, message: args.map(formatArg).join(" "), requestId: currentRequestId() });
1176
1699
  } catch {
1177
1700
  }
1178
1701
  }
@@ -1216,8 +1739,13 @@ var LogsProvider = class {
1216
1739
  }
1217
1740
  start() {
1218
1741
  if (this.release) return;
1219
- this.release = interceptConsole(({ level, message }) => {
1220
- this.buffer.push({ level, message, timestamp: Date.now() });
1742
+ this.release = interceptConsole(({ level, message, requestId }) => {
1743
+ this.buffer.push({
1744
+ level,
1745
+ message,
1746
+ timestamp: Date.now(),
1747
+ ...requestId !== void 0 ? { requestId } : {}
1748
+ });
1221
1749
  });
1222
1750
  }
1223
1751
  stop() {
@@ -1241,6 +1769,160 @@ var LogsProvider = class {
1241
1769
  }
1242
1770
  };
1243
1771
 
1772
+ // src/request-detail.ts
1773
+ var DEFAULT_REQUEST_DETAIL = {
1774
+ query: true,
1775
+ headers: true,
1776
+ bodies: false,
1777
+ maxBodyBytes: 4096
1778
+ };
1779
+ function resolveRequestDetail(option, bodiesFromEnv) {
1780
+ if (option === false) {
1781
+ return { query: false, headers: false, bodies: false, maxBodyBytes: 0 };
1782
+ }
1783
+ const given = typeof option === "object" ? option : {};
1784
+ const maxBodyBytes = given.maxBodyBytes ?? DEFAULT_REQUEST_DETAIL.maxBodyBytes;
1785
+ if (!Number.isInteger(maxBodyBytes) || maxBodyBytes <= 0 || maxBodyBytes > 1048576) {
1786
+ throw new Error("captureRequestDetail.maxBodyBytes must be an integer in 1..1048576");
1787
+ }
1788
+ return {
1789
+ query: given.query ?? DEFAULT_REQUEST_DETAIL.query,
1790
+ headers: given.headers ?? DEFAULT_REQUEST_DETAIL.headers,
1791
+ bodies: given.bodies ?? (bodiesFromEnv || DEFAULT_REQUEST_DETAIL.bodies),
1792
+ maxBodyBytes
1793
+ };
1794
+ }
1795
+ var ALWAYS_REDACT = /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key)$/i;
1796
+ var TEXTUAL_TYPE = /json|text|xml|x-www-form-urlencoded|javascript|graphql/i;
1797
+ function queryOf(req) {
1798
+ const raw = req.originalUrl ?? req.url ?? "";
1799
+ const index = raw.indexOf("?");
1800
+ if (index === -1) return void 0;
1801
+ const out = {};
1802
+ for (const [key, value] of new URLSearchParams(raw.slice(index + 1))) {
1803
+ out[key] = SECRET_KEY_PATTERN.test(key) ? SECRET_MASKED : value;
1804
+ }
1805
+ return Object.keys(out).length > 0 ? out : void 0;
1806
+ }
1807
+ function headersOf(req) {
1808
+ const out = {};
1809
+ for (const [name, value] of Object.entries(req.headers)) {
1810
+ if (value === void 0) continue;
1811
+ out[name] = ALWAYS_REDACT.test(name) || SECRET_KEY_PATTERN.test(name) ? SECRET_MASKED : Array.isArray(value) ? value.join(", ") : value;
1812
+ }
1813
+ return out;
1814
+ }
1815
+ function responseType(res) {
1816
+ const header = res.getHeader("content-type");
1817
+ if (header !== void 0) return String(header);
1818
+ const raw = res._header;
1819
+ const match = typeof raw === "string" ? /^content-type:[ \t]*(.*?)\r?$/im.exec(raw) : null;
1820
+ return match?.[1] ?? "";
1821
+ }
1822
+ var BodyCollector = class {
1823
+ constructor(max) {
1824
+ this.max = max;
1825
+ }
1826
+ max;
1827
+ chunks = [];
1828
+ size = 0;
1829
+ total = 0;
1830
+ add(chunk, encoding) {
1831
+ let buf;
1832
+ if (Buffer.isBuffer(chunk)) buf = chunk;
1833
+ else if (typeof chunk === "string") {
1834
+ buf = Buffer.from(
1835
+ chunk,
1836
+ typeof encoding === "string" ? encoding : "utf8"
1837
+ );
1838
+ } else if (chunk instanceof Uint8Array) buf = Buffer.from(chunk);
1839
+ else return;
1840
+ this.total += buf.length;
1841
+ if (this.size >= this.max) return;
1842
+ const slice = buf.subarray(0, this.max - this.size);
1843
+ this.chunks.push(slice);
1844
+ this.size += slice.length;
1845
+ }
1846
+ get truncated() {
1847
+ return this.total > this.size;
1848
+ }
1849
+ text() {
1850
+ return Buffer.concat(this.chunks).toString("utf8");
1851
+ }
1852
+ };
1853
+ function maskBody(text, contentType, mask, truncated) {
1854
+ if (!mask) return text;
1855
+ if (!truncated && /json/i.test(contentType)) {
1856
+ try {
1857
+ return JSON.stringify(maskSecrets(JSON.parse(text)));
1858
+ } catch {
1859
+ }
1860
+ }
1861
+ return maskSecretText(text);
1862
+ }
1863
+ function captureRequestDetail(req, res, settings, mask) {
1864
+ let reqBody = null;
1865
+ let resBody = null;
1866
+ if (settings.bodies) {
1867
+ const reqType = String(req.headers["content-type"] ?? "");
1868
+ if (TEXTUAL_TYPE.test(reqType)) {
1869
+ const collector2 = new BodyCollector(settings.maxBodyBytes);
1870
+ reqBody = collector2;
1871
+ const emit3 = req.emit.bind(req);
1872
+ req.emit = ((event, ...args) => {
1873
+ if (event === "data") {
1874
+ try {
1875
+ collector2.add(args[0]);
1876
+ } catch {
1877
+ }
1878
+ }
1879
+ return emit3(event, ...args);
1880
+ });
1881
+ }
1882
+ const collector = new BodyCollector(settings.maxBodyBytes);
1883
+ resBody = collector;
1884
+ const write = res.write.bind(res);
1885
+ const end = res.end.bind(res);
1886
+ const textual = () => TEXTUAL_TYPE.test(responseType(res));
1887
+ res.write = ((chunk, ...rest) => {
1888
+ try {
1889
+ if (textual()) collector.add(chunk, rest[0]);
1890
+ } catch {
1891
+ }
1892
+ return write(chunk, ...rest);
1893
+ });
1894
+ res.end = ((chunk, ...rest) => {
1895
+ try {
1896
+ if (chunk && typeof chunk !== "function" && textual()) collector.add(chunk, rest[0]);
1897
+ } catch {
1898
+ }
1899
+ return end(chunk, ...rest);
1900
+ });
1901
+ }
1902
+ return () => {
1903
+ const detail = {};
1904
+ if (settings.query) {
1905
+ const query = queryOf(req);
1906
+ if (query) detail.query = query;
1907
+ }
1908
+ if (settings.headers) detail.headers = headersOf(req);
1909
+ if (reqBody && reqBody.total > 0) {
1910
+ detail.requestBody = maskBody(
1911
+ reqBody.text(),
1912
+ String(req.headers["content-type"] ?? ""),
1913
+ mask,
1914
+ reqBody.truncated
1915
+ );
1916
+ if (reqBody.truncated) detail.requestBodyTruncated = true;
1917
+ }
1918
+ if (resBody && resBody.total > 0) {
1919
+ detail.responseBody = maskBody(resBody.text(), responseType(res), mask, resBody.truncated);
1920
+ if (resBody.truncated) detail.responseBodyTruncated = true;
1921
+ }
1922
+ return detail;
1923
+ };
1924
+ }
1925
+
1244
1926
  // src/sse.ts
1245
1927
  function startSse(res, extraHeaders = {}) {
1246
1928
  let closed = false;
@@ -1273,8 +1955,260 @@ function startSse(res, extraHeaders = {}) {
1273
1955
 
1274
1956
  // src/server.ts
1275
1957
  var import_node_os3 = require("os");
1276
- var import_node_path3 = require("path");
1277
- var STATIC_ROOT = (0, import_node_path3.resolve)(__dirname, "..", "static");
1958
+ var import_node_path5 = require("path");
1959
+
1960
+ // src/otlp.ts
1961
+ var import_node_crypto4 = require("crypto");
1962
+ var KIND_SERVER = 2;
1963
+ var KIND_CLIENT = 3;
1964
+ var STATUS_ERROR = 2;
1965
+ var MAX_QUEUE = 1e3;
1966
+ var MAX_BATCH = 200;
1967
+ var MAX_TRACKED_REQUESTS = 1e3;
1968
+ var hex = (bytes) => (0, import_node_crypto4.randomBytes)(bytes).toString("hex");
1969
+ var nanos = (ms) => (BigInt(Math.round(ms * 1e3)) * 1000n).toString();
1970
+ var str = (key, value) => ({ key, value: { stringValue: value } });
1971
+ var int = (key, value) => ({
1972
+ key,
1973
+ value: { intValue: String(Math.round(value)) }
1974
+ });
1975
+ function tracesUrl(endpoint) {
1976
+ const trimmed = endpoint.replace(/\/+$/, "");
1977
+ return trimmed.endsWith("/v1/traces") ? trimmed : `${trimmed}/v1/traces`;
1978
+ }
1979
+ var OtlpExporter = class {
1980
+ constructor(options, env = process.env) {
1981
+ this.options = options;
1982
+ this.url = tracesUrl(options.endpoint);
1983
+ this.serviceName = options.serviceName ?? env.OTEL_SERVICE_NAME ?? "nodeui-app";
1984
+ }
1985
+ options;
1986
+ url;
1987
+ serviceName;
1988
+ queue = [];
1989
+ timer = null;
1990
+ contexts = /* @__PURE__ */ new Map();
1991
+ warned = false;
1992
+ exported = 0;
1993
+ dropped = 0;
1994
+ /** Stable ids for a request so children recorded earlier link to its span. */
1995
+ contextFor(requestId) {
1996
+ let ctx = this.contexts.get(requestId);
1997
+ if (!ctx) {
1998
+ ctx = { traceId: hex(16), spanId: hex(8) };
1999
+ this.contexts.set(requestId, ctx);
2000
+ if (this.contexts.size > MAX_TRACKED_REQUESTS) {
2001
+ const oldest = this.contexts.keys().next().value;
2002
+ if (oldest !== void 0) this.contexts.delete(oldest);
2003
+ }
2004
+ }
2005
+ return ctx;
2006
+ }
2007
+ request(entry) {
2008
+ const ctx = this.contextFor(entry.id);
2009
+ const attributes = [
2010
+ str("http.request.method", entry.method),
2011
+ str("url.path", maskSecretText(entry.path)),
2012
+ int("http.response.status_code", entry.status),
2013
+ str("client.address", entry.ip)
2014
+ ];
2015
+ if (entry.route) attributes.push(str("http.route", entry.route));
2016
+ this.push({
2017
+ ...ctx,
2018
+ name: `${entry.method} ${entry.route ?? entry.path}`,
2019
+ kind: KIND_SERVER,
2020
+ startTimeUnixNano: nanos(entry.timestampMs),
2021
+ endTimeUnixNano: nanos(entry.timestampMs + entry.durationMs),
2022
+ attributes,
2023
+ ...entry.status >= 500 ? { status: { code: STATUS_ERROR } } : {}
2024
+ });
2025
+ }
2026
+ outgoing(entry) {
2027
+ const attributes = [
2028
+ str("http.request.method", entry.method),
2029
+ str("url.full", maskSecretText(entry.url))
2030
+ ];
2031
+ if (entry.status !== null) attributes.push(int("http.response.status_code", entry.status));
2032
+ const failed = Boolean(entry.error) || entry.status !== null && entry.status >= 500;
2033
+ this.child(entry.requestId, {
2034
+ name: `${entry.method} ${entry.url}`,
2035
+ timestampMs: entry.timestampMs,
2036
+ durationMs: entry.durationMs,
2037
+ attributes,
2038
+ ...failed ? { status: { code: STATUS_ERROR, message: entry.error } } : {}
2039
+ });
2040
+ }
2041
+ query(entry) {
2042
+ const statement = maskSecretText(entry.sql);
2043
+ this.child(entry.requestId, {
2044
+ name: `${statement.split(/\s+/)[0]?.toUpperCase() ?? "QUERY"} ${entry.system}`,
2045
+ timestampMs: entry.timestampMs,
2046
+ durationMs: entry.durationMs,
2047
+ attributes: [str("db.system.name", entry.system), str("db.query.text", statement)],
2048
+ ...entry.error ? { status: { code: STATUS_ERROR, message: entry.error } } : {}
2049
+ });
2050
+ }
2051
+ child(requestId, span) {
2052
+ const parent = requestId === void 0 ? void 0 : this.contextFor(requestId);
2053
+ this.push({
2054
+ traceId: parent?.traceId ?? hex(16),
2055
+ spanId: hex(8),
2056
+ ...parent ? { parentSpanId: parent.spanId } : {},
2057
+ name: span.name,
2058
+ kind: KIND_CLIENT,
2059
+ startTimeUnixNano: nanos(span.timestampMs),
2060
+ endTimeUnixNano: nanos(span.timestampMs + span.durationMs),
2061
+ attributes: span.attributes,
2062
+ ...span.status ? { status: span.status } : {}
2063
+ });
2064
+ }
2065
+ push(span) {
2066
+ if (this.queue.length >= MAX_QUEUE) {
2067
+ this.queue.shift();
2068
+ this.dropped += 1;
2069
+ }
2070
+ this.queue.push(span);
2071
+ if (!this.timer) {
2072
+ this.timer = setTimeout(() => void this.flush(), this.options.intervalMs ?? 2e3);
2073
+ if (typeof this.timer.unref === "function") this.timer.unref();
2074
+ }
2075
+ }
2076
+ body(spans) {
2077
+ return JSON.stringify({
2078
+ resourceSpans: [
2079
+ {
2080
+ resource: { attributes: [str("service.name", this.serviceName)] },
2081
+ scopeSpans: [{ scope: { name: "@singhak/nodeui" }, spans }]
2082
+ }
2083
+ ]
2084
+ });
2085
+ }
2086
+ async flush() {
2087
+ this.timer = null;
2088
+ while (this.queue.length > 0) {
2089
+ const batch = this.queue.splice(0, MAX_BATCH);
2090
+ try {
2091
+ const res = await runSuppressed(
2092
+ () => fetch(this.url, {
2093
+ method: "POST",
2094
+ headers: { "content-type": "application/json", ...this.options.headers },
2095
+ body: this.body(batch),
2096
+ signal: AbortSignal.timeout(5e3)
2097
+ })
2098
+ );
2099
+ if (!res.ok) throw new Error(`collector answered ${res.status}`);
2100
+ this.exported += batch.length;
2101
+ } catch (err) {
2102
+ this.dropped += batch.length;
2103
+ if (!this.warned) {
2104
+ this.warned = true;
2105
+ console.warn(
2106
+ `[nodeui] OTLP export to ${this.url} failed (${err instanceof Error ? err.message : String(err)}); dropping spans.`
2107
+ );
2108
+ }
2109
+ return;
2110
+ }
2111
+ }
2112
+ }
2113
+ /** Cancels the timer and makes a best-effort final export. */
2114
+ close() {
2115
+ if (this.timer) clearTimeout(this.timer);
2116
+ this.timer = null;
2117
+ void this.flush();
2118
+ }
2119
+ };
2120
+
2121
+ // src/persistence.ts
2122
+ var import_node_fs2 = require("fs");
2123
+ var import_node_path4 = require("path");
2124
+ var DEFAULT_PERSIST_MAX_BYTES = 5 * 1024 * 1024;
2125
+ var FLUSH_DELAY_MS = 250;
2126
+ var Persistence = class {
2127
+ constructor(file, maxBytes = DEFAULT_PERSIST_MAX_BYTES) {
2128
+ this.file = file;
2129
+ this.maxBytes = maxBytes;
2130
+ try {
2131
+ (0, import_node_fs2.mkdirSync)((0, import_node_path4.dirname)(file), { recursive: true });
2132
+ } catch {
2133
+ }
2134
+ }
2135
+ file;
2136
+ maxBytes;
2137
+ pending = [];
2138
+ timer = null;
2139
+ writing = false;
2140
+ append(kind, data) {
2141
+ try {
2142
+ this.pending.push(`${JSON.stringify({ kind, data })}
2143
+ `);
2144
+ } catch {
2145
+ return;
2146
+ }
2147
+ if (!this.timer) {
2148
+ this.timer = setTimeout(() => this.flush(), FLUSH_DELAY_MS);
2149
+ if (typeof this.timer.unref === "function") this.timer.unref();
2150
+ }
2151
+ }
2152
+ /** Reads the journal (rotated file first) in chronological order. */
2153
+ load() {
2154
+ const out = [];
2155
+ for (const path of [`${this.file}.1`, this.file]) {
2156
+ let text;
2157
+ try {
2158
+ text = (0, import_node_fs2.readFileSync)(path, "utf8");
2159
+ } catch {
2160
+ continue;
2161
+ }
2162
+ for (const line of text.split("\n")) {
2163
+ if (!line) continue;
2164
+ try {
2165
+ const parsed = JSON.parse(line);
2166
+ if (parsed && typeof parsed.kind === "string") out.push(parsed);
2167
+ } catch {
2168
+ }
2169
+ }
2170
+ }
2171
+ return out;
2172
+ }
2173
+ rotateIfNeeded() {
2174
+ try {
2175
+ if ((0, import_node_fs2.existsSync)(this.file) && (0, import_node_fs2.statSync)(this.file).size >= this.maxBytes) {
2176
+ (0, import_node_fs2.renameSync)(this.file, `${this.file}.1`);
2177
+ }
2178
+ } catch {
2179
+ }
2180
+ }
2181
+ flush() {
2182
+ this.timer = null;
2183
+ if (this.writing || this.pending.length === 0) {
2184
+ if (this.pending.length > 0) this.timer = setTimeout(() => this.flush(), FLUSH_DELAY_MS);
2185
+ return;
2186
+ }
2187
+ this.rotateIfNeeded();
2188
+ const chunk = this.pending.join("");
2189
+ this.pending = [];
2190
+ this.writing = true;
2191
+ (0, import_node_fs2.appendFile)(this.file, chunk, { mode: 384 }, () => {
2192
+ this.writing = false;
2193
+ });
2194
+ }
2195
+ /** Writes anything still buffered, synchronously (used on shutdown). */
2196
+ close() {
2197
+ if (this.timer) {
2198
+ clearTimeout(this.timer);
2199
+ this.timer = null;
2200
+ }
2201
+ if (this.pending.length === 0) return;
2202
+ try {
2203
+ this.rotateIfNeeded();
2204
+ (0, import_node_fs2.appendFileSync)(this.file, this.pending.join(""), { mode: 384 });
2205
+ } catch {
2206
+ }
2207
+ this.pending = [];
2208
+ }
2209
+ };
2210
+
2211
+ // src/server.ts
1278
2212
  var SSE_HEARTBEAT_MS = 15e3;
1279
2213
  function serializeEnvelope(envelope, mask = true) {
1280
2214
  return JSON.stringify(mask ? maskSecrets(envelope) : envelope);
@@ -1291,7 +2225,9 @@ var BUILT_IN_PANELS = /* @__PURE__ */ new Set([
1291
2225
  "routes",
1292
2226
  "logs",
1293
2227
  "metrics",
1294
- "outgoing"
2228
+ "outgoing",
2229
+ "errors",
2230
+ "queries"
1295
2231
  ]);
1296
2232
  var PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
1297
2233
  var RESERVED_API_NAMES = /* @__PURE__ */ new Set(["config", "live", "confirmations"]);
@@ -1306,6 +2242,13 @@ function listFrom(value, raw) {
1306
2242
  if (value) return value;
1307
2243
  return (raw ?? "").split(",").map((v) => v.trim()).filter(Boolean);
1308
2244
  }
2245
+ function otlpHostname(endpoint) {
2246
+ try {
2247
+ return new URL(endpoint).hostname.replace(/^\[|\]$/g, "");
2248
+ } catch {
2249
+ throw new Error(`nodeui otlp endpoint "${endpoint}" is not a valid URL`);
2250
+ }
2251
+ }
1309
2252
  function pathnameOf(req) {
1310
2253
  const raw = req.originalUrl ?? req.url ?? "/";
1311
2254
  try {
@@ -1319,6 +2262,13 @@ function normalizePath(value) {
1319
2262
  if (!trimmed.startsWith("/")) throw new Error(`NODEUI_PATH must start with "/", got "${value}"`);
1320
2263
  return trimmed.replace(/\/+$/, "") || "/";
1321
2264
  }
2265
+ function resolveCapture(option, env) {
2266
+ const value = option ?? env ?? "always";
2267
+ if (value !== "always" && value !== "lazy") {
2268
+ throw new Error(`nodeui capture must be 'always' or 'lazy', got "${String(value)}"`);
2269
+ }
2270
+ return value;
2271
+ }
1322
2272
  function positiveInt(value, fallback, name) {
1323
2273
  const n = value === void 0 ? fallback : value;
1324
2274
  if (!Number.isInteger(n) || n <= 0) throw new Error(`${name} must be a positive integer`);
@@ -1354,6 +2304,18 @@ function createNodeUI(options = {}) {
1354
2304
  enabled: activation.active,
1355
2305
  activationReason: activation.reason,
1356
2306
  maskSecrets: options.maskSecrets ?? true,
2307
+ capture: resolveCapture(options.capture, env.NODEUI_CAPTURE),
2308
+ persistFile: (typeof options.persist === "string" ? options.persist : options.persist?.file) ?? env.NODEUI_PERSIST_FILE ?? null,
2309
+ persistMaxBytes: positiveInt(
2310
+ typeof options.persist === "object" ? options.persist.maxBytes : void 0,
2311
+ DEFAULT_PERSIST_MAX_BYTES,
2312
+ "persist.maxBytes"
2313
+ ),
2314
+ otlp: typeof options.otlp === "string" ? { endpoint: options.otlp } : options.otlp ?? (env.NODEUI_OTLP_ENDPOINT ? { endpoint: env.NODEUI_OTLP_ENDPOINT } : null),
2315
+ requestDetail: resolveRequestDetail(
2316
+ options.captureRequestDetail,
2317
+ env.NODEUI_CAPTURE_BODIES === "true"
2318
+ ),
1357
2319
  inactivityTimeoutMs: positiveInt(
1358
2320
  options.inactivityTimeoutMs,
1359
2321
  env.NODEUI_INACTIVITY_TIMEOUT_MS ? Number(env.NODEUI_INACTIVITY_TIMEOUT_MS) : 6e4,
@@ -1364,7 +2326,7 @@ function createNodeUI(options = {}) {
1364
2326
  env.NODEUI_CONFIRM_TTL_MS ? Number(env.NODEUI_CONFIRM_TTL_MS) : 6e4,
1365
2327
  "NODEUI_CONFIRM_TTL_MS"
1366
2328
  ),
1367
- heapSnapshotDir: options.heapSnapshotDir ?? env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_path3.join)((0, import_node_os3.tmpdir)(), "nodeui-heap"),
2329
+ heapSnapshotDir: options.heapSnapshotDir ?? env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_path5.join)((0, import_node_os3.tmpdir)(), "nodeui-heap"),
1368
2330
  allowedHosts: listFrom(options.allowedHosts, env.NODEUI_ALLOWED_HOSTS),
1369
2331
  allowedOrigins: listFrom(options.allowedOrigins, env.NODEUI_ALLOWED_ORIGINS),
1370
2332
  allowedRemoteAddresses: listFrom(options.allowedRemoteAddresses, env.NODEUI_ALLOWED_REMOTE),
@@ -1395,6 +2357,15 @@ function createNodeUI(options = {}) {
1395
2357
  "[nodeui] running with NODE_ENV=production. The console exposes environment, logs and heap data; keep it behind loopback and an auth token."
1396
2358
  );
1397
2359
  }
2360
+ if (config.enabled) {
2361
+ for (const entry of config.allowedRemoteAddresses) {
2362
+ if (!isValidAddressEntry(entry)) {
2363
+ console.warn(
2364
+ `[nodeui] ignoring allowedRemoteAddresses entry "${entry}": expected an IP or a CIDR such as 172.16.0.0/12 or fd00::/8; it will never match.`
2365
+ );
2366
+ }
2367
+ }
2368
+ }
1398
2369
  if (config.enabled && !config.maskSecrets) {
1399
2370
  console.warn("[nodeui] secret masking is disabled; panels may show credentials.");
1400
2371
  }
@@ -1425,9 +2396,59 @@ function createNodeUI(options = {}) {
1425
2396
  registry.register(envProvider);
1426
2397
  const routesProvider = new RoutesProvider();
1427
2398
  registry.register(routesProvider);
1428
- registry.register(
1429
- new OutgoingProvider(positiveInt(options.outgoingLogSize, 200, "outgoingLogSize"))
2399
+ const outgoingProvider = new OutgoingProvider(
2400
+ positiveInt(options.outgoingLogSize, 200, "outgoingLogSize")
1430
2401
  );
2402
+ registry.register(outgoingProvider);
2403
+ const queriesProvider = new QueriesProvider({
2404
+ size: positiveInt(options.queryLogSize, 200, "queryLogSize"),
2405
+ slowQueryMs: positiveInt(options.slowQueryMs, 100, "slowQueryMs")
2406
+ });
2407
+ registry.register(queriesProvider);
2408
+ const errorsProvider = new ErrorsProvider();
2409
+ registry.register(errorsProvider);
2410
+ if (config.enabled) errorsProvider.attach();
2411
+ const persistence = config.enabled && config.persistFile ? new Persistence(config.persistFile, config.persistMaxBytes) : null;
2412
+ const exporter = config.enabled && config.otlp ? new OtlpExporter(config.otlp) : null;
2413
+ const pinned = /* @__PURE__ */ new Set();
2414
+ const otlpHost = config.otlp ? otlpHostname(config.otlp.endpoint) : null;
2415
+ if (exporter && config.otlp && !isLoopbackHostname(otlpHost)) {
2416
+ console.warn(
2417
+ `[nodeui] exporting telemetry to non-local OTLP endpoint ${config.otlp.endpoint}; requests, queries and outgoing URLs leave this machine (masked).`
2418
+ );
2419
+ }
2420
+ if (persistence) {
2421
+ const byKind = (kind) => persistence.load().filter((r) => r.kind === kind).map((r) => r.data);
2422
+ requestsProvider.restore(byKind("request"));
2423
+ outgoingProvider.restore(byKind("outgoing"));
2424
+ queriesProvider.restore(byKind("query"));
2425
+ errorsProvider.restore(byKind("error"));
2426
+ }
2427
+ if (persistence || exporter) {
2428
+ const save = (kind, data) => persistence?.append(kind, config.maskSecrets ? maskSecrets(data) : data);
2429
+ requestsProvider.onRecord = (e) => {
2430
+ save("request", e);
2431
+ exporter?.request(e);
2432
+ };
2433
+ outgoingProvider.onRecord = (e) => {
2434
+ save("outgoing", e);
2435
+ exporter?.outgoing(e);
2436
+ };
2437
+ queriesProvider.onRecord = (e) => {
2438
+ save("query", e);
2439
+ exporter?.query(e);
2440
+ };
2441
+ errorsProvider.onRecord = (e) => save("error", e);
2442
+ }
2443
+ const pinnedProviders = [];
2444
+ if (persistence || exporter || config.enabled && config.capture === "always") {
2445
+ pinnedProviders.push(outgoingProvider, queriesProvider);
2446
+ }
2447
+ if (config.enabled && config.capture === "always") pinnedProviders.push(logsProvider);
2448
+ for (const provider of pinnedProviders) {
2449
+ provider.start?.(ctx);
2450
+ pinned.add(provider.id);
2451
+ }
1431
2452
  const pluginMeta = [];
1432
2453
  for (const plugin of options.plugins ?? []) {
1433
2454
  if (!PLUGIN_ID_PATTERN.test(plugin.id) || BUILT_IN_PANELS.has(plugin.id) || RESERVED_API_NAMES.has(plugin.id) || registry.get(plugin.id)) {
@@ -1444,7 +2465,7 @@ function createNodeUI(options = {}) {
1444
2465
  let sseClients = 0;
1445
2466
  function stopAll() {
1446
2467
  for (const id of activeProviders) {
1447
- registry.get(id)?.stop?.(ctx);
2468
+ if (!pinned.has(id)) registry.get(id)?.stop?.(ctx);
1448
2469
  }
1449
2470
  activeProviders.clear();
1450
2471
  if (inactivityTimer) {
@@ -1516,7 +2537,7 @@ function createNodeUI(options = {}) {
1516
2537
  pollIntervalMs: config.pollIntervalMs,
1517
2538
  panels: registry.ids(),
1518
2539
  masking: { enabled: config.maskSecrets, pattern: SECRET_KEY_PATTERN.source },
1519
- authRequired: config.authRequired,
2540
+ locked: config.authRequired,
1520
2541
  plugins: pluginMeta
1521
2542
  };
1522
2543
  }
@@ -1616,7 +2637,7 @@ function createNodeUI(options = {}) {
1616
2637
  }
1617
2638
  function serveStatic(req, res, urlPath) {
1618
2639
  const relative = urlPath.slice(path.length);
1619
- const asset = resolveStaticAsset(STATIC_ROOT, relative);
2640
+ const asset = resolveStaticAsset(uiAssetsDir(), relative);
1620
2641
  if (!asset) {
1621
2642
  sendJson(res, 404, { ok: false, error: { code: "not-found", message: "Not found" } });
1622
2643
  return;
@@ -1677,21 +2698,37 @@ function createNodeUI(options = {}) {
1677
2698
  ctx.store["express-router"] = router;
1678
2699
  }
1679
2700
  }
2701
+ function routePatternOf(req) {
2702
+ const r = req;
2703
+ if (typeof r.nodeuiRoute === "string") return r.nodeuiRoute;
2704
+ const path2 = r.route?.path;
2705
+ if (typeof path2 !== "string") return void 0;
2706
+ return `${typeof r.baseUrl === "string" ? r.baseUrl : ""}${path2}` || void 0;
2707
+ }
1680
2708
  function recordAppRequest(req, res) {
1681
2709
  const started = process.hrtime.bigint();
1682
2710
  const timestampMs = Date.now();
2711
+ const requestId = requestsProvider.reserveId();
2712
+ bindRequestId(req, requestId);
2713
+ const collectDetail = captureRequestDetail(req, res, config.requestDetail, config.maskSecrets);
1683
2714
  res.on("finish", () => {
1684
2715
  const durationMs = Number(process.hrtime.bigint() - started) / 1e6;
1685
- requestsProvider.record({
1686
- method: req.method ?? "?",
1687
- path: pathnameOf(req),
1688
- status: res.statusCode,
1689
- durationMs,
1690
- timestampMs,
1691
- ip: req.socket.remoteAddress ?? "unknown"
1692
- });
2716
+ requestsProvider.record(
2717
+ {
2718
+ method: req.method ?? "?",
2719
+ path: pathnameOf(req),
2720
+ status: res.statusCode,
2721
+ durationMs,
2722
+ timestampMs,
2723
+ ip: req.socket.remoteAddress ?? "unknown",
2724
+ route: routePatternOf(req),
2725
+ ...collectDetail()
2726
+ },
2727
+ requestId
2728
+ );
1693
2729
  metricsProvider.record(res.statusCode);
1694
2730
  });
2731
+ return requestId;
1695
2732
  }
1696
2733
  const server = {
1697
2734
  get config() {
@@ -1715,8 +2752,8 @@ function createNodeUI(options = {}) {
1715
2752
  void handleNodeUIPath(req, res, urlPath);
1716
2753
  return;
1717
2754
  }
1718
- recordAppRequest(req, res);
1719
- next();
2755
+ const requestId = recordAppRequest(req, res);
2756
+ runWithRequestId(requestId, next);
1720
2757
  };
1721
2758
  },
1722
2759
  async handle(req, res) {
@@ -1744,7 +2781,35 @@ function createNodeUI(options = {}) {
1744
2781
  addLogSource(entry) {
1745
2782
  logsProvider.addSource(entry);
1746
2783
  },
2784
+ recordQuery(query) {
2785
+ if (!config.enabled) return;
2786
+ queriesProvider.record({
2787
+ ...query,
2788
+ sql: query.sql.slice(0, 2e3),
2789
+ timestampMs: Date.now() - query.durationMs,
2790
+ requestId: currentRequestId()
2791
+ });
2792
+ },
2793
+ trackPrisma(client) {
2794
+ if (!config.enabled) return;
2795
+ client.$on(
2796
+ "query",
2797
+ (e) => server.recordQuery({ system: "prisma", sql: e.query, durationMs: Number(e.duration) })
2798
+ );
2799
+ },
2800
+ recordError(error, context) {
2801
+ if (!config.enabled) return;
2802
+ const e = error;
2803
+ const status = e?.status ?? e?.statusCode;
2804
+ if (typeof status === "number" && status >= 400 && status < 500) return;
2805
+ errorsProvider.record(error, "request", context);
2806
+ },
1747
2807
  shutdown() {
2808
+ errorsProvider.detach();
2809
+ persistence?.close();
2810
+ exporter?.close();
2811
+ for (const id of pinned) registry.get(id)?.stop?.(ctx);
2812
+ pinned.clear();
1748
2813
  stopAll();
1749
2814
  }
1750
2815
  };
@@ -1756,15 +2821,19 @@ function createNodeUI(options = {}) {
1756
2821
  ConfirmationStore,
1757
2822
  CpuProvider,
1758
2823
  DEFAULT_LOG_SIZE,
2824
+ DEFAULT_REQUEST_DETAIL,
1759
2825
  EnvProvider,
2826
+ ErrorsProvider,
1760
2827
  EventLoopLagProvider,
1761
2828
  HealthProvider,
1762
2829
  HeapSnapshotProvider,
1763
2830
  LogsProvider,
1764
2831
  MemoryProvider,
1765
2832
  MetricsProvider,
2833
+ N_PLUS_ONE_THRESHOLD,
1766
2834
  OutgoingProvider,
1767
2835
  ProviderRegistry,
2836
+ QueriesProvider,
1768
2837
  RequestsProvider,
1769
2838
  RingBuffer,
1770
2839
  RoutesProvider,
@@ -1774,18 +2843,27 @@ function createNodeUI(options = {}) {
1774
2843
  StartupTracker,
1775
2844
  createGuard,
1776
2845
  createNodeUI,
2846
+ currentRequestId,
2847
+ enterRequestContext,
1777
2848
  extractRoutes,
2849
+ fingerprintError,
1778
2850
  hostnameFromHostHeader,
1779
2851
  interceptConsole,
1780
2852
  interceptOutgoing,
2853
+ interceptQueries,
1781
2854
  isLoopbackAddress,
1782
2855
  isLoopbackHostname,
2856
+ isValidAddressEntry,
1783
2857
  maskSecretText,
1784
2858
  maskSecrets,
1785
2859
  matchesAddress,
2860
+ normalizeSql,
1786
2861
  resolveActivation,
1787
2862
  resolveStaticAsset,
2863
+ runInRequestContext,
1788
2864
  serializeEnvelope,
1789
- startSse
2865
+ startSse,
2866
+ summarizeRequests,
2867
+ uiAssetsDir
1790
2868
  });
1791
2869
  //# sourceMappingURL=index.cjs.map