silgi 0.1.0-beta.13 → 0.1.0-beta.14

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.
@@ -52,6 +52,156 @@ var RingBuffer = class {
52
52
  return this.#count;
53
53
  }
54
54
  };
55
+ const REDACTED = "[REDACTED]";
56
+ const SENSITIVE_HEADER_KEYS = new Set([
57
+ "authorization",
58
+ "cookie",
59
+ "set-cookie",
60
+ "x-api-key",
61
+ "x-auth-token",
62
+ "proxy-authorization"
63
+ ]);
64
+ function shouldRedactSensitiveData() {
65
+ return process.env.NODE_ENV === "production";
66
+ }
67
+ function redactHeaderValue(key, value) {
68
+ return shouldRedactSensitiveData() && SENSITIVE_HEADER_KEYS.has(key.toLowerCase()) ? REDACTED : value;
69
+ }
70
+ function asRecord(value) {
71
+ return value && typeof value === "object" ? value : null;
72
+ }
73
+ function normalizeString(value, fallback = "") {
74
+ return typeof value === "string" ? value : fallback;
75
+ }
76
+ function normalizeNumber(value, fallback = 0) {
77
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
78
+ }
79
+ function normalizeBoolean(value, fallback = false) {
80
+ return typeof value === "boolean" ? value : fallback;
81
+ }
82
+ function normalizeStringMap(value) {
83
+ const record = asRecord(value);
84
+ if (!record) return {};
85
+ const result = {};
86
+ for (const [key, raw] of Object.entries(record)) if (typeof raw === "string") result[key] = raw;
87
+ return result;
88
+ }
89
+ function normalizeSpanKind(value) {
90
+ switch (value) {
91
+ case "db":
92
+ case "http":
93
+ case "cache":
94
+ case "queue":
95
+ case "email":
96
+ case "ai":
97
+ case "custom": return value;
98
+ default: return "custom";
99
+ }
100
+ }
101
+ function normalizeTraceSpans(value) {
102
+ if (!Array.isArray(value)) return [];
103
+ const spans = [];
104
+ for (const entry of value) {
105
+ const span = asRecord(entry);
106
+ if (!span) continue;
107
+ const attributes = asRecord(span.attributes);
108
+ const normalizedAttributes = {};
109
+ if (attributes) {
110
+ for (const [key, raw] of Object.entries(attributes)) if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") normalizedAttributes[key] = raw;
111
+ }
112
+ spans.push({
113
+ name: normalizeString(span.name, "unknown"),
114
+ kind: normalizeSpanKind(span.kind),
115
+ durationMs: normalizeNumber(span.durationMs),
116
+ startOffsetMs: typeof span.startOffsetMs === "number" ? span.startOffsetMs : void 0,
117
+ detail: typeof span.detail === "string" ? span.detail : void 0,
118
+ input: span.input,
119
+ output: span.output,
120
+ error: typeof span.error === "string" ? span.error : void 0,
121
+ attributes: Object.keys(normalizedAttributes).length > 0 ? normalizedAttributes : void 0
122
+ });
123
+ }
124
+ return spans;
125
+ }
126
+ function inferPathFromUrl(url) {
127
+ if (!url) return "";
128
+ try {
129
+ return new URL(url).pathname || "";
130
+ } catch {
131
+ return "";
132
+ }
133
+ }
134
+ function normalizeProcedureCall(value, fallback) {
135
+ const record = asRecord(value);
136
+ return {
137
+ procedure: normalizeString(record?.procedure, fallback.path.replace(/^\//, "") || fallback.path || "request"),
138
+ durationMs: normalizeNumber(record?.durationMs, fallback.durationMs),
139
+ status: normalizeNumber(record?.status, fallback.status),
140
+ input: record?.input ?? null,
141
+ output: record?.output ?? null,
142
+ spans: normalizeTraceSpans(record?.spans),
143
+ error: typeof record?.error === "string" ? record.error : void 0
144
+ };
145
+ }
146
+ function normalizeRequestEntry(value, fallbackId) {
147
+ const record = asRecord(value);
148
+ if (!record) return null;
149
+ const url = normalizeString(record.url);
150
+ const path = normalizeString(record.path, inferPathFromUrl(url));
151
+ const method = normalizeString(record.method, "GET");
152
+ const status = normalizeNumber(record.status, 200);
153
+ const durationMs = normalizeNumber(record.durationMs);
154
+ const procedureFallback = {
155
+ path: path || "/",
156
+ durationMs,
157
+ status
158
+ };
159
+ const proceduresRaw = Array.isArray(record.procedures) ? record.procedures : [];
160
+ const procedures = proceduresRaw.length > 0 ? proceduresRaw.map((entry) => normalizeProcedureCall(entry, procedureFallback)) : [normalizeProcedureCall(null, procedureFallback)];
161
+ return {
162
+ id: normalizeNumber(record.id, fallbackId),
163
+ requestId: normalizeString(record.requestId, String(normalizeNumber(record.id, fallbackId))),
164
+ sessionId: normalizeString(record.sessionId),
165
+ timestamp: normalizeNumber(record.timestamp),
166
+ durationMs,
167
+ method,
168
+ url: url || path,
169
+ path,
170
+ ip: normalizeString(record.ip),
171
+ headers: normalizeStringMap(record.headers),
172
+ responseHeaders: normalizeStringMap(record.responseHeaders),
173
+ userAgent: normalizeString(record.userAgent),
174
+ status,
175
+ procedures,
176
+ isBatch: normalizeBoolean(record.isBatch, procedures.length > 1)
177
+ };
178
+ }
179
+ function normalizeErrorEntry(value, fallbackId) {
180
+ const record = asRecord(value);
181
+ if (!record) return null;
182
+ return {
183
+ id: normalizeNumber(record.id, fallbackId),
184
+ requestId: normalizeString(record.requestId),
185
+ timestamp: normalizeNumber(record.timestamp),
186
+ procedure: normalizeString(record.procedure, "request"),
187
+ error: normalizeString(record.error, "Unknown error"),
188
+ code: normalizeString(record.code, "INTERNAL_SERVER_ERROR"),
189
+ status: normalizeNumber(record.status, 500),
190
+ stack: normalizeString(record.stack),
191
+ input: record.input ?? null,
192
+ headers: normalizeStringMap(record.headers),
193
+ durationMs: normalizeNumber(record.durationMs),
194
+ spans: normalizeTraceSpans(record.spans)
195
+ };
196
+ }
197
+ function normalizeRequestEntries(value) {
198
+ if (!Array.isArray(value)) return [];
199
+ return value.map((entry, index) => normalizeRequestEntry(entry, index + 1)).filter((entry) => entry !== null);
200
+ }
201
+ function normalizeErrorEntries(value) {
202
+ if (!Array.isArray(value)) return [];
203
+ return value.map((entry, index) => normalizeErrorEntry(entry, index + 1)).filter((entry) => entry !== null);
204
+ }
55
205
  let _lastTime = 0;
56
206
  let _counter = 0;
57
207
  function generateRequestId() {
@@ -147,14 +297,12 @@ var AnalyticsStore = class {
147
297
  #storage;
148
298
  #pendingRequests = [];
149
299
  #pendingErrors = [];
150
- #maxRequests;
151
- #maxErrors;
300
+ #retentionMs;
152
301
  #timer = null;
153
302
  #flushing = false;
154
- constructor(maxRequests, maxErrors, flushInterval) {
303
+ constructor(flushInterval, retentionDays) {
155
304
  this.#storage = useStorage("data");
156
- this.#maxRequests = maxRequests;
157
- this.#maxErrors = maxErrors;
305
+ this.#retentionMs = retentionDays * 864e5;
158
306
  this.#timer = setInterval(() => this.flush(), flushInterval);
159
307
  if (typeof this.#timer === "object" && "unref" in this.#timer) this.#timer.unref();
160
308
  }
@@ -171,12 +319,13 @@ var AnalyticsStore = class {
171
319
  if (requests.length === 0 && errors.length === 0) return;
172
320
  this.#flushing = true;
173
321
  try {
322
+ const cutoff = Date.now() - this.#retentionMs;
174
323
  if (requests.length > 0) {
175
- const merged = [...await this.#storage.getItem("analytics:requests") ?? [], ...requests].slice(-this.#maxRequests);
324
+ const merged = [...normalizeRequestEntries(await this.#storage.getItem("analytics:requests")).filter((entry) => isTrackedRequestPath(entry.path)), ...requests].filter((e) => e.timestamp >= cutoff);
176
325
  await this.#storage.setItem("analytics:requests", merged);
177
326
  }
178
327
  if (errors.length > 0) {
179
- const merged = [...await this.#storage.getItem("analytics:errors") ?? [], ...errors].slice(-this.#maxErrors);
328
+ const merged = [...normalizeErrorEntries(await this.#storage.getItem("analytics:errors")).filter((entry) => isTrackedRequestPath(entry.procedure)), ...errors].filter((e) => e.timestamp >= cutoff);
180
329
  await this.#storage.setItem("analytics:errors", merged);
181
330
  }
182
331
  } catch {
@@ -187,14 +336,18 @@ var AnalyticsStore = class {
187
336
  }
188
337
  }
189
338
  async getRequests() {
190
- const stored = await this.#storage.getItem("analytics:requests") ?? [];
191
- if (this.#pendingRequests.length === 0) return stored;
192
- return [...stored, ...this.#pendingRequests].slice(-this.#maxRequests);
339
+ const cutoff = Date.now() - this.#retentionMs;
340
+ const stored = normalizeRequestEntries(await this.#storage.getItem("analytics:requests")).filter((entry) => isTrackedRequestPath(entry.path) && entry.timestamp >= cutoff);
341
+ const pending = this.#pendingRequests.filter((entry) => isTrackedRequestPath(entry.path) && entry.timestamp >= cutoff);
342
+ if (pending.length === 0) return stored;
343
+ return [...stored, ...pending];
193
344
  }
194
345
  async getErrors() {
195
- const stored = await this.#storage.getItem("analytics:errors") ?? [];
196
- if (this.#pendingErrors.length === 0) return stored;
197
- return [...stored, ...this.#pendingErrors].slice(-this.#maxErrors);
346
+ const cutoff = Date.now() - this.#retentionMs;
347
+ const stored = normalizeErrorEntries(await this.#storage.getItem("analytics:errors")).filter((entry) => isTrackedRequestPath(entry.procedure) && entry.timestamp >= cutoff);
348
+ const pending = this.#pendingErrors.filter((entry) => isTrackedRequestPath(entry.procedure) && entry.timestamp >= cutoff);
349
+ if (pending.length === 0) return stored;
350
+ return [...stored, ...pending];
198
351
  }
199
352
  async hydrate() {
200
353
  try {
@@ -217,12 +370,27 @@ var AnalyticsStore = class {
217
370
  });
218
371
  } catch {}
219
372
  }
373
+ async loadHiddenPaths() {
374
+ try {
375
+ const paths = await this.#storage.getItem("analytics:hiddenPaths");
376
+ return Array.isArray(paths) ? paths : [];
377
+ } catch {
378
+ return [];
379
+ }
380
+ }
381
+ saveHiddenPaths(paths) {
382
+ this.#storage.setItem("analytics:hiddenPaths", paths).catch(() => {});
383
+ }
220
384
  async dispose() {
221
385
  if (this.#timer) clearInterval(this.#timer);
222
386
  this.#timer = null;
223
387
  await this.flush();
224
388
  }
225
389
  };
390
+ /** Internal in-memory buffer caps — not user-configurable */
391
+ const MEM_MAX_REQUESTS = 1e4;
392
+ const MEM_MAX_ERRORS = 1e4;
393
+ const MEM_MAX_TASKS = 1e4;
226
394
  var AnalyticsCollector = class {
227
395
  #procedures = /* @__PURE__ */ new Map();
228
396
  #startTime = Date.now();
@@ -230,9 +398,6 @@ var AnalyticsCollector = class {
230
398
  #totalErrors = 0;
231
399
  #bufferSize;
232
400
  #historySeconds;
233
- #maxErrors;
234
- #maxRequests;
235
- #maxTasks;
236
401
  #timeSeries = [];
237
402
  #currentWindow;
238
403
  #errors = [];
@@ -244,22 +409,50 @@ var AnalyticsCollector = class {
244
409
  #taskStats = /* @__PURE__ */ new Map();
245
410
  #store;
246
411
  #counterFlushCounter = 0;
412
+ /** Server-side ignore — from config, prevents recording entirely */
413
+ #ignorePaths;
414
+ /** Client-side hide — from dashboard, filters display only */
415
+ #hiddenPaths = /* @__PURE__ */ new Set();
247
416
  constructor(options = {}) {
248
417
  this.#bufferSize = options.bufferSize ?? 1024;
249
418
  this.#historySeconds = options.historySeconds ?? 120;
250
- this.#maxErrors = options.maxErrors ?? 100;
251
- this.#maxRequests = options.maxRequests ?? 200;
252
- this.#maxTasks = options.maxTasks ?? 200;
419
+ this.#ignorePaths = new Set((options.ignorePaths ?? []).map((p) => p.startsWith("/") ? p.slice(1) : p));
253
420
  this.#currentWindow = {
254
421
  time: Math.floor(Date.now() / 1e3),
255
422
  count: 0,
256
423
  errors: 0
257
424
  };
258
- this.#store = new AnalyticsStore(this.#maxRequests, this.#maxErrors, options.flushInterval ?? 5e3);
425
+ this.#store = new AnalyticsStore(options.flushInterval ?? 5e3, options.retentionDays ?? 30);
259
426
  this.#store.hydrate().then((c) => {
260
427
  this.#totalRequests += c.totalRequests;
261
428
  this.#totalErrors += c.totalErrors;
262
429
  });
430
+ this.#store.loadHiddenPaths().then((paths) => {
431
+ for (const p of paths) this.#hiddenPaths.add(p);
432
+ });
433
+ }
434
+ /** Check if a path is server-side ignored (from config). */
435
+ isIgnored(pathname) {
436
+ if (this.#ignorePaths.size === 0) return false;
437
+ return matchesPathPrefix(pathname, this.#ignorePaths);
438
+ }
439
+ /** Check if a path is hidden in the dashboard (from runtime API). */
440
+ isHidden(pathname) {
441
+ if (this.#hiddenPaths.size === 0) return false;
442
+ return matchesPathPrefix(pathname, this.#hiddenPaths);
443
+ }
444
+ addHiddenPath(path) {
445
+ const normalized = path.startsWith("/") ? path.slice(1) : path;
446
+ this.#hiddenPaths.add(normalized);
447
+ this.#store.saveHiddenPaths([...this.#hiddenPaths]);
448
+ }
449
+ removeHiddenPath(path) {
450
+ const normalized = path.startsWith("/") ? path.slice(1) : path;
451
+ this.#hiddenPaths.delete(normalized);
452
+ this.#store.saveHiddenPaths([...this.#hiddenPaths]);
453
+ }
454
+ getHiddenPaths() {
455
+ return [...this.#hiddenPaths];
263
456
  }
264
457
  record(path, durationMs) {
265
458
  this.#totalRequests++;
@@ -285,7 +478,7 @@ var AnalyticsCollector = class {
285
478
  id: this.#nextErrorId++
286
479
  };
287
480
  this.#errors.push(full);
288
- if (this.#errors.length > this.#maxErrors) this.#errors.shift();
481
+ if (this.#errors.length > MEM_MAX_ERRORS) this.#errors.shift();
289
482
  this.#store.enqueueError(full);
290
483
  }
291
484
  recordDetailedRequest(entry) {
@@ -294,7 +487,7 @@ var AnalyticsCollector = class {
294
487
  id: this.#nextRequestId++
295
488
  };
296
489
  this.#requests.push(full);
297
- if (this.#requests.length > this.#maxRequests) this.#requests.shift();
490
+ if (this.#requests.length > MEM_MAX_REQUESTS) this.#requests.shift();
298
491
  this.#store.enqueueRequest(full);
299
492
  this.#flushCountersIfNeeded();
300
493
  }
@@ -304,7 +497,7 @@ var AnalyticsCollector = class {
304
497
  id: this.#nextTaskId++
305
498
  };
306
499
  this.#taskExecutions.push(full);
307
- if (this.#taskExecutions.length > this.#maxTasks) this.#taskExecutions.shift();
500
+ if (this.#taskExecutions.length > MEM_MAX_TASKS) this.#taskExecutions.shift();
308
501
  let stats = this.#taskStats.get(entry.taskName);
309
502
  if (!stats) {
310
503
  stats = {
@@ -472,11 +665,11 @@ var RequestAccumulator = class {
472
665
  const durationMs = round(performance.now() - this.t0);
473
666
  const headers = {};
474
667
  this.#request.headers.forEach((v, k) => {
475
- headers[k] = k === "authorization" || k === "cookie" ? "[REDACTED]" : v;
668
+ headers[k] = redactHeaderValue(k, v);
476
669
  });
477
670
  const responseHeaders = {};
478
671
  res.headers.forEach((v, k) => {
479
- responseHeaders[k] = k === "set-cookie" ? "[REDACTED]" : v;
672
+ responseHeaders[k] = redactHeaderValue(k, v);
480
673
  });
481
674
  let worstStatus = 200;
482
675
  for (const p of this.#procedures) if (p.status > worstStatus) worstStatus = p.status;
@@ -490,6 +683,7 @@ var RequestAccumulator = class {
490
683
  timestamp: Date.now(),
491
684
  durationMs,
492
685
  method: this.#request.method,
686
+ url: this.#request.url,
493
687
  path,
494
688
  ip: headers["x-forwarded-for"] || headers["x-real-ip"] || "",
495
689
  headers,
@@ -517,8 +711,7 @@ function errorToMarkdown(e) {
517
711
  if (e.stack) md += `### Stack Trace\n\n\`\`\`\n${e.stack}\n\`\`\`\n\n`;
518
712
  if (Object.keys(e.headers).length > 0) {
519
713
  md += `### Request Headers\n\n`;
520
- for (const [k, v] of Object.entries(e.headers)) if (k === "authorization") md += `- \`${k}\`: \`[REDACTED]\`\n`;
521
- else md += `- \`${k}\`: \`${v}\`\n`;
714
+ for (const [k, v] of Object.entries(e.headers)) md += `- \`${k}\`: \`${redactHeaderValue(k, v)}\`\n`;
522
715
  md += "\n";
523
716
  }
524
717
  if (e.spans.length > 0) {
@@ -541,6 +734,7 @@ function requestToMarkdown(r) {
541
734
  md += `| Request ID | \`${r.requestId}\` |\n`;
542
735
  md += `| Session ID | \`${r.sessionId}\` |\n`;
543
736
  md += `| Method | ${r.method} |\n`;
737
+ md += `| URL | \`${r.url}\` |\n`;
544
738
  md += `| Path | \`${r.path}\` |\n`;
545
739
  md += `| Status | ${r.status} |\n`;
546
740
  md += `| Duration | ${r.durationMs}ms |\n`;
@@ -554,6 +748,7 @@ function requestToMarkdown(r) {
554
748
  const pEmoji = p.status >= 400 ? "⚠️" : "✅";
555
749
  md += `### ${pEmoji} ${i + 1}. \`${p.procedure}\` → ${p.status} (${p.durationMs}ms)\n\n`;
556
750
  if (p.input !== void 0 && p.input !== null) md += `#### Input\n\n\`\`\`json\n${safeStringify(p.input)}\n\`\`\`\n\n`;
751
+ if (p.output !== void 0 && p.output !== null) md += `#### Output\n\n\`\`\`json\n${safeStringify(p.output)}\n\`\`\`\n\n`;
557
752
  if (p.spans.length > 0) {
558
753
  const byKind = /* @__PURE__ */ new Map();
559
754
  for (const s of p.spans) byKind.set(s.kind, (byKind.get(s.kind) ?? 0) + s.durationMs);
@@ -583,6 +778,63 @@ function requestToMarkdown(r) {
583
778
  if (r.durationMs > 100) md += `- ⚠️ This request took ${r.durationMs}ms — what is the bottleneck?\n`;
584
779
  return md;
585
780
  }
781
+ async function captureResponseBody(response) {
782
+ const lowered = (response.headers.get("content-type") ?? "").toLowerCase();
783
+ if (!response.body || lowered.includes("text/event-stream") || lowered.includes("application/octet-stream")) return { output: null };
784
+ try {
785
+ const text = await response.clone().text();
786
+ if (!text) return { output: null };
787
+ let output = text;
788
+ if (lowered.includes("application/json") || lowered.includes("+json")) try {
789
+ output = JSON.parse(text);
790
+ } catch {
791
+ output = text;
792
+ }
793
+ if (response.status < 400) return { output };
794
+ if (output && typeof output === "object") {
795
+ const record = output;
796
+ const message = typeof record.message === "string" ? record.message : null;
797
+ const code = typeof record.code === "string" ? record.code : null;
798
+ if (message && code) return {
799
+ output,
800
+ error: `${code}: ${message}`
801
+ };
802
+ if (message) return {
803
+ output,
804
+ error: message
805
+ };
806
+ }
807
+ return {
808
+ output,
809
+ error: typeof output === "string" ? output : safeStringify(output)
810
+ };
811
+ } catch {
812
+ return { output: null };
813
+ }
814
+ }
815
+ function extractResponseError(output, status, fallback) {
816
+ if (output && typeof output === "object") {
817
+ const record = output;
818
+ const code = typeof record.code === "string" ? record.code : null;
819
+ const message = typeof record.message === "string" ? record.message : null;
820
+ if (code && message) return {
821
+ code,
822
+ message
823
+ };
824
+ if (message) return {
825
+ code: `HTTP_${status}`,
826
+ message
827
+ };
828
+ }
829
+ if (fallback) return {
830
+ code: `HTTP_${status}`,
831
+ message: fallback
832
+ };
833
+ return {
834
+ code: `HTTP_${status}`,
835
+ message: `Request failed with status ${status}`
836
+ };
837
+ }
586
838
  function safeStringify(v) {
587
839
  try {
588
840
  return JSON.stringify(v, null, 2);
@@ -590,6 +842,28 @@ function safeStringify(v) {
590
842
  return String(v);
591
843
  }
592
844
  }
845
+ function matchesPathPrefix(pathname, prefixes) {
846
+ const normalized = pathname.startsWith("/") ? pathname.slice(1) : pathname;
847
+ for (const prefix of prefixes) if (normalized === prefix || normalized.startsWith(prefix + "/")) return true;
848
+ return false;
849
+ }
850
+ function isTrackedRequestPath(pathname) {
851
+ const normalized = pathname.startsWith("/") ? pathname.slice(1) : pathname;
852
+ return normalized === "api" || normalized.startsWith("api/") || normalized === "graphql" || normalized.startsWith("graphql/");
853
+ }
854
+ function isAnalyticsPath(pathname) {
855
+ return pathname === "api/analytics" || pathname.startsWith("api/analytics/");
856
+ }
857
+ function parseAnalyticsDetailPath(pathname, prefix) {
858
+ if (!pathname.startsWith(prefix)) return null;
859
+ const rawId = pathname.slice(prefix.length);
860
+ if (!rawId || rawId.includes("/")) return null;
861
+ const id = Number(rawId);
862
+ return {
863
+ id: Number.isFinite(id) ? id : null,
864
+ rawId: decodeURIComponent(rawId)
865
+ };
866
+ }
593
867
  const __analytics_dirname = dirname(fileURLToPath(import.meta.url));
594
868
  let _dashboardCache;
595
869
  function analyticsHTML() {
@@ -621,15 +895,8 @@ function checkAnalyticsAuth(request, auth) {
621
895
  }
622
896
  function sanitizeHeaders(headers) {
623
897
  const result = {};
624
- const sensitiveKeys = new Set([
625
- "authorization",
626
- "cookie",
627
- "x-api-key",
628
- "x-auth-token",
629
- "proxy-authorization"
630
- ]);
631
898
  headers.forEach((value, key) => {
632
- result[key] = sensitiveKeys.has(key.toLowerCase()) ? "[REDACTED]" : value;
899
+ result[key] = redactHeaderValue(key, value);
633
900
  });
634
901
  return result;
635
902
  }
@@ -676,7 +943,7 @@ location.reload();
676
943
  /** Return auth-failure response for analytics routes. */
677
944
  function analyticsAuthResponse(pathname) {
678
945
  const jsonHeaders = { "content-type": "application/json" };
679
- if (pathname !== "api/analytics") return new Response(JSON.stringify({
946
+ if (pathname !== "api/analytics" && pathname !== "api/analytics/") return new Response(JSON.stringify({
680
947
  code: "UNAUTHORIZED",
681
948
  status: 401,
682
949
  message: "Invalid token"
@@ -692,8 +959,22 @@ function analyticsAuthResponse(pathname) {
692
959
  function jsonResponse(data, headers) {
693
960
  return new Response(JSON.stringify(data), { headers });
694
961
  }
962
+ function paginatedResponse(items, params, headers) {
963
+ const page = Math.max(1, Number(params.get("page")) || 1);
964
+ const limit = Math.min(200, Math.max(1, Number(params.get("limit")) || 50));
965
+ const total = items.length;
966
+ const totalPages = Math.ceil(total / limit);
967
+ const start = (page - 1) * limit;
968
+ return jsonResponse({
969
+ data: items.slice(start, start + limit),
970
+ page,
971
+ limit,
972
+ total,
973
+ totalPages
974
+ }, headers);
975
+ }
695
976
  /** Serve analytics dashboard and API routes. */
696
- async function serveAnalyticsRoute(pathname, collector, dashboardHtml) {
977
+ async function serveAnalyticsRoute(pathname, request, collector, dashboardHtml) {
697
978
  const jsonCacheHeaders = {
698
979
  "content-type": "application/json",
699
980
  "cache-control": "no-cache"
@@ -702,32 +983,75 @@ async function serveAnalyticsRoute(pathname, collector, dashboardHtml) {
702
983
  "content-type": "text/markdown; charset=utf-8",
703
984
  "cache-control": "no-cache"
704
985
  };
986
+ const url = new URL(request.url);
987
+ if (pathname === "api/analytics" || pathname === "api/analytics/") return new Response(dashboardHtml, { headers: { "content-type": "text/html" } });
705
988
  if (pathname === "api/analytics/stats") return jsonResponse(collector.toJSON(), jsonCacheHeaders);
706
- if (pathname === "api/analytics/errors") return jsonResponse(await collector.getErrors(), jsonCacheHeaders);
707
- if (pathname === "api/analytics/requests") return jsonResponse(await collector.getRequests(), jsonCacheHeaders);
708
- if (pathname === "api/analytics/tasks") return jsonResponse(await collector.getTaskExecutions(), jsonCacheHeaders);
989
+ if (pathname === "api/analytics/hidden") {
990
+ if (request.method === "GET") return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
991
+ if (request.method === "POST") {
992
+ const body = await request.json();
993
+ if (typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
994
+ status: 400,
995
+ headers: jsonCacheHeaders
996
+ });
997
+ collector.addHiddenPath(body.path);
998
+ return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
999
+ }
1000
+ if (request.method === "DELETE") {
1001
+ const body = await request.json();
1002
+ if (typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
1003
+ status: 400,
1004
+ headers: jsonCacheHeaders
1005
+ });
1006
+ collector.removeHiddenPath(body.path);
1007
+ return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
1008
+ }
1009
+ }
1010
+ if (pathname === "api/analytics/errors") return paginatedResponse((await collector.getErrors()).filter((e) => !collector.isHidden(e.procedure)), url.searchParams, jsonCacheHeaders);
1011
+ if (pathname === "api/analytics/requests") return paginatedResponse((await collector.getRequests()).filter((r) => !collector.isHidden(r.path)), url.searchParams, jsonCacheHeaders);
1012
+ if (pathname === "api/analytics/tasks") return paginatedResponse(await collector.getTaskExecutions(), url.searchParams, jsonCacheHeaders);
709
1013
  if (pathname === "api/analytics/scheduled") {
710
1014
  const { getScheduledTasks } = await import("../core/task.mjs");
711
1015
  return jsonResponse(getScheduledTasks(), jsonCacheHeaders);
712
1016
  }
713
1017
  if (pathname.startsWith("api/analytics/requests/") && pathname.endsWith("/md")) {
714
- const id = Number(pathname.slice(23, -3));
715
- const entry = (await collector.getRequests()).find((r) => r.id === id);
1018
+ const rawId = pathname.slice(23, -3);
1019
+ const parsedId = Number(rawId);
1020
+ const requestId = decodeURIComponent(rawId);
1021
+ const entry = (await collector.getRequests()).find((r) => r.id === parsedId || r.requestId === requestId);
716
1022
  if (entry) return new Response(requestToMarkdown(entry), { headers: mdHeaders });
717
1023
  return new Response("not found", { status: 404 });
718
1024
  }
1025
+ const requestDetail = parseAnalyticsDetailPath(pathname, "api/analytics/requests/");
1026
+ if (requestDetail) {
1027
+ const entry = (await collector.getRequests()).find((r) => r.id === requestDetail.id || r.requestId === requestDetail.rawId);
1028
+ return entry ? jsonResponse(entry, jsonCacheHeaders) : new Response("not found", { status: 404 });
1029
+ }
719
1030
  if (pathname.startsWith("api/analytics/errors/") && pathname.endsWith("/md")) {
720
- const id = Number(pathname.slice(21, -3));
1031
+ const rawId = pathname.slice(21, -3);
1032
+ const id = Number(rawId);
721
1033
  const entry = (await collector.getErrors()).find((e) => e.id === id);
722
1034
  if (entry) return new Response(errorToMarkdown(entry), { headers: mdHeaders });
723
1035
  return new Response("not found", { status: 404 });
724
1036
  }
1037
+ const errorDetail = parseAnalyticsDetailPath(pathname, "api/analytics/errors/");
1038
+ if (errorDetail) {
1039
+ const entry = (await collector.getErrors()).find((e) => e.id === errorDetail.id);
1040
+ return entry ? jsonResponse(entry, jsonCacheHeaders) : new Response("not found", { status: 404 });
1041
+ }
725
1042
  if (pathname === "api/analytics/errors/md") {
726
1043
  const errors = await collector.getErrors();
727
1044
  const md = errors.length === 0 ? "No errors.\n" : `# Errors (${errors.length})\n\n` + errors.map((e) => errorToMarkdown(e)).join("\n\n---\n\n");
728
1045
  return new Response(md, { headers: mdHeaders });
729
1046
  }
730
- return new Response(dashboardHtml, { headers: { "content-type": "text/html" } });
1047
+ return new Response(JSON.stringify({
1048
+ code: "NOT_FOUND",
1049
+ status: 404,
1050
+ message: "Analytics route not found"
1051
+ }), {
1052
+ status: 404,
1053
+ headers: jsonCacheHeaders
1054
+ });
731
1055
  }
732
1056
  /**
733
1057
  * Wrap a fetch handler with analytics collection.
@@ -749,13 +1073,14 @@ function wrapWithAnalytics(handler, options = {}) {
749
1073
  const qMark = url.indexOf("?", pathStart);
750
1074
  const fullPath = qMark === -1 ? url.slice(pathStart) : url.slice(pathStart, qMark);
751
1075
  const pathname = fullPath.length > 1 ? fullPath.slice(1) : "";
752
- if (pathname.startsWith("api/analytics")) {
1076
+ if (isAnalyticsPath(pathname)) {
753
1077
  if (auth) {
754
1078
  const authResult = checkAnalyticsAuth(request, auth);
755
1079
  if (!(authResult instanceof Promise ? await authResult : authResult)) return analyticsAuthResponse(pathname);
756
1080
  }
757
- return serveAnalyticsRoute(pathname, collector, dashboardHtml);
1081
+ return serveAnalyticsRoute(pathname, request, collector, dashboardHtml);
758
1082
  }
1083
+ if (!isTrackedRequestPath(pathname) || collector.isIgnored(pathname)) return handler(request);
759
1084
  const acc = new RequestAccumulator(request, collector);
760
1085
  const reqTrace = new RequestTrace();
761
1086
  const t0 = performance.now();
@@ -764,15 +1089,37 @@ function wrapWithAnalytics(handler, options = {}) {
764
1089
  try {
765
1090
  response = await handler(request);
766
1091
  const durationMs = round(performance.now() - t0);
1092
+ const captured = await captureResponseBody(response);
1093
+ const procedureInput = reqTrace.procedureInput ?? null;
1094
+ const procedureOutput = reqTrace.procedureOutput ?? captured.output;
1095
+ const procedureSpans = reqTrace.spans ?? [];
767
1096
  collector.record(pathname, durationMs);
768
1097
  acc.addProcedure({
769
1098
  procedure: pathname,
770
1099
  durationMs,
771
1100
  status: response.status,
772
- input: null,
773
- output: null,
774
- spans: reqTrace.spans ?? []
1101
+ input: procedureInput,
1102
+ output: procedureOutput,
1103
+ spans: procedureSpans,
1104
+ error: captured.error
775
1105
  });
1106
+ if (response.status >= 400) {
1107
+ const { code, message } = extractResponseError(procedureOutput, response.status, captured.error);
1108
+ collector.recordError(pathname, durationMs, message);
1109
+ collector.recordDetailedError({
1110
+ requestId: acc.requestId,
1111
+ timestamp: Date.now(),
1112
+ procedure: pathname,
1113
+ error: message,
1114
+ code,
1115
+ status: response.status,
1116
+ stack: "",
1117
+ input: procedureInput,
1118
+ headers: sanitizeHeaders(request.headers),
1119
+ durationMs,
1120
+ spans: procedureSpans
1121
+ });
1122
+ }
776
1123
  } catch (error) {
777
1124
  const durationMs = round(performance.now() - t0);
778
1125
  const errorMsg = error instanceof Error ? error.message : String(error);