silgi 0.1.0-beta.13 → 0.1.0-beta.15
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/core/context-bridge.mjs +11 -0
- package/dist/core/handler.mjs +3 -2
- package/dist/core/sse.mjs +1 -1
- package/dist/core/storage.mjs +3 -2
- package/dist/index.mjs +1 -1
- package/dist/integrations/better-auth/index.d.mts +0 -20
- package/dist/integrations/better-auth/index.mjs +3 -4
- package/dist/integrations/drizzle/index.mjs +6 -7
- package/dist/plugins/analytics-alerts.d.mts +59 -0
- package/dist/plugins/analytics-alerts.mjs +140 -0
- package/dist/plugins/analytics-cost.d.mts +61 -0
- package/dist/plugins/analytics-cost.mjs +97 -0
- package/dist/plugins/analytics-query.mjs +164 -0
- package/dist/plugins/analytics-sse.d.mts +31 -0
- package/dist/plugins/analytics-sse.mjs +74 -0
- package/dist/plugins/analytics-timeseries.d.mts +50 -0
- package/dist/plugins/analytics-timeseries.mjs +169 -0
- package/dist/plugins/analytics.d.mts +38 -20
- package/dist/plugins/analytics.mjs +454 -52
- package/lib/dashboard/index.html +56 -59
- package/package.json +1 -1
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { ValidationError } from "../core/schema.mjs";
|
|
2
2
|
import { SilgiError, toSilgiError } from "../core/error.mjs";
|
|
3
|
+
import { AlertEngine } from "./analytics-alerts.mjs";
|
|
4
|
+
import { CostTracker } from "./analytics-cost.mjs";
|
|
5
|
+
import { parseQueryParams, queryErrors, queryRequests, queryTasks } from "./analytics-query.mjs";
|
|
6
|
+
import { AnalyticsSSEHub } from "./analytics-sse.mjs";
|
|
7
|
+
import { TimeSeriesAggregator } from "./analytics-timeseries.mjs";
|
|
3
8
|
import { useStorage } from "../core/storage.mjs";
|
|
4
9
|
import { readFileSync } from "node:fs";
|
|
5
10
|
import { dirname, resolve } from "node:path";
|
|
@@ -52,6 +57,158 @@ var RingBuffer = class {
|
|
|
52
57
|
return this.#count;
|
|
53
58
|
}
|
|
54
59
|
};
|
|
60
|
+
const REDACTED = "[REDACTED]";
|
|
61
|
+
const SENSITIVE_HEADER_KEYS = new Set([
|
|
62
|
+
"authorization",
|
|
63
|
+
"cookie",
|
|
64
|
+
"set-cookie",
|
|
65
|
+
"x-api-key",
|
|
66
|
+
"x-auth-token",
|
|
67
|
+
"proxy-authorization"
|
|
68
|
+
]);
|
|
69
|
+
function shouldRedactSensitiveData() {
|
|
70
|
+
return process.env.NODE_ENV === "production";
|
|
71
|
+
}
|
|
72
|
+
function redactHeaderValue(key, value) {
|
|
73
|
+
return shouldRedactSensitiveData() && SENSITIVE_HEADER_KEYS.has(key.toLowerCase()) ? REDACTED : value;
|
|
74
|
+
}
|
|
75
|
+
function asRecord(value) {
|
|
76
|
+
return value && typeof value === "object" ? value : null;
|
|
77
|
+
}
|
|
78
|
+
function normalizeString(value, fallback = "") {
|
|
79
|
+
return typeof value === "string" ? value : fallback;
|
|
80
|
+
}
|
|
81
|
+
function normalizeNumber(value, fallback = 0) {
|
|
82
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
83
|
+
}
|
|
84
|
+
function normalizeBoolean(value, fallback = false) {
|
|
85
|
+
return typeof value === "boolean" ? value : fallback;
|
|
86
|
+
}
|
|
87
|
+
function normalizeStringMap(value) {
|
|
88
|
+
const record = asRecord(value);
|
|
89
|
+
if (!record) return {};
|
|
90
|
+
const result = {};
|
|
91
|
+
for (const [key, raw] of Object.entries(record)) if (typeof raw === "string") result[key] = raw;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
function normalizeSpanKind(value) {
|
|
95
|
+
switch (value) {
|
|
96
|
+
case "db":
|
|
97
|
+
case "http":
|
|
98
|
+
case "cache":
|
|
99
|
+
case "queue":
|
|
100
|
+
case "email":
|
|
101
|
+
case "ai":
|
|
102
|
+
case "custom": return value;
|
|
103
|
+
default: return "custom";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function normalizeTraceSpans(value) {
|
|
107
|
+
if (!Array.isArray(value)) return [];
|
|
108
|
+
const spans = [];
|
|
109
|
+
for (const entry of value) {
|
|
110
|
+
const span = asRecord(entry);
|
|
111
|
+
if (!span) continue;
|
|
112
|
+
const attributes = asRecord(span.attributes);
|
|
113
|
+
const normalizedAttributes = {};
|
|
114
|
+
if (attributes) {
|
|
115
|
+
for (const [key, raw] of Object.entries(attributes)) if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") normalizedAttributes[key] = raw;
|
|
116
|
+
}
|
|
117
|
+
spans.push({
|
|
118
|
+
name: normalizeString(span.name, "unknown"),
|
|
119
|
+
kind: normalizeSpanKind(span.kind),
|
|
120
|
+
durationMs: normalizeNumber(span.durationMs),
|
|
121
|
+
startOffsetMs: typeof span.startOffsetMs === "number" ? span.startOffsetMs : void 0,
|
|
122
|
+
detail: typeof span.detail === "string" ? span.detail : void 0,
|
|
123
|
+
input: span.input,
|
|
124
|
+
output: span.output,
|
|
125
|
+
error: typeof span.error === "string" ? span.error : void 0,
|
|
126
|
+
attributes: Object.keys(normalizedAttributes).length > 0 ? normalizedAttributes : void 0
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return spans;
|
|
130
|
+
}
|
|
131
|
+
function inferPathFromUrl(url) {
|
|
132
|
+
if (!url) return "";
|
|
133
|
+
try {
|
|
134
|
+
return new URL(url).pathname || "";
|
|
135
|
+
} catch {
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function normalizeProcedureCall(value, fallback) {
|
|
140
|
+
const record = asRecord(value);
|
|
141
|
+
return {
|
|
142
|
+
procedure: normalizeString(record?.procedure, fallback.path.replace(/^\//, "") || fallback.path || "request"),
|
|
143
|
+
durationMs: normalizeNumber(record?.durationMs, fallback.durationMs),
|
|
144
|
+
status: normalizeNumber(record?.status, fallback.status),
|
|
145
|
+
input: record?.input ?? null,
|
|
146
|
+
output: record?.output ?? null,
|
|
147
|
+
spans: normalizeTraceSpans(record?.spans),
|
|
148
|
+
error: typeof record?.error === "string" ? record.error : void 0
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function normalizeRequestEntry(value, fallbackId) {
|
|
152
|
+
const record = asRecord(value);
|
|
153
|
+
if (!record) return null;
|
|
154
|
+
const url = normalizeString(record.url);
|
|
155
|
+
const path = normalizeString(record.path, inferPathFromUrl(url));
|
|
156
|
+
const method = normalizeString(record.method, "GET");
|
|
157
|
+
const status = normalizeNumber(record.status, 200);
|
|
158
|
+
const durationMs = normalizeNumber(record.durationMs);
|
|
159
|
+
const procedureFallback = {
|
|
160
|
+
path: path || "/",
|
|
161
|
+
durationMs,
|
|
162
|
+
status
|
|
163
|
+
};
|
|
164
|
+
const proceduresRaw = Array.isArray(record.procedures) ? record.procedures : [];
|
|
165
|
+
const procedures = proceduresRaw.length > 0 ? proceduresRaw.map((entry) => normalizeProcedureCall(entry, procedureFallback)) : [normalizeProcedureCall(null, procedureFallback)];
|
|
166
|
+
return {
|
|
167
|
+
id: normalizeNumber(record.id, fallbackId),
|
|
168
|
+
requestId: normalizeString(record.requestId, String(normalizeNumber(record.id, fallbackId))),
|
|
169
|
+
sessionId: normalizeString(record.sessionId),
|
|
170
|
+
timestamp: normalizeNumber(record.timestamp),
|
|
171
|
+
durationMs,
|
|
172
|
+
method,
|
|
173
|
+
url: url || path,
|
|
174
|
+
path,
|
|
175
|
+
ip: normalizeString(record.ip),
|
|
176
|
+
headers: normalizeStringMap(record.headers),
|
|
177
|
+
responseHeaders: normalizeStringMap(record.responseHeaders),
|
|
178
|
+
userAgent: normalizeString(record.userAgent),
|
|
179
|
+
status,
|
|
180
|
+
procedures,
|
|
181
|
+
isBatch: normalizeBoolean(record.isBatch, procedures.length > 1),
|
|
182
|
+
traceId: normalizeString(record.traceId) || void 0,
|
|
183
|
+
parentRequestId: normalizeString(record.parentRequestId) || void 0
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function normalizeErrorEntry(value, fallbackId) {
|
|
187
|
+
const record = asRecord(value);
|
|
188
|
+
if (!record) return null;
|
|
189
|
+
return {
|
|
190
|
+
id: normalizeNumber(record.id, fallbackId),
|
|
191
|
+
requestId: normalizeString(record.requestId),
|
|
192
|
+
timestamp: normalizeNumber(record.timestamp),
|
|
193
|
+
procedure: normalizeString(record.procedure, "request"),
|
|
194
|
+
error: normalizeString(record.error, "Unknown error"),
|
|
195
|
+
code: normalizeString(record.code, "INTERNAL_SERVER_ERROR"),
|
|
196
|
+
status: normalizeNumber(record.status, 500),
|
|
197
|
+
stack: normalizeString(record.stack),
|
|
198
|
+
input: record.input ?? null,
|
|
199
|
+
headers: normalizeStringMap(record.headers),
|
|
200
|
+
durationMs: normalizeNumber(record.durationMs),
|
|
201
|
+
spans: normalizeTraceSpans(record.spans)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function normalizeRequestEntries(value) {
|
|
205
|
+
if (!Array.isArray(value)) return [];
|
|
206
|
+
return value.map((entry, index) => normalizeRequestEntry(entry, index + 1)).filter((entry) => entry !== null);
|
|
207
|
+
}
|
|
208
|
+
function normalizeErrorEntries(value) {
|
|
209
|
+
if (!Array.isArray(value)) return [];
|
|
210
|
+
return value.map((entry, index) => normalizeErrorEntry(entry, index + 1)).filter((entry) => entry !== null);
|
|
211
|
+
}
|
|
55
212
|
let _lastTime = 0;
|
|
56
213
|
let _counter = 0;
|
|
57
214
|
function generateRequestId() {
|
|
@@ -147,14 +304,12 @@ var AnalyticsStore = class {
|
|
|
147
304
|
#storage;
|
|
148
305
|
#pendingRequests = [];
|
|
149
306
|
#pendingErrors = [];
|
|
150
|
-
#
|
|
151
|
-
#maxErrors;
|
|
307
|
+
#retentionMs;
|
|
152
308
|
#timer = null;
|
|
153
309
|
#flushing = false;
|
|
154
|
-
constructor(
|
|
310
|
+
constructor(flushInterval, retentionDays) {
|
|
155
311
|
this.#storage = useStorage("data");
|
|
156
|
-
this.#
|
|
157
|
-
this.#maxErrors = maxErrors;
|
|
312
|
+
this.#retentionMs = retentionDays * 864e5;
|
|
158
313
|
this.#timer = setInterval(() => this.flush(), flushInterval);
|
|
159
314
|
if (typeof this.#timer === "object" && "unref" in this.#timer) this.#timer.unref();
|
|
160
315
|
}
|
|
@@ -171,12 +326,13 @@ var AnalyticsStore = class {
|
|
|
171
326
|
if (requests.length === 0 && errors.length === 0) return;
|
|
172
327
|
this.#flushing = true;
|
|
173
328
|
try {
|
|
329
|
+
const cutoff = Date.now() - this.#retentionMs;
|
|
174
330
|
if (requests.length > 0) {
|
|
175
|
-
const merged = [...await this.#storage.getItem("analytics:requests")
|
|
331
|
+
const merged = [...normalizeRequestEntries(await this.#storage.getItem("analytics:requests")).filter((entry) => isTrackedRequestPath(entry.path)), ...requests].filter((e) => e.timestamp >= cutoff);
|
|
176
332
|
await this.#storage.setItem("analytics:requests", merged);
|
|
177
333
|
}
|
|
178
334
|
if (errors.length > 0) {
|
|
179
|
-
const merged = [...await this.#storage.getItem("analytics:errors")
|
|
335
|
+
const merged = [...normalizeErrorEntries(await this.#storage.getItem("analytics:errors")).filter((entry) => isTrackedRequestPath(entry.procedure)), ...errors].filter((e) => e.timestamp >= cutoff);
|
|
180
336
|
await this.#storage.setItem("analytics:errors", merged);
|
|
181
337
|
}
|
|
182
338
|
} catch {
|
|
@@ -187,14 +343,18 @@ var AnalyticsStore = class {
|
|
|
187
343
|
}
|
|
188
344
|
}
|
|
189
345
|
async getRequests() {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
346
|
+
const cutoff = Date.now() - this.#retentionMs;
|
|
347
|
+
const stored = normalizeRequestEntries(await this.#storage.getItem("analytics:requests")).filter((entry) => isTrackedRequestPath(entry.path) && entry.timestamp >= cutoff);
|
|
348
|
+
const pending = this.#pendingRequests.filter((entry) => isTrackedRequestPath(entry.path) && entry.timestamp >= cutoff);
|
|
349
|
+
if (pending.length === 0) return stored;
|
|
350
|
+
return [...stored, ...pending];
|
|
193
351
|
}
|
|
194
352
|
async getErrors() {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
353
|
+
const cutoff = Date.now() - this.#retentionMs;
|
|
354
|
+
const stored = normalizeErrorEntries(await this.#storage.getItem("analytics:errors")).filter((entry) => isTrackedRequestPath(entry.procedure) && entry.timestamp >= cutoff);
|
|
355
|
+
const pending = this.#pendingErrors.filter((entry) => isTrackedRequestPath(entry.procedure) && entry.timestamp >= cutoff);
|
|
356
|
+
if (pending.length === 0) return stored;
|
|
357
|
+
return [...stored, ...pending];
|
|
198
358
|
}
|
|
199
359
|
async hydrate() {
|
|
200
360
|
try {
|
|
@@ -217,12 +377,27 @@ var AnalyticsStore = class {
|
|
|
217
377
|
});
|
|
218
378
|
} catch {}
|
|
219
379
|
}
|
|
380
|
+
async loadHiddenPaths() {
|
|
381
|
+
try {
|
|
382
|
+
const paths = await this.#storage.getItem("analytics:hiddenPaths");
|
|
383
|
+
return Array.isArray(paths) ? paths : [];
|
|
384
|
+
} catch {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
saveHiddenPaths(paths) {
|
|
389
|
+
this.#storage.setItem("analytics:hiddenPaths", paths).catch(() => {});
|
|
390
|
+
}
|
|
220
391
|
async dispose() {
|
|
221
392
|
if (this.#timer) clearInterval(this.#timer);
|
|
222
393
|
this.#timer = null;
|
|
223
394
|
await this.flush();
|
|
224
395
|
}
|
|
225
396
|
};
|
|
397
|
+
/** Internal in-memory buffer caps — not user-configurable */
|
|
398
|
+
const MEM_MAX_REQUESTS = 1e4;
|
|
399
|
+
const MEM_MAX_ERRORS = 1e4;
|
|
400
|
+
const MEM_MAX_TASKS = 1e4;
|
|
226
401
|
var AnalyticsCollector = class {
|
|
227
402
|
#procedures = /* @__PURE__ */ new Map();
|
|
228
403
|
#startTime = Date.now();
|
|
@@ -230,9 +405,6 @@ var AnalyticsCollector = class {
|
|
|
230
405
|
#totalErrors = 0;
|
|
231
406
|
#bufferSize;
|
|
232
407
|
#historySeconds;
|
|
233
|
-
#maxErrors;
|
|
234
|
-
#maxRequests;
|
|
235
|
-
#maxTasks;
|
|
236
408
|
#timeSeries = [];
|
|
237
409
|
#currentWindow;
|
|
238
410
|
#errors = [];
|
|
@@ -244,22 +416,65 @@ var AnalyticsCollector = class {
|
|
|
244
416
|
#taskStats = /* @__PURE__ */ new Map();
|
|
245
417
|
#store;
|
|
246
418
|
#counterFlushCounter = 0;
|
|
419
|
+
/** Server-side ignore — from config, prevents recording entirely */
|
|
420
|
+
#ignorePaths;
|
|
421
|
+
/** Client-side hide — from dashboard, filters display only */
|
|
422
|
+
#hiddenPaths = /* @__PURE__ */ new Set();
|
|
423
|
+
/** SSE hub for real-time streaming */
|
|
424
|
+
sseHub;
|
|
425
|
+
/** Multi-tier time-series aggregation */
|
|
426
|
+
timeseries;
|
|
427
|
+
/** Alert engine */
|
|
428
|
+
alertEngine = null;
|
|
429
|
+
/** Cost tracker */
|
|
430
|
+
costTracker;
|
|
247
431
|
constructor(options = {}) {
|
|
248
432
|
this.#bufferSize = options.bufferSize ?? 1024;
|
|
249
433
|
this.#historySeconds = options.historySeconds ?? 120;
|
|
250
|
-
this.#
|
|
251
|
-
this.#maxRequests = options.maxRequests ?? 200;
|
|
252
|
-
this.#maxTasks = options.maxTasks ?? 200;
|
|
434
|
+
this.#ignorePaths = new Set((options.ignorePaths ?? []).map((p) => p.startsWith("/") ? p.slice(1) : p));
|
|
253
435
|
this.#currentWindow = {
|
|
254
436
|
time: Math.floor(Date.now() / 1e3),
|
|
255
437
|
count: 0,
|
|
256
438
|
errors: 0
|
|
257
439
|
};
|
|
258
|
-
this
|
|
440
|
+
this.sseHub = new AnalyticsSSEHub();
|
|
441
|
+
this.sseHub.startStatsBroadcast(() => this.toJSON());
|
|
442
|
+
this.timeseries = new TimeSeriesAggregator();
|
|
443
|
+
this.costTracker = new CostTracker(options.budgets, (rule, current) => {
|
|
444
|
+
if (this.alertEngine) console.warn(`[silgi:budget] "${rule.name}" exceeded: $${current.toFixed(2)} / $${rule.limit}`);
|
|
445
|
+
});
|
|
446
|
+
if (options.alerts && options.alerts.length > 0) this.alertEngine = new AlertEngine(options.alerts);
|
|
447
|
+
this.#store = new AnalyticsStore(options.flushInterval ?? 5e3, options.retentionDays ?? 30);
|
|
259
448
|
this.#store.hydrate().then((c) => {
|
|
260
449
|
this.#totalRequests += c.totalRequests;
|
|
261
450
|
this.#totalErrors += c.totalErrors;
|
|
262
451
|
});
|
|
452
|
+
this.#store.loadHiddenPaths().then((paths) => {
|
|
453
|
+
for (const p of paths) this.#hiddenPaths.add(p);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
/** Check if a path is server-side ignored (from config). */
|
|
457
|
+
isIgnored(pathname) {
|
|
458
|
+
if (this.#ignorePaths.size === 0) return false;
|
|
459
|
+
return matchesPathPrefix(pathname, this.#ignorePaths);
|
|
460
|
+
}
|
|
461
|
+
/** Check if a path is hidden in the dashboard (from runtime API). */
|
|
462
|
+
isHidden(pathname) {
|
|
463
|
+
if (this.#hiddenPaths.size === 0) return false;
|
|
464
|
+
return matchesPathPrefix(pathname, this.#hiddenPaths);
|
|
465
|
+
}
|
|
466
|
+
addHiddenPath(path) {
|
|
467
|
+
const normalized = path.startsWith("/") ? path.slice(1) : path;
|
|
468
|
+
this.#hiddenPaths.add(normalized);
|
|
469
|
+
this.#store.saveHiddenPaths([...this.#hiddenPaths]);
|
|
470
|
+
}
|
|
471
|
+
removeHiddenPath(path) {
|
|
472
|
+
const normalized = path.startsWith("/") ? path.slice(1) : path;
|
|
473
|
+
this.#hiddenPaths.delete(normalized);
|
|
474
|
+
this.#store.saveHiddenPaths([...this.#hiddenPaths]);
|
|
475
|
+
}
|
|
476
|
+
getHiddenPaths() {
|
|
477
|
+
return [...this.#hiddenPaths];
|
|
263
478
|
}
|
|
264
479
|
record(path, durationMs) {
|
|
265
480
|
this.#totalRequests++;
|
|
@@ -267,6 +482,8 @@ var AnalyticsCollector = class {
|
|
|
267
482
|
entry.count++;
|
|
268
483
|
entry.latencies.push(durationMs);
|
|
269
484
|
this.#tick(false);
|
|
485
|
+
this.timeseries.record(durationMs, false);
|
|
486
|
+
this.alertEngine?.record(durationMs, false, path);
|
|
270
487
|
}
|
|
271
488
|
recordError(path, durationMs, errorMsg) {
|
|
272
489
|
this.#totalRequests++;
|
|
@@ -278,6 +495,8 @@ var AnalyticsCollector = class {
|
|
|
278
495
|
entry.lastError = errorMsg;
|
|
279
496
|
entry.lastErrorTime = Date.now();
|
|
280
497
|
this.#tick(true);
|
|
498
|
+
this.timeseries.record(durationMs, true);
|
|
499
|
+
this.alertEngine?.record(durationMs, true, path);
|
|
281
500
|
}
|
|
282
501
|
recordDetailedError(entry) {
|
|
283
502
|
const full = {
|
|
@@ -285,8 +504,12 @@ var AnalyticsCollector = class {
|
|
|
285
504
|
id: this.#nextErrorId++
|
|
286
505
|
};
|
|
287
506
|
this.#errors.push(full);
|
|
288
|
-
if (this.#errors.length >
|
|
507
|
+
if (this.#errors.length > MEM_MAX_ERRORS) this.#errors.shift();
|
|
289
508
|
this.#store.enqueueError(full);
|
|
509
|
+
this.sseHub.broadcast({
|
|
510
|
+
type: "error",
|
|
511
|
+
data: full
|
|
512
|
+
});
|
|
290
513
|
}
|
|
291
514
|
recordDetailedRequest(entry) {
|
|
292
515
|
const full = {
|
|
@@ -294,8 +517,12 @@ var AnalyticsCollector = class {
|
|
|
294
517
|
id: this.#nextRequestId++
|
|
295
518
|
};
|
|
296
519
|
this.#requests.push(full);
|
|
297
|
-
if (this.#requests.length >
|
|
520
|
+
if (this.#requests.length > MEM_MAX_REQUESTS) this.#requests.shift();
|
|
298
521
|
this.#store.enqueueRequest(full);
|
|
522
|
+
this.sseHub.broadcast({
|
|
523
|
+
type: "request",
|
|
524
|
+
data: full
|
|
525
|
+
});
|
|
299
526
|
this.#flushCountersIfNeeded();
|
|
300
527
|
}
|
|
301
528
|
recordTask(entry) {
|
|
@@ -304,7 +531,11 @@ var AnalyticsCollector = class {
|
|
|
304
531
|
id: this.#nextTaskId++
|
|
305
532
|
};
|
|
306
533
|
this.#taskExecutions.push(full);
|
|
307
|
-
if (this.#taskExecutions.length >
|
|
534
|
+
if (this.#taskExecutions.length > MEM_MAX_TASKS) this.#taskExecutions.shift();
|
|
535
|
+
this.sseHub.broadcast({
|
|
536
|
+
type: "task",
|
|
537
|
+
data: full
|
|
538
|
+
});
|
|
308
539
|
let stats = this.#taskStats.get(entry.taskName);
|
|
309
540
|
if (!stats) {
|
|
310
541
|
stats = {
|
|
@@ -437,14 +668,18 @@ const SESSION_MAX_AGE = 365 * 24 * 60 * 60;
|
|
|
437
668
|
var RequestAccumulator = class {
|
|
438
669
|
requestId;
|
|
439
670
|
sessionId;
|
|
671
|
+
traceId;
|
|
672
|
+
parentRequestId;
|
|
440
673
|
/** True if a new session cookie needs to be set. */
|
|
441
674
|
isNewSession;
|
|
442
675
|
t0;
|
|
443
676
|
#request;
|
|
444
677
|
#procedures = [];
|
|
445
678
|
#collector;
|
|
446
|
-
constructor(request, collector) {
|
|
679
|
+
constructor(request, collector, traceId, parentRequestId) {
|
|
447
680
|
this.requestId = generateRequestId();
|
|
681
|
+
this.traceId = traceId ?? this.requestId;
|
|
682
|
+
this.parentRequestId = parentRequestId;
|
|
448
683
|
this.t0 = performance.now();
|
|
449
684
|
this.#request = request;
|
|
450
685
|
this.#collector = collector;
|
|
@@ -472,11 +707,11 @@ var RequestAccumulator = class {
|
|
|
472
707
|
const durationMs = round(performance.now() - this.t0);
|
|
473
708
|
const headers = {};
|
|
474
709
|
this.#request.headers.forEach((v, k) => {
|
|
475
|
-
headers[k] = k
|
|
710
|
+
headers[k] = redactHeaderValue(k, v);
|
|
476
711
|
});
|
|
477
712
|
const responseHeaders = {};
|
|
478
713
|
res.headers.forEach((v, k) => {
|
|
479
|
-
responseHeaders[k] = k
|
|
714
|
+
responseHeaders[k] = redactHeaderValue(k, v);
|
|
480
715
|
});
|
|
481
716
|
let worstStatus = 200;
|
|
482
717
|
for (const p of this.#procedures) if (p.status > worstStatus) worstStatus = p.status;
|
|
@@ -490,6 +725,7 @@ var RequestAccumulator = class {
|
|
|
490
725
|
timestamp: Date.now(),
|
|
491
726
|
durationMs,
|
|
492
727
|
method: this.#request.method,
|
|
728
|
+
url: this.#request.url,
|
|
493
729
|
path,
|
|
494
730
|
ip: headers["x-forwarded-for"] || headers["x-real-ip"] || "",
|
|
495
731
|
headers,
|
|
@@ -497,7 +733,9 @@ var RequestAccumulator = class {
|
|
|
497
733
|
userAgent: this.#request.headers.get("user-agent") ?? "",
|
|
498
734
|
status: worstStatus,
|
|
499
735
|
procedures: this.#procedures,
|
|
500
|
-
isBatch: this.#procedures.length > 1
|
|
736
|
+
isBatch: this.#procedures.length > 1,
|
|
737
|
+
traceId: this.traceId,
|
|
738
|
+
parentRequestId: this.parentRequestId
|
|
501
739
|
});
|
|
502
740
|
}
|
|
503
741
|
/** Whether any procedures have been recorded. */
|
|
@@ -517,8 +755,7 @@ function errorToMarkdown(e) {
|
|
|
517
755
|
if (e.stack) md += `### Stack Trace\n\n\`\`\`\n${e.stack}\n\`\`\`\n\n`;
|
|
518
756
|
if (Object.keys(e.headers).length > 0) {
|
|
519
757
|
md += `### Request Headers\n\n`;
|
|
520
|
-
for (const [k, v] of Object.entries(e.headers))
|
|
521
|
-
else md += `- \`${k}\`: \`${v}\`\n`;
|
|
758
|
+
for (const [k, v] of Object.entries(e.headers)) md += `- \`${k}\`: \`${redactHeaderValue(k, v)}\`\n`;
|
|
522
759
|
md += "\n";
|
|
523
760
|
}
|
|
524
761
|
if (e.spans.length > 0) {
|
|
@@ -541,6 +778,7 @@ function requestToMarkdown(r) {
|
|
|
541
778
|
md += `| Request ID | \`${r.requestId}\` |\n`;
|
|
542
779
|
md += `| Session ID | \`${r.sessionId}\` |\n`;
|
|
543
780
|
md += `| Method | ${r.method} |\n`;
|
|
781
|
+
md += `| URL | \`${r.url}\` |\n`;
|
|
544
782
|
md += `| Path | \`${r.path}\` |\n`;
|
|
545
783
|
md += `| Status | ${r.status} |\n`;
|
|
546
784
|
md += `| Duration | ${r.durationMs}ms |\n`;
|
|
@@ -554,6 +792,7 @@ function requestToMarkdown(r) {
|
|
|
554
792
|
const pEmoji = p.status >= 400 ? "⚠️" : "✅";
|
|
555
793
|
md += `### ${pEmoji} ${i + 1}. \`${p.procedure}\` → ${p.status} (${p.durationMs}ms)\n\n`;
|
|
556
794
|
if (p.input !== void 0 && p.input !== null) md += `#### Input\n\n\`\`\`json\n${safeStringify(p.input)}\n\`\`\`\n\n`;
|
|
795
|
+
if (p.output !== void 0 && p.output !== null) md += `#### Output\n\n\`\`\`json\n${safeStringify(p.output)}\n\`\`\`\n\n`;
|
|
557
796
|
if (p.spans.length > 0) {
|
|
558
797
|
const byKind = /* @__PURE__ */ new Map();
|
|
559
798
|
for (const s of p.spans) byKind.set(s.kind, (byKind.get(s.kind) ?? 0) + s.durationMs);
|
|
@@ -583,6 +822,63 @@ function requestToMarkdown(r) {
|
|
|
583
822
|
if (r.durationMs > 100) md += `- ⚠️ This request took ${r.durationMs}ms — what is the bottleneck?\n`;
|
|
584
823
|
return md;
|
|
585
824
|
}
|
|
825
|
+
async function captureResponseBody(response) {
|
|
826
|
+
const lowered = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
827
|
+
if (!response.body || lowered.includes("text/event-stream") || lowered.includes("application/octet-stream")) return { output: null };
|
|
828
|
+
try {
|
|
829
|
+
const text = await response.clone().text();
|
|
830
|
+
if (!text) return { output: null };
|
|
831
|
+
let output = text;
|
|
832
|
+
if (lowered.includes("application/json") || lowered.includes("+json")) try {
|
|
833
|
+
output = JSON.parse(text);
|
|
834
|
+
} catch {
|
|
835
|
+
output = text;
|
|
836
|
+
}
|
|
837
|
+
if (response.status < 400) return { output };
|
|
838
|
+
if (output && typeof output === "object") {
|
|
839
|
+
const record = output;
|
|
840
|
+
const message = typeof record.message === "string" ? record.message : null;
|
|
841
|
+
const code = typeof record.code === "string" ? record.code : null;
|
|
842
|
+
if (message && code) return {
|
|
843
|
+
output,
|
|
844
|
+
error: `${code}: ${message}`
|
|
845
|
+
};
|
|
846
|
+
if (message) return {
|
|
847
|
+
output,
|
|
848
|
+
error: message
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
output,
|
|
853
|
+
error: typeof output === "string" ? output : safeStringify(output)
|
|
854
|
+
};
|
|
855
|
+
} catch {
|
|
856
|
+
return { output: null };
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function extractResponseError(output, status, fallback) {
|
|
860
|
+
if (output && typeof output === "object") {
|
|
861
|
+
const record = output;
|
|
862
|
+
const code = typeof record.code === "string" ? record.code : null;
|
|
863
|
+
const message = typeof record.message === "string" ? record.message : null;
|
|
864
|
+
if (code && message) return {
|
|
865
|
+
code,
|
|
866
|
+
message
|
|
867
|
+
};
|
|
868
|
+
if (message) return {
|
|
869
|
+
code: `HTTP_${status}`,
|
|
870
|
+
message
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
if (fallback) return {
|
|
874
|
+
code: `HTTP_${status}`,
|
|
875
|
+
message: fallback
|
|
876
|
+
};
|
|
877
|
+
return {
|
|
878
|
+
code: `HTTP_${status}`,
|
|
879
|
+
message: `Request failed with status ${status}`
|
|
880
|
+
};
|
|
881
|
+
}
|
|
586
882
|
function safeStringify(v) {
|
|
587
883
|
try {
|
|
588
884
|
return JSON.stringify(v, null, 2);
|
|
@@ -590,6 +886,28 @@ function safeStringify(v) {
|
|
|
590
886
|
return String(v);
|
|
591
887
|
}
|
|
592
888
|
}
|
|
889
|
+
function matchesPathPrefix(pathname, prefixes) {
|
|
890
|
+
const normalized = pathname.startsWith("/") ? pathname.slice(1) : pathname;
|
|
891
|
+
for (const prefix of prefixes) if (normalized === prefix || normalized.startsWith(prefix + "/")) return true;
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
function isTrackedRequestPath(pathname) {
|
|
895
|
+
const normalized = pathname.startsWith("/") ? pathname.slice(1) : pathname;
|
|
896
|
+
return normalized === "api" || normalized.startsWith("api/") || normalized === "graphql" || normalized.startsWith("graphql/");
|
|
897
|
+
}
|
|
898
|
+
function isAnalyticsPath(pathname) {
|
|
899
|
+
return pathname === "api/analytics" || pathname.startsWith("api/analytics/");
|
|
900
|
+
}
|
|
901
|
+
function parseAnalyticsDetailPath(pathname, prefix) {
|
|
902
|
+
if (!pathname.startsWith(prefix)) return null;
|
|
903
|
+
const rawId = pathname.slice(prefix.length);
|
|
904
|
+
if (!rawId || rawId.includes("/")) return null;
|
|
905
|
+
const id = Number(rawId);
|
|
906
|
+
return {
|
|
907
|
+
id: Number.isFinite(id) ? id : null,
|
|
908
|
+
rawId: decodeURIComponent(rawId)
|
|
909
|
+
};
|
|
910
|
+
}
|
|
593
911
|
const __analytics_dirname = dirname(fileURLToPath(import.meta.url));
|
|
594
912
|
let _dashboardCache;
|
|
595
913
|
function analyticsHTML() {
|
|
@@ -621,15 +939,8 @@ function checkAnalyticsAuth(request, auth) {
|
|
|
621
939
|
}
|
|
622
940
|
function sanitizeHeaders(headers) {
|
|
623
941
|
const result = {};
|
|
624
|
-
const sensitiveKeys = new Set([
|
|
625
|
-
"authorization",
|
|
626
|
-
"cookie",
|
|
627
|
-
"x-api-key",
|
|
628
|
-
"x-auth-token",
|
|
629
|
-
"proxy-authorization"
|
|
630
|
-
]);
|
|
631
942
|
headers.forEach((value, key) => {
|
|
632
|
-
result[key] =
|
|
943
|
+
result[key] = redactHeaderValue(key, value);
|
|
633
944
|
});
|
|
634
945
|
return result;
|
|
635
946
|
}
|
|
@@ -676,7 +987,7 @@ location.reload();
|
|
|
676
987
|
/** Return auth-failure response for analytics routes. */
|
|
677
988
|
function analyticsAuthResponse(pathname) {
|
|
678
989
|
const jsonHeaders = { "content-type": "application/json" };
|
|
679
|
-
if (pathname !== "api/analytics") return new Response(JSON.stringify({
|
|
990
|
+
if (pathname !== "api/analytics" && pathname !== "api/analytics/") return new Response(JSON.stringify({
|
|
680
991
|
code: "UNAUTHORIZED",
|
|
681
992
|
status: 401,
|
|
682
993
|
message: "Invalid token"
|
|
@@ -693,7 +1004,7 @@ function jsonResponse(data, headers) {
|
|
|
693
1004
|
return new Response(JSON.stringify(data), { headers });
|
|
694
1005
|
}
|
|
695
1006
|
/** Serve analytics dashboard and API routes. */
|
|
696
|
-
async function serveAnalyticsRoute(pathname, collector, dashboardHtml) {
|
|
1007
|
+
async function serveAnalyticsRoute(pathname, request, collector, dashboardHtml) {
|
|
697
1008
|
const jsonCacheHeaders = {
|
|
698
1009
|
"content-type": "application/json",
|
|
699
1010
|
"cache-control": "no-cache"
|
|
@@ -702,32 +1013,96 @@ async function serveAnalyticsRoute(pathname, collector, dashboardHtml) {
|
|
|
702
1013
|
"content-type": "text/markdown; charset=utf-8",
|
|
703
1014
|
"cache-control": "no-cache"
|
|
704
1015
|
};
|
|
1016
|
+
const url = new URL(request.url);
|
|
1017
|
+
if (pathname === "api/analytics" || pathname === "api/analytics/") return new Response(dashboardHtml, { headers: { "content-type": "text/html" } });
|
|
705
1018
|
if (pathname === "api/analytics/stats") return jsonResponse(collector.toJSON(), jsonCacheHeaders);
|
|
706
|
-
if (pathname === "api/analytics/
|
|
707
|
-
|
|
708
|
-
|
|
1019
|
+
if (pathname === "api/analytics/hidden") {
|
|
1020
|
+
if (request.method === "GET") return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
|
|
1021
|
+
if (request.method === "POST") {
|
|
1022
|
+
const body = await request.json();
|
|
1023
|
+
if (typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
|
|
1024
|
+
status: 400,
|
|
1025
|
+
headers: jsonCacheHeaders
|
|
1026
|
+
});
|
|
1027
|
+
collector.addHiddenPath(body.path);
|
|
1028
|
+
return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
|
|
1029
|
+
}
|
|
1030
|
+
if (request.method === "DELETE") {
|
|
1031
|
+
const body = await request.json();
|
|
1032
|
+
if (typeof body.path !== "string") return new Response("{\"error\":\"path required\"}", {
|
|
1033
|
+
status: 400,
|
|
1034
|
+
headers: jsonCacheHeaders
|
|
1035
|
+
});
|
|
1036
|
+
collector.removeHiddenPath(body.path);
|
|
1037
|
+
return jsonResponse(collector.getHiddenPaths(), jsonCacheHeaders);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (pathname === "api/analytics/errors") return jsonResponse(queryErrors((await collector.getErrors()).filter((e) => !collector.isHidden(e.procedure)), parseQueryParams(url.searchParams)), jsonCacheHeaders);
|
|
1041
|
+
if (pathname === "api/analytics/requests") return jsonResponse(queryRequests((await collector.getRequests()).filter((r) => !collector.isHidden(r.path)), parseQueryParams(url.searchParams)), jsonCacheHeaders);
|
|
1042
|
+
if (pathname === "api/analytics/tasks") return jsonResponse(queryTasks(await collector.getTaskExecutions(), parseQueryParams(url.searchParams)), jsonCacheHeaders);
|
|
709
1043
|
if (pathname === "api/analytics/scheduled") {
|
|
710
1044
|
const { getScheduledTasks } = await import("../core/task.mjs");
|
|
711
1045
|
return jsonResponse(getScheduledTasks(), jsonCacheHeaders);
|
|
712
1046
|
}
|
|
1047
|
+
if (pathname === "api/analytics/stream") {
|
|
1048
|
+
const stream = collector.sseHub.createStream();
|
|
1049
|
+
return new Response(stream, { headers: {
|
|
1050
|
+
"content-type": "text/event-stream",
|
|
1051
|
+
"cache-control": "no-cache",
|
|
1052
|
+
"connection": "keep-alive"
|
|
1053
|
+
} });
|
|
1054
|
+
}
|
|
1055
|
+
if (pathname === "api/analytics/timeseries") {
|
|
1056
|
+
const range = url.searchParams.get("range") || "1h";
|
|
1057
|
+
return jsonResponse(collector.timeseries.query(range), jsonCacheHeaders);
|
|
1058
|
+
}
|
|
1059
|
+
if (pathname === "api/analytics/alerts") return jsonResponse({
|
|
1060
|
+
history: collector.alertEngine?.getHistory() ?? [],
|
|
1061
|
+
states: collector.alertEngine?.getStates() ?? {}
|
|
1062
|
+
}, jsonCacheHeaders);
|
|
1063
|
+
if (pathname === "api/analytics/cost") return jsonResponse(collector.costTracker.getSummary(), jsonCacheHeaders);
|
|
1064
|
+
if (pathname.startsWith("api/analytics/traces/")) {
|
|
1065
|
+
const traceId = decodeURIComponent(pathname.slice(21));
|
|
1066
|
+
if (traceId) return jsonResponse((await collector.getRequests()).filter((r) => r.traceId === traceId), jsonCacheHeaders);
|
|
1067
|
+
}
|
|
713
1068
|
if (pathname.startsWith("api/analytics/requests/") && pathname.endsWith("/md")) {
|
|
714
|
-
const
|
|
715
|
-
const
|
|
1069
|
+
const rawId = pathname.slice(23, -3);
|
|
1070
|
+
const parsedId = Number(rawId);
|
|
1071
|
+
const requestId = decodeURIComponent(rawId);
|
|
1072
|
+
const entry = (await collector.getRequests()).find((r) => r.id === parsedId || r.requestId === requestId);
|
|
716
1073
|
if (entry) return new Response(requestToMarkdown(entry), { headers: mdHeaders });
|
|
717
1074
|
return new Response("not found", { status: 404 });
|
|
718
1075
|
}
|
|
1076
|
+
const requestDetail = parseAnalyticsDetailPath(pathname, "api/analytics/requests/");
|
|
1077
|
+
if (requestDetail) {
|
|
1078
|
+
const entry = (await collector.getRequests()).find((r) => r.id === requestDetail.id || r.requestId === requestDetail.rawId);
|
|
1079
|
+
return entry ? jsonResponse(entry, jsonCacheHeaders) : new Response("not found", { status: 404 });
|
|
1080
|
+
}
|
|
719
1081
|
if (pathname.startsWith("api/analytics/errors/") && pathname.endsWith("/md")) {
|
|
720
|
-
const
|
|
1082
|
+
const rawId = pathname.slice(21, -3);
|
|
1083
|
+
const id = Number(rawId);
|
|
721
1084
|
const entry = (await collector.getErrors()).find((e) => e.id === id);
|
|
722
1085
|
if (entry) return new Response(errorToMarkdown(entry), { headers: mdHeaders });
|
|
723
1086
|
return new Response("not found", { status: 404 });
|
|
724
1087
|
}
|
|
1088
|
+
const errorDetail = parseAnalyticsDetailPath(pathname, "api/analytics/errors/");
|
|
1089
|
+
if (errorDetail) {
|
|
1090
|
+
const entry = (await collector.getErrors()).find((e) => e.id === errorDetail.id);
|
|
1091
|
+
return entry ? jsonResponse(entry, jsonCacheHeaders) : new Response("not found", { status: 404 });
|
|
1092
|
+
}
|
|
725
1093
|
if (pathname === "api/analytics/errors/md") {
|
|
726
1094
|
const errors = await collector.getErrors();
|
|
727
1095
|
const md = errors.length === 0 ? "No errors.\n" : `# Errors (${errors.length})\n\n` + errors.map((e) => errorToMarkdown(e)).join("\n\n---\n\n");
|
|
728
1096
|
return new Response(md, { headers: mdHeaders });
|
|
729
1097
|
}
|
|
730
|
-
return new Response(
|
|
1098
|
+
return new Response(JSON.stringify({
|
|
1099
|
+
code: "NOT_FOUND",
|
|
1100
|
+
status: 404,
|
|
1101
|
+
message: "Analytics route not found"
|
|
1102
|
+
}), {
|
|
1103
|
+
status: 404,
|
|
1104
|
+
headers: jsonCacheHeaders
|
|
1105
|
+
});
|
|
731
1106
|
}
|
|
732
1107
|
/**
|
|
733
1108
|
* Wrap a fetch handler with analytics collection.
|
|
@@ -749,14 +1124,18 @@ function wrapWithAnalytics(handler, options = {}) {
|
|
|
749
1124
|
const qMark = url.indexOf("?", pathStart);
|
|
750
1125
|
const fullPath = qMark === -1 ? url.slice(pathStart) : url.slice(pathStart, qMark);
|
|
751
1126
|
const pathname = fullPath.length > 1 ? fullPath.slice(1) : "";
|
|
752
|
-
if (pathname
|
|
1127
|
+
if (isAnalyticsPath(pathname)) {
|
|
753
1128
|
if (auth) {
|
|
754
1129
|
const authResult = checkAnalyticsAuth(request, auth);
|
|
755
1130
|
if (!(authResult instanceof Promise ? await authResult : authResult)) return analyticsAuthResponse(pathname);
|
|
756
1131
|
}
|
|
757
|
-
return serveAnalyticsRoute(pathname, collector, dashboardHtml);
|
|
1132
|
+
return serveAnalyticsRoute(pathname, request, collector, dashboardHtml);
|
|
758
1133
|
}
|
|
759
|
-
|
|
1134
|
+
if (!isTrackedRequestPath(pathname) || collector.isIgnored(pathname)) return handler(request);
|
|
1135
|
+
const incomingTraceId = request.headers.get("x-trace-id");
|
|
1136
|
+
const parentRequestId = request.headers.get("x-parent-request-id");
|
|
1137
|
+
const traceId = incomingTraceId || generateRequestId();
|
|
1138
|
+
const acc = new RequestAccumulator(request, collector, traceId, parentRequestId ?? void 0);
|
|
760
1139
|
const reqTrace = new RequestTrace();
|
|
761
1140
|
const t0 = performance.now();
|
|
762
1141
|
analyticsTraceMap.set(request, reqTrace);
|
|
@@ -764,15 +1143,37 @@ function wrapWithAnalytics(handler, options = {}) {
|
|
|
764
1143
|
try {
|
|
765
1144
|
response = await handler(request);
|
|
766
1145
|
const durationMs = round(performance.now() - t0);
|
|
1146
|
+
const captured = await captureResponseBody(response);
|
|
1147
|
+
const procedureInput = reqTrace.procedureInput ?? null;
|
|
1148
|
+
const procedureOutput = reqTrace.procedureOutput ?? captured.output;
|
|
1149
|
+
const procedureSpans = reqTrace.spans ?? [];
|
|
767
1150
|
collector.record(pathname, durationMs);
|
|
768
1151
|
acc.addProcedure({
|
|
769
1152
|
procedure: pathname,
|
|
770
1153
|
durationMs,
|
|
771
1154
|
status: response.status,
|
|
772
|
-
input:
|
|
773
|
-
output:
|
|
774
|
-
spans:
|
|
1155
|
+
input: procedureInput,
|
|
1156
|
+
output: procedureOutput,
|
|
1157
|
+
spans: procedureSpans,
|
|
1158
|
+
error: captured.error
|
|
775
1159
|
});
|
|
1160
|
+
if (response.status >= 400) {
|
|
1161
|
+
const { code, message } = extractResponseError(procedureOutput, response.status, captured.error);
|
|
1162
|
+
collector.recordError(pathname, durationMs, message);
|
|
1163
|
+
collector.recordDetailedError({
|
|
1164
|
+
requestId: acc.requestId,
|
|
1165
|
+
timestamp: Date.now(),
|
|
1166
|
+
procedure: pathname,
|
|
1167
|
+
error: message,
|
|
1168
|
+
code,
|
|
1169
|
+
status: response.status,
|
|
1170
|
+
stack: "",
|
|
1171
|
+
input: procedureInput,
|
|
1172
|
+
headers: sanitizeHeaders(request.headers),
|
|
1173
|
+
durationMs,
|
|
1174
|
+
spans: procedureSpans
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
776
1177
|
} catch (error) {
|
|
777
1178
|
const durationMs = round(performance.now() - t0);
|
|
778
1179
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -799,6 +1200,7 @@ function wrapWithAnalytics(handler, options = {}) {
|
|
|
799
1200
|
}
|
|
800
1201
|
const headers = new Headers(response.headers);
|
|
801
1202
|
headers.set("x-request-id", acc.requestId);
|
|
1203
|
+
headers.set("x-trace-id", traceId);
|
|
802
1204
|
const cookie = acc.getSessionCookie();
|
|
803
1205
|
if (cookie) headers.append("set-cookie", cookie);
|
|
804
1206
|
const injected = new Response(response.body, {
|