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.
@@ -0,0 +1,164 @@
1
+ //#region src/plugins/analytics-query.ts
2
+ function parseQueryParams(params) {
3
+ const q = {};
4
+ const cursor = params.get("cursor");
5
+ if (cursor) q.cursor = Number(cursor);
6
+ const before = params.get("before");
7
+ if (before) q.before = Number(before);
8
+ const limit = params.get("limit");
9
+ q.limit = limit ? Math.max(1, Number(limit)) : 50;
10
+ q.sort = params.get("sort") ?? void 0;
11
+ q.order = params.get("order") ?? void 0;
12
+ q.status = params.get("status") ?? void 0;
13
+ q.path = params.get("path") ?? void 0;
14
+ q.search = params.get("search") ?? void 0;
15
+ q.procedure = params.get("procedure") ?? void 0;
16
+ q.session = params.get("session") ?? void 0;
17
+ const minDuration = params.get("minDuration");
18
+ if (minDuration) q.minDuration = Number(minDuration);
19
+ const maxDuration = params.get("maxDuration");
20
+ if (maxDuration) q.maxDuration = Number(maxDuration);
21
+ return q;
22
+ }
23
+ function matchesStatus(entryStatus, filter) {
24
+ const exact = Number(filter);
25
+ if (Number.isFinite(exact)) return entryStatus === exact;
26
+ if (/^[1-5]xx$/i.test(filter)) {
27
+ const cls = Number(filter[0]);
28
+ return Math.floor(entryStatus / 100) === cls;
29
+ }
30
+ const rangeMatch = filter.match(/^([<>]=?)(\d+)$/);
31
+ if (rangeMatch) {
32
+ const [, op, val] = rangeMatch;
33
+ const n = Number(val);
34
+ switch (op) {
35
+ case ">": return entryStatus > n;
36
+ case ">=": return entryStatus >= n;
37
+ case "<": return entryStatus < n;
38
+ case "<=": return entryStatus <= n;
39
+ }
40
+ }
41
+ return true;
42
+ }
43
+ function queryRequests(entries, params) {
44
+ let filtered = entries;
45
+ if (params.status) {
46
+ const status = params.status;
47
+ filtered = filtered.filter((r) => matchesStatus(r.status, status));
48
+ }
49
+ if (params.path) {
50
+ const prefix = params.path.toLowerCase();
51
+ filtered = filtered.filter((r) => r.path.toLowerCase().includes(prefix));
52
+ }
53
+ if (params.procedure) {
54
+ const proc = params.procedure.toLowerCase();
55
+ filtered = filtered.filter((r) => r.procedures.some((p) => p.procedure.toLowerCase().includes(proc)));
56
+ }
57
+ if (params.session) {
58
+ const session = params.session;
59
+ filtered = filtered.filter((r) => r.sessionId === session);
60
+ }
61
+ if (params.minDuration != null) {
62
+ const min = params.minDuration;
63
+ filtered = filtered.filter((r) => r.durationMs >= min);
64
+ }
65
+ if (params.maxDuration != null) {
66
+ const max = params.maxDuration;
67
+ filtered = filtered.filter((r) => r.durationMs <= max);
68
+ }
69
+ if (params.search) {
70
+ const term = params.search.toLowerCase();
71
+ filtered = filtered.filter((r) => r.path?.toLowerCase().includes(term) || r.procedures?.some((p) => p.procedure.toLowerCase().includes(term)) || r.method?.toLowerCase().includes(term) || r.requestId?.includes(term));
72
+ }
73
+ const sortField = params.sort ?? "timestamp";
74
+ const desc = (params.order ?? "desc") === "desc";
75
+ filtered = sortEntries(filtered, sortField, desc);
76
+ return paginate(filtered, params);
77
+ }
78
+ function queryErrors(entries, params) {
79
+ let filtered = entries;
80
+ if (params.status) {
81
+ const status = params.status;
82
+ filtered = filtered.filter((e) => matchesStatus(e.status, status));
83
+ }
84
+ if (params.procedure) {
85
+ const proc = params.procedure.toLowerCase();
86
+ filtered = filtered.filter((e) => e.procedure.toLowerCase().includes(proc));
87
+ }
88
+ if (params.path) {
89
+ const path = params.path.toLowerCase();
90
+ filtered = filtered.filter((e) => e.procedure.toLowerCase().includes(path));
91
+ }
92
+ if (params.minDuration != null) {
93
+ const min = params.minDuration;
94
+ filtered = filtered.filter((e) => e.durationMs >= min);
95
+ }
96
+ if (params.maxDuration != null) {
97
+ const max = params.maxDuration;
98
+ filtered = filtered.filter((e) => e.durationMs <= max);
99
+ }
100
+ if (params.search) {
101
+ const term = params.search.toLowerCase();
102
+ filtered = filtered.filter((e) => e.procedure.toLowerCase().includes(term) || e.error.toLowerCase().includes(term) || e.code.toLowerCase().includes(term) || e.requestId.includes(term));
103
+ }
104
+ const sortField = params.sort ?? "timestamp";
105
+ const desc = (params.order ?? "desc") === "desc";
106
+ filtered = sortEntries(filtered, sortField, desc);
107
+ return paginate(filtered, params);
108
+ }
109
+ function queryTasks(entries, params) {
110
+ let filtered = entries;
111
+ if (params.status) {
112
+ const status = params.status;
113
+ filtered = filtered.filter((t) => status === "error" ? t.status === "error" : t.status === "success");
114
+ }
115
+ if (params.search) {
116
+ const term = params.search.toLowerCase();
117
+ filtered = filtered.filter((t) => t.taskName.toLowerCase().includes(term) || (t.error?.toLowerCase().includes(term) ?? false));
118
+ }
119
+ if (params.minDuration != null) {
120
+ const min = params.minDuration;
121
+ filtered = filtered.filter((t) => t.durationMs >= min);
122
+ }
123
+ const sortField = params.sort ?? "timestamp";
124
+ const desc = (params.order ?? "desc") === "desc";
125
+ filtered = sortEntries(filtered, sortField, desc);
126
+ return paginate(filtered, params);
127
+ }
128
+ function sortEntries(entries, field, desc) {
129
+ const sorted = [...entries];
130
+ sorted.sort((a, b) => {
131
+ const va = a[field];
132
+ const vb = b[field];
133
+ if (va == null && vb == null) return 0;
134
+ if (va == null) return 1;
135
+ if (vb == null) return -1;
136
+ if (typeof va === "number" && typeof vb === "number") return desc ? vb - va : va - vb;
137
+ if (typeof va === "string" && typeof vb === "string") return desc ? vb.localeCompare(va) : va.localeCompare(vb);
138
+ return 0;
139
+ });
140
+ return sorted;
141
+ }
142
+ function paginate(entries, params) {
143
+ const limit = params.limit ?? 50;
144
+ const total = entries.length;
145
+ let start = 0;
146
+ if (params.cursor != null) {
147
+ const idx = entries.findIndex((e) => e.id === params.cursor);
148
+ start = idx === -1 ? 0 : idx + 1;
149
+ } else if (params.before != null) {
150
+ const idx = entries.findIndex((e) => e.id === params.before);
151
+ start = idx === -1 ? 0 : Math.max(0, idx - limit);
152
+ }
153
+ const data = entries.slice(start, start + limit);
154
+ const hasMore = start + limit < total;
155
+ return {
156
+ data,
157
+ total,
158
+ hasMore,
159
+ nextCursor: hasMore && data.length > 0 ? data[data.length - 1].id : null,
160
+ prevCursor: start > 0 && data.length > 0 ? data[0].id : null
161
+ };
162
+ }
163
+ //#endregion
164
+ export { parseQueryParams, queryErrors, queryRequests, queryTasks };
@@ -0,0 +1,31 @@
1
+ import { ErrorEntry, RequestEntry, TaskExecution } from "./analytics.mjs";
2
+
3
+ //#region src/plugins/analytics-sse.d.ts
4
+ type AnalyticsEvent = {
5
+ type: 'request';
6
+ data: RequestEntry;
7
+ } | {
8
+ type: 'error';
9
+ data: ErrorEntry;
10
+ } | {
11
+ type: 'task';
12
+ data: TaskExecution;
13
+ } | {
14
+ type: 'stats';
15
+ data: unknown;
16
+ };
17
+ declare class AnalyticsSSEHub {
18
+ #private;
19
+ constructor();
20
+ /** Start periodic stats broadcast. */
21
+ startStatsBroadcast(getStats: () => unknown, intervalMs?: number): void;
22
+ /** Broadcast an event to all connected clients. */
23
+ broadcast(event: AnalyticsEvent): void;
24
+ /** Create an SSE ReadableStream for a new client connection. */
25
+ createStream(): ReadableStream<Uint8Array>;
26
+ /** Number of connected clients. */
27
+ get clientCount(): number;
28
+ dispose(): void;
29
+ }
30
+ //#endregion
31
+ export { AnalyticsSSEHub };
@@ -0,0 +1,74 @@
1
+ import { encodeEventMessage } from "../core/sse.mjs";
2
+ //#region src/plugins/analytics-sse.ts
3
+ /**
4
+ * Analytics SSE — real-time event streaming for the analytics dashboard.
5
+ */
6
+ var AnalyticsSSEHub = class {
7
+ #clients = /* @__PURE__ */ new Set();
8
+ #statsInterval = null;
9
+ #getStats = null;
10
+ constructor() {}
11
+ /** Start periodic stats broadcast. */
12
+ startStatsBroadcast(getStats, intervalMs = 5e3) {
13
+ this.#getStats = getStats;
14
+ this.#statsInterval = setInterval(() => {
15
+ if (this.#clients.size > 0 && this.#getStats) this.broadcast({
16
+ type: "stats",
17
+ data: this.#getStats()
18
+ });
19
+ }, intervalMs);
20
+ if (typeof this.#statsInterval === "object" && "unref" in this.#statsInterval) this.#statsInterval.unref();
21
+ }
22
+ /** Broadcast an event to all connected clients. */
23
+ broadcast(event) {
24
+ if (this.#clients.size === 0) return;
25
+ const message = encodeEventMessage({
26
+ event: event.type,
27
+ data: JSON.stringify(event.data)
28
+ });
29
+ for (const controller of this.#clients) try {
30
+ controller.enqueue(message);
31
+ } catch {
32
+ this.#clients.delete(controller);
33
+ }
34
+ }
35
+ /** Create an SSE ReadableStream for a new client connection. */
36
+ createStream() {
37
+ let controller;
38
+ let keepAliveTimer;
39
+ return new ReadableStream({
40
+ start: (ctrl) => {
41
+ controller = ctrl;
42
+ this.#clients.add(controller);
43
+ controller.enqueue(encodeEventMessage({ comment: "connected" }));
44
+ keepAliveTimer = setInterval(() => {
45
+ try {
46
+ controller.enqueue(encodeEventMessage({ comment: "keepalive" }));
47
+ } catch {
48
+ clearInterval(keepAliveTimer);
49
+ this.#clients.delete(controller);
50
+ }
51
+ }, 15e3);
52
+ if (typeof keepAliveTimer === "object" && "unref" in keepAliveTimer) keepAliveTimer.unref();
53
+ },
54
+ cancel: () => {
55
+ clearInterval(keepAliveTimer);
56
+ this.#clients.delete(controller);
57
+ }
58
+ }).pipeThrough(new TextEncoderStream());
59
+ }
60
+ /** Number of connected clients. */
61
+ get clientCount() {
62
+ return this.#clients.size;
63
+ }
64
+ dispose() {
65
+ if (this.#statsInterval) clearInterval(this.#statsInterval);
66
+ this.#statsInterval = null;
67
+ for (const controller of this.#clients) try {
68
+ controller.close();
69
+ } catch {}
70
+ this.#clients.clear();
71
+ }
72
+ };
73
+ //#endregion
74
+ export { AnalyticsSSEHub };
@@ -0,0 +1,50 @@
1
+ //#region src/plugins/analytics-timeseries.d.ts
2
+ /**
3
+ * Analytics Time-Series — multi-tier bucketed aggregation with automatic downsampling.
4
+ *
5
+ * Three tiers:
6
+ * - minute: 1-minute buckets, last 60 minutes
7
+ * - hour: 1-hour buckets, last 24 hours
8
+ * - day: 1-day buckets, last 30 days
9
+ */
10
+ interface TimeSeriesBucket {
11
+ time: number;
12
+ count: number;
13
+ errors: number;
14
+ totalLatency: number;
15
+ minLatency: number;
16
+ maxLatency: number;
17
+ }
18
+ interface TimeSeriesSnapshot {
19
+ time: number;
20
+ count: number;
21
+ errors: number;
22
+ errorRate: number;
23
+ avgLatency: number;
24
+ minLatency: number;
25
+ maxLatency: number;
26
+ }
27
+ type TimeRange = '1h' | '6h' | '24h' | '7d' | '30d';
28
+ declare class TimeSeriesAggregator {
29
+ #private;
30
+ constructor();
31
+ /** Record a request. */
32
+ record(durationMs: number, isError: boolean): void;
33
+ /** Get time-series for a given range. */
34
+ query(range: TimeRange): TimeSeriesSnapshot[];
35
+ /** Export state for persistence. */
36
+ toJSON(): {
37
+ minutes: TimeSeriesBucket[];
38
+ hours: TimeSeriesBucket[];
39
+ days: TimeSeriesBucket[];
40
+ };
41
+ /** Restore from persisted state. */
42
+ hydrate(data: {
43
+ minutes?: TimeSeriesBucket[];
44
+ hours?: TimeSeriesBucket[];
45
+ days?: TimeSeriesBucket[];
46
+ }): void;
47
+ dispose(): void;
48
+ }
49
+ //#endregion
50
+ export { TimeSeriesAggregator };
@@ -0,0 +1,169 @@
1
+ //#region src/plugins/analytics-timeseries.ts
2
+ const MINUTE_MS = 6e4;
3
+ const HOUR_MS = 36e5;
4
+ const DAY_MS = 864e5;
5
+ const MAX_MINUTE_BUCKETS = 60;
6
+ const MAX_HOUR_BUCKETS = 24;
7
+ const MAX_DAY_BUCKETS = 30;
8
+ var TimeSeriesAggregator = class {
9
+ #minutes = [];
10
+ #hours = [];
11
+ #days = [];
12
+ #currentMinute = null;
13
+ #rollupTimer = null;
14
+ constructor() {
15
+ this.#rollupTimer = setInterval(() => this.#rollup(), MINUTE_MS);
16
+ if (typeof this.#rollupTimer === "object" && "unref" in this.#rollupTimer) this.#rollupTimer.unref();
17
+ }
18
+ /** Record a request. */
19
+ record(durationMs, isError) {
20
+ const now = Date.now();
21
+ const minuteStart = now - now % MINUTE_MS;
22
+ if (!this.#currentMinute || this.#currentMinute.time !== minuteStart) {
23
+ if (this.#currentMinute) {
24
+ this.#minutes.push(this.#currentMinute);
25
+ if (this.#minutes.length > MAX_MINUTE_BUCKETS) this.#minutes.shift();
26
+ }
27
+ this.#currentMinute = createBucket(minuteStart);
28
+ }
29
+ this.#currentMinute.count++;
30
+ if (isError) this.#currentMinute.errors++;
31
+ this.#currentMinute.totalLatency += durationMs;
32
+ if (durationMs < this.#currentMinute.minLatency) this.#currentMinute.minLatency = durationMs;
33
+ if (durationMs > this.#currentMinute.maxLatency) this.#currentMinute.maxLatency = durationMs;
34
+ }
35
+ /** Get time-series for a given range. */
36
+ query(range) {
37
+ this.#flushCurrentMinute();
38
+ switch (range) {
39
+ case "1h": return this.#minutes.map(toSnapshot);
40
+ case "6h": {
41
+ const cutoff = Date.now() - 6 * HOUR_MS;
42
+ const hourData = this.#hours.filter((b) => b.time >= cutoff).map(toSnapshot);
43
+ const minuteData = this.#minutes.map(toSnapshot);
44
+ return [...hourData, ...minuteData];
45
+ }
46
+ case "24h": return this.#hours.map(toSnapshot);
47
+ case "7d": {
48
+ const cutoff = Date.now() - 7 * DAY_MS;
49
+ return this.#days.filter((b) => b.time >= cutoff).map(toSnapshot);
50
+ }
51
+ case "30d": return this.#days.map(toSnapshot);
52
+ }
53
+ }
54
+ /** Export state for persistence. */
55
+ toJSON() {
56
+ this.#flushCurrentMinute();
57
+ return {
58
+ minutes: this.#minutes,
59
+ hours: this.#hours,
60
+ days: this.#days
61
+ };
62
+ }
63
+ /** Restore from persisted state. */
64
+ hydrate(data) {
65
+ if (Array.isArray(data.minutes)) this.#minutes = data.minutes;
66
+ if (Array.isArray(data.hours)) this.#hours = data.hours;
67
+ if (Array.isArray(data.days)) this.#days = data.days;
68
+ }
69
+ #flushCurrentMinute() {
70
+ if (this.#currentMinute && this.#currentMinute.count > 0) {
71
+ const minuteStart = Date.now() - Date.now() % MINUTE_MS;
72
+ if (this.#currentMinute.time !== minuteStart) {
73
+ this.#minutes.push(this.#currentMinute);
74
+ if (this.#minutes.length > MAX_MINUTE_BUCKETS) this.#minutes.shift();
75
+ this.#currentMinute = null;
76
+ }
77
+ }
78
+ }
79
+ #rollup() {
80
+ const now = Date.now();
81
+ const hourStart = now - now % HOUR_MS;
82
+ const completedMinutes = this.#minutes.filter((b) => b.time < hourStart);
83
+ if (completedMinutes.length > 0) {
84
+ const byHour = /* @__PURE__ */ new Map();
85
+ for (const b of completedMinutes) {
86
+ const hStart = b.time - b.time % HOUR_MS;
87
+ const arr = byHour.get(hStart);
88
+ if (arr) arr.push(b);
89
+ else byHour.set(hStart, [b]);
90
+ }
91
+ for (const [hStart, buckets] of byHour) {
92
+ const existing = this.#hours.find((h) => h.time === hStart);
93
+ if (existing) mergeBucketInto(existing, buckets);
94
+ else {
95
+ this.#hours.push(mergeBuckets(hStart, buckets));
96
+ if (this.#hours.length > MAX_HOUR_BUCKETS) this.#hours.shift();
97
+ }
98
+ }
99
+ this.#minutes = this.#minutes.filter((b) => b.time >= hourStart);
100
+ }
101
+ const dayStart = now - now % DAY_MS;
102
+ const completedHours = this.#hours.filter((b) => b.time < dayStart);
103
+ if (completedHours.length > 0) {
104
+ const byDay = /* @__PURE__ */ new Map();
105
+ for (const b of completedHours) {
106
+ const dStart = b.time - b.time % DAY_MS;
107
+ const arr = byDay.get(dStart);
108
+ if (arr) arr.push(b);
109
+ else byDay.set(dStart, [b]);
110
+ }
111
+ for (const [dStart, buckets] of byDay) {
112
+ const existing = this.#days.find((d) => d.time === dStart);
113
+ if (existing) mergeBucketInto(existing, buckets);
114
+ else {
115
+ this.#days.push(mergeBuckets(dStart, buckets));
116
+ if (this.#days.length > MAX_DAY_BUCKETS) this.#days.shift();
117
+ }
118
+ }
119
+ this.#hours = this.#hours.filter((b) => b.time >= dayStart);
120
+ }
121
+ }
122
+ dispose() {
123
+ if (this.#rollupTimer) clearInterval(this.#rollupTimer);
124
+ this.#rollupTimer = null;
125
+ }
126
+ };
127
+ function createBucket(time) {
128
+ return {
129
+ time,
130
+ count: 0,
131
+ errors: 0,
132
+ totalLatency: 0,
133
+ minLatency: Infinity,
134
+ maxLatency: 0
135
+ };
136
+ }
137
+ function mergeBuckets(time, buckets) {
138
+ const merged = createBucket(time);
139
+ for (const b of buckets) {
140
+ merged.count += b.count;
141
+ merged.errors += b.errors;
142
+ merged.totalLatency += b.totalLatency;
143
+ if (b.minLatency < merged.minLatency) merged.minLatency = b.minLatency;
144
+ if (b.maxLatency > merged.maxLatency) merged.maxLatency = b.maxLatency;
145
+ }
146
+ return merged;
147
+ }
148
+ function mergeBucketInto(target, buckets) {
149
+ for (const b of buckets) {
150
+ target.count += b.count;
151
+ target.errors += b.errors;
152
+ target.totalLatency += b.totalLatency;
153
+ if (b.minLatency < target.minLatency) target.minLatency = b.minLatency;
154
+ if (b.maxLatency > target.maxLatency) target.maxLatency = b.maxLatency;
155
+ }
156
+ }
157
+ function toSnapshot(b) {
158
+ return {
159
+ time: b.time,
160
+ count: b.count,
161
+ errors: b.errors,
162
+ errorRate: b.count > 0 ? b.errors / b.count * 100 : 0,
163
+ avgLatency: b.count > 0 ? b.totalLatency / b.count : 0,
164
+ minLatency: b.minLatency === Infinity ? 0 : b.minLatency,
165
+ maxLatency: b.maxLatency
166
+ };
167
+ }
168
+ //#endregion
169
+ export { TimeSeriesAggregator };
@@ -1,18 +1,10 @@
1
+ import { BudgetRule, CostTracker, SpanCost } from "./analytics-cost.mjs";
2
+ import { AlertEngine, AlertRule } from "./analytics-alerts.mjs";
3
+ import { AnalyticsSSEHub } from "./analytics-sse.mjs";
4
+ import { TimeSeriesAggregator } from "./analytics-timeseries.mjs";
1
5
  import { FetchHandler } from "../core/handler.mjs";
2
6
 
3
7
  //#region src/plugins/analytics.d.ts
4
- /**
5
- * Built-in analytics plugin — zero-dependency monitoring with deep error tracing.
6
- *
7
- * - Per-procedure metrics (count, errors, latency percentiles) via ring buffers
8
- * - Full error log with input, headers, stack trace, custom spans
9
- * - `trace()` helper for measuring DB queries, API calls, etc.
10
- * - "Copy for AI" — one-click markdown export of any error
11
- * - HTTP-level request tracking with procedure grouping (batch support)
12
- * - Unique request IDs via `x-request-id` response header
13
- *
14
- * Dashboard at /api/analytics, JSON API at /api/analytics/stats, errors at /api/analytics/errors.
15
- */
16
8
  interface TimeWindow {
17
9
  time: number;
18
10
  count: number;
@@ -30,6 +22,8 @@ interface TraceSpan {
30
22
  error?: string;
31
23
  /** Structured key-value attributes (db.name, auth.operation, user.id, etc.) */
32
24
  attributes?: Record<string, string | number | boolean>;
25
+ /** Cost metadata for this span (tokens, price, provider). */
26
+ cost?: SpanCost;
33
27
  }
34
28
  interface ErrorEntry {
35
29
  id: number;
@@ -66,6 +60,7 @@ interface RequestEntry {
66
60
  timestamp: number;
67
61
  durationMs: number;
68
62
  method: string;
63
+ url: string;
69
64
  path: string;
70
65
  ip: string;
71
66
  headers: Record<string, string>;
@@ -74,6 +69,10 @@ interface RequestEntry {
74
69
  status: number;
75
70
  procedures: ProcedureCall[];
76
71
  isBatch: boolean;
72
+ /** Trace ID for correlating related requests across services. */
73
+ traceId?: string;
74
+ /** Parent request ID — links child requests to the originating request. */
75
+ parentRequestId?: string;
77
76
  }
78
77
  /** A background task execution record. */
79
78
  interface TaskExecution {
@@ -93,12 +92,6 @@ interface AnalyticsOptions {
93
92
  bufferSize?: number;
94
93
  /** Time-series history in seconds (default: 120) */
95
94
  historySeconds?: number;
96
- /** Max error entries to keep (default: 100) */
97
- maxErrors?: number;
98
- /** Max recent request entries to keep (default: 200) */
99
- maxRequests?: number;
100
- /** Max task execution entries to keep (default: 200) */
101
- maxTasks?: number;
102
95
  /**
103
96
  * Protect dashboard access.
104
97
  * - `string` — secret token checked against `Authorization: Bearer <token>` header or `?token=` query param
@@ -108,6 +101,14 @@ interface AnalyticsOptions {
108
101
  auth?: string | ((req: Request) => boolean | Promise<boolean>);
109
102
  /** Interval in ms between storage flushes (default: 5000) */
110
103
  flushInterval?: number;
104
+ /** Days to retain entries in storage (default: 30). Entries older than this are pruned on flush. */
105
+ retentionDays?: number;
106
+ /** Path prefixes to exclude from tracking. Can also be managed at runtime via the dashboard or API. */
107
+ ignorePaths?: string[];
108
+ /** Alert rules — fire actions when conditions are met within a sliding window. */
109
+ alerts?: AlertRule[];
110
+ /** Budget rules for cost tracking. */
111
+ budgets?: BudgetRule[];
111
112
  }
112
113
  interface ProcedureSnapshot {
113
114
  count: number;
@@ -188,7 +189,22 @@ declare function trace<T>(ctx: Record<string, unknown>, name: string, fn: () =>
188
189
  }): Promise<T>;
189
190
  declare class AnalyticsCollector {
190
191
  #private;
192
+ /** SSE hub for real-time streaming */
193
+ sseHub: AnalyticsSSEHub;
194
+ /** Multi-tier time-series aggregation */
195
+ timeseries: TimeSeriesAggregator;
196
+ /** Alert engine */
197
+ alertEngine: AlertEngine | null;
198
+ /** Cost tracker */
199
+ costTracker: CostTracker;
191
200
  constructor(options?: AnalyticsOptions);
201
+ /** Check if a path is server-side ignored (from config). */
202
+ isIgnored(pathname: string): boolean;
203
+ /** Check if a path is hidden in the dashboard (from runtime API). */
204
+ isHidden(pathname: string): boolean;
205
+ addHiddenPath(path: string): void;
206
+ removeHiddenPath(path: string): void;
207
+ getHiddenPaths(): string[];
192
208
  record(path: string, durationMs: number): void;
193
209
  recordError(path: string, durationMs: number, errorMsg: string): void;
194
210
  recordDetailedError(entry: Omit<ErrorEntry, 'id'>): void;
@@ -204,10 +220,12 @@ declare class RequestAccumulator {
204
220
  #private;
205
221
  readonly requestId: string;
206
222
  readonly sessionId: string;
223
+ readonly traceId: string;
224
+ readonly parentRequestId?: string;
207
225
  /** True if a new session cookie needs to be set. */
208
226
  readonly isNewSession: boolean;
209
227
  t0: number;
210
- constructor(request: Request, collector: AnalyticsCollector);
228
+ constructor(request: Request, collector: AnalyticsCollector, traceId?: string, parentRequestId?: string);
211
229
  addProcedure(call: ProcedureCall): void;
212
230
  /** Get Set-Cookie header value (only if new session). */
213
231
  getSessionCookie(): string | null;
@@ -224,7 +242,7 @@ declare function sanitizeHeaders(headers: Headers): Record<string, string>;
224
242
  /** Return auth-failure response for analytics routes. */
225
243
  declare function analyticsAuthResponse(pathname: string): Response;
226
244
  /** Serve analytics dashboard and API routes. */
227
- declare function serveAnalyticsRoute(pathname: string, collector: AnalyticsCollector, dashboardHtml: string | undefined): Promise<Response>;
245
+ declare function serveAnalyticsRoute(pathname: string, request: Request, collector: AnalyticsCollector, dashboardHtml: string | undefined): Promise<Response>;
228
246
  /**
229
247
  * Wrap a fetch handler with analytics collection.
230
248
  * Intercepts analytics dashboard routes and instruments every request.