brakit 0.8.7 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,48 +14,10 @@ var __export = (target, all) => {
14
14
  __defProp(target, name, { get: all[name], enumerable: true });
15
15
  };
16
16
 
17
- // src/constants/routes.ts
18
- var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, VALID_TABS_TUPLE, VALID_TABS;
19
- var init_routes = __esm({
20
- "src/constants/routes.ts"() {
21
- "use strict";
22
- DASHBOARD_PREFIX = "/__brakit";
23
- DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
24
- DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
25
- DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
26
- DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
27
- DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
28
- DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
29
- DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
30
- DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
31
- DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
32
- DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
33
- DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
34
- DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
35
- DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
36
- DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
37
- DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
38
- DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
39
- DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
40
- VALID_TABS_TUPLE = [
41
- "overview",
42
- "actions",
43
- "requests",
44
- "fetches",
45
- "queries",
46
- "errors",
47
- "logs",
48
- "performance",
49
- "security"
50
- ];
51
- VALID_TABS = new Set(VALID_TABS_TUPLE);
52
- }
53
- });
54
-
55
- // src/constants/limits.ts
56
- var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS, MAX_JSON_BODY_BYTES, ANALYSIS_DEBOUNCE_MS, ISSUE_ID_HASH_LENGTH, ISSUES_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER, PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_API_LIMIT, MAX_OBJECT_SCAN_DEPTH, MAX_UNIQUE_ENDPOINTS, MAX_ACCUMULATOR_ENTRIES, ISSUE_PRUNE_TTL_MS;
57
- var init_limits = __esm({
58
- "src/constants/limits.ts"() {
17
+ // src/constants/config.ts
18
+ var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS, MAX_JSON_BODY_BYTES, ANALYSIS_DEBOUNCE_MS, ISSUE_ID_HASH_LENGTH, ISSUES_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER, PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_API_LIMIT, MAX_OBJECT_SCAN_DEPTH, MAX_UNIQUE_ENDPOINTS, MAX_ACCUMULATOR_ENTRIES, ISSUE_PRUNE_TTL_MS, FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, CLEAN_HITS_FOR_RESOLUTION, STALE_ISSUE_TTL_MS, STRICT_MODE_MAX_GAP_MS, BASELINE_MIN_SESSIONS, BASELINE_MIN_REQUESTS_PER_SESSION, BASELINE_PENDING_POINTS_MIN, METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS, SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS, VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES, TELEMETRY_EVENT_SETUP_COMPLETED, TELEMETRY_EVENT_FIRST_REQUEST, TELEMETRY_EVENT_DASHBOARD_VIEWED, TELEMETRY_EVENT_SESSION, EXIT_REASON_CLEAN, EXIT_REASON_SIGINT, EXIT_REASON_SIGTERM, DETAIL_PREVIEW_LENGTH, KNOWN_DEPENDENCY_NAMES;
19
+ var init_config = __esm({
20
+ "src/constants/config.ts"() {
59
21
  "use strict";
60
22
  MAX_REQUEST_ENTRIES = 1e3;
61
23
  DEFAULT_MAX_BODY_CAPTURE = 10240;
@@ -82,21 +44,12 @@ var init_limits = __esm({
82
44
  MAX_UNIQUE_ENDPOINTS = 500;
83
45
  MAX_ACCUMULATOR_ENTRIES = 1e3;
84
46
  ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
85
- }
86
- });
87
-
88
- // src/constants/thresholds.ts
89
- var FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, CLEAN_HITS_FOR_RESOLUTION, STALE_ISSUE_TTL_MS;
90
- var init_thresholds = __esm({
91
- "src/constants/thresholds.ts"() {
92
- "use strict";
93
47
  FLOW_GAP_MS = 5e3;
94
48
  SLOW_REQUEST_THRESHOLD_MS = 2e3;
95
49
  MIN_POLLING_SEQUENCE = 3;
96
50
  ENDPOINT_TRUNCATE_LENGTH = 12;
97
51
  N1_QUERY_THRESHOLD = 5;
98
52
  ERROR_RATE_THRESHOLD_PCT = 20;
99
- SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
100
53
  MIN_REQUESTS_FOR_INSIGHT = 2;
101
54
  HIGH_QUERY_COUNT_PER_REQ = 5;
102
55
  CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
@@ -119,25 +72,10 @@ var init_thresholds = __esm({
119
72
  INSIGHT_WINDOW_PER_ENDPOINT = 20;
120
73
  CLEAN_HITS_FOR_RESOLUTION = 5;
121
74
  STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
122
- }
123
- });
124
-
125
- // src/constants/transport.ts
126
- var SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS;
127
- var init_transport = __esm({
128
- "src/constants/transport.ts"() {
129
- "use strict";
130
- SSE_HEARTBEAT_INTERVAL_MS = 3e4;
131
- NOISE_HOSTS = ["registry.npmjs.org", "telemetry.nextjs.org", "vitejs.dev"];
132
- NOISE_PATH_PATTERNS = [".hot-update.", "__webpack", "__vite"];
133
- }
134
- });
135
-
136
- // src/constants/metrics.ts
137
- var METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS;
138
- var init_metrics = __esm({
139
- "src/constants/metrics.ts"() {
140
- "use strict";
75
+ STRICT_MODE_MAX_GAP_MS = 2e3;
76
+ BASELINE_MIN_SESSIONS = 2;
77
+ BASELINE_MIN_REQUESTS_PER_SESSION = 3;
78
+ BASELINE_PENDING_POINTS_MIN = 3;
141
79
  METRICS_DIR = ".brakit";
142
80
  METRICS_FILE = "metrics.json";
143
81
  PORT_FILE = ".brakit/port";
@@ -146,14 +84,74 @@ var init_metrics = __esm({
146
84
  METRICS_MAX_SESSIONS = 50;
147
85
  METRICS_MAX_DATA_POINTS = 200;
148
86
  ISSUES_FLUSH_INTERVAL_MS = 1e4;
87
+ SSE_HEARTBEAT_INTERVAL_MS = 3e4;
88
+ NOISE_HOSTS = ["registry.npmjs.org", "telemetry.nextjs.org", "vitejs.dev"];
89
+ NOISE_PATH_PATTERNS = [".hot-update.", "__webpack", "__vite"];
90
+ VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
91
+ VALID_ISSUE_CATEGORIES = /* @__PURE__ */ new Set(["security", "performance", "reliability"]);
92
+ VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
93
+ TELEMETRY_EVENT_SETUP_COMPLETED = "setup_completed";
94
+ TELEMETRY_EVENT_FIRST_REQUEST = "first_request";
95
+ TELEMETRY_EVENT_DASHBOARD_VIEWED = "dashboard_viewed";
96
+ TELEMETRY_EVENT_SESSION = "session";
97
+ EXIT_REASON_CLEAN = "clean";
98
+ EXIT_REASON_SIGINT = "sigint";
99
+ EXIT_REASON_SIGTERM = "sigterm";
100
+ DETAIL_PREVIEW_LENGTH = 120;
101
+ KNOWN_DEPENDENCY_NAMES = [
102
+ "next",
103
+ "@remix-run/dev",
104
+ "nuxt",
105
+ "vite",
106
+ "astro",
107
+ "express",
108
+ "fastify",
109
+ "hono",
110
+ "koa",
111
+ "nest",
112
+ "prisma",
113
+ "drizzle-orm",
114
+ "typeorm",
115
+ "sequelize"
116
+ ];
149
117
  }
150
118
  });
151
119
 
152
- // src/constants/headers.ts
153
- var BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES;
154
- var init_headers = __esm({
155
- "src/constants/headers.ts"() {
120
+ // src/constants/labels.ts
121
+ var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, VALID_TABS_TUPLE, VALID_TABS, BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES, HTTP_OK, HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, HTTP_INTERNAL_ERROR, SECURITY_HEADERS, CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE, SEVERITY_ICON, SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES, SDK_EVENT_REQUEST, SDK_EVENT_DB_QUERY, SDK_EVENT_FETCH, SDK_EVENT_LOG, SDK_EVENT_ERROR, SDK_EVENT_AUTH_CHECK, POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS, TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY;
122
+ var init_labels = __esm({
123
+ "src/constants/labels.ts"() {
156
124
  "use strict";
125
+ DASHBOARD_PREFIX = "/__brakit";
126
+ DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
127
+ DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
128
+ DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
129
+ DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
130
+ DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
131
+ DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
132
+ DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
133
+ DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
134
+ DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
135
+ DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
136
+ DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
137
+ DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
138
+ DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
139
+ DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
140
+ DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
141
+ DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
142
+ DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
143
+ VALID_TABS_TUPLE = [
144
+ "overview",
145
+ "actions",
146
+ "requests",
147
+ "fetches",
148
+ "queries",
149
+ "errors",
150
+ "logs",
151
+ "performance",
152
+ "security"
153
+ ];
154
+ VALID_TABS = new Set(VALID_TABS_TUPLE);
157
155
  BRAKIT_REQUEST_ID_HEADER = "x-brakit-request-id";
158
156
  BRAKIT_FETCH_ID_HEADER = "x-brakit-fetch-id";
159
157
  SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
@@ -164,13 +162,53 @@ var init_headers = __esm({
164
162
  "x-api-key",
165
163
  "x-auth-token"
166
164
  ]);
165
+ HTTP_OK = 200;
166
+ HTTP_NO_CONTENT = 204;
167
+ HTTP_BAD_REQUEST = 400;
168
+ HTTP_NOT_FOUND = 404;
169
+ HTTP_METHOD_NOT_ALLOWED = 405;
170
+ HTTP_PAYLOAD_TOO_LARGE = 413;
171
+ HTTP_INTERNAL_ERROR = 500;
172
+ SECURITY_HEADERS = {
173
+ "x-content-type-options": "nosniff",
174
+ "x-frame-options": "DENY",
175
+ "referrer-policy": "no-referrer",
176
+ "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
177
+ };
178
+ CONTENT_ENCODING_GZIP = "gzip";
179
+ CONTENT_ENCODING_BR = "br";
180
+ CONTENT_ENCODING_DEFLATE = "deflate";
181
+ SEVERITY_ICON = {
182
+ critical: "\u2717",
183
+ warning: "\u26A0",
184
+ info: "\u2139"
185
+ };
186
+ SSE_EVENT_FETCH = "fetch";
187
+ SSE_EVENT_LOG = "log";
188
+ SSE_EVENT_ERROR = "error_event";
189
+ SSE_EVENT_QUERY = "query";
190
+ SSE_EVENT_ISSUES = "issues";
191
+ SDK_EVENT_REQUEST = "request";
192
+ SDK_EVENT_DB_QUERY = "db.query";
193
+ SDK_EVENT_FETCH = "fetch";
194
+ SDK_EVENT_LOG = "log";
195
+ SDK_EVENT_ERROR = "error";
196
+ SDK_EVENT_AUTH_CHECK = "auth.check";
197
+ POSTHOG_HOST = "https://us.i.posthog.com";
198
+ POSTHOG_CAPTURE_PATH = "/i/v0/e/";
199
+ POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
200
+ SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
201
+ TIMELINE_FETCH = "fetch";
202
+ TIMELINE_LOG = "log";
203
+ TIMELINE_ERROR = "error";
204
+ TIMELINE_QUERY = "query";
167
205
  }
168
206
  });
169
207
 
170
- // src/constants/network.ts
208
+ // src/constants/features.ts
171
209
  var CLOUD_SIGNALS, MAX_HEALTH_ERRORS, RECOVERY_WINDOW_MS, LOCALHOST_IPS, LOCALHOST_HOSTNAMES, URL_PARSE_BASE, DIR_MODE_OWNER_ONLY, FILE_MODE_OWNER_ONLY;
172
- var init_network = __esm({
173
- "src/constants/network.ts"() {
210
+ var init_features = __esm({
211
+ "src/constants/features.ts"() {
174
212
  "use strict";
175
213
  CLOUD_SIGNALS = [
176
214
  "VERCEL",
@@ -203,112 +241,13 @@ var init_network = __esm({
203
241
  }
204
242
  });
205
243
 
206
- // src/constants/mcp.ts
207
- var init_mcp = __esm({
208
- "src/constants/mcp.ts"() {
209
- "use strict";
210
- }
211
- });
212
-
213
- // src/constants/encoding.ts
214
- var CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE;
215
- var init_encoding = __esm({
216
- "src/constants/encoding.ts"() {
217
- "use strict";
218
- CONTENT_ENCODING_GZIP = "gzip";
219
- CONTENT_ENCODING_BR = "br";
220
- CONTENT_ENCODING_DEFLATE = "deflate";
221
- }
222
- });
223
-
224
- // src/constants/severity.ts
225
- var SEVERITY_ICON;
226
- var init_severity = __esm({
227
- "src/constants/severity.ts"() {
228
- "use strict";
229
- SEVERITY_ICON = {
230
- critical: "\u2717",
231
- warning: "\u26A0",
232
- info: "\u2139"
233
- };
234
- }
235
- });
236
-
237
- // src/constants/telemetry.ts
238
- var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS;
239
- var init_telemetry = __esm({
240
- "src/constants/telemetry.ts"() {
241
- "use strict";
242
- POSTHOG_HOST = "https://us.i.posthog.com";
243
- POSTHOG_CAPTURE_PATH = "/i/v0/e/";
244
- POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
245
- SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
246
- }
247
- });
248
-
249
- // src/constants/lifecycle.ts
250
- var VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES;
251
- var init_lifecycle = __esm({
252
- "src/constants/lifecycle.ts"() {
253
- "use strict";
254
- VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
255
- VALID_ISSUE_CATEGORIES = /* @__PURE__ */ new Set(["security", "performance", "reliability"]);
256
- VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
257
- }
258
- });
259
-
260
- // src/constants/cli.ts
261
- var init_cli = __esm({
262
- "src/constants/cli.ts"() {
263
- "use strict";
264
- }
265
- });
266
-
267
- // src/constants/timeline.ts
268
- var TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY;
269
- var init_timeline = __esm({
270
- "src/constants/timeline.ts"() {
271
- "use strict";
272
- TIMELINE_FETCH = "fetch";
273
- TIMELINE_LOG = "log";
274
- TIMELINE_ERROR = "error";
275
- TIMELINE_QUERY = "query";
276
- }
277
- });
278
-
279
- // src/constants/sdk-events.ts
280
- var SDK_EVENT_REQUEST, SDK_EVENT_DB_QUERY, SDK_EVENT_FETCH, SDK_EVENT_LOG, SDK_EVENT_ERROR, SDK_EVENT_AUTH_CHECK;
281
- var init_sdk_events = __esm({
282
- "src/constants/sdk-events.ts"() {
283
- "use strict";
284
- SDK_EVENT_REQUEST = "request";
285
- SDK_EVENT_DB_QUERY = "db.query";
286
- SDK_EVENT_FETCH = "fetch";
287
- SDK_EVENT_LOG = "log";
288
- SDK_EVENT_ERROR = "error";
289
- SDK_EVENT_AUTH_CHECK = "auth.check";
290
- }
291
- });
292
-
293
244
  // src/constants/index.ts
294
245
  var init_constants = __esm({
295
246
  "src/constants/index.ts"() {
296
247
  "use strict";
297
- init_routes();
298
- init_limits();
299
- init_thresholds();
300
- init_transport();
301
- init_metrics();
302
- init_headers();
303
- init_network();
304
- init_mcp();
305
- init_encoding();
306
- init_severity();
307
- init_telemetry();
308
- init_lifecycle();
309
- init_cli();
310
- init_timeline();
311
- init_sdk_events();
248
+ init_config();
249
+ init_labels();
250
+ init_features();
312
251
  }
313
252
  });
314
253
 
@@ -377,7 +316,7 @@ function setupFetchHook(emit) {
377
316
  statusCode: msg.response.statusCode ?? 0,
378
317
  durationMs: Math.round(performance.now() - info.startTime),
379
318
  parentRequestId: info.parentRequestId,
380
- timestamp: Date.now()
319
+ timestamp: performance.now()
381
320
  }
382
321
  });
383
322
  });
@@ -407,7 +346,7 @@ function setupConsoleHook(emit) {
407
346
  const ctx = getRequestContext();
408
347
  if (!ctx) return;
409
348
  const message = format(...args);
410
- const timestamp = Date.now();
349
+ const timestamp = performance.now();
411
350
  const parentRequestId = ctx.requestId;
412
351
  if (level === "error") {
413
352
  const errorArg = args.find((a) => a instanceof Error);
@@ -474,7 +413,7 @@ function createCaptureError(emit) {
474
413
  message: error.message,
475
414
  stack: error.stack ?? "",
476
415
  parentRequestId: ctx?.requestId ?? null,
477
- timestamp: Date.now()
416
+ timestamp: performance.now()
478
417
  }
479
418
  });
480
419
  };
@@ -507,6 +446,7 @@ var init_adapter_registry = __esm({
507
446
  constructor() {
508
447
  this.adapters = [];
509
448
  this.active = [];
449
+ this.failed = [];
510
450
  }
511
451
  register(adapter) {
512
452
  this.adapters.push(adapter);
@@ -519,6 +459,7 @@ var init_adapter_registry = __esm({
519
459
  this.active.push(adapter);
520
460
  }
521
461
  } catch {
462
+ this.failed.push(adapter.name);
522
463
  }
523
464
  }
524
465
  }
@@ -534,69 +475,35 @@ var init_adapter_registry = __esm({
534
475
  getActive() {
535
476
  return this.active;
536
477
  }
478
+ getFailed() {
479
+ return this.failed;
480
+ }
537
481
  };
538
482
  }
539
483
  });
540
484
 
541
- // src/instrument/adapters/shared.ts
542
- import { createRequire } from "module";
543
- function tryRequire(id) {
544
- try {
545
- return appRequire(id);
546
- } catch {
547
- return null;
548
- }
549
- }
550
- function captureRequestId() {
551
- return getRequestContext()?.requestId ?? null;
552
- }
553
- var appRequire;
554
- var init_shared = __esm({
555
- "src/instrument/adapters/shared.ts"() {
556
- "use strict";
557
- init_context();
558
- appRequire = createRequire(process.cwd() + "/index.js");
559
- }
560
- });
561
-
562
485
  // src/instrument/adapters/normalize.ts
563
486
  function normalizeSQL(sql) {
564
487
  if (!sql) return { op: "OTHER", table: "" };
565
488
  const trimmed = sql.trim();
566
- const op = trimmed.split(/\s+/)[0].toUpperCase();
567
- if (/SELECT\s+COUNT/i.test(trimmed)) {
568
- const match = trimmed.match(/FROM\s+"?\w+"?\."?(\w+)"?/i);
569
- return { op: "SELECT", table: match?.[1] ?? "" };
570
- }
571
- const tableMatch = trimmed.match(/(?:FROM|INTO|UPDATE)\s+"?\w+"?\."?(\w+)"?/i);
572
- const table = tableMatch?.[1] ?? "";
573
- switch (op) {
574
- case "SELECT":
575
- return { op: "SELECT", table };
576
- case "INSERT":
577
- return { op: "INSERT", table };
578
- case "UPDATE":
579
- return { op: "UPDATE", table };
580
- case "DELETE":
581
- return { op: "DELETE", table };
582
- default:
583
- return { op: "OTHER", table };
584
- }
489
+ const keyword = trimmed.split(/\s+/, 1)[0].toUpperCase();
490
+ const op = VALID_OPS.has(keyword) ? keyword : "OTHER";
491
+ const table = trimmed.match(TABLE_RE)?.[1] ?? "";
492
+ return { op, table };
585
493
  }
586
494
  function normalizePrismaOp(operation) {
587
495
  return PRISMA_OP_MAP[operation] ?? "OTHER";
588
496
  }
589
497
  function normalizeQueryParams(sql) {
590
498
  if (!sql) return null;
591
- let n = sql.replace(/'[^']*'/g, "?");
592
- n = n.replace(/\b\d+(\.\d+)?\b/g, "?");
593
- n = n.replace(/\$\d+/g, "?");
594
- return n;
499
+ return sql.replace(SQL_PARAM_MARKER, "?").replace(SQL_STRING_LITERAL, "?").replace(SQL_NUMBER_LITERAL, "?");
595
500
  }
596
- var PRISMA_OP_MAP;
501
+ var VALID_OPS, TABLE_RE, PRISMA_OP_MAP, SQL_PARAM_MARKER, SQL_STRING_LITERAL, SQL_NUMBER_LITERAL;
597
502
  var init_normalize = __esm({
598
503
  "src/instrument/adapters/normalize.ts"() {
599
504
  "use strict";
505
+ VALID_OPS = /* @__PURE__ */ new Set(["SELECT", "INSERT", "UPDATE", "DELETE"]);
506
+ TABLE_RE = /(?:FROM|INTO|UPDATE)\s+(?:"?\w+"?\.)?"?(\w+)"?/i;
600
507
  PRISMA_OP_MAP = {
601
508
  findUnique: "SELECT",
602
509
  findUniqueOrThrow: "SELECT",
@@ -615,156 +522,240 @@ var init_normalize = __esm({
615
522
  delete: "DELETE",
616
523
  deleteMany: "DELETE"
617
524
  };
525
+ SQL_PARAM_MARKER = /\$\d+/g;
526
+ SQL_STRING_LITERAL = /'[^']*'/g;
527
+ SQL_NUMBER_LITERAL = /\b\d+(\.\d+)?\b/g;
618
528
  }
619
529
  });
620
530
 
621
- // src/instrument/adapters/pg.ts
622
- var origQuery, proto, pgAdapter;
623
- var init_pg = __esm({
624
- "src/instrument/adapters/pg.ts"() {
625
- "use strict";
626
- init_shared();
627
- init_normalize();
628
- origQuery = null;
629
- proto = null;
630
- pgAdapter = {
631
- name: "pg",
632
- detect() {
633
- return tryRequire("pg") !== null;
634
- },
635
- patch(emit) {
636
- const pg = tryRequire("pg");
637
- if (!pg) return;
638
- const Client = pg.default?.Client ?? pg.Client;
639
- if (!Client || typeof Client !== "function") return;
640
- proto = Client.prototype ?? null;
641
- if (!proto?.query) return;
642
- origQuery = proto.query;
643
- const saved = origQuery;
644
- proto.query = function(...args) {
645
- const first = args[0];
646
- const sql = typeof first === "string" ? first : typeof first === "object" && first !== null && "text" in first ? first.text : void 0;
647
- const start = performance.now();
648
- const requestId = captureRequestId();
649
- const { op, table } = normalizeSQL(sql ?? "");
650
- const emitQuery = (rowCount) => {
651
- emit({
652
- type: "query",
653
- data: {
654
- driver: "pg",
655
- source: "pg",
656
- sql,
657
- normalizedOp: op,
658
- table,
659
- durationMs: Math.round(performance.now() - start),
660
- rowCount: rowCount ?? void 0,
661
- parentRequestId: requestId,
662
- timestamp: Date.now()
663
- }
664
- });
665
- };
666
- const lastIdx = args.length - 1;
667
- if (lastIdx >= 0 && typeof args[lastIdx] === "function") {
668
- const origCb = args[lastIdx];
669
- args[lastIdx] = function(err, res) {
670
- emitQuery(res?.rowCount ?? void 0);
671
- return origCb.call(this, err, res);
672
- };
673
- return saved.apply(this, args);
674
- }
675
- const result = saved.apply(this, args);
676
- if (result && typeof result.then === "function") {
677
- return result.then((res) => {
678
- try {
679
- emitQuery(res?.rowCount ?? void 0);
680
- } catch {
681
- }
682
- return res;
683
- });
684
- }
685
- if (result && typeof result.on === "function") {
686
- result.on("end", (res) => emitQuery(res?.rowCount ?? void 0));
687
- return result;
688
- }
689
- return result;
690
- };
691
- },
692
- unpatch() {
693
- if (proto && origQuery) {
694
- proto.query = origQuery;
695
- origQuery = null;
696
- proto = null;
697
- }
698
- }
699
- };
700
- }
701
- });
702
-
703
- // src/instrument/adapters/mysql2.ts
704
- var originals2, proto2, mysql2Adapter;
705
- var init_mysql2 = __esm({
531
+ // src/utils/type-guards.ts
532
+ function isString(val) {
533
+ return typeof val === "string";
534
+ }
535
+ function isNumber(val) {
536
+ return typeof val === "number" && !isNaN(val);
537
+ }
538
+ function isBoolean(val) {
539
+ return typeof val === "boolean";
540
+ }
541
+ function isThenable(value) {
542
+ return value != null && typeof value.then === "function";
543
+ }
544
+ function getErrorMessage(err) {
545
+ if (err instanceof Error) return err.message;
546
+ if (typeof err === "string") return err;
547
+ return String(err);
548
+ }
549
+ function isValidIssueState(val) {
550
+ return typeof val === "string" && VALID_ISSUE_STATES.has(val);
551
+ }
552
+ function isValidIssueCategory(val) {
553
+ return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
554
+ }
555
+ function isValidAiFixStatus(val) {
556
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
557
+ }
558
+ function validateIssuesData(parsed) {
559
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
560
+ const obj = parsed;
561
+ if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
562
+ return parsed;
563
+ }
564
+ return null;
565
+ }
566
+ function validateMetricsData(parsed) {
567
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
568
+ const obj = parsed;
569
+ if (obj.version === 1 && Array.isArray(obj.endpoints)) {
570
+ return parsed;
571
+ }
572
+ return null;
573
+ }
574
+ var init_type_guards = __esm({
575
+ "src/utils/type-guards.ts"() {
576
+ "use strict";
577
+ init_config();
578
+ }
579
+ });
580
+
581
+ // src/utils/log.ts
582
+ function brakitWarn(message) {
583
+ process.stderr.write(`${PREFIX} ${message}
584
+ `);
585
+ }
586
+ function brakitDebug(message) {
587
+ if (process.env.DEBUG_BRAKIT) {
588
+ process.stderr.write(`${PREFIX}:debug ${message}
589
+ `);
590
+ }
591
+ }
592
+ var PREFIX;
593
+ var init_log = __esm({
594
+ "src/utils/log.ts"() {
595
+ "use strict";
596
+ PREFIX = "[brakit]";
597
+ }
598
+ });
599
+
600
+ // src/instrument/adapters/shared.ts
601
+ import { createRequire } from "module";
602
+ function tryRequire(id) {
603
+ try {
604
+ return appRequire(id);
605
+ } catch {
606
+ return null;
607
+ }
608
+ }
609
+ function getActiveRequestId() {
610
+ return getRequestContext()?.requestId ?? null;
611
+ }
612
+ function getPrototype(lib, className) {
613
+ const defaultExport = lib.default;
614
+ const cls = defaultExport?.[className] ?? lib[className];
615
+ if (!cls || typeof cls !== "function") return null;
616
+ return cls.prototype ?? null;
617
+ }
618
+ function buildQueryEvent(config, sql, op, table, start, requestId, rowCount) {
619
+ return {
620
+ type: "query",
621
+ data: {
622
+ driver: config.driver,
623
+ source: config.driver,
624
+ sql,
625
+ normalizedOp: op,
626
+ table,
627
+ durationMs: Math.round(performance.now() - start),
628
+ rowCount: rowCount ?? void 0,
629
+ parentRequestId: requestId,
630
+ timestamp: performance.now()
631
+ }
632
+ };
633
+ }
634
+ function wrapQueryMethod(original, emit, config) {
635
+ return function(...args) {
636
+ const sql = config.extractSql(args);
637
+ const start = performance.now();
638
+ const requestId = getActiveRequestId();
639
+ const { op, table } = normalizeSQL(sql ?? "");
640
+ const emitQuery = (result2) => {
641
+ const rowCount = config.extractRowCount?.(result2);
642
+ emit(buildQueryEvent(config, sql, op, table, start, requestId, rowCount));
643
+ };
644
+ const lastIdx = args.length - 1;
645
+ if (lastIdx >= 0 && typeof args[lastIdx] === "function") {
646
+ const originalCallback = args[lastIdx];
647
+ args[lastIdx] = function(...callbackArgs) {
648
+ emitQuery(callbackArgs[1]);
649
+ return originalCallback.apply(this, callbackArgs);
650
+ };
651
+ return original.apply(this, args);
652
+ }
653
+ const result = original.apply(this, args);
654
+ if (isThenable(result)) {
655
+ return result.then((res) => {
656
+ try {
657
+ emitQuery(res);
658
+ } catch (e) {
659
+ brakitDebug(`query telemetry: ${getErrorMessage(e)}`);
660
+ }
661
+ return res;
662
+ });
663
+ }
664
+ if (config.supportsEventEmitter && result && typeof result.on === "function") {
665
+ result.on(
666
+ "end",
667
+ (res) => emitQuery(res)
668
+ );
669
+ return result;
670
+ }
671
+ return result;
672
+ };
673
+ }
674
+ var appRequire;
675
+ var init_shared = __esm({
676
+ "src/instrument/adapters/shared.ts"() {
677
+ "use strict";
678
+ init_context();
679
+ init_normalize();
680
+ init_type_guards();
681
+ init_log();
682
+ appRequire = createRequire(process.cwd() + "/index.js");
683
+ }
684
+ });
685
+
686
+ // src/instrument/adapters/pg.ts
687
+ var origQuery, proto, pgConfig, pgAdapter;
688
+ var init_pg = __esm({
689
+ "src/instrument/adapters/pg.ts"() {
690
+ "use strict";
691
+ init_shared();
692
+ origQuery = null;
693
+ proto = null;
694
+ pgConfig = {
695
+ driver: "pg",
696
+ extractSql: (args) => {
697
+ const q = args[0];
698
+ if (typeof q === "string") return q;
699
+ if (typeof q === "object" && q !== null && "text" in q) return q.text;
700
+ return void 0;
701
+ },
702
+ extractRowCount: (result) => result?.rowCount,
703
+ supportsEventEmitter: true
704
+ };
705
+ pgAdapter = {
706
+ name: "pg",
707
+ detect() {
708
+ return tryRequire("pg") !== null;
709
+ },
710
+ /** Monkeypatches pg's Client prototype to intercept database queries and emit telemetry events. */
711
+ patch(emit) {
712
+ const pg = tryRequire("pg");
713
+ if (!pg) return;
714
+ proto = getPrototype(pg, "Client");
715
+ if (!proto?.query) return;
716
+ origQuery = proto.query;
717
+ proto.query = wrapQueryMethod(origQuery, emit, pgConfig);
718
+ },
719
+ unpatch() {
720
+ if (proto && origQuery) {
721
+ proto.query = origQuery;
722
+ origQuery = null;
723
+ proto = null;
724
+ }
725
+ }
726
+ };
727
+ }
728
+ });
729
+
730
+ // src/instrument/adapters/mysql2.ts
731
+ var PATCHED_METHODS, originals2, proto2, mysql2Config, mysql2Adapter;
732
+ var init_mysql2 = __esm({
706
733
  "src/instrument/adapters/mysql2.ts"() {
707
734
  "use strict";
708
735
  init_shared();
709
- init_normalize();
736
+ PATCHED_METHODS = ["query", "execute"];
710
737
  originals2 = /* @__PURE__ */ new Map();
711
738
  proto2 = null;
739
+ mysql2Config = {
740
+ driver: "mysql2",
741
+ extractSql: (args) => typeof args[0] === "string" ? args[0] : void 0
742
+ };
712
743
  mysql2Adapter = {
713
744
  name: "mysql2",
714
745
  detect() {
715
746
  return tryRequire("mysql2") !== null;
716
747
  },
748
+ /** Monkeypatches mysql2's Connection prototype to intercept database queries and emit telemetry events. */
717
749
  patch(emit) {
718
750
  const mysql2 = tryRequire("mysql2");
719
751
  if (!mysql2) return;
720
- proto2 = mysql2.Connection?.prototype ?? null;
752
+ proto2 = getPrototype(mysql2, "Connection");
721
753
  if (!proto2) return;
722
- for (const method of ["query", "execute"]) {
754
+ for (const method of PATCHED_METHODS) {
723
755
  const orig = proto2[method];
724
756
  if (typeof orig !== "function") continue;
725
757
  originals2.set(method, orig);
726
- proto2[method] = function(...args) {
727
- const first = args[0];
728
- const sql = typeof first === "string" ? first : void 0;
729
- const start = performance.now();
730
- const requestId = captureRequestId();
731
- const { op, table } = normalizeSQL(sql ?? "");
732
- const emitQuery = () => {
733
- emit({
734
- type: "query",
735
- data: {
736
- driver: "mysql2",
737
- source: "mysql2",
738
- sql,
739
- normalizedOp: op,
740
- table,
741
- durationMs: Math.round(performance.now() - start),
742
- parentRequestId: requestId,
743
- timestamp: Date.now()
744
- }
745
- });
746
- };
747
- const lastIdx = args.length - 1;
748
- if (lastIdx >= 0 && typeof args[lastIdx] === "function") {
749
- const origCb = args[lastIdx];
750
- args[lastIdx] = function() {
751
- emitQuery();
752
- return origCb.apply(this, arguments);
753
- };
754
- return orig.apply(this, args);
755
- }
756
- const result = orig.apply(this, args);
757
- if (result && typeof result.then === "function") {
758
- return result.then((res) => {
759
- try {
760
- emitQuery();
761
- } catch {
762
- }
763
- return res;
764
- });
765
- }
766
- return result;
767
- };
758
+ proto2[method] = wrapQueryMethod(orig, emit, mysql2Config);
768
759
  }
769
760
  },
770
761
  unpatch() {
@@ -797,9 +788,8 @@ var init_prisma = __esm({
797
788
  patch(emit) {
798
789
  const prismaModule = tryRequire("@prisma/client");
799
790
  if (!prismaModule) return;
800
- const PrismaClient = prismaModule.default?.PrismaClient ?? prismaModule.PrismaClient;
801
- if (!PrismaClient || typeof PrismaClient !== "function") return;
802
- prismaProto = PrismaClient.prototype;
791
+ prismaProto = getPrototype(prismaModule, "PrismaClient");
792
+ if (!prismaProto) return;
803
793
  origConnect = prismaProto.$connect;
804
794
  if (typeof origConnect !== "function") return;
805
795
  const saved = origConnect;
@@ -815,7 +805,7 @@ var init_prisma = __esm({
815
805
  args: opArgs,
816
806
  query
817
807
  }) {
818
- const requestId = captureRequestId();
808
+ const requestId = getActiveRequestId();
819
809
  const start = performance.now();
820
810
  const result = await query(opArgs);
821
811
  emit({
@@ -829,7 +819,7 @@ var init_prisma = __esm({
829
819
  table: model,
830
820
  durationMs: Math.round(performance.now() - start),
831
821
  parentRequestId: requestId,
832
- timestamp: Date.now()
822
+ timestamp: performance.now()
833
823
  }
834
824
  });
835
825
  return result;
@@ -874,24 +864,35 @@ var init_adapters = __esm({
874
864
  }
875
865
  });
876
866
 
877
- // src/constants/http.ts
878
- var HTTP_OK, HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, HTTP_INTERNAL_ERROR, SECURITY_HEADERS;
879
- var init_http = __esm({
880
- "src/constants/http.ts"() {
867
+ // src/utils/endpoint.ts
868
+ function isDynamicSegment(segment) {
869
+ return UUID_RE.test(segment) || NUMERIC_ID_RE.test(segment) || HEX_HASH_RE.test(segment) || ALPHA_TOKEN_RE.test(segment);
870
+ }
871
+ function normalizePath(path) {
872
+ const qIdx = path.indexOf("?");
873
+ const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
874
+ return pathname.split("/").map((seg) => seg && isDynamicSegment(seg) ? DYNAMIC_SEGMENT_PLACEHOLDER : seg).join("/");
875
+ }
876
+ function getEndpointKey(method, path) {
877
+ return `${method} ${normalizePath(path)}`;
878
+ }
879
+ function extractEndpointFromDesc(desc) {
880
+ return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
881
+ }
882
+ function stripQueryString(path) {
883
+ const i = path.indexOf("?");
884
+ return i === -1 ? path : path.slice(0, i);
885
+ }
886
+ var UUID_RE, NUMERIC_ID_RE, HEX_HASH_RE, ALPHA_TOKEN_RE, DYNAMIC_SEGMENT_PLACEHOLDER, ENDPOINT_PREFIX_RE;
887
+ var init_endpoint = __esm({
888
+ "src/utils/endpoint.ts"() {
881
889
  "use strict";
882
- HTTP_OK = 200;
883
- HTTP_NO_CONTENT = 204;
884
- HTTP_BAD_REQUEST = 400;
885
- HTTP_NOT_FOUND = 404;
886
- HTTP_METHOD_NOT_ALLOWED = 405;
887
- HTTP_PAYLOAD_TOO_LARGE = 413;
888
- HTTP_INTERNAL_ERROR = 500;
889
- SECURITY_HEADERS = {
890
- "x-content-type-options": "nosniff",
891
- "x-frame-options": "DENY",
892
- "referrer-policy": "no-referrer",
893
- "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
894
- };
890
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
891
+ NUMERIC_ID_RE = /^\d+$/;
892
+ HEX_HASH_RE = /^[0-9a-f]{12,}$/i;
893
+ ALPHA_TOKEN_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/;
894
+ DYNAMIC_SEGMENT_PLACEHOLDER = ":id";
895
+ ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
895
896
  }
896
897
  });
897
898
 
@@ -915,6 +916,7 @@ var init_http_status = __esm({
915
916
  function detectCategory(req) {
916
917
  const { method, url, statusCode, responseHeaders } = req;
917
918
  if (req.isStatic) return "static";
919
+ if (req.isHealthCheck) return "health-check";
918
920
  if (statusCode === 307 && (url.includes("__clerk_handshake") || url.includes("__clerk_db_jwt"))) {
919
921
  return "auth-handshake";
920
922
  }
@@ -1079,20 +1081,41 @@ var init_label = __esm({
1079
1081
  });
1080
1082
 
1081
1083
  // src/analysis/transforms.ts
1082
- function markDuplicates(requests) {
1084
+ function isDuplicateCandidate(req) {
1085
+ return DUPLICATE_CATEGORIES.has(req.category);
1086
+ }
1087
+ function buildRequestKey(req) {
1088
+ return `${req.method} ${stripQueryString(getEffectivePath(req))}`;
1089
+ }
1090
+ function isStrictModePattern(requests, counts) {
1091
+ if (counts.size === 0 || ![...counts.values()].every((c) => c === 2)) {
1092
+ return false;
1093
+ }
1094
+ const firstByKey = /* @__PURE__ */ new Map();
1095
+ for (const req of requests) {
1096
+ if (!isDuplicateCandidate(req)) continue;
1097
+ const key = buildRequestKey(req);
1098
+ const first = firstByKey.get(key);
1099
+ if (!first) {
1100
+ firstByKey.set(key, req);
1101
+ } else if (Math.abs(req.startedAt - first.startedAt) > STRICT_MODE_MAX_GAP_MS) {
1102
+ return false;
1103
+ }
1104
+ }
1105
+ return true;
1106
+ }
1107
+ function flagDuplicateRequests(requests) {
1083
1108
  const counts = /* @__PURE__ */ new Map();
1084
1109
  for (const req of requests) {
1085
- if (req.category !== "data-fetch" && req.category !== "auth-check")
1086
- continue;
1087
- const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
1110
+ if (!isDuplicateCandidate(req)) continue;
1111
+ const key = buildRequestKey(req);
1088
1112
  counts.set(key, (counts.get(key) ?? 0) + 1);
1089
1113
  }
1090
- const isStrictMode = counts.size > 0 && [...counts.values()].every((c) => c === 2);
1114
+ const isStrictMode = isStrictModePattern(requests, counts);
1091
1115
  const seen = /* @__PURE__ */ new Set();
1092
1116
  for (const req of requests) {
1093
- if (req.category !== "data-fetch" && req.category !== "auth-check")
1094
- continue;
1095
- const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
1117
+ if (!isDuplicateCandidate(req)) continue;
1118
+ const key = buildRequestKey(req);
1096
1119
  if (seen.has(key)) {
1097
1120
  if (isStrictMode) {
1098
1121
  req.isStrictModeDupe = true;
@@ -1104,20 +1127,20 @@ function markDuplicates(requests) {
1104
1127
  }
1105
1128
  }
1106
1129
  }
1107
- function collapsePolling(requests) {
1130
+ function mergePollingSequences(requests) {
1108
1131
  const result = [];
1109
1132
  let i = 0;
1110
1133
  while (i < requests.length) {
1111
1134
  const current = requests[i];
1112
- const currentEffective = getEffectivePath(current).split("?")[0];
1135
+ const currentEffective = stripQueryString(getEffectivePath(current));
1113
1136
  if (current.method === "GET" && current.category === "data-fetch") {
1114
- let j = i + 1;
1115
- while (j < requests.length && requests[j].method === "GET" && getEffectivePath(requests[j]).split("?")[0] === currentEffective) {
1116
- j++;
1137
+ let nextIndex = i + 1;
1138
+ while (nextIndex < requests.length && requests[nextIndex].method === "GET" && stripQueryString(getEffectivePath(requests[nextIndex])) === currentEffective) {
1139
+ nextIndex++;
1117
1140
  }
1118
- const count = j - i;
1141
+ const count = nextIndex - i;
1119
1142
  if (count >= MIN_POLLING_SEQUENCE) {
1120
- const last = requests[j - 1];
1143
+ const last = requests[nextIndex - 1];
1121
1144
  const pollingDuration = last.startedAt + last.durationMs - current.startedAt;
1122
1145
  const endpointName = prettifyEndpoint(currentEffective);
1123
1146
  result.push({
@@ -1128,7 +1151,7 @@ function collapsePolling(requests) {
1128
1151
  pollingDurationMs: pollingDuration,
1129
1152
  isDuplicate: false
1130
1153
  });
1131
- i = j;
1154
+ i = nextIndex;
1132
1155
  continue;
1133
1156
  }
1134
1157
  }
@@ -1141,18 +1164,18 @@ function formatDurationLabel(ms) {
1141
1164
  if (ms < 1e3) return `${ms}ms`;
1142
1165
  return `${(ms / 1e3).toFixed(1)}s`;
1143
1166
  }
1144
- function detectWarnings(requests) {
1167
+ function collectRequestWarnings(requests) {
1145
1168
  const warnings = [];
1146
1169
  const duplicateCount = requests.filter((r) => r.isDuplicate).length;
1147
1170
  if (duplicateCount > 0) {
1148
1171
  const unique = new Set(
1149
- requests.filter((r) => r.isDuplicate).map((r) => `${r.method} ${getEffectivePath(r).split("?")[0]}`)
1172
+ requests.filter((r) => r.isDuplicate).map((r) => buildRequestKey(r))
1150
1173
  );
1151
1174
  const endpoints = unique.size;
1152
1175
  const sameData = requests.filter((r) => r.isDuplicate).every((r) => {
1153
- const key = `${r.method} ${getEffectivePath(r).split("?")[0]}`;
1176
+ const key = buildRequestKey(r);
1154
1177
  const first = requests.find(
1155
- (o) => !o.isDuplicate && `${o.method} ${getEffectivePath(o).split("?")[0]}` === key
1178
+ (o) => !o.isDuplicate && buildRequestKey(o) === key
1156
1179
  );
1157
1180
  return first && first.responseBody === r.responseBody;
1158
1181
  });
@@ -1173,18 +1196,30 @@ function detectWarnings(requests) {
1173
1196
  }
1174
1197
  return warnings;
1175
1198
  }
1199
+ var DUPLICATE_CATEGORIES;
1176
1200
  var init_transforms = __esm({
1177
1201
  "src/analysis/transforms.ts"() {
1178
1202
  "use strict";
1179
1203
  init_constants();
1204
+ init_config();
1180
1205
  init_categorize();
1181
1206
  init_label();
1182
1207
  init_http_status();
1208
+ init_endpoint();
1209
+ DUPLICATE_CATEGORIES = /* @__PURE__ */ new Set(["data-fetch", "auth-check"]);
1183
1210
  }
1184
1211
  });
1185
1212
 
1186
1213
  // src/analysis/group.ts
1187
1214
  import { randomUUID as randomUUID3 } from "crypto";
1215
+ function shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, startedAt) {
1216
+ if (currentRequests.length === 0) return false;
1217
+ const sourcePage = labeled.sourcePage;
1218
+ const isNewPage = sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
1219
+ const isTimeGap = startedAt - lastEndTime > FLOW_GAP_MS;
1220
+ const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
1221
+ return isNewPage || isTimeGap || isPageLoad;
1222
+ }
1188
1223
  function groupRequestsIntoFlows(requests) {
1189
1224
  if (requests.length === 0) return [];
1190
1225
  const flows = [];
@@ -1195,17 +1230,12 @@ function groupRequestsIntoFlows(requests) {
1195
1230
  if (req.path.startsWith(DASHBOARD_PREFIX)) continue;
1196
1231
  const labeled = labelRequest(req);
1197
1232
  if (labeled.category === "static") continue;
1198
- const sourcePage = labeled.sourcePage;
1199
- const gap = currentRequests.length > 0 ? req.startedAt - lastEndTime : 0;
1200
- const isNewPage = currentRequests.length > 0 && sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
1201
- const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
1202
- const isTimeGap = currentRequests.length > 0 && gap > FLOW_GAP_MS;
1203
- if (currentRequests.length > 0 && (isNewPage || isTimeGap || isPageLoad)) {
1233
+ if (shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, req.startedAt)) {
1204
1234
  flows.push(buildFlow(currentRequests));
1205
1235
  currentRequests = [];
1206
1236
  }
1207
1237
  currentRequests.push(labeled);
1208
- currentSourcePage = sourcePage ?? currentSourcePage;
1238
+ currentSourcePage = labeled.sourcePage ?? currentSourcePage;
1209
1239
  lastEndTime = Math.max(lastEndTime, req.startedAt + req.durationMs);
1210
1240
  }
1211
1241
  if (currentRequests.length > 0) {
@@ -1214,8 +1244,8 @@ function groupRequestsIntoFlows(requests) {
1214
1244
  return flows;
1215
1245
  }
1216
1246
  function buildFlow(rawRequests) {
1217
- markDuplicates(rawRequests);
1218
- const requests = collapsePolling(rawRequests);
1247
+ flagDuplicateRequests(rawRequests);
1248
+ const requests = mergePollingSequences(rawRequests);
1219
1249
  const first = requests[0];
1220
1250
  const startTime = first.startedAt;
1221
1251
  const endTime = Math.max(
@@ -1234,7 +1264,7 @@ function buildFlow(rawRequests) {
1234
1264
  startTime,
1235
1265
  totalDurationMs: Math.round(endTime - startTime),
1236
1266
  hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
1237
- warnings: detectWarnings(rawRequests),
1267
+ warnings: collectRequestWarnings(rawRequests),
1238
1268
  sourcePage,
1239
1269
  redundancyPct
1240
1270
  };
@@ -1246,20 +1276,20 @@ function getDominantSourcePage(requests) {
1246
1276
  counts.set(req.sourcePage, (counts.get(req.sourcePage) ?? 0) + 1);
1247
1277
  }
1248
1278
  }
1249
- let best = "";
1250
- let bestCount = 0;
1279
+ let mostCommonPage = "";
1280
+ let highestCount = 0;
1251
1281
  for (const [page, count] of counts) {
1252
- if (count > bestCount) {
1253
- best = page;
1254
- bestCount = count;
1282
+ if (count > highestCount) {
1283
+ mostCommonPage = page;
1284
+ highestCount = count;
1255
1285
  }
1256
1286
  }
1257
- return best || requests[0]?.path?.split("?")[0] || "/";
1287
+ return mostCommonPage || (requests[0]?.path ? stripQueryString(requests[0].path) : "") || "/";
1258
1288
  }
1259
1289
  function deriveFlowLabel(requests, sourcePage) {
1260
1290
  const trigger = requests.find((r) => r.category === "api-call") ?? requests.find((r) => r.category === "server-action") ?? requests.find((r) => r.category === "page-load") ?? requests.find((r) => r.category === "navigation") ?? requests.find((r) => r.category === "data-fetch") ?? requests[0];
1261
1291
  if (trigger.category === "page-load" || trigger.category === "navigation") {
1262
- const pageName = prettifyPageName(trigger.path.split("?")[0]);
1292
+ const pageName = prettifyPageName(stripQueryString(trigger.path));
1263
1293
  return `${pageName} Page`;
1264
1294
  }
1265
1295
  if (trigger.category === "api-call") {
@@ -1288,6 +1318,7 @@ var init_group = __esm({
1288
1318
  "use strict";
1289
1319
  init_constants();
1290
1320
  init_http_status();
1321
+ init_endpoint();
1291
1322
  init_label();
1292
1323
  init_categorize();
1293
1324
  init_transforms();
@@ -1385,12 +1416,28 @@ var init_shared2 = __esm({
1385
1416
  "src/dashboard/api/shared.ts"() {
1386
1417
  "use strict";
1387
1418
  init_constants();
1388
- init_limits();
1389
- init_http();
1419
+ init_config();
1420
+ init_labels();
1390
1421
  }
1391
1422
  });
1392
1423
 
1393
1424
  // src/dashboard/api/handlers.ts
1425
+ function filterByStatusRange(requests, statusStr) {
1426
+ if (statusStr.endsWith("xx")) {
1427
+ const prefix = parseInt(statusStr[0], 10);
1428
+ return requests.filter(
1429
+ (r) => Math.floor(r.statusCode / 100) === prefix
1430
+ );
1431
+ }
1432
+ const code = parseInt(statusStr, 10);
1433
+ return requests.filter((r) => r.statusCode === code);
1434
+ }
1435
+ function filterBySearch(requests, searchQuery) {
1436
+ const lower = searchQuery.toLowerCase();
1437
+ return requests.filter(
1438
+ (r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
1439
+ );
1440
+ }
1394
1441
  function sanitizeRequest(r) {
1395
1442
  return {
1396
1443
  ...r,
@@ -1398,7 +1445,7 @@ function sanitizeRequest(r) {
1398
1445
  responseHeaders: maskSensitiveHeaders(r.responseHeaders)
1399
1446
  };
1400
1447
  }
1401
- function createRequestsHandler(registry) {
1448
+ function createRequestsHandler(services) {
1402
1449
  return (req, res) => {
1403
1450
  if (!requireGet(req, res)) return;
1404
1451
  const url = parseRequestUrl(req);
@@ -1408,26 +1455,15 @@ function createRequestsHandler(registry) {
1408
1455
  const rawLimit = parseInt(url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT), 10);
1409
1456
  const limit = Math.min(Math.max(rawLimit || DEFAULT_API_LIMIT, 1), MAX_API_LIMIT);
1410
1457
  const offset = Math.max(parseInt(url.searchParams.get("offset") ?? "0", 10) || 0, 0);
1411
- let results = [...registry.get("request-store").getAll()].reverse();
1458
+ let results = [...services.requestStore.getAll()].reverse();
1412
1459
  if (method) {
1413
1460
  results = results.filter((r) => r.method === method.toUpperCase());
1414
1461
  }
1415
1462
  if (status) {
1416
- if (status.endsWith("xx")) {
1417
- const prefix = parseInt(status[0], 10);
1418
- results = results.filter(
1419
- (r) => Math.floor(r.statusCode / 100) === prefix
1420
- );
1421
- } else {
1422
- const code = parseInt(status, 10);
1423
- results = results.filter((r) => r.statusCode === code);
1424
- }
1463
+ results = filterByStatusRange(results, status);
1425
1464
  }
1426
1465
  if (search) {
1427
- const lower = search.toLowerCase();
1428
- results = results.filter(
1429
- (r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
1430
- );
1466
+ results = filterBySearch(results, search);
1431
1467
  }
1432
1468
  const total = results.length;
1433
1469
  results = results.slice(offset, offset + limit);
@@ -1435,96 +1471,84 @@ function createRequestsHandler(registry) {
1435
1471
  sendJson(req, res, HTTP_OK, { total, requests: sanitized });
1436
1472
  };
1437
1473
  }
1438
- function createFlowsHandler(registry) {
1474
+ function createFlowsHandler(services) {
1439
1475
  return (req, res) => {
1440
1476
  if (!requireGet(req, res)) return;
1441
- const flows = groupRequestsIntoFlows(registry.get("request-store").getAll()).reverse().map((flow) => ({
1477
+ const flows = groupRequestsIntoFlows(services.requestStore.getAll()).reverse().map((flow) => ({
1442
1478
  ...flow,
1443
1479
  requests: flow.requests.map(sanitizeRequest)
1444
1480
  }));
1445
1481
  sendJson(req, res, HTTP_OK, { total: flows.length, flows });
1446
1482
  };
1447
1483
  }
1448
- function createClearHandler(registry) {
1484
+ function createClearHandler(services) {
1449
1485
  return (req, res) => {
1450
1486
  if (req.method !== "POST") {
1451
1487
  sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1452
1488
  return;
1453
1489
  }
1454
- registry.get("request-store").clear();
1455
- registry.get("fetch-store").clear();
1456
- registry.get("log-store").clear();
1457
- registry.get("error-store").clear();
1458
- registry.get("query-store").clear();
1459
- registry.get("metrics-store").reset();
1460
- if (registry.has("issue-store")) registry.get("issue-store").clear();
1461
- registry.get("event-bus").emit("store:cleared", void 0);
1490
+ services.requestStore.clear();
1491
+ services.fetchStore.clear();
1492
+ services.logStore.clear();
1493
+ services.errorStore.clear();
1494
+ services.queryStore.clear();
1495
+ services.metricsStore.reset();
1496
+ services.issueStore.clear();
1497
+ services.bus.emit("store:cleared", void 0);
1462
1498
  sendJson(req, res, HTTP_OK, { cleared: true });
1463
1499
  };
1464
1500
  }
1465
- function createFetchesHandler(registry) {
1466
- return (req, res) => handleTelemetryGet(req, res, registry.get("fetch-store"));
1501
+ function createFetchesHandler(services) {
1502
+ return (req, res) => handleTelemetryGet(req, res, services.fetchStore);
1467
1503
  }
1468
- function createLogsHandler(registry) {
1469
- return (req, res) => handleTelemetryGet(req, res, registry.get("log-store"));
1504
+ function createLogsHandler(services) {
1505
+ return (req, res) => handleTelemetryGet(req, res, services.logStore);
1470
1506
  }
1471
- function createErrorsHandler(registry) {
1472
- return (req, res) => handleTelemetryGet(req, res, registry.get("error-store"));
1507
+ function createErrorsHandler(services) {
1508
+ return (req, res) => handleTelemetryGet(req, res, services.errorStore);
1473
1509
  }
1474
- function createQueriesHandler(registry) {
1475
- return (req, res) => handleTelemetryGet(req, res, registry.get("query-store"));
1510
+ function createQueriesHandler(services) {
1511
+ return (req, res) => handleTelemetryGet(req, res, services.queryStore);
1476
1512
  }
1477
1513
  var init_handlers = __esm({
1478
1514
  "src/dashboard/api/handlers.ts"() {
1479
1515
  "use strict";
1480
1516
  init_group();
1481
1517
  init_constants();
1482
- init_http();
1518
+ init_labels();
1483
1519
  init_shared2();
1484
1520
  }
1485
1521
  });
1486
1522
 
1487
- // src/utils/type-guards.ts
1488
- function isString(val) {
1489
- return typeof val === "string";
1490
- }
1491
- function isNumber(val) {
1492
- return typeof val === "number" && !isNaN(val);
1493
- }
1494
- function isBoolean(val) {
1495
- return typeof val === "boolean";
1496
- }
1497
- function getErrorMessage(err) {
1498
- if (err instanceof Error) return err.message;
1499
- if (typeof err === "string") return err;
1500
- return String(err);
1501
- }
1502
- function isValidIssueState(val) {
1503
- return typeof val === "string" && VALID_ISSUE_STATES.has(val);
1504
- }
1505
- function isValidIssueCategory(val) {
1506
- return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
1507
- }
1508
- function isValidAiFixStatus(val) {
1509
- return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
1510
- }
1511
- function validateIssuesData(parsed) {
1512
- if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === ISSUES_DATA_VERSION && Array.isArray(parsed.issues)) {
1513
- return parsed;
1514
- }
1515
- return null;
1523
+ // src/utils/static-patterns.ts
1524
+ function isStaticPath(urlPath) {
1525
+ return STATIC_PATTERNS.some((p) => p.test(urlPath));
1516
1526
  }
1517
- function validateMetricsData(parsed) {
1518
- if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === 1 && Array.isArray(parsed.endpoints)) {
1519
- return parsed;
1520
- }
1521
- return null;
1527
+ function isHealthCheckPath(urlPath) {
1528
+ return HEALTH_CHECK_PATTERNS.some((p) => p.test(urlPath));
1522
1529
  }
1523
- var init_type_guards = __esm({
1524
- "src/utils/type-guards.ts"() {
1530
+ var STATIC_PATTERNS, HEALTH_CHECK_PATTERNS;
1531
+ var init_static_patterns = __esm({
1532
+ "src/utils/static-patterns.ts"() {
1525
1533
  "use strict";
1526
- init_lifecycle();
1527
- init_limits();
1534
+ STATIC_PATTERNS = [
1535
+ /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
1536
+ /^\/favicon/,
1537
+ /^\/node_modules\//,
1538
+ // Framework-specific static/internal paths
1539
+ /^\/_next\//,
1540
+ /^\/__nextjs/,
1541
+ /^\/@vite\//,
1542
+ /^\/__vite/
1543
+ ];
1544
+ HEALTH_CHECK_PATTERNS = [
1545
+ /^\/health(z|check)?$/i,
1546
+ /^\/ping$/i,
1547
+ /^\/(ready|readiness|liveness)$/i,
1548
+ /^\/status$/i,
1549
+ /^\/__health$/i,
1550
+ /^\/api\/health(z|check)?$/i
1551
+ ];
1528
1552
  }
1529
1553
  });
1530
1554
 
@@ -1545,8 +1569,8 @@ function numOrUndef(val) {
1545
1569
  function headers(val) {
1546
1570
  if (val && typeof val === "object" && !Array.isArray(val)) {
1547
1571
  const result = {};
1548
- for (const [k, v] of Object.entries(val)) {
1549
- if (typeof v === "string") result[k] = v;
1572
+ for (const [key, value] of Object.entries(val)) {
1573
+ if (typeof value === "string") result[key] = value;
1550
1574
  }
1551
1575
  return result;
1552
1576
  }
@@ -1610,7 +1634,7 @@ function parseRequestEvent(data, ts) {
1610
1634
  id: str(data.id, randomUUID4()),
1611
1635
  method: str(data.method, "GET"),
1612
1636
  url,
1613
- path: url.split("?")[0],
1637
+ path: stripQueryString(url),
1614
1638
  headers: headers(data.headers),
1615
1639
  requestBody: isString(data.requestBody) ? data.requestBody : null,
1616
1640
  statusCode: num(data.statusCode, 200),
@@ -1619,7 +1643,8 @@ function parseRequestEvent(data, ts) {
1619
1643
  startedAt: ts,
1620
1644
  durationMs: num(data.durationMs, 0),
1621
1645
  responseSize: num(data.responseSize, 0),
1622
- isStatic: isBoolean(data.isStatic) ? data.isStatic : false
1646
+ isStatic: isBoolean(data.isStatic) ? data.isStatic : false,
1647
+ isHealthCheck: isBoolean(data.isHealthCheck) ? data.isHealthCheck : isHealthCheckPath(stripQueryString(url))
1623
1648
  };
1624
1649
  }
1625
1650
  function routeSDKEvent(event, stores) {
@@ -1653,7 +1678,9 @@ var init_sdk_event_parser = __esm({
1653
1678
  "src/dashboard/api/sdk-event-parser.ts"() {
1654
1679
  "use strict";
1655
1680
  init_type_guards();
1656
- init_sdk_events();
1681
+ init_labels();
1682
+ init_static_patterns();
1683
+ init_endpoint();
1657
1684
  LOG_LEVEL_MAP = {
1658
1685
  debug: "debug",
1659
1686
  info: "info",
@@ -1673,28 +1700,28 @@ function isBrakitBatch(msg) {
1673
1700
  function isSDKPayload(msg) {
1674
1701
  return typeof msg === "object" && msg !== null && "_brakit" in msg && "version" in msg && typeof msg.version === "number";
1675
1702
  }
1676
- function createIngestHandler(registry) {
1703
+ function createIngestHandler(services) {
1677
1704
  const routeEvent = (event) => {
1678
1705
  switch (event.type) {
1679
1706
  case TIMELINE_FETCH:
1680
- registry.get("fetch-store").add(event.data);
1707
+ services.fetchStore.add(event.data);
1681
1708
  break;
1682
1709
  case TIMELINE_LOG:
1683
- registry.get("log-store").add(event.data);
1710
+ services.logStore.add(event.data);
1684
1711
  break;
1685
1712
  case TIMELINE_ERROR:
1686
- registry.get("error-store").add(event.data);
1713
+ services.errorStore.add(event.data);
1687
1714
  break;
1688
1715
  case TIMELINE_QUERY:
1689
- registry.get("query-store").add(event.data);
1716
+ services.queryStore.add(event.data);
1690
1717
  break;
1691
1718
  }
1692
1719
  };
1693
- const queryStore = registry.get("query-store");
1694
- const fetchStore = registry.get("fetch-store");
1695
- const logStore = registry.get("log-store");
1696
- const errorStore = registry.get("error-store");
1697
- const requestStore = registry.get("request-store");
1720
+ const queryStore = services.queryStore;
1721
+ const fetchStore = services.fetchStore;
1722
+ const logStore = services.logStore;
1723
+ const errorStore = services.errorStore;
1724
+ const requestStore = services.requestStore;
1698
1725
  const stores = {
1699
1726
  addQuery: (data) => queryStore.add(data),
1700
1727
  addFetch: (data) => fetchStore.add(data),
@@ -1754,9 +1781,8 @@ function createIngestHandler(registry) {
1754
1781
  var init_ingest = __esm({
1755
1782
  "src/dashboard/api/ingest.ts"() {
1756
1783
  "use strict";
1757
- init_limits();
1758
- init_http();
1759
- init_timeline();
1784
+ init_config();
1785
+ init_labels();
1760
1786
  init_shared2();
1761
1787
  init_sdk_event_parser();
1762
1788
  }
@@ -1776,11 +1802,11 @@ function createMetricsHandler(metricsStore) {
1776
1802
  sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
1777
1803
  };
1778
1804
  }
1779
- var init_metrics2 = __esm({
1805
+ var init_metrics = __esm({
1780
1806
  "src/dashboard/api/metrics.ts"() {
1781
1807
  "use strict";
1782
1808
  init_shared2();
1783
- init_http();
1809
+ init_labels();
1784
1810
  }
1785
1811
  });
1786
1812
 
@@ -1798,61 +1824,55 @@ var init_metrics_live = __esm({
1798
1824
  }
1799
1825
  });
1800
1826
 
1801
- // src/utils/log.ts
1802
- function brakitWarn(message) {
1803
- process.stderr.write(`${PREFIX} ${message}
1804
- `);
1805
- }
1806
- function brakitDebug(message) {
1807
- if (process.env.DEBUG_BRAKIT) {
1808
- process.stderr.write(`${PREFIX}:debug ${message}
1809
- `);
1810
- }
1811
- }
1812
- var PREFIX;
1813
- var init_log = __esm({
1814
- "src/utils/log.ts"() {
1815
- "use strict";
1816
- PREFIX = "[brakit]";
1817
- }
1818
- });
1819
-
1820
1827
  // src/dashboard/api/activity.ts
1821
- function createActivityHandler(registry) {
1828
+ function buildTimeline(services, requestId) {
1829
+ const fetches = services.fetchStore.getByRequest(requestId);
1830
+ const logs = services.logStore.getByRequest(requestId);
1831
+ const errors = services.errorStore.getByRequest(requestId);
1832
+ const queries = services.queryStore.getByRequest(requestId);
1833
+ const timeline = [];
1834
+ for (const fetch of fetches)
1835
+ timeline.push({ type: TIMELINE_FETCH, timestamp: fetch.timestamp, data: fetch });
1836
+ for (const log of logs)
1837
+ timeline.push({ type: TIMELINE_LOG, timestamp: log.timestamp, data: log });
1838
+ for (const error of errors)
1839
+ timeline.push({ type: TIMELINE_ERROR, timestamp: error.timestamp, data: error });
1840
+ for (const query of queries)
1841
+ timeline.push({ type: TIMELINE_QUERY, timestamp: query.timestamp, data: query });
1842
+ timeline.sort((a, b) => a.timestamp - b.timestamp);
1843
+ return {
1844
+ total: timeline.length,
1845
+ timeline,
1846
+ counts: {
1847
+ fetches: fetches.length,
1848
+ logs: logs.length,
1849
+ errors: errors.length,
1850
+ queries: queries.length
1851
+ }
1852
+ };
1853
+ }
1854
+ function createActivityHandler(services) {
1822
1855
  return (req, res) => {
1823
1856
  if (!requireGet(req, res)) return;
1824
1857
  try {
1825
1858
  const url = parseRequestUrl(req);
1826
1859
  const requestId = url.searchParams.get("requestId");
1827
- if (!requestId) {
1828
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId parameter required" });
1860
+ const requestIds = url.searchParams.get("requestIds");
1861
+ if (!requestId && !requestIds) {
1862
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId or requestIds parameter required" });
1829
1863
  return;
1830
1864
  }
1831
- const fetches = registry.get("fetch-store").getByRequest(requestId);
1832
- const logs = registry.get("log-store").getByRequest(requestId);
1833
- const errors = registry.get("error-store").getByRequest(requestId);
1834
- const queries = registry.get("query-store").getByRequest(requestId);
1835
- const timeline = [];
1836
- for (const f of fetches)
1837
- timeline.push({ type: TIMELINE_FETCH, timestamp: f.timestamp, data: f });
1838
- for (const l of logs)
1839
- timeline.push({ type: TIMELINE_LOG, timestamp: l.timestamp, data: l });
1840
- for (const e of errors)
1841
- timeline.push({ type: TIMELINE_ERROR, timestamp: e.timestamp, data: e });
1842
- for (const q of queries)
1843
- timeline.push({ type: TIMELINE_QUERY, timestamp: q.timestamp, data: q });
1844
- timeline.sort((a, b) => a.timestamp - b.timestamp);
1845
- sendJson(req, res, HTTP_OK, {
1846
- requestId,
1847
- total: timeline.length,
1848
- timeline,
1849
- counts: {
1850
- fetches: fetches.length,
1851
- logs: logs.length,
1852
- errors: errors.length,
1853
- queries: queries.length
1854
- }
1855
- });
1865
+ if (requestId) {
1866
+ const result = buildTimeline(services, requestId);
1867
+ sendJson(req, res, HTTP_OK, { requestId, ...result });
1868
+ return;
1869
+ }
1870
+ const ids = (requestIds || "").split(",").filter(Boolean).slice(0, MAX_BATCH_IDS);
1871
+ const activities = {};
1872
+ for (const id of ids) {
1873
+ activities[id] = buildTimeline(services, id);
1874
+ }
1875
+ sendJson(req, res, HTTP_OK, { requestIds: ids, activities });
1856
1876
  } catch (err) {
1857
1877
  brakitDebug(`activity handler error: ${err}`);
1858
1878
  if (!res.headersSent) {
@@ -1861,13 +1881,14 @@ function createActivityHandler(registry) {
1861
1881
  }
1862
1882
  };
1863
1883
  }
1884
+ var MAX_BATCH_IDS;
1864
1885
  var init_activity = __esm({
1865
1886
  "src/dashboard/api/activity.ts"() {
1866
1887
  "use strict";
1867
1888
  init_shared2();
1868
- init_http();
1869
- init_timeline();
1889
+ init_labels();
1870
1890
  init_log();
1891
+ MAX_BATCH_IDS = 50;
1871
1892
  }
1872
1893
  });
1873
1894
 
@@ -1877,7 +1898,7 @@ var init_api = __esm({
1877
1898
  "use strict";
1878
1899
  init_handlers();
1879
1900
  init_ingest();
1880
- init_metrics2();
1901
+ init_metrics();
1881
1902
  init_metrics_live();
1882
1903
  init_activity();
1883
1904
  }
@@ -1952,25 +1973,12 @@ var init_issues = __esm({
1952
1973
  "use strict";
1953
1974
  init_shared2();
1954
1975
  init_type_guards();
1955
- init_http();
1956
- }
1957
- });
1958
-
1959
- // src/constants/events.ts
1960
- var SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES;
1961
- var init_events = __esm({
1962
- "src/constants/events.ts"() {
1963
- "use strict";
1964
- SSE_EVENT_FETCH = "fetch";
1965
- SSE_EVENT_LOG = "log";
1966
- SSE_EVENT_ERROR = "error_event";
1967
- SSE_EVENT_QUERY = "query";
1968
- SSE_EVENT_ISSUES = "issues";
1976
+ init_labels();
1969
1977
  }
1970
1978
  });
1971
1979
 
1972
1980
  // src/dashboard/sse.ts
1973
- function createSSEHandler(registry) {
1981
+ function createSSEHandler(services) {
1974
1982
  const clients = /* @__PURE__ */ new Set();
1975
1983
  function broadcast(eventType, data) {
1976
1984
  if (clients.size === 0) return;
@@ -1992,7 +2000,7 @@ data: ${data}
1992
2000
  }
1993
2001
  }
1994
2002
  }
1995
- const bus = registry.get("event-bus");
2003
+ const bus = services.bus;
1996
2004
  bus.on("request:completed", (r) => broadcast(null, JSON.stringify(r)));
1997
2005
  bus.on("telemetry:fetch", (e) => broadcast(SSE_EVENT_FETCH, JSON.stringify(e)));
1998
2006
  bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
@@ -2042,8 +2050,7 @@ var init_sse = __esm({
2042
2050
  "src/dashboard/sse.ts"() {
2043
2051
  "use strict";
2044
2052
  init_constants();
2045
- init_http();
2046
- init_events();
2053
+ init_labels();
2047
2054
  init_shared2();
2048
2055
  }
2049
2056
  });
@@ -2098,7 +2105,7 @@ async function ensureGitignoreAsync(dir, entry) {
2098
2105
  var init_fs = __esm({
2099
2106
  "src/utils/fs.ts"() {
2100
2107
  "use strict";
2101
- init_limits();
2108
+ init_config();
2102
2109
  init_log();
2103
2110
  init_type_guards();
2104
2111
  }
@@ -2187,7 +2194,7 @@ function computeIssueId(issue) {
2187
2194
  var init_issue_id = __esm({
2188
2195
  "src/utils/issue-id.ts"() {
2189
2196
  "use strict";
2190
- init_limits();
2197
+ init_config();
2191
2198
  }
2192
2199
  });
2193
2200
 
@@ -2200,10 +2207,7 @@ var init_issue_store = __esm({
2200
2207
  "src/store/issue-store.ts"() {
2201
2208
  "use strict";
2202
2209
  init_fs();
2203
- init_metrics();
2204
- init_limits();
2205
- init_thresholds();
2206
- init_limits();
2210
+ init_config();
2207
2211
  init_atomic_writer();
2208
2212
  init_log();
2209
2213
  init_type_guards();
@@ -2446,7 +2450,7 @@ function unwrapResponse(parsed) {
2446
2450
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
2447
2451
  const obj = parsed;
2448
2452
  const keys = Object.keys(obj);
2449
- if (keys.length > 3) return parsed;
2453
+ if (keys.length > MAX_WRAPPER_KEYS) return parsed;
2450
2454
  let best = null;
2451
2455
  let bestSize = 0;
2452
2456
  for (const key of keys) {
@@ -2464,10 +2468,12 @@ function unwrapResponse(parsed) {
2464
2468
  }
2465
2469
  return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
2466
2470
  }
2471
+ var MAX_WRAPPER_KEYS;
2467
2472
  var init_response = __esm({
2468
2473
  "src/utils/response.ts"() {
2469
2474
  "use strict";
2470
- init_thresholds();
2475
+ init_config();
2476
+ MAX_WRAPPER_KEYS = 3;
2471
2477
  }
2472
2478
  });
2473
2479
 
@@ -2505,92 +2511,154 @@ var init_patterns = __esm({
2505
2511
  }
2506
2512
  });
2507
2513
 
2508
- // src/analysis/rules/exposed-secret.ts
2509
- function findSecretKeys(obj, prefix, depth = 0) {
2510
- const found = [];
2511
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
2512
- if (!obj || typeof obj !== "object") return found;
2513
- if (Array.isArray(obj)) {
2514
- for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
2515
- found.push(...findSecretKeys(obj[i], prefix, depth + 1));
2514
+ // src/utils/collections.ts
2515
+ function getOrCreate(map, key, create) {
2516
+ let value = map.get(key);
2517
+ if (value === void 0) {
2518
+ value = create();
2519
+ map.set(key, value);
2520
+ }
2521
+ return value;
2522
+ }
2523
+ function deduplicateFindings(items, extract) {
2524
+ const seen = /* @__PURE__ */ new Map();
2525
+ const findings = [];
2526
+ for (const item of items) {
2527
+ const result = extract(item);
2528
+ if (!result) continue;
2529
+ const existing = seen.get(result.key);
2530
+ if (existing) {
2531
+ existing.count++;
2532
+ continue;
2533
+ }
2534
+ seen.set(result.key, result.finding);
2535
+ findings.push(result.finding);
2536
+ }
2537
+ return findings;
2538
+ }
2539
+ function groupBy(items, keyFn) {
2540
+ const map = /* @__PURE__ */ new Map();
2541
+ for (const item of items) {
2542
+ const key = keyFn(item);
2543
+ if (key == null) continue;
2544
+ let arr = map.get(key);
2545
+ if (!arr) {
2546
+ arr = [];
2547
+ map.set(key, arr);
2516
2548
  }
2517
- return found;
2549
+ arr.push(item);
2550
+ }
2551
+ return map;
2552
+ }
2553
+ var init_collections = __esm({
2554
+ "src/utils/collections.ts"() {
2555
+ "use strict";
2518
2556
  }
2519
- for (const k of Object.keys(obj)) {
2520
- const val = obj[k];
2521
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
2522
- found.push(k);
2557
+ });
2558
+
2559
+ // src/utils/object-scan.ts
2560
+ function walkObject(obj, visitor, options) {
2561
+ const opts = { ...DEFAULTS, ...options };
2562
+ walk(obj, visitor, opts, 0);
2563
+ }
2564
+ function walk(obj, visitor, opts, depth) {
2565
+ if (depth >= opts.maxDepth) return;
2566
+ if (!obj || typeof obj !== "object") return;
2567
+ if (Array.isArray(obj)) {
2568
+ for (let i = 0; i < Math.min(obj.length, opts.arrayLimit); i++) {
2569
+ walk(obj[i], visitor, opts, depth + 1);
2523
2570
  }
2571
+ return;
2572
+ }
2573
+ for (const key of Object.keys(obj)) {
2574
+ const val = obj[key];
2575
+ visitor(key, val, depth);
2524
2576
  if (typeof val === "object" && val !== null) {
2525
- found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
2577
+ walk(val, visitor, opts, depth + 1);
2526
2578
  }
2527
2579
  }
2528
- return found;
2529
2580
  }
2530
- var exposedSecretRule;
2531
- var init_exposed_secret = __esm({
2532
- "src/analysis/rules/exposed-secret.ts"() {
2581
+ function collectFromObject(obj, match, options) {
2582
+ const results = [];
2583
+ walkObject(obj, (key, value) => {
2584
+ const result = match(key, value);
2585
+ if (result !== null) results.push(result);
2586
+ }, options);
2587
+ return results;
2588
+ }
2589
+ var DEFAULTS;
2590
+ var init_object_scan = __esm({
2591
+ "src/utils/object-scan.ts"() {
2592
+ "use strict";
2593
+ init_config();
2594
+ DEFAULTS = {
2595
+ maxDepth: MAX_OBJECT_SCAN_DEPTH,
2596
+ arrayLimit: SECRET_SCAN_ARRAY_LIMIT
2597
+ };
2598
+ }
2599
+ });
2600
+
2601
+ // src/analysis/rules/auth-rules.ts
2602
+ function findSecretKeys(obj) {
2603
+ return collectFromObject(
2604
+ obj,
2605
+ (key, val) => SECRET_KEYS.test(key) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val) ? key : null
2606
+ );
2607
+ }
2608
+ function isFrameworkResponse(request) {
2609
+ if (isRedirect(request.statusCode)) return true;
2610
+ if (request.path?.startsWith("/__")) return true;
2611
+ if (request.responseHeaders?.["x-middleware-rewrite"]) return true;
2612
+ return false;
2613
+ }
2614
+ var exposedSecretRule, tokenInUrlRule, insecureCookieRule, corsCredentialsRule;
2615
+ var init_auth_rules = __esm({
2616
+ "src/analysis/rules/auth-rules.ts"() {
2533
2617
  "use strict";
2534
2618
  init_patterns();
2535
- init_limits();
2619
+ init_config();
2536
2620
  init_http_status();
2621
+ init_collections();
2622
+ init_object_scan();
2537
2623
  exposedSecretRule = {
2538
2624
  id: "exposed-secret",
2539
2625
  severity: "critical",
2540
2626
  name: "Exposed Secret in Response",
2541
2627
  hint: RULE_HINTS["exposed-secret"],
2542
2628
  check(ctx) {
2543
- const findings = [];
2544
- const seen = /* @__PURE__ */ new Map();
2545
- for (const r of ctx.requests) {
2546
- if (isErrorStatus(r.statusCode)) continue;
2547
- const parsed = ctx.parsedBodies.response.get(r.id);
2548
- if (!parsed) continue;
2549
- const keys = findSecretKeys(parsed, "");
2550
- if (keys.length === 0) continue;
2551
- const ep = `${r.method} ${r.path}`;
2552
- const dedupKey = `${ep}:${keys.sort().join(",")}`;
2553
- const existing = seen.get(dedupKey);
2554
- if (existing) {
2555
- existing.count++;
2556
- continue;
2557
- }
2558
- const finding = {
2559
- severity: "critical",
2560
- rule: "exposed-secret",
2561
- title: "Exposed Secret in Response",
2562
- desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
2563
- hint: this.hint,
2564
- endpoint: ep,
2565
- count: 1
2629
+ return deduplicateFindings(ctx.requests, (request) => {
2630
+ if (isErrorStatus(request.statusCode)) return null;
2631
+ const parsed = ctx.parsedBodies.response.get(request.id);
2632
+ if (!parsed) return null;
2633
+ const keys = findSecretKeys(parsed);
2634
+ if (keys.length === 0) return null;
2635
+ const ep = `${request.method} ${request.path}`;
2636
+ return {
2637
+ key: `${ep}:${keys.sort().join(",")}`,
2638
+ finding: {
2639
+ severity: "critical",
2640
+ rule: "exposed-secret",
2641
+ title: "Exposed Secret in Response",
2642
+ desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
2643
+ hint: this.hint,
2644
+ detail: `Exposed fields: ${keys.join(", ")}. ${keys.length} unmasked secret value${keys.length !== 1 ? "s" : ""} in response body.`,
2645
+ endpoint: ep,
2646
+ count: 1
2647
+ }
2566
2648
  };
2567
- seen.set(dedupKey, finding);
2568
- findings.push(finding);
2569
- }
2570
- return findings;
2649
+ });
2571
2650
  }
2572
2651
  };
2573
- }
2574
- });
2575
-
2576
- // src/analysis/rules/token-in-url.ts
2577
- var tokenInUrlRule;
2578
- var init_token_in_url = __esm({
2579
- "src/analysis/rules/token-in-url.ts"() {
2580
- "use strict";
2581
- init_patterns();
2582
2652
  tokenInUrlRule = {
2583
2653
  id: "token-in-url",
2584
2654
  severity: "critical",
2585
2655
  name: "Auth Token in URL",
2586
2656
  hint: RULE_HINTS["token-in-url"],
2587
2657
  check(ctx) {
2588
- const findings = [];
2589
- const seen = /* @__PURE__ */ new Map();
2590
- for (const r of ctx.requests) {
2591
- const qIdx = r.url.indexOf("?");
2592
- if (qIdx === -1) continue;
2593
- const params = r.url.substring(qIdx + 1).split("&");
2658
+ return deduplicateFindings(ctx.requests, (request) => {
2659
+ const qIdx = request.url.indexOf("?");
2660
+ if (qIdx === -1) return null;
2661
+ const params = request.url.substring(qIdx + 1).split("&");
2594
2662
  const flagged = [];
2595
2663
  for (const param of params) {
2596
2664
  const [name, ...rest] = param.split("=");
@@ -2600,222 +2668,64 @@ var init_token_in_url = __esm({
2600
2668
  flagged.push(name);
2601
2669
  }
2602
2670
  }
2603
- if (flagged.length === 0) continue;
2604
- const ep = `${r.method} ${r.path}`;
2605
- const dedupKey = `${ep}:${flagged.sort().join(",")}`;
2606
- const existing = seen.get(dedupKey);
2607
- if (existing) {
2608
- existing.count++;
2609
- continue;
2610
- }
2611
- const finding = {
2612
- severity: "critical",
2613
- rule: "token-in-url",
2614
- title: "Auth Token in URL",
2615
- desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
2616
- hint: this.hint,
2617
- endpoint: ep,
2618
- count: 1
2619
- };
2620
- seen.set(dedupKey, finding);
2621
- findings.push(finding);
2622
- }
2623
- return findings;
2624
- }
2625
- };
2626
- }
2627
- });
2628
-
2629
- // src/analysis/rules/stack-trace-leak.ts
2630
- var stackTraceLeakRule;
2631
- var init_stack_trace_leak = __esm({
2632
- "src/analysis/rules/stack-trace-leak.ts"() {
2633
- "use strict";
2634
- init_patterns();
2635
- stackTraceLeakRule = {
2636
- id: "stack-trace-leak",
2637
- severity: "critical",
2638
- name: "Stack Trace Leaked to Client",
2639
- hint: RULE_HINTS["stack-trace-leak"],
2640
- check(ctx) {
2641
- const findings = [];
2642
- const seen = /* @__PURE__ */ new Map();
2643
- for (const r of ctx.requests) {
2644
- if (!r.responseBody) continue;
2645
- if (!STACK_TRACE_RE.test(r.responseBody)) continue;
2646
- const ep = `${r.method} ${r.path}`;
2647
- const existing = seen.get(ep);
2648
- if (existing) {
2649
- existing.count++;
2650
- continue;
2651
- }
2652
- const finding = {
2653
- severity: "critical",
2654
- rule: "stack-trace-leak",
2655
- title: "Stack Trace Leaked to Client",
2656
- desc: `${ep} \u2014 response exposes internal stack trace`,
2657
- hint: this.hint,
2658
- endpoint: ep,
2659
- count: 1
2660
- };
2661
- seen.set(ep, finding);
2662
- findings.push(finding);
2663
- }
2664
- return findings;
2665
- }
2666
- };
2667
- }
2668
- });
2669
-
2670
- // src/analysis/rules/error-info-leak.ts
2671
- var CRITICAL_PATTERNS, errorInfoLeakRule;
2672
- var init_error_info_leak = __esm({
2673
- "src/analysis/rules/error-info-leak.ts"() {
2674
- "use strict";
2675
- init_patterns();
2676
- CRITICAL_PATTERNS = [
2677
- { re: DB_CONN_RE, label: "database connection string" },
2678
- { re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
2679
- { re: SECRET_VAL_RE, label: "secret value" }
2680
- ];
2681
- errorInfoLeakRule = {
2682
- id: "error-info-leak",
2683
- severity: "critical",
2684
- name: "Sensitive Data in Error Response",
2685
- hint: RULE_HINTS["error-info-leak"],
2686
- check(ctx) {
2687
- const findings = [];
2688
- const seen = /* @__PURE__ */ new Map();
2689
- for (const r of ctx.requests) {
2690
- if (r.statusCode < 400) continue;
2691
- if (!r.responseBody) continue;
2692
- if (r.responseHeaders["x-nextjs-error"] || r.responseHeaders["x-nextjs-matched-path"]) continue;
2693
- const ep = `${r.method} ${r.path}`;
2694
- for (const p of CRITICAL_PATTERNS) {
2695
- if (!p.re.test(r.responseBody)) continue;
2696
- const dedupKey = `${ep}:${p.label}`;
2697
- const existing = seen.get(dedupKey);
2698
- if (existing) {
2699
- existing.count++;
2700
- continue;
2701
- }
2702
- const finding = {
2671
+ if (flagged.length === 0) return null;
2672
+ const ep = `${request.method} ${request.path}`;
2673
+ return {
2674
+ key: `${ep}:${flagged.sort().join(",")}`,
2675
+ finding: {
2703
2676
  severity: "critical",
2704
- rule: "error-info-leak",
2705
- title: "Sensitive Data in Error Response",
2706
- desc: `${ep} \u2014 error response exposes ${p.label}`,
2677
+ rule: "token-in-url",
2678
+ title: "Auth Token in URL",
2679
+ desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
2707
2680
  hint: this.hint,
2681
+ detail: `Parameters in URL: ${flagged.join(", ")}. Auth tokens in URLs are logged by proxies, browsers, and CDNs.`,
2708
2682
  endpoint: ep,
2709
2683
  count: 1
2710
- };
2711
- seen.set(dedupKey, finding);
2712
- findings.push(finding);
2713
- }
2714
- }
2715
- return findings;
2684
+ }
2685
+ };
2686
+ });
2716
2687
  }
2717
2688
  };
2718
- }
2719
- });
2720
-
2721
- // src/analysis/rules/insecure-cookie.ts
2722
- function isFrameworkResponse(r) {
2723
- if (isRedirect(r.statusCode)) return true;
2724
- if (r.path?.startsWith("/__")) return true;
2725
- if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
2726
- return false;
2727
- }
2728
- var insecureCookieRule;
2729
- var init_insecure_cookie = __esm({
2730
- "src/analysis/rules/insecure-cookie.ts"() {
2731
- "use strict";
2732
- init_patterns();
2733
- init_http_status();
2734
2689
  insecureCookieRule = {
2735
2690
  id: "insecure-cookie",
2736
2691
  severity: "warning",
2737
2692
  name: "Insecure Cookie",
2738
2693
  hint: RULE_HINTS["insecure-cookie"],
2739
2694
  check(ctx) {
2740
- const findings = [];
2741
- const seen = /* @__PURE__ */ new Map();
2742
- for (const r of ctx.requests) {
2743
- if (!r.responseHeaders) continue;
2744
- if (isFrameworkResponse(r)) continue;
2745
- const setCookie = r.responseHeaders["set-cookie"];
2695
+ const cookieEntries = [];
2696
+ for (const request of ctx.requests) {
2697
+ if (!request.responseHeaders) continue;
2698
+ if (isFrameworkResponse(request)) continue;
2699
+ const setCookie = request.responseHeaders["set-cookie"];
2746
2700
  if (!setCookie) continue;
2747
2701
  const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
2748
2702
  for (const cookie of cookies) {
2749
- const cookieName = cookie.trim().split("=")[0].trim();
2750
- const lower = cookie.toLowerCase();
2751
- const issues = [];
2752
- if (!lower.includes("httponly")) issues.push("HttpOnly");
2753
- if (!lower.includes("samesite")) issues.push("SameSite");
2754
- if (issues.length === 0) continue;
2755
- const dedupKey = `${cookieName}:${issues.join(",")}`;
2756
- const existing = seen.get(dedupKey);
2757
- if (existing) {
2758
- existing.count++;
2759
- continue;
2760
- }
2761
- const finding = {
2703
+ cookieEntries.push({ cookie });
2704
+ }
2705
+ }
2706
+ return deduplicateFindings(cookieEntries, ({ cookie }) => {
2707
+ const cookieName = cookie.trim().split("=")[0].trim();
2708
+ const lower = cookie.toLowerCase();
2709
+ const issues = [];
2710
+ if (!lower.includes("httponly")) issues.push("HttpOnly");
2711
+ if (!lower.includes("samesite")) issues.push("SameSite");
2712
+ if (issues.length === 0) return null;
2713
+ return {
2714
+ key: `${cookieName}:${issues.join(",")}`,
2715
+ finding: {
2762
2716
  severity: "warning",
2763
2717
  rule: "insecure-cookie",
2764
2718
  title: "Insecure Cookie",
2765
2719
  desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
2766
2720
  hint: this.hint,
2721
+ detail: `Missing: ${issues.join(", ")}. ${issues.includes("HttpOnly") ? "Cookie accessible via JavaScript (XSS risk). " : ""}${issues.includes("SameSite") ? "Cookie sent on cross-site requests (CSRF risk)." : ""}`,
2767
2722
  endpoint: cookieName,
2768
2723
  count: 1
2769
- };
2770
- seen.set(dedupKey, finding);
2771
- findings.push(finding);
2772
- }
2773
- }
2774
- return findings;
2775
- }
2776
- };
2777
- }
2778
- });
2779
-
2780
- // src/analysis/rules/sensitive-logs.ts
2781
- var sensitiveLogsRule;
2782
- var init_sensitive_logs = __esm({
2783
- "src/analysis/rules/sensitive-logs.ts"() {
2784
- "use strict";
2785
- init_patterns();
2786
- sensitiveLogsRule = {
2787
- id: "sensitive-logs",
2788
- severity: "warning",
2789
- name: "Sensitive Data in Logs",
2790
- hint: RULE_HINTS["sensitive-logs"],
2791
- check(ctx) {
2792
- let count = 0;
2793
- for (const log of ctx.logs) {
2794
- if (!log.message) continue;
2795
- if (log.message.startsWith("[brakit]")) continue;
2796
- if (LOG_SECRET_RE.test(log.message)) count++;
2797
- }
2798
- if (count === 0) return [];
2799
- return [{
2800
- severity: "warning",
2801
- rule: "sensitive-logs",
2802
- title: "Sensitive Data in Logs",
2803
- desc: `Console output contains secret/token values \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
2804
- hint: this.hint,
2805
- endpoint: "console",
2806
- count
2807
- }];
2724
+ }
2725
+ };
2726
+ });
2808
2727
  }
2809
2728
  };
2810
- }
2811
- });
2812
-
2813
- // src/analysis/rules/cors-credentials.ts
2814
- var corsCredentialsRule;
2815
- var init_cors_credentials = __esm({
2816
- "src/analysis/rules/cors-credentials.ts"() {
2817
- "use strict";
2818
- init_patterns();
2819
2729
  corsCredentialsRule = {
2820
2730
  id: "cors-credentials",
2821
2731
  severity: "warning",
@@ -2824,12 +2734,12 @@ var init_cors_credentials = __esm({
2824
2734
  check(ctx) {
2825
2735
  const findings = [];
2826
2736
  const seen = /* @__PURE__ */ new Set();
2827
- for (const r of ctx.requests) {
2828
- if (!r.responseHeaders) continue;
2829
- const origin = r.responseHeaders["access-control-allow-origin"];
2830
- const creds = r.responseHeaders["access-control-allow-credentials"];
2737
+ for (const request of ctx.requests) {
2738
+ if (!request.responseHeaders) continue;
2739
+ const origin = request.responseHeaders["access-control-allow-origin"];
2740
+ const creds = request.responseHeaders["access-control-allow-credentials"];
2831
2741
  if (origin !== "*" || creds !== "true") continue;
2832
- const ep = `${r.method} ${r.path}`;
2742
+ const ep = `${request.method} ${request.path}`;
2833
2743
  if (seen.has(ep)) continue;
2834
2744
  seen.add(ep);
2835
2745
  findings.push({
@@ -2848,25 +2758,13 @@ var init_cors_credentials = __esm({
2848
2758
  }
2849
2759
  });
2850
2760
 
2851
- // src/analysis/rules/response-pii-leak.ts
2852
- function findEmails(obj, depth = 0) {
2853
- const emails = [];
2854
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
2855
- if (!obj || typeof obj !== "object") return emails;
2856
- if (Array.isArray(obj)) {
2857
- for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
2858
- emails.push(...findEmails(obj[i], depth + 1));
2859
- }
2860
- return emails;
2861
- }
2862
- for (const v of Object.values(obj)) {
2863
- if (typeof v === "string" && EMAIL_RE.test(v)) {
2864
- emails.push(v);
2865
- } else if (typeof v === "object" && v !== null) {
2866
- emails.push(...findEmails(v, depth + 1));
2867
- }
2868
- }
2869
- return emails;
2761
+ // src/analysis/rules/data-rules.ts
2762
+ function findEmails(obj) {
2763
+ return collectFromObject(
2764
+ obj,
2765
+ (_key, val) => typeof val === "string" && EMAIL_RE.test(val) ? val : null,
2766
+ { arrayLimit: PII_SCAN_ARRAY_LIMIT }
2767
+ );
2870
2768
  }
2871
2769
  function topLevelFieldCount(obj) {
2872
2770
  if (Array.isArray(obj)) {
@@ -2939,56 +2837,147 @@ function detectPII(method, reqBody, resBody) {
2939
2837
  const target = unwrapResponse(resBody);
2940
2838
  return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
2941
2839
  }
2942
- var WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
2943
- var init_response_pii_leak = __esm({
2944
- "src/analysis/rules/response-pii-leak.ts"() {
2840
+ var stackTraceLeakRule, CRITICAL_PATTERNS, errorInfoLeakRule, sensitiveLogsRule, WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
2841
+ var init_data_rules = __esm({
2842
+ "src/analysis/rules/data-rules.ts"() {
2945
2843
  "use strict";
2844
+ init_collections();
2946
2845
  init_patterns();
2947
2846
  init_response();
2948
- init_limits();
2847
+ init_config();
2949
2848
  init_http_status();
2950
- WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
2951
- REASON_LABELS = {
2952
- echo: "echoes back PII from the request body",
2953
- "full-record": "returns a full record with email and internal IDs",
2954
- "list-pii": "returns a list of records containing email addresses",
2955
- "sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
2956
- };
2957
- responsePiiLeakRule = {
2958
- id: "response-pii-leak",
2959
- severity: "warning",
2960
- name: "PII Leak in Response",
2961
- hint: RULE_HINTS["response-pii-leak"],
2849
+ init_object_scan();
2850
+ stackTraceLeakRule = {
2851
+ id: "stack-trace-leak",
2852
+ severity: "critical",
2853
+ name: "Stack Trace Leaked to Client",
2854
+ hint: RULE_HINTS["stack-trace-leak"],
2962
2855
  check(ctx) {
2963
- const findings = [];
2964
- const seen = /* @__PURE__ */ new Map();
2965
- for (const r of ctx.requests) {
2966
- if (isErrorStatus(r.statusCode)) continue;
2967
- if (SELF_SERVICE_PATH.test(r.path)) continue;
2968
- const resJson = ctx.parsedBodies.response.get(r.id);
2969
- if (!resJson) continue;
2970
- const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
2971
- const detection = detectPII(r.method, reqJson, resJson);
2972
- if (!detection) continue;
2973
- const ep = `${r.method} ${r.path}`;
2974
- const existing = seen.get(ep);
2975
- if (existing) {
2976
- existing.count++;
2977
- continue;
2978
- }
2979
- const finding = {
2980
- severity: "warning",
2981
- rule: "response-pii-leak",
2982
- title: "PII Leak in Response",
2983
- desc: `${ep} \u2014 exposes PII in response`,
2984
- hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
2985
- endpoint: ep,
2986
- count: 1
2856
+ return deduplicateFindings(ctx.requests, (request) => {
2857
+ if (!request.responseBody) return null;
2858
+ if (!STACK_TRACE_RE.test(request.responseBody)) return null;
2859
+ const ep = `${request.method} ${request.path}`;
2860
+ const firstLine = request.responseBody.split("\n").find((l) => STACK_TRACE_RE.test(l))?.trim() ?? "";
2861
+ return {
2862
+ key: ep,
2863
+ finding: {
2864
+ severity: "critical",
2865
+ rule: "stack-trace-leak",
2866
+ title: "Stack Trace Leaked to Client",
2867
+ desc: `${ep} \u2014 response exposes internal stack trace`,
2868
+ hint: this.hint,
2869
+ detail: firstLine ? `Stack trace: ${firstLine.slice(0, DETAIL_PREVIEW_LENGTH)}` : void 0,
2870
+ endpoint: ep,
2871
+ count: 1
2872
+ }
2987
2873
  };
2988
- seen.set(ep, finding);
2989
- findings.push(finding);
2874
+ });
2875
+ }
2876
+ };
2877
+ CRITICAL_PATTERNS = [
2878
+ { re: DB_CONN_RE, label: "database connection string" },
2879
+ { re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
2880
+ { re: SECRET_VAL_RE, label: "secret value" }
2881
+ ];
2882
+ errorInfoLeakRule = {
2883
+ id: "error-info-leak",
2884
+ severity: "critical",
2885
+ name: "Sensitive Data in Error Response",
2886
+ hint: RULE_HINTS["error-info-leak"],
2887
+ check(ctx) {
2888
+ const entries = [];
2889
+ for (const request of ctx.requests) {
2890
+ if (request.statusCode < 400) continue;
2891
+ if (!request.responseBody) continue;
2892
+ if (request.responseHeaders["x-nextjs-error"] || request.responseHeaders["x-nextjs-matched-path"]) continue;
2893
+ const ep = `${request.method} ${request.path}`;
2894
+ for (const pattern of CRITICAL_PATTERNS) {
2895
+ if (pattern.re.test(request.responseBody)) {
2896
+ entries.push({ ep, pattern, body: request.responseBody });
2897
+ }
2898
+ }
2990
2899
  }
2991
- return findings;
2900
+ return deduplicateFindings(entries, ({ ep, pattern }) => {
2901
+ return {
2902
+ key: `${ep}:${pattern.label}`,
2903
+ finding: {
2904
+ severity: "critical",
2905
+ rule: "error-info-leak",
2906
+ title: "Sensitive Data in Error Response",
2907
+ desc: `${ep} \u2014 error response exposes ${pattern.label}`,
2908
+ hint: this.hint,
2909
+ detail: `Detected: ${pattern.label} in error response body`,
2910
+ endpoint: ep,
2911
+ count: 1
2912
+ }
2913
+ };
2914
+ });
2915
+ }
2916
+ };
2917
+ sensitiveLogsRule = {
2918
+ id: "sensitive-logs",
2919
+ severity: "warning",
2920
+ name: "Sensitive Data in Logs",
2921
+ hint: RULE_HINTS["sensitive-logs"],
2922
+ check(ctx) {
2923
+ let count = 0;
2924
+ for (const log of ctx.logs) {
2925
+ if (!log.message) continue;
2926
+ if (log.message.startsWith("[brakit]")) continue;
2927
+ if (LOG_SECRET_RE.test(log.message)) count++;
2928
+ }
2929
+ if (count === 0) return [];
2930
+ return [{
2931
+ severity: "warning",
2932
+ rule: "sensitive-logs",
2933
+ title: "Sensitive Data in Logs",
2934
+ desc: `Console output contains secret/token values \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
2935
+ hint: this.hint,
2936
+ endpoint: "console",
2937
+ count
2938
+ }];
2939
+ }
2940
+ };
2941
+ WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
2942
+ REASON_LABELS = {
2943
+ echo: "echoes back PII from the request body",
2944
+ "full-record": "returns a full record with email and internal IDs",
2945
+ "list-pii": "returns a list of records containing email addresses",
2946
+ "sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
2947
+ };
2948
+ responsePiiLeakRule = {
2949
+ id: "response-pii-leak",
2950
+ severity: "warning",
2951
+ name: "PII Leak in Response",
2952
+ hint: RULE_HINTS["response-pii-leak"],
2953
+ check(ctx) {
2954
+ return deduplicateFindings(ctx.requests, (request) => {
2955
+ if (isErrorStatus(request.statusCode)) return null;
2956
+ if (SELF_SERVICE_PATH.test(request.path)) return null;
2957
+ const resJson = ctx.parsedBodies.response.get(request.id);
2958
+ if (!resJson) return null;
2959
+ const reqJson = ctx.parsedBodies.request.get(request.id) ?? null;
2960
+ const detection = detectPII(request.method, reqJson, resJson);
2961
+ if (!detection) return null;
2962
+ const ep = `${request.method} ${request.path}`;
2963
+ const fieldCount = topLevelFieldCount(resJson);
2964
+ const detailParts = [`Pattern: ${REASON_LABELS[detection.reason]}`];
2965
+ if (detection.emailCount > 0) detailParts.push(`${detection.emailCount} email${detection.emailCount !== 1 ? "s" : ""} detected`);
2966
+ if (fieldCount > 0) detailParts.push(`${fieldCount} fields per record`);
2967
+ return {
2968
+ key: ep,
2969
+ finding: {
2970
+ severity: "warning",
2971
+ rule: "response-pii-leak",
2972
+ title: "PII Leak in Response",
2973
+ desc: `${ep} \u2014 exposes PII in response`,
2974
+ hint: this.hint,
2975
+ detail: detailParts.join(". "),
2976
+ endpoint: ep,
2977
+ count: 1
2978
+ }
2979
+ };
2980
+ });
2992
2981
  }
2993
2982
  };
2994
2983
  }
@@ -2998,14 +2987,14 @@ var init_response_pii_leak = __esm({
2998
2987
  function buildBodyCache(requests) {
2999
2988
  const response = /* @__PURE__ */ new Map();
3000
2989
  const request = /* @__PURE__ */ new Map();
3001
- for (const r of requests) {
3002
- if (r.responseBody) {
3003
- const parsed = tryParseJson(r.responseBody);
3004
- if (parsed != null) response.set(r.id, parsed);
2990
+ for (const req of requests) {
2991
+ if (req.responseBody) {
2992
+ const parsed = tryParseJson(req.responseBody);
2993
+ if (parsed != null) response.set(req.id, parsed);
3005
2994
  }
3006
- if (r.requestBody) {
3007
- const parsed = tryParseJson(r.requestBody);
3008
- if (parsed != null) request.set(r.id, parsed);
2995
+ if (req.requestBody) {
2996
+ const parsed = tryParseJson(req.requestBody);
2997
+ if (parsed != null) request.set(req.id, parsed);
3009
2998
  }
3010
2999
  }
3011
3000
  return { response, request };
@@ -3027,14 +3016,10 @@ var init_scanner = __esm({
3027
3016
  "src/analysis/rules/scanner.ts"() {
3028
3017
  "use strict";
3029
3018
  init_response();
3030
- init_exposed_secret();
3031
- init_token_in_url();
3032
- init_stack_trace_leak();
3033
- init_error_info_leak();
3034
- init_insecure_cookie();
3035
- init_sensitive_logs();
3036
- init_cors_credentials();
3037
- init_response_pii_leak();
3019
+ init_log();
3020
+ init_type_guards();
3021
+ init_auth_rules();
3022
+ init_data_rules();
3038
3023
  SecurityScanner = class {
3039
3024
  constructor() {
3040
3025
  this.rules = [];
@@ -3051,7 +3036,8 @@ var init_scanner = __esm({
3051
3036
  for (const rule of this.rules) {
3052
3037
  try {
3053
3038
  findings.push(...rule.check(ctx));
3054
- } catch {
3039
+ } catch (e) {
3040
+ brakitDebug(`rule ${rule.id} failed: ${getErrorMessage(e)}`);
3055
3041
  }
3056
3042
  }
3057
3043
  return findings;
@@ -3068,14 +3054,8 @@ var init_rules = __esm({
3068
3054
  "src/analysis/rules/index.ts"() {
3069
3055
  "use strict";
3070
3056
  init_scanner();
3071
- init_exposed_secret();
3072
- init_token_in_url();
3073
- init_stack_trace_leak();
3074
- init_error_info_leak();
3075
- init_insecure_cookie();
3076
- init_sensitive_logs();
3077
- init_cors_credentials();
3078
- init_response_pii_leak();
3057
+ init_auth_rules();
3058
+ init_data_rules();
3079
3059
  }
3080
3060
  });
3081
3061
 
@@ -3099,48 +3079,6 @@ var init_disposable = __esm({
3099
3079
  }
3100
3080
  });
3101
3081
 
3102
- // src/utils/collections.ts
3103
- function groupBy(items, keyFn) {
3104
- const map = /* @__PURE__ */ new Map();
3105
- for (const item of items) {
3106
- const key = keyFn(item);
3107
- if (key == null) continue;
3108
- let arr = map.get(key);
3109
- if (!arr) {
3110
- arr = [];
3111
- map.set(key, arr);
3112
- }
3113
- arr.push(item);
3114
- }
3115
- return map;
3116
- }
3117
- var init_collections = __esm({
3118
- "src/utils/collections.ts"() {
3119
- "use strict";
3120
- }
3121
- });
3122
-
3123
- // src/utils/endpoint.ts
3124
- function normalizePath(path) {
3125
- const qIdx = path.indexOf("?");
3126
- const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
3127
- return pathname.split("/").map((seg) => seg && DYNAMIC_SEGMENT_RE.test(seg) ? ":id" : seg).join("/");
3128
- }
3129
- function getEndpointKey(method, path) {
3130
- return `${method} ${normalizePath(path)}`;
3131
- }
3132
- function extractEndpointFromDesc(desc) {
3133
- return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
3134
- }
3135
- var DYNAMIC_SEGMENT_RE, ENDPOINT_PREFIX_RE;
3136
- var init_endpoint = __esm({
3137
- "src/utils/endpoint.ts"() {
3138
- "use strict";
3139
- DYNAMIC_SEGMENT_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$|^\d+$|^[0-9a-f]{12,}$|^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/i;
3140
- ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
3141
- }
3142
- });
3143
-
3144
3082
  // src/analysis/insights/query-helpers.ts
3145
3083
  function getQueryShape(q) {
3146
3084
  if (q.sql) return normalizeQueryParams(q.sql) ?? "";
@@ -3161,7 +3099,7 @@ var init_query_helpers = __esm({
3161
3099
  });
3162
3100
 
3163
3101
  // src/analysis/insights/prepare.ts
3164
- function createEndpointGroup() {
3102
+ function emptyEndpointGroup() {
3165
3103
  return {
3166
3104
  total: 0,
3167
3105
  errors: 0,
@@ -3173,16 +3111,12 @@ function createEndpointGroup() {
3173
3111
  queryShapeDurations: /* @__PURE__ */ new Map()
3174
3112
  };
3175
3113
  }
3176
- function windowByEndpoint(requests) {
3114
+ function keepRecentPerEndpoint(requests) {
3177
3115
  const byEndpoint = /* @__PURE__ */ new Map();
3178
- for (const r of requests) {
3179
- const ep = getEndpointKey(r.method, r.path);
3180
- let list = byEndpoint.get(ep);
3181
- if (!list) {
3182
- list = [];
3183
- byEndpoint.set(ep, list);
3184
- }
3185
- list.push(r);
3116
+ for (const request of requests) {
3117
+ const endpointKey = getEndpointKey(request.method, request.path);
3118
+ const list = getOrCreate(byEndpoint, endpointKey, () => []);
3119
+ list.push(request);
3186
3120
  }
3187
3121
  const windowed = [];
3188
3122
  for (const [, reqs] of byEndpoint) {
@@ -3190,54 +3124,67 @@ function windowByEndpoint(requests) {
3190
3124
  }
3191
3125
  return windowed;
3192
3126
  }
3127
+ function filterUserRequests(requests) {
3128
+ return requests.filter(
3129
+ (request) => !request.isStatic && !request.isHealthCheck && (!request.path || !request.path.startsWith(DASHBOARD_PREFIX))
3130
+ );
3131
+ }
3193
3132
  function extractActiveEndpoints(requests) {
3194
3133
  const endpoints = /* @__PURE__ */ new Set();
3195
- for (const r of requests) {
3196
- if (!r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))) {
3197
- endpoints.add(getEndpointKey(r.method, r.path));
3198
- }
3134
+ for (const request of filterUserRequests(requests)) {
3135
+ endpoints.add(getEndpointKey(request.method, request.path));
3199
3136
  }
3200
3137
  return endpoints;
3201
3138
  }
3202
- function prepareContext(ctx) {
3203
- const nonStatic = ctx.requests.filter(
3204
- (r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
3205
- );
3206
- const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
3207
- const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
3208
- const reqById = new Map(nonStatic.map((r) => [r.id, r]));
3209
- const recent = windowByEndpoint(nonStatic);
3139
+ function aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq) {
3210
3140
  const endpointGroups = /* @__PURE__ */ new Map();
3211
- for (const r of recent) {
3212
- const ep = getEndpointKey(r.method, r.path);
3213
- let g = endpointGroups.get(ep);
3214
- if (!g) {
3215
- g = createEndpointGroup();
3216
- endpointGroups.set(ep, g);
3141
+ for (const request of recent) {
3142
+ const endpointKey = getEndpointKey(request.method, request.path);
3143
+ const group = getOrCreate(endpointGroups, endpointKey, emptyEndpointGroup);
3144
+ group.total++;
3145
+ if (isErrorStatus(request.statusCode)) group.errors++;
3146
+ group.totalDuration += request.durationMs;
3147
+ group.totalSize += request.responseSize ?? 0;
3148
+ const reqQueries = queriesByReq.get(request.id) ?? [];
3149
+ group.queryCount += reqQueries.length;
3150
+ for (const query of reqQueries) {
3151
+ group.totalQueryTimeMs += query.durationMs;
3152
+ const shape = getQueryShape(query);
3153
+ const info = getQueryInfo(query);
3154
+ const shapeDuration = getOrCreate(group.queryShapeDurations, shape, () => ({
3155
+ totalMs: 0,
3156
+ count: 0,
3157
+ label: info.op + (info.table ? ` ${info.table}` : "")
3158
+ }));
3159
+ shapeDuration.totalMs += query.durationMs;
3160
+ shapeDuration.count++;
3217
3161
  }
3218
- g.total++;
3219
- if (isErrorStatus(r.statusCode)) g.errors++;
3220
- g.totalDuration += r.durationMs;
3221
- g.totalSize += r.responseSize ?? 0;
3222
- const reqQueries = queriesByReq.get(r.id) ?? [];
3223
- g.queryCount += reqQueries.length;
3224
- for (const q of reqQueries) {
3225
- g.totalQueryTimeMs += q.durationMs;
3226
- const shape = getQueryShape(q);
3227
- const info = getQueryInfo(q);
3228
- let sd = g.queryShapeDurations.get(shape);
3229
- if (!sd) {
3230
- sd = { totalMs: 0, count: 0, label: info.op + (info.table ? ` ${info.table}` : "") };
3231
- g.queryShapeDurations.set(shape, sd);
3232
- }
3233
- sd.totalMs += q.durationMs;
3234
- sd.count++;
3162
+ const reqFetches = fetchesByReq.get(request.id) ?? [];
3163
+ for (const fetch of reqFetches) {
3164
+ group.totalFetchTimeMs += fetch.durationMs;
3235
3165
  }
3236
- const reqFetches = fetchesByReq.get(r.id) ?? [];
3237
- for (const f of reqFetches) {
3238
- g.totalFetchTimeMs += f.durationMs;
3166
+ }
3167
+ return endpointGroups;
3168
+ }
3169
+ function collectStrictModeDupeIds(ctx) {
3170
+ const ids = /* @__PURE__ */ new Set();
3171
+ for (const flow of ctx.flows) {
3172
+ for (const req of flow.requests) {
3173
+ if (req.isStrictModeDupe) ids.add(req.id);
3239
3174
  }
3240
3175
  }
3176
+ return ids;
3177
+ }
3178
+ function buildInsightContext(ctx) {
3179
+ const strictModeDupeIds = collectStrictModeDupeIds(ctx);
3180
+ const nonStatic = filterUserRequests(ctx.requests).filter((req) => !strictModeDupeIds.has(req.id));
3181
+ const filteredQueries = strictModeDupeIds.size > 0 ? ctx.queries.filter((q) => !q.parentRequestId || !strictModeDupeIds.has(q.parentRequestId)) : ctx.queries;
3182
+ const filteredFetches = strictModeDupeIds.size > 0 ? ctx.fetches.filter((f) => !f.parentRequestId || !strictModeDupeIds.has(f.parentRequestId)) : ctx.fetches;
3183
+ const queriesByReq = groupBy(filteredQueries, (query) => query.parentRequestId);
3184
+ const fetchesByReq = groupBy(filteredFetches, (fetch) => fetch.parentRequestId);
3185
+ const reqById = new Map(nonStatic.map((request) => [request.id, request]));
3186
+ const recent = keepRecentPerEndpoint(nonStatic);
3187
+ const endpointGroups = aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq);
3241
3188
  return {
3242
3189
  ...ctx,
3243
3190
  nonStatic,
@@ -3254,7 +3201,7 @@ var init_prepare = __esm({
3254
3201
  init_endpoint();
3255
3202
  init_constants();
3256
3203
  init_http_status();
3257
- init_thresholds();
3204
+ init_config();
3258
3205
  init_query_helpers();
3259
3206
  }
3260
3207
  });
@@ -3265,6 +3212,8 @@ var init_runner = __esm({
3265
3212
  "src/analysis/insights/runner.ts"() {
3266
3213
  "use strict";
3267
3214
  init_prepare();
3215
+ init_log();
3216
+ init_type_guards();
3268
3217
  SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
3269
3218
  InsightRunner = class {
3270
3219
  constructor() {
@@ -3274,12 +3223,13 @@ var init_runner = __esm({
3274
3223
  this.rules.push(rule);
3275
3224
  }
3276
3225
  run(ctx) {
3277
- const prepared = prepareContext(ctx);
3226
+ const prepared = buildInsightContext(ctx);
3278
3227
  const insights = [];
3279
3228
  for (const rule of this.rules) {
3280
3229
  try {
3281
3230
  insights.push(...rule.check(prepared));
3282
- } catch {
3231
+ } catch (e) {
3232
+ brakitDebug(`insight rule ${rule.id} failed: ${getErrorMessage(e)}`);
3283
3233
  }
3284
3234
  }
3285
3235
  insights.sort(
@@ -3291,420 +3241,129 @@ var init_runner = __esm({
3291
3241
  }
3292
3242
  });
3293
3243
 
3294
- // src/analysis/insights/rules/n1.ts
3295
- var n1Rule;
3296
- var init_n1 = __esm({
3297
- "src/analysis/insights/rules/n1.ts"() {
3244
+ // src/analysis/insights/rules/query-rules.ts
3245
+ var n1Rule, redundantQueryRule, selectStarRule, highRowsRule, queryHeavyRule;
3246
+ var init_query_rules = __esm({
3247
+ "src/analysis/insights/rules/query-rules.ts"() {
3298
3248
  "use strict";
3299
3249
  init_query_helpers();
3300
3250
  init_endpoint();
3301
3251
  init_constants();
3252
+ init_patterns();
3302
3253
  n1Rule = {
3303
3254
  id: "n1",
3304
3255
  check(ctx) {
3305
3256
  const insights = [];
3306
- const seen = /* @__PURE__ */ new Set();
3257
+ const reportedKeys = /* @__PURE__ */ new Set();
3307
3258
  for (const [reqId, reqQueries] of ctx.queriesByReq) {
3308
3259
  const req = ctx.reqById.get(reqId);
3309
3260
  if (!req) continue;
3310
3261
  const endpoint = getEndpointKey(req.method, req.path);
3311
3262
  const shapeGroups = /* @__PURE__ */ new Map();
3312
- for (const q of reqQueries) {
3313
- const shape = getQueryShape(q);
3263
+ for (const query of reqQueries) {
3264
+ const shape = getQueryShape(query);
3314
3265
  let group = shapeGroups.get(shape);
3315
3266
  if (!group) {
3316
- group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: q };
3267
+ group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: query };
3317
3268
  shapeGroups.set(shape, group);
3318
3269
  }
3319
3270
  group.count++;
3320
- group.distinctSql.add(q.sql ?? shape);
3271
+ group.distinctSql.add(query.sql ?? shape);
3321
3272
  }
3322
- for (const [, sg] of shapeGroups) {
3323
- if (sg.count <= N1_QUERY_THRESHOLD || sg.distinctSql.size <= 1) continue;
3324
- const info = getQueryInfo(sg.first);
3273
+ for (const [, shapeGroup] of shapeGroups) {
3274
+ if (shapeGroup.count <= N1_QUERY_THRESHOLD || shapeGroup.distinctSql.size <= 1) continue;
3275
+ const info = getQueryInfo(shapeGroup.first);
3325
3276
  const key = `${endpoint}:${info.op}:${info.table || "unknown"}`;
3326
- if (seen.has(key)) continue;
3327
- seen.add(key);
3277
+ if (reportedKeys.has(key)) continue;
3278
+ reportedKeys.add(key);
3328
3279
  insights.push({
3329
3280
  severity: "critical",
3330
3281
  type: "n1",
3331
3282
  title: "N+1 Query Pattern",
3332
- desc: `${endpoint} runs ${sg.count}x ${info.op} ${info.table} with different params in a single request`,
3283
+ desc: `${endpoint} runs ${shapeGroup.count}x ${info.op} ${info.table} with different params in a single request`,
3333
3284
  hint: "This typically happens when fetching related data in a loop. Use a batch query, JOIN, or include/eager-load to fetch all records at once.",
3334
- nav: "queries"
3285
+ detail: `${shapeGroup.count} queries with ${shapeGroup.distinctSql.size} distinct param variations. Example: ${[...shapeGroup.distinctSql][0]?.slice(0, DETAIL_PREVIEW_LENGTH) ?? info.op + " " + info.table}`
3335
3286
  });
3336
3287
  }
3337
3288
  }
3338
3289
  return insights;
3339
3290
  }
3340
3291
  };
3341
- }
3342
- });
3343
-
3344
- // src/analysis/insights/rules/cross-endpoint.ts
3345
- var crossEndpointRule;
3346
- var init_cross_endpoint = __esm({
3347
- "src/analysis/insights/rules/cross-endpoint.ts"() {
3348
- "use strict";
3349
- init_query_helpers();
3350
- init_endpoint();
3351
- init_constants();
3352
- crossEndpointRule = {
3353
- id: "cross-endpoint",
3354
- check(ctx) {
3355
- const insights = [];
3356
- const queryMap = /* @__PURE__ */ new Map();
3357
- const allEndpoints = /* @__PURE__ */ new Set();
3358
- for (const [reqId, reqQueries] of ctx.queriesByReq) {
3359
- const req = ctx.reqById.get(reqId);
3360
- if (!req) continue;
3361
- const endpoint = getEndpointKey(req.method, req.path);
3362
- allEndpoints.add(endpoint);
3363
- const seenInReq = /* @__PURE__ */ new Set();
3364
- for (const q of reqQueries) {
3365
- const shape = getQueryShape(q);
3366
- let entry = queryMap.get(shape);
3367
- if (!entry) {
3368
- entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: q };
3369
- queryMap.set(shape, entry);
3370
- }
3371
- entry.count++;
3372
- if (!seenInReq.has(shape)) {
3373
- seenInReq.add(shape);
3374
- entry.endpoints.add(endpoint);
3375
- }
3376
- }
3377
- }
3378
- if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
3379
- for (const [, cem] of queryMap) {
3380
- if (cem.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
3381
- if (cem.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
3382
- const p = Math.round(cem.endpoints.size / allEndpoints.size * 100);
3383
- if (p < CROSS_ENDPOINT_PCT) continue;
3384
- const info = getQueryInfo(cem.first);
3385
- const label = info.op + (info.table ? ` ${info.table}` : "");
3386
- insights.push({
3387
- severity: "warning",
3388
- type: "cross-endpoint",
3389
- title: "Repeated Query Across Endpoints",
3390
- desc: `${label} runs on ${cem.endpoints.size} of ${allEndpoints.size} endpoints (${p}%).`,
3391
- hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
3392
- nav: "queries"
3393
- });
3394
- }
3395
- }
3396
- return insights;
3397
- }
3398
- };
3399
- }
3400
- });
3401
-
3402
- // src/analysis/insights/rules/redundant-query.ts
3403
- var redundantQueryRule;
3404
- var init_redundant_query = __esm({
3405
- "src/analysis/insights/rules/redundant-query.ts"() {
3406
- "use strict";
3407
- init_query_helpers();
3408
- init_endpoint();
3409
- init_constants();
3410
3292
  redundantQueryRule = {
3411
3293
  id: "redundant-query",
3412
3294
  check(ctx) {
3413
3295
  const insights = [];
3414
- const seen = /* @__PURE__ */ new Set();
3296
+ const reportedKeys = /* @__PURE__ */ new Set();
3415
3297
  for (const [reqId, reqQueries] of ctx.queriesByReq) {
3416
3298
  const req = ctx.reqById.get(reqId);
3417
3299
  if (!req) continue;
3418
3300
  const endpoint = getEndpointKey(req.method, req.path);
3419
- const exact = /* @__PURE__ */ new Map();
3420
- for (const q of reqQueries) {
3421
- if (!q.sql) continue;
3422
- let entry = exact.get(q.sql);
3301
+ const identicalQueryMap = /* @__PURE__ */ new Map();
3302
+ for (const query of reqQueries) {
3303
+ if (!query.sql) continue;
3304
+ let entry = identicalQueryMap.get(query.sql);
3423
3305
  if (!entry) {
3424
- entry = { count: 0, first: q };
3425
- exact.set(q.sql, entry);
3306
+ entry = { count: 0, first: query };
3307
+ identicalQueryMap.set(query.sql, entry);
3426
3308
  }
3427
3309
  entry.count++;
3428
3310
  }
3429
- for (const [, e] of exact) {
3430
- if (e.count < REDUNDANT_QUERY_MIN_COUNT) continue;
3431
- const info = getQueryInfo(e.first);
3311
+ for (const [, entry] of identicalQueryMap) {
3312
+ if (entry.count < REDUNDANT_QUERY_MIN_COUNT) continue;
3313
+ const info = getQueryInfo(entry.first);
3432
3314
  const label = info.op + (info.table ? ` ${info.table}` : "");
3433
- const dedupKey = `${endpoint}:${label}`;
3434
- if (seen.has(dedupKey)) continue;
3435
- seen.add(dedupKey);
3315
+ const deduplicationKey = `${endpoint}:${label}`;
3316
+ if (reportedKeys.has(deduplicationKey)) continue;
3317
+ reportedKeys.add(deduplicationKey);
3436
3318
  insights.push({
3437
3319
  severity: "warning",
3438
3320
  type: "redundant-query",
3439
3321
  title: "Redundant Query",
3440
- desc: `${label} runs ${e.count}x with identical params in ${endpoint}.`,
3322
+ desc: `${label} runs ${entry.count}x with identical params in ${endpoint}.`,
3441
3323
  hint: "The exact same query with identical parameters runs multiple times in one request. Cache the first result or lift the query to a shared function.",
3442
- nav: "queries"
3324
+ detail: entry.first.sql ? `Query: ${entry.first.sql.slice(0, DETAIL_PREVIEW_LENGTH)}` : void 0
3443
3325
  });
3444
3326
  }
3445
3327
  }
3446
3328
  return insights;
3447
3329
  }
3448
3330
  };
3449
- }
3450
- });
3451
-
3452
- // src/analysis/insights/rules/error.ts
3453
- var errorRule;
3454
- var init_error = __esm({
3455
- "src/analysis/insights/rules/error.ts"() {
3456
- "use strict";
3457
- errorRule = {
3458
- id: "error",
3459
- check(ctx) {
3460
- if (ctx.errors.length === 0) return [];
3461
- const insights = [];
3462
- const groups = /* @__PURE__ */ new Map();
3463
- for (const e of ctx.errors) {
3464
- const name = e.name || "Error";
3465
- groups.set(name, (groups.get(name) ?? 0) + 1);
3466
- }
3467
- for (const [name, cnt] of groups) {
3468
- insights.push({
3469
- severity: "critical",
3470
- type: "error",
3471
- title: "Unhandled Error",
3472
- desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
3473
- hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
3474
- nav: "errors"
3475
- });
3476
- }
3477
- return insights;
3478
- }
3479
- };
3480
- }
3481
- });
3482
-
3483
- // src/analysis/insights/rules/error-hotspot.ts
3484
- var errorHotspotRule;
3485
- var init_error_hotspot = __esm({
3486
- "src/analysis/insights/rules/error-hotspot.ts"() {
3487
- "use strict";
3488
- init_constants();
3489
- errorHotspotRule = {
3490
- id: "error-hotspot",
3491
- check(ctx) {
3492
- const insights = [];
3493
- for (const [ep, g] of ctx.endpointGroups) {
3494
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3495
- const errorRate = Math.round(g.errors / g.total * 100);
3496
- if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
3497
- insights.push({
3498
- severity: "critical",
3499
- type: "error-hotspot",
3500
- title: "Error Hotspot",
3501
- desc: `${ep} \u2014 ${errorRate}% error rate (${g.errors}/${g.total} requests)`,
3502
- hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces.",
3503
- nav: "requests"
3504
- });
3505
- }
3506
- }
3507
- return insights;
3508
- }
3509
- };
3510
- }
3511
- });
3512
-
3513
- // src/analysis/insights/rules/duplicate.ts
3514
- var duplicateRule;
3515
- var init_duplicate = __esm({
3516
- "src/analysis/insights/rules/duplicate.ts"() {
3517
- "use strict";
3518
- init_constants();
3519
- duplicateRule = {
3520
- id: "duplicate",
3521
- check(ctx) {
3522
- const dupCounts = /* @__PURE__ */ new Map();
3523
- const flowCount = /* @__PURE__ */ new Map();
3524
- for (const flow of ctx.flows) {
3525
- if (!flow.requests) continue;
3526
- const seenInFlow = /* @__PURE__ */ new Set();
3527
- for (const fr of flow.requests) {
3528
- if (!fr.isDuplicate) continue;
3529
- const dupKey = `${fr.method} ${fr.label ?? fr.path ?? fr.url}`;
3530
- dupCounts.set(dupKey, (dupCounts.get(dupKey) ?? 0) + 1);
3531
- if (!seenInFlow.has(dupKey)) {
3532
- seenInFlow.add(dupKey);
3533
- flowCount.set(dupKey, (flowCount.get(dupKey) ?? 0) + 1);
3534
- }
3535
- }
3536
- }
3537
- const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
3538
- const insights = [];
3539
- for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
3540
- const d = dupEntries[i];
3541
- insights.push({
3542
- severity: "warning",
3543
- type: "duplicate",
3544
- title: "Duplicate API Call",
3545
- desc: `${d.key} loaded ${d.count}x as duplicate across ${d.flows} action${d.flows !== 1 ? "s" : ""}`,
3546
- hint: "Multiple components independently fetch the same endpoint. Lift the fetch to a parent component, use a data cache, or deduplicate with React Query / SWR.",
3547
- nav: "actions"
3548
- });
3549
- }
3550
- return insights;
3551
- }
3552
- };
3553
- }
3554
- });
3555
-
3556
- // src/utils/format.ts
3557
- function formatDuration(ms) {
3558
- if (ms < 1e3) return `${ms}ms`;
3559
- return `${(ms / 1e3).toFixed(1)}s`;
3560
- }
3561
- function formatSize(bytes) {
3562
- if (bytes < 1024) return `${bytes}B`;
3563
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
3564
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
3565
- }
3566
- function pct(part, total) {
3567
- return total > 0 ? Math.round(part / total * 100) : 0;
3568
- }
3569
- var init_format = __esm({
3570
- "src/utils/format.ts"() {
3571
- "use strict";
3572
- }
3573
- });
3574
-
3575
- // src/analysis/insights/rules/slow.ts
3576
- var slowRule;
3577
- var init_slow = __esm({
3578
- "src/analysis/insights/rules/slow.ts"() {
3579
- "use strict";
3580
- init_format();
3581
- init_constants();
3582
- slowRule = {
3583
- id: "slow",
3584
- check(ctx) {
3585
- const insights = [];
3586
- for (const [ep, g] of ctx.endpointGroups) {
3587
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3588
- const avgMs = Math.round(g.totalDuration / g.total);
3589
- if (avgMs < SLOW_ENDPOINT_THRESHOLD_MS) continue;
3590
- const avgQueryMs = Math.round(g.totalQueryTimeMs / g.total);
3591
- const avgFetchMs = Math.round(g.totalFetchTimeMs / g.total);
3592
- const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
3593
- const parts = [];
3594
- if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
3595
- if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
3596
- if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
3597
- const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
3598
- let detail;
3599
- let slowestMs = 0;
3600
- for (const [, sd] of g.queryShapeDurations) {
3601
- const avgShapeMs = sd.totalMs / sd.count;
3602
- if (avgShapeMs > slowestMs) {
3603
- slowestMs = avgShapeMs;
3604
- detail = `Slowest query: ${sd.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${sd.count}x)`;
3605
- }
3606
- }
3607
- insights.push({
3608
- severity: "warning",
3609
- type: "slow",
3610
- title: "Slow Endpoint",
3611
- desc: `${ep} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
3612
- hint: avgQueryMs >= avgFetchMs && avgQueryMs >= avgAppMs ? "Most time is in database queries. Check the Queries tab for slow or redundant queries." : avgFetchMs >= avgQueryMs && avgFetchMs >= avgAppMs ? "Most time is in outbound HTTP calls. Check if upstream services are slow or if calls can be parallelized." : "Most time is in application code. Profile the handler for CPU-heavy operations or blocking calls.",
3613
- detail,
3614
- nav: "requests"
3615
- });
3616
- }
3617
- return insights;
3618
- }
3619
- };
3620
- }
3621
- });
3622
-
3623
- // src/analysis/insights/rules/query-heavy.ts
3624
- var queryHeavyRule;
3625
- var init_query_heavy = __esm({
3626
- "src/analysis/insights/rules/query-heavy.ts"() {
3627
- "use strict";
3628
- init_constants();
3629
- queryHeavyRule = {
3630
- id: "query-heavy",
3631
- check(ctx) {
3632
- const insights = [];
3633
- for (const [ep, g] of ctx.endpointGroups) {
3634
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3635
- const avgQueries = Math.round(g.queryCount / g.total);
3636
- if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
3637
- insights.push({
3638
- severity: "warning",
3639
- type: "query-heavy",
3640
- title: "Query-Heavy Endpoint",
3641
- desc: `${ep} \u2014 avg ${avgQueries} queries/request`,
3642
- hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches.",
3643
- nav: "queries"
3644
- });
3645
- }
3646
- }
3647
- return insights;
3648
- }
3649
- };
3650
- }
3651
- });
3652
-
3653
- // src/analysis/insights/rules/select-star.ts
3654
- var selectStarRule;
3655
- var init_select_star = __esm({
3656
- "src/analysis/insights/rules/select-star.ts"() {
3657
- "use strict";
3658
- init_query_helpers();
3659
- init_constants();
3660
- init_patterns();
3661
3331
  selectStarRule = {
3662
3332
  id: "select-star",
3663
3333
  check(ctx) {
3664
- const seen = /* @__PURE__ */ new Map();
3334
+ const tableCounts = /* @__PURE__ */ new Map();
3665
3335
  for (const [, reqQueries] of ctx.queriesByReq) {
3666
- for (const q of reqQueries) {
3667
- if (!q.sql) continue;
3668
- const isSelectStar = SELECT_STAR_RE.test(q.sql.trim()) || SELECT_DOT_STAR_RE.test(q.sql);
3336
+ for (const query of reqQueries) {
3337
+ if (!query.sql) continue;
3338
+ const isSelectStar = SELECT_STAR_RE.test(query.sql.trim()) || SELECT_DOT_STAR_RE.test(query.sql);
3669
3339
  if (!isSelectStar) continue;
3670
- const info = getQueryInfo(q);
3671
- const key = info.table || "unknown";
3672
- seen.set(key, (seen.get(key) ?? 0) + 1);
3340
+ const info = getQueryInfo(query);
3341
+ const table = info.table || "unknown";
3342
+ tableCounts.set(table, (tableCounts.get(table) ?? 0) + 1);
3673
3343
  }
3674
3344
  }
3675
3345
  const insights = [];
3676
- for (const [table, count] of seen) {
3346
+ for (const [table, count] of tableCounts) {
3677
3347
  if (count < OVERFETCH_MIN_REQUESTS) continue;
3678
3348
  insights.push({
3679
3349
  severity: "warning",
3680
3350
  type: "select-star",
3681
3351
  title: "SELECT * Query",
3682
3352
  desc: `SELECT * on ${table} \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
3683
- hint: "SELECT * fetches all columns including ones you don\u2019t need. Specify only required columns to reduce data transfer and memory usage.",
3684
- nav: "queries"
3353
+ hint: "SELECT * fetches all columns including ones you don\u2019t need. Specify only required columns to reduce data transfer and memory usage."
3685
3354
  });
3686
3355
  }
3687
3356
  return insights;
3688
3357
  }
3689
3358
  };
3690
- }
3691
- });
3692
-
3693
- // src/analysis/insights/rules/high-rows.ts
3694
- var highRowsRule;
3695
- var init_high_rows = __esm({
3696
- "src/analysis/insights/rules/high-rows.ts"() {
3697
- "use strict";
3698
- init_query_helpers();
3699
- init_constants();
3700
3359
  highRowsRule = {
3701
3360
  id: "high-rows",
3702
3361
  check(ctx) {
3703
3362
  const seen = /* @__PURE__ */ new Map();
3704
3363
  for (const [, reqQueries] of ctx.queriesByReq) {
3705
- for (const q of reqQueries) {
3706
- if (!q.rowCount || q.rowCount <= HIGH_ROW_COUNT) continue;
3707
- const info = getQueryInfo(q);
3364
+ for (const query of reqQueries) {
3365
+ if (!query.rowCount || query.rowCount <= HIGH_ROW_COUNT) continue;
3366
+ const info = getQueryInfo(query);
3708
3367
  const key = `${info.op} ${info.table || "unknown"}`;
3709
3368
  let entry = seen.get(key);
3710
3369
  if (!entry) {
@@ -3712,7 +3371,7 @@ var init_high_rows = __esm({
3712
3371
  seen.set(key, entry);
3713
3372
  }
3714
3373
  entry.count++;
3715
- if (q.rowCount > entry.max) entry.max = q.rowCount;
3374
+ if (query.rowCount > entry.max) entry.max = query.rowCount;
3716
3375
  }
3717
3376
  }
3718
3377
  const insights = [];
@@ -3723,39 +3382,81 @@ var init_high_rows = __esm({
3723
3382
  type: "high-rows",
3724
3383
  title: "Large Result Set",
3725
3384
  desc: `${key} returns ${hrs.max}+ rows (${hrs.count}x)`,
3726
- hint: "Fetching many rows slows responses and wastes memory. Add a LIMIT clause, implement pagination, or filter with a WHERE condition.",
3727
- nav: "queries"
3385
+ hint: "Fetching many rows slows responses and wastes memory. Add a LIMIT clause, implement pagination, or filter with a WHERE condition."
3728
3386
  });
3729
3387
  }
3730
3388
  return insights;
3731
3389
  }
3732
3390
  };
3391
+ queryHeavyRule = {
3392
+ id: "query-heavy",
3393
+ check(ctx) {
3394
+ const insights = [];
3395
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3396
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3397
+ const avgQueries = Math.round(group.queryCount / group.total);
3398
+ if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
3399
+ insights.push({
3400
+ severity: "warning",
3401
+ type: "query-heavy",
3402
+ title: "Query-Heavy Endpoint",
3403
+ desc: `${endpointKey} \u2014 avg ${avgQueries} queries/request`,
3404
+ hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches."
3405
+ });
3406
+ }
3407
+ }
3408
+ return insights;
3409
+ }
3410
+ };
3733
3411
  }
3734
3412
  });
3735
3413
 
3736
- // src/analysis/insights/rules/response-overfetch.ts
3737
- var responseOverfetchRule;
3738
- var init_response_overfetch = __esm({
3739
- "src/analysis/insights/rules/response-overfetch.ts"() {
3414
+ // src/utils/format.ts
3415
+ function formatDuration(ms) {
3416
+ if (ms < 1e3) return `${ms}ms`;
3417
+ return `${(ms / 1e3).toFixed(1)}s`;
3418
+ }
3419
+ function formatSize(bytes) {
3420
+ if (bytes < 1024) return `${bytes}B`;
3421
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
3422
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
3423
+ }
3424
+ function pct(part, total) {
3425
+ return total > 0 ? Math.round(part / total * 100) : 0;
3426
+ }
3427
+ var init_format = __esm({
3428
+ "src/utils/format.ts"() {
3429
+ "use strict";
3430
+ }
3431
+ });
3432
+
3433
+ // src/analysis/insights/rules/response-rules.ts
3434
+ var responseOverfetchRule, largeResponseRule;
3435
+ var init_response_rules = __esm({
3436
+ "src/analysis/insights/rules/response-rules.ts"() {
3740
3437
  "use strict";
3741
3438
  init_endpoint();
3742
3439
  init_response();
3743
3440
  init_http_status();
3441
+ init_format();
3744
3442
  init_patterns();
3443
+ init_log();
3444
+ init_type_guards();
3745
3445
  init_constants();
3746
3446
  responseOverfetchRule = {
3747
3447
  id: "response-overfetch",
3748
3448
  check(ctx) {
3749
3449
  const insights = [];
3750
3450
  const seen = /* @__PURE__ */ new Set();
3751
- for (const r of ctx.nonStatic) {
3752
- if (isErrorStatus(r.statusCode) || !r.responseBody) continue;
3753
- const ep = getEndpointKey(r.method, r.path);
3754
- if (seen.has(ep)) continue;
3451
+ for (const request of ctx.nonStatic) {
3452
+ if (isErrorStatus(request.statusCode) || !request.responseBody) continue;
3453
+ const endpointKey = getEndpointKey(request.method, request.path);
3454
+ if (seen.has(endpointKey)) continue;
3755
3455
  let parsed;
3756
3456
  try {
3757
- parsed = JSON.parse(r.responseBody);
3758
- } catch {
3457
+ parsed = JSON.parse(request.responseBody);
3458
+ } catch (e) {
3459
+ brakitDebug(`json parse: ${getErrorMessage(e)}`);
3759
3460
  continue;
3760
3461
  }
3761
3462
  const target = unwrapResponse(parsed);
@@ -3778,45 +3479,33 @@ var init_response_overfetch = __esm({
3778
3479
  reasons.push(`${fields.length} fields returned`);
3779
3480
  }
3780
3481
  if (reasons.length > 0) {
3781
- seen.add(ep);
3482
+ seen.add(endpointKey);
3782
3483
  insights.push({
3783
3484
  severity: "info",
3784
3485
  type: "response-overfetch",
3785
3486
  title: "Response Overfetch",
3786
- desc: `${ep} \u2014 ${reasons.join(", ")}`,
3787
- hint: "This response returns more data than the client likely needs. Use a DTO or select only required fields to reduce payload size and avoid leaking internal structure.",
3788
- nav: "requests"
3487
+ desc: `${endpointKey} \u2014 ${reasons.join(", ")}`,
3488
+ hint: "This response returns more data than the client likely needs. Use a DTO or select only required fields to reduce payload size and avoid leaking internal structure."
3789
3489
  });
3790
3490
  }
3791
3491
  }
3792
3492
  return insights;
3793
3493
  }
3794
3494
  };
3795
- }
3796
- });
3797
-
3798
- // src/analysis/insights/rules/large-response.ts
3799
- var largeResponseRule;
3800
- var init_large_response = __esm({
3801
- "src/analysis/insights/rules/large-response.ts"() {
3802
- "use strict";
3803
- init_format();
3804
- init_constants();
3805
3495
  largeResponseRule = {
3806
3496
  id: "large-response",
3807
3497
  check(ctx) {
3808
3498
  const insights = [];
3809
- for (const [ep, g] of ctx.endpointGroups) {
3810
- if (g.total < OVERFETCH_MIN_REQUESTS) continue;
3811
- const avgSize = Math.round(g.totalSize / g.total);
3499
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3500
+ if (group.total < OVERFETCH_MIN_REQUESTS) continue;
3501
+ const avgSize = Math.round(group.totalSize / group.total);
3812
3502
  if (avgSize > LARGE_RESPONSE_BYTES) {
3813
3503
  insights.push({
3814
3504
  severity: "info",
3815
3505
  type: "large-response",
3816
3506
  title: "Large Response",
3817
- desc: `${ep} \u2014 avg ${formatSize(avgSize)} response`,
3818
- hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression.",
3819
- nav: "requests"
3507
+ desc: `${endpointKey} \u2014 avg ${formatSize(avgSize)} response`,
3508
+ hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression."
3820
3509
  });
3821
3510
  }
3822
3511
  }
@@ -3826,13 +3515,66 @@ var init_large_response = __esm({
3826
3515
  }
3827
3516
  });
3828
3517
 
3829
- // src/analysis/insights/rules/regression.ts
3830
- var regressionRule;
3831
- var init_regression = __esm({
3832
- "src/analysis/insights/rules/regression.ts"() {
3518
+ // src/analysis/insights/rules/reliability-rules.ts
3519
+ function getAdaptiveSlowThreshold(endpointKey, previousMetrics) {
3520
+ if (!previousMetrics) return null;
3521
+ const ep = previousMetrics.find((m) => m.endpoint === endpointKey);
3522
+ if (!ep || ep.sessions.length < BASELINE_MIN_SESSIONS) return null;
3523
+ const valid = ep.sessions.filter((s) => s.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION);
3524
+ if (valid.length < BASELINE_MIN_SESSIONS) return null;
3525
+ const p95s = valid.map((s) => s.p95DurationMs).sort((a, b) => a - b);
3526
+ const medianP95 = p95s[Math.floor(p95s.length / 2)];
3527
+ return medianP95 * 2;
3528
+ }
3529
+ var errorRule, errorHotspotRule, regressionRule, slowRule;
3530
+ var init_reliability_rules = __esm({
3531
+ "src/analysis/insights/rules/reliability-rules.ts"() {
3833
3532
  "use strict";
3834
3533
  init_format();
3835
3534
  init_constants();
3535
+ errorRule = {
3536
+ id: "error",
3537
+ check(ctx) {
3538
+ if (ctx.errors.length === 0) return [];
3539
+ const insights = [];
3540
+ const groups = /* @__PURE__ */ new Map();
3541
+ for (const error of ctx.errors) {
3542
+ const name = error.name || "Error";
3543
+ groups.set(name, (groups.get(name) ?? 0) + 1);
3544
+ }
3545
+ for (const [name, cnt] of groups) {
3546
+ insights.push({
3547
+ severity: "critical",
3548
+ type: "error",
3549
+ title: "Unhandled Error",
3550
+ desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
3551
+ hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
3552
+ detail: ctx.errors.find((e) => e.name === name)?.message
3553
+ });
3554
+ }
3555
+ return insights;
3556
+ }
3557
+ };
3558
+ errorHotspotRule = {
3559
+ id: "error-hotspot",
3560
+ check(ctx) {
3561
+ const insights = [];
3562
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3563
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3564
+ const errorRate = Math.round(group.errors / group.total * 100);
3565
+ if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
3566
+ insights.push({
3567
+ severity: "critical",
3568
+ type: "error-hotspot",
3569
+ title: "Error Hotspot",
3570
+ desc: `${endpointKey} \u2014 ${errorRate}% error rate (${group.errors}/${group.total} requests)`,
3571
+ hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces."
3572
+ });
3573
+ }
3574
+ }
3575
+ return insights;
3576
+ }
3577
+ };
3836
3578
  regressionRule = {
3837
3579
  id: "regression",
3838
3580
  check(ctx) {
@@ -3851,8 +3593,7 @@ var init_regression = __esm({
3851
3593
  type: "regression",
3852
3594
  title: "Performance Regression",
3853
3595
  desc: `${epMetrics.endpoint} p95 degraded ${formatDuration(prev.p95DurationMs)} \u2192 ${formatDuration(current.p95DurationMs)} (+${p95PctChange}%)`,
3854
- hint: "This endpoint is slower than the previous session. Check if recent code changes added queries or processing.",
3855
- nav: "graph"
3596
+ hint: "This endpoint is slower than the previous session. Check if recent code changes added queries or processing."
3856
3597
  });
3857
3598
  }
3858
3599
  if (prev.avgQueryCount > 0 && current.avgQueryCount > prev.avgQueryCount * QUERY_COUNT_REGRESSION_RATIO) {
@@ -3861,8 +3602,136 @@ var init_regression = __esm({
3861
3602
  type: "regression",
3862
3603
  title: "Query Count Regression",
3863
3604
  desc: `${epMetrics.endpoint} queries/request increased ${prev.avgQueryCount} \u2192 ${current.avgQueryCount}`,
3864
- hint: "This endpoint is making more database queries than before. Check for new N+1 patterns or removed query optimizations.",
3865
- nav: "queries"
3605
+ hint: "This endpoint is making more database queries than before. Check for new N+1 patterns or removed query optimizations."
3606
+ });
3607
+ }
3608
+ }
3609
+ return insights;
3610
+ }
3611
+ };
3612
+ slowRule = {
3613
+ id: "slow",
3614
+ check(ctx) {
3615
+ const insights = [];
3616
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3617
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3618
+ const avgMs = Math.round(group.totalDuration / group.total);
3619
+ const threshold = getAdaptiveSlowThreshold(endpointKey, ctx.previousMetrics);
3620
+ if (threshold === null || avgMs < threshold) continue;
3621
+ const avgQueryMs = Math.round(group.totalQueryTimeMs / group.total);
3622
+ const avgFetchMs = Math.round(group.totalFetchTimeMs / group.total);
3623
+ const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
3624
+ const parts = [];
3625
+ if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
3626
+ if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
3627
+ if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
3628
+ const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
3629
+ let detail;
3630
+ let slowestMs = 0;
3631
+ for (const [, shapeDuration] of group.queryShapeDurations) {
3632
+ const avgShapeMs = shapeDuration.totalMs / shapeDuration.count;
3633
+ if (avgShapeMs > slowestMs) {
3634
+ slowestMs = avgShapeMs;
3635
+ detail = `Slowest query: ${shapeDuration.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${shapeDuration.count}x)`;
3636
+ }
3637
+ }
3638
+ insights.push({
3639
+ severity: "warning",
3640
+ type: "slow",
3641
+ title: "Slow Endpoint",
3642
+ desc: `${endpointKey} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
3643
+ hint: avgQueryMs >= avgFetchMs && avgQueryMs >= avgAppMs ? "Most time is in database queries. Check the Queries tab for slow or redundant queries." : avgFetchMs >= avgQueryMs && avgFetchMs >= avgAppMs ? "Most time is in outbound HTTP calls. Check if upstream services are slow or if calls can be parallelized." : "Most time is in application code. Profile the handler for CPU-heavy operations or blocking calls.",
3644
+ detail
3645
+ });
3646
+ }
3647
+ return insights;
3648
+ }
3649
+ };
3650
+ }
3651
+ });
3652
+
3653
+ // src/analysis/insights/rules/pattern-rules.ts
3654
+ var duplicateRule, crossEndpointRule;
3655
+ var init_pattern_rules = __esm({
3656
+ "src/analysis/insights/rules/pattern-rules.ts"() {
3657
+ "use strict";
3658
+ init_query_helpers();
3659
+ init_endpoint();
3660
+ init_constants();
3661
+ duplicateRule = {
3662
+ id: "duplicate",
3663
+ check(ctx) {
3664
+ const dupCounts = /* @__PURE__ */ new Map();
3665
+ const flowCount = /* @__PURE__ */ new Map();
3666
+ for (const flow of ctx.flows) {
3667
+ if (!flow.requests) continue;
3668
+ const seenInFlow = /* @__PURE__ */ new Set();
3669
+ for (const request of flow.requests) {
3670
+ if (!request.isDuplicate) continue;
3671
+ const deduplicationKey = `${request.method} ${request.label ?? request.path ?? request.url}`;
3672
+ dupCounts.set(deduplicationKey, (dupCounts.get(deduplicationKey) ?? 0) + 1);
3673
+ if (!seenInFlow.has(deduplicationKey)) {
3674
+ seenInFlow.add(deduplicationKey);
3675
+ flowCount.set(deduplicationKey, (flowCount.get(deduplicationKey) ?? 0) + 1);
3676
+ }
3677
+ }
3678
+ }
3679
+ const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
3680
+ const insights = [];
3681
+ for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
3682
+ const duplicate = dupEntries[i];
3683
+ insights.push({
3684
+ severity: "warning",
3685
+ type: "duplicate",
3686
+ title: "Duplicate API Call",
3687
+ desc: `${duplicate.key} loaded ${duplicate.count}x as duplicate across ${duplicate.flows} action${duplicate.flows !== 1 ? "s" : ""}`,
3688
+ hint: "Multiple components independently fetch the same endpoint. Lift the fetch to a parent component, use a data cache, or deduplicate with React Query / SWR."
3689
+ });
3690
+ }
3691
+ return insights;
3692
+ }
3693
+ };
3694
+ crossEndpointRule = {
3695
+ id: "cross-endpoint",
3696
+ check(ctx) {
3697
+ const insights = [];
3698
+ const queryMap = /* @__PURE__ */ new Map();
3699
+ const allEndpoints = /* @__PURE__ */ new Set();
3700
+ for (const [reqId, reqQueries] of ctx.queriesByReq) {
3701
+ const req = ctx.reqById.get(reqId);
3702
+ if (!req) continue;
3703
+ const endpoint = getEndpointKey(req.method, req.path);
3704
+ allEndpoints.add(endpoint);
3705
+ const seenInReq = /* @__PURE__ */ new Set();
3706
+ for (const query of reqQueries) {
3707
+ const shape = getQueryShape(query);
3708
+ let entry = queryMap.get(shape);
3709
+ if (!entry) {
3710
+ entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: query };
3711
+ queryMap.set(shape, entry);
3712
+ }
3713
+ entry.count++;
3714
+ if (!seenInReq.has(shape)) {
3715
+ seenInReq.add(shape);
3716
+ entry.endpoints.add(endpoint);
3717
+ }
3718
+ }
3719
+ }
3720
+ if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
3721
+ for (const [, queryMetric] of queryMap) {
3722
+ if (queryMetric.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
3723
+ if (queryMetric.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
3724
+ const coveragePct = Math.round(queryMetric.endpoints.size / allEndpoints.size * 100);
3725
+ if (coveragePct < CROSS_ENDPOINT_PCT) continue;
3726
+ const info = getQueryInfo(queryMetric.first);
3727
+ const label = info.op + (info.table ? ` ${info.table}` : "");
3728
+ insights.push({
3729
+ severity: "warning",
3730
+ type: "cross-endpoint",
3731
+ title: "Repeated Query Across Endpoints",
3732
+ desc: `${label} runs on ${queryMetric.endpoints.size} of ${allEndpoints.size} endpoints (${coveragePct}%).`,
3733
+ hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
3734
+ detail: `Endpoints: ${[...queryMetric.endpoints].slice(0, 5).join(", ")}${queryMetric.endpoints.size > 5 ? ` +${queryMetric.endpoints.size - 5} more` : ""}. Total: ${queryMetric.count} executions.`
3866
3735
  });
3867
3736
  }
3868
3737
  }
@@ -3881,13 +3750,13 @@ var init_security = __esm({
3881
3750
  id: "security",
3882
3751
  check(ctx) {
3883
3752
  if (!ctx.securityFindings) return [];
3884
- return ctx.securityFindings.map((f) => ({
3885
- severity: f.severity,
3753
+ return ctx.securityFindings.map((finding) => ({
3754
+ severity: finding.severity,
3886
3755
  type: "security",
3887
- title: f.title,
3888
- desc: f.desc,
3889
- hint: f.hint,
3890
- nav: "security"
3756
+ title: finding.title,
3757
+ desc: finding.desc,
3758
+ hint: finding.hint,
3759
+ detail: finding.detail
3891
3760
  }));
3892
3761
  }
3893
3762
  };
@@ -3898,19 +3767,10 @@ var init_security = __esm({
3898
3767
  var init_rules2 = __esm({
3899
3768
  "src/analysis/insights/rules/index.ts"() {
3900
3769
  "use strict";
3901
- init_n1();
3902
- init_cross_endpoint();
3903
- init_redundant_query();
3904
- init_error();
3905
- init_error_hotspot();
3906
- init_duplicate();
3907
- init_slow();
3908
- init_query_heavy();
3909
- init_select_star();
3910
- init_high_rows();
3911
- init_response_overfetch();
3912
- init_large_response();
3913
- init_regression();
3770
+ init_query_rules();
3771
+ init_response_rules();
3772
+ init_reliability_rules();
3773
+ init_pattern_rules();
3914
3774
  init_security();
3915
3775
  }
3916
3776
  });
@@ -3969,8 +3829,7 @@ function insightToIssue(insight) {
3969
3829
  desc: insight.desc,
3970
3830
  hint: insight.hint,
3971
3831
  detail: insight.detail,
3972
- endpoint: extractEndpointFromDesc(insight.desc) ?? void 0,
3973
- nav: insight.nav
3832
+ endpoint: extractEndpointFromDesc(insight.desc) ?? void 0
3974
3833
  };
3975
3834
  }
3976
3835
  function securityFindingToIssue(finding) {
@@ -3981,8 +3840,8 @@ function securityFindingToIssue(finding) {
3981
3840
  title: finding.title,
3982
3841
  desc: finding.desc,
3983
3842
  hint: finding.hint,
3984
- endpoint: finding.endpoint,
3985
- nav: "security"
3843
+ detail: finding.detail,
3844
+ endpoint: finding.endpoint
3986
3845
  };
3987
3846
  }
3988
3847
  var init_issue_mappers = __esm({
@@ -3997,7 +3856,7 @@ var AnalysisEngine;
3997
3856
  var init_engine = __esm({
3998
3857
  "src/analysis/engine.ts"() {
3999
3858
  "use strict";
4000
- init_limits();
3859
+ init_config();
4001
3860
  init_disposable();
4002
3861
  init_group();
4003
3862
  init_rules();
@@ -4006,8 +3865,8 @@ var init_engine = __esm({
4006
3865
  init_issue_id();
4007
3866
  init_prepare();
4008
3867
  AnalysisEngine = class {
4009
- constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
4010
- this.registry = registry;
3868
+ constructor(services, debounceMs = ANALYSIS_DEBOUNCE_MS) {
3869
+ this.services = services;
4011
3870
  this.debounceMs = debounceMs;
4012
3871
  this.cachedInsights = [];
4013
3872
  this.cachedFindings = [];
@@ -4016,7 +3875,7 @@ var init_engine = __esm({
4016
3875
  this.scanner = createDefaultScanner();
4017
3876
  }
4018
3877
  start() {
4019
- const bus = this.registry.get("event-bus");
3878
+ const bus = this.services.bus;
4020
3879
  this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
4021
3880
  this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
4022
3881
  this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
@@ -4043,12 +3902,12 @@ var init_engine = __esm({
4043
3902
  }, this.debounceMs);
4044
3903
  }
4045
3904
  recompute() {
4046
- const allRequests = this.registry.get("request-store").getAll();
4047
- const queries = this.registry.get("query-store").getAll();
4048
- const errors = this.registry.get("error-store").getAll();
4049
- const logs = this.registry.get("log-store").getAll();
4050
- const fetches = this.registry.get("fetch-store").getAll();
4051
- const requests = windowByEndpoint(allRequests);
3905
+ const allRequests = this.services.requestStore.getAll();
3906
+ const queries = this.services.queryStore.getAll();
3907
+ const errors = this.services.errorStore.getAll();
3908
+ const logs = this.services.logStore.getAll();
3909
+ const fetches = this.services.fetchStore.getAll();
3910
+ const requests = keepRecentPerEndpoint(allRequests);
4052
3911
  const flows = groupRequestsIntoFlows(requests);
4053
3912
  this.cachedFindings = this.scanner.scan({ requests, logs });
4054
3913
  this.cachedInsights = computeInsights({
@@ -4057,33 +3916,29 @@ var init_engine = __esm({
4057
3916
  errors,
4058
3917
  flows,
4059
3918
  fetches,
4060
- previousMetrics: this.registry.get("metrics-store").getAll(),
3919
+ previousMetrics: this.services.metricsStore.getAll(),
4061
3920
  securityFindings: this.cachedFindings
4062
3921
  });
4063
- if (this.registry.has("issue-store")) {
4064
- const issueStore = this.registry.get("issue-store");
4065
- for (const finding of this.cachedFindings) {
4066
- issueStore.upsert(securityFindingToIssue(finding), "passive");
4067
- }
4068
- for (const insight of this.cachedInsights) {
4069
- issueStore.upsert(insightToIssue(insight), "passive");
4070
- }
4071
- const currentIssueIds = /* @__PURE__ */ new Set();
4072
- for (const finding of this.cachedFindings) {
4073
- currentIssueIds.add(computeIssueId(securityFindingToIssue(finding)));
4074
- }
4075
- for (const insight of this.cachedInsights) {
4076
- currentIssueIds.add(computeIssueId(insightToIssue(insight)));
4077
- }
4078
- const activeEndpoints = extractActiveEndpoints(allRequests);
4079
- issueStore.reconcile(currentIssueIds, activeEndpoints);
4080
- const update = {
4081
- insights: this.cachedInsights,
4082
- findings: this.cachedFindings,
4083
- issues: issueStore.getAll()
4084
- };
4085
- this.registry.get("event-bus").emit("analysis:updated", update);
3922
+ const issueStore = this.services.issueStore;
3923
+ const currentIssueIds = /* @__PURE__ */ new Set();
3924
+ for (const finding of this.cachedFindings) {
3925
+ const issue = securityFindingToIssue(finding);
3926
+ issueStore.upsert(issue, "passive");
3927
+ currentIssueIds.add(computeIssueId(issue));
4086
3928
  }
3929
+ for (const insight of this.cachedInsights) {
3930
+ const issue = insightToIssue(insight);
3931
+ issueStore.upsert(issue, "passive");
3932
+ currentIssueIds.add(computeIssueId(issue));
3933
+ }
3934
+ const activeEndpoints = extractActiveEndpoints(allRequests);
3935
+ issueStore.reconcile(currentIssueIds, activeEndpoints);
3936
+ const update = {
3937
+ insights: this.cachedInsights,
3938
+ findings: this.cachedFindings,
3939
+ issues: issueStore.getAll()
3940
+ };
3941
+ this.services.bus.emit("analysis:updated", update);
4087
3942
  }
4088
3943
  };
4089
3944
  }
@@ -4101,7 +3956,7 @@ var init_src = __esm({
4101
3956
  init_engine();
4102
3957
  init_insights2();
4103
3958
  init_insights();
4104
- VERSION = "0.8.7";
3959
+ VERSION = "0.9.1";
4105
3960
  }
4106
3961
  });
4107
3962
 
@@ -4121,11 +3976,13 @@ function getBaseStyles() {
4121
3976
  --red:#dc2626;
4122
3977
  --cyan:#0891b2;
4123
3978
  --green-bg:rgba(22,163,74,0.08);--green-bg-subtle:rgba(22,163,74,0.05);--green-border:rgba(22,163,74,0.2);--green-border-subtle:rgba(22,163,74,0.15);
3979
+ --amber-bg:rgba(217,119,6,0.07);--red-bg:rgba(220,38,38,0.07);--blue-bg:rgba(37,99,235,0.08);--cyan-bg:rgba(8,145,178,0.07);
4124
3980
  --sidebar-width:232px;--header-height:52px;
4125
3981
  --radius:8px;--radius-sm:6px;
4126
- --shadow-sm:0 1px 2px rgba(0,0,0,0.05);
4127
- --shadow-md:0 1px 3px rgba(0,0,0,0.08),0 1px 2px rgba(0,0,0,0.04);
4128
- --shadow-lg:0 4px 12px rgba(0,0,0,0.08),0 1px 4px rgba(0,0,0,0.04);
3982
+ --shadow-sm:0 1px 3px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.03);
3983
+ --shadow-md:0 2px 6px rgba(0,0,0,0.08),0 1px 3px rgba(0,0,0,0.04);
3984
+ --shadow-lg:0 4px 16px rgba(0,0,0,0.1),0 2px 6px rgba(0,0,0,0.05);
3985
+ --transition:0.15s ease;
4129
3986
  --breakdown-db:#6366f1;--breakdown-fetch:#f59e0b;--breakdown-app:#94a3b8;
4130
3987
  --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
4131
3988
  --sans:Inter,system-ui,-apple-system,sans-serif;
@@ -4243,8 +4100,8 @@ function getFlowStyles() {
4243
4100
  .flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
4244
4101
  .flow-badge-pill{font-size:11px;flex-shrink:0;font-family:var(--mono);font-weight:600;padding:2px 10px;border-radius:10px;text-align:center}
4245
4102
  .flow-badge-pill.badge-clean{background:var(--green-bg);color:var(--green)}
4246
- .flow-badge-pill.badge-warn{background:rgba(217,119,6,0.07);color:var(--amber)}
4247
- .flow-badge-pill.badge-error{background:rgba(220,38,38,0.07);color:var(--red)}
4103
+ .flow-badge-pill.badge-warn{background:var(--amber-bg);color:var(--amber)}
4104
+ .flow-badge-pill.badge-error{background:var(--red-bg);color:var(--red)}
4248
4105
  .flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
4249
4106
 
4250
4107
  /* Flow expand panel */
@@ -4263,23 +4120,23 @@ function getFlowStyles() {
4263
4120
  /* Method badges */
4264
4121
  .method-badge{display:inline-flex;align-items:center;justify-content:center;padding:3px 8px;border-radius:5px;font-size:10px;font-weight:700;font-family:var(--mono);letter-spacing:.3px;flex-shrink:0}
4265
4122
  .method-badge-GET{background:var(--green-bg);color:var(--green)}
4266
- .method-badge-POST{background:rgba(37,99,235,0.08);color:var(--blue)}
4267
- .method-badge-PUT,.method-badge-PATCH{background:rgba(217,119,6,0.08);color:var(--amber)}
4268
- .method-badge-DELETE{background:rgba(220,38,38,0.08);color:var(--red)}
4123
+ .method-badge-POST{background:var(--blue-bg);color:var(--blue)}
4124
+ .method-badge-PUT,.method-badge-PATCH{background:var(--amber-bg);color:var(--amber)}
4125
+ .method-badge-DELETE{background:var(--red-bg);color:var(--red)}
4269
4126
  .method-badge-HEAD,.method-badge-OPTIONS{background:var(--bg-muted);color:var(--text-muted)}
4270
4127
 
4271
4128
  /* Status pills */
4272
4129
  .status-pill{display:inline-flex;align-items:center;padding:1px 7px;border-radius:4px;font-size:11px;font-weight:600;font-family:var(--mono);flex-shrink:0}
4273
4130
  .status-pill-2xx{background:var(--green-bg);color:var(--green)}
4274
- .status-pill-3xx{background:rgba(8,145,178,0.07);color:var(--cyan)}
4275
- .status-pill-4xx{background:rgba(217,119,6,0.07);color:var(--amber)}
4276
- .status-pill-5xx{background:rgba(220,38,38,0.07);color:var(--red)}
4131
+ .status-pill-3xx{background:var(--cyan-bg);color:var(--cyan)}
4132
+ .status-pill-4xx{background:var(--amber-bg);color:var(--amber)}
4133
+ .status-pill-5xx{background:var(--red-bg);color:var(--red)}
4277
4134
 
4278
4135
  .traffic-card-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;font-size:13px}
4279
4136
  .traffic-card-path.is-dup{color:var(--text-muted);font-weight:400}
4280
4137
  .traffic-card-dur{color:var(--text-muted);font-size:12px;flex-shrink:0}
4281
4138
  .traffic-card-size{color:var(--text-muted);font-size:11px;flex-shrink:0}
4282
- .traffic-card-dup{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:rgba(217,119,6,0.07);padding:1px 7px;border-radius:4px}
4139
+ .traffic-card-dup{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:var(--amber-bg);padding:1px 7px;border-radius:4px}
4283
4140
 
4284
4141
  /* Body toggles */
4285
4142
  .traffic-body{padding:0;margin-top:8px}
@@ -4308,7 +4165,7 @@ function getFlowStyles() {
4308
4165
  .flow-subreq .subreq-label.is-dup{color:var(--text-muted);font-weight:400}
4309
4166
  .flow-subreq .subreq-status{flex-shrink:0}
4310
4167
  .flow-subreq .subreq-dur{color:var(--text-muted);font-size:12px;text-align:right;flex-shrink:0}
4311
- .flow-subreq .subreq-dup-tag{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:rgba(217,119,6,0.07);padding:1px 7px;border-radius:4px}
4168
+ .flow-subreq .subreq-dup-tag{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:var(--amber-bg);padding:1px 7px;border-radius:4px}
4312
4169
  .flow-subreq-detail{display:none;padding:12px 0;border-bottom:1px solid var(--border-subtle)}
4313
4170
  .flow-subreq-detail.open{display:block}
4314
4171
 
@@ -4337,6 +4194,41 @@ function getFlowStyles() {
4337
4194
  /* Strict Mode duplicate banner */
4338
4195
  .strict-mode-dupe{opacity:0.55}
4339
4196
  .strict-mode-banner{font-size:11px;color:var(--text-muted);padding:6px 0 0;font-family:var(--mono)}
4197
+
4198
+ /* Flow detail tabs */
4199
+ .flow-detail-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--border)}
4200
+ .flow-tab{padding:8px 16px;font-size:12px;font-family:var(--mono);font-weight:600;color:var(--text-muted);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .15s;letter-spacing:.3px}
4201
+ .flow-tab:hover{color:var(--text)}
4202
+ .flow-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
4203
+
4204
+ /* Waterfall chart \u2014 request bars on time axis, sub-events as text rows */
4205
+ .flow-waterfall{padding:0;font-family:var(--mono);font-size:11px}
4206
+ .wf-time-axis{display:flex;justify-content:space-between;font-size:9px;color:var(--text-muted);padding:0 0 6px;margin-left:180px;margin-right:56px;border-bottom:1px solid var(--border);margin-bottom:2px}
4207
+ .wf-rows{display:flex;flex-direction:column;gap:0}
4208
+
4209
+ /* Request group \u2014 request bar + its sub-events */
4210
+ .wf-request-group{border-bottom:1px solid var(--border-subtle);padding:2px 0}
4211
+ .wf-request-group:last-child{border-bottom:none}
4212
+
4213
+ /* Request row \u2014 label | bar on time axis | duration */
4214
+ .wf-req-row{display:flex;align-items:center;gap:0;height:24px;transition:background .1s}
4215
+ .wf-req-row:hover{background:var(--bg-hover)}
4216
+ .wf-req-label{width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;padding:0 10px 0 0;font-size:11px}
4217
+ .wf-bar-track{flex:1;position:relative;height:14px;min-width:0;overflow:hidden}
4218
+ .wf-bar{position:absolute;top:1px;height:12px;border-radius:3px;opacity:0.8;min-width:3px}
4219
+ .wf-req-row:hover .wf-bar{opacity:1}
4220
+ .wf-req-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-muted);font-size:10px;padding-left:8px}
4221
+
4222
+ /* Sub-event rows \u2014 same layout as request rows: label | bar track | duration */
4223
+ .wf-sub-row{display:flex;align-items:center;gap:0;height:20px;transition:background .1s}
4224
+ .wf-sub-row:hover{background:var(--bg-hover)}
4225
+ .wf-sub-label{width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);font-size:10px;padding-left:14px;display:flex;align-items:center;gap:6px}
4226
+ .wf-sub-dot{width:6px;height:6px;border-radius:2px;flex-shrink:0}
4227
+ .wf-sub-bar-sized{height:8px !important;top:3px !important;opacity:0.65}
4228
+ .wf-sub-row:hover .wf-sub-bar-sized{opacity:0.9}
4229
+ .wf-sub-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-dim);font-size:9px;padding-left:8px}
4230
+
4231
+ .wf-loading{color:var(--text-muted);padding:12px 0;font-size:11px;font-family:var(--mono)}
4340
4232
  `;
4341
4233
  }
4342
4234
  var init_flows = __esm({
@@ -4445,22 +4337,39 @@ function getPerformanceStyles() {
4445
4337
  .perf-badge-lg{padding:4px 12px;font-size:13px;border-radius:var(--radius-sm)}
4446
4338
  .perf-badge-sm{padding:1px 6px;font-size:9px}
4447
4339
 
4448
- /* Overview: endpoint list with inline scatter charts */
4449
- .perf-endpoint-list{padding:16px 28px;display:flex;flex-direction:column;gap:8px}
4450
- .perf-endpoint-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:14px 20px 8px;cursor:pointer;transition:all .15s;box-shadow:var(--shadow-sm)}
4451
- .perf-endpoint-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md)}
4452
- .perf-ep-header{display:flex;align-items:center;gap:12px;margin-bottom:8px;flex-wrap:wrap}
4453
- .perf-ep-name{flex:1;font-family:var(--mono);font-size:13px;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:120px}
4454
- .perf-ep-stats{display:flex;align-items:center;gap:14px;flex-shrink:0}
4455
- .perf-ep-stat{font-size:11px;font-family:var(--mono);color:var(--text-muted)}
4456
- .perf-ep-stat-err{color:var(--red)}
4457
- .perf-ep-stat-warn{color:var(--amber)}
4458
- .perf-ep-stat-muted{color:var(--text-dim)}
4459
- .perf-inline-canvas{width:100%;height:88px;border-radius:var(--radius-sm);background:var(--bg-muted);border:1px solid var(--border);display:block}
4340
+ /* Overview: summary cards */
4341
+ .perf-overview{padding:16px 28px}
4342
+ .perf-summary-row{display:flex;gap:8px;margin-bottom:16px}
4343
+ .perf-summary-card{flex:1;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;display:flex;flex-direction:column;gap:4px;box-shadow:var(--shadow-sm)}
4344
+ .perf-summary-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-family:var(--sans);font-weight:600}
4345
+ .perf-summary-value{font-size:20px;font-weight:700;font-family:var(--mono);color:var(--text)}
4346
+ .perf-summary-value-sm{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4347
+
4348
+ /* Shared table styles */
4349
+ .perf-table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px}
4350
+ .perf-table thead th{text-align:left;font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600;font-family:var(--sans);padding:10px 14px;border-bottom:2px solid var(--border);white-space:nowrap}
4351
+ .perf-table tbody td{padding:11px 14px;border-bottom:1px solid var(--border-subtle);color:var(--text)}
4352
+ .perf-table-row{cursor:pointer;transition:background var(--transition, .15s ease)}
4353
+ .perf-table-row:hover{background:var(--bg-hover)}
4354
+ .perf-table tbody tr:last-child td{border-bottom:none}
4355
+ .perf-th-right{text-align:right !important}
4356
+ .perf-th-center{text-align:center !important}
4357
+ .perf-td-right{text-align:right}
4358
+ .perf-td-center{text-align:center}
4359
+ .perf-td-name{font-weight:600;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4360
+ .perf-td-muted{color:var(--text-dim)}
4361
+ .perf-row-err{background:var(--red-bg)}
4362
+ .perf-row-err:hover{background:rgba(220,38,38,0.1)}
4363
+
4364
+ /* Heat map table wrapper */
4365
+ .perf-heatmap{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm)}
4366
+ .perf-hm-p95{display:inline-flex;align-items:center;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;border:1px solid}
4367
+ .perf-hm-split-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;background:var(--bg-muted);width:100%;min-width:80px}
4460
4368
 
4461
4369
  /* Detail view */
4462
4370
  .perf-detail-header{padding:20px 28px 16px;border-bottom:1px solid var(--border-subtle)}
4463
4371
  .perf-detail-title{display:flex;align-items:center;gap:12px;font-size:17px;font-weight:600;color:var(--text);font-family:var(--mono)}
4372
+ .perf-baseline-hint{font-size:11px;font-weight:400;color:var(--text-muted);padding:2px 8px;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius-sm)}
4464
4373
  .perf-metric-row{display:flex;gap:4px;padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
4465
4374
  .perf-metric-card{flex:1;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px 16px;display:flex;flex-direction:column;gap:4px}
4466
4375
  .perf-metric-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-family:var(--sans);font-weight:600}
@@ -4495,22 +4404,42 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
4495
4404
  .perf-canvas{border-radius:var(--radius);background:var(--bg-muted);border:1px solid var(--border)}
4496
4405
  .perf-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:10px}
4497
4406
 
4498
- /* Request history table */
4407
+ /* Request history */
4499
4408
  .perf-history-wrap{padding:0 28px 20px}
4500
- .perf-history-wrap .col-header{padding:8px 0;margin:0;position:static;background:var(--bg);gap:0}
4501
- .perf-hist-row{display:flex;align-items:center;padding:10px 0;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
4502
- .perf-hist-row:hover{background:var(--bg-hover);margin:0 -28px;padding-left:28px;padding-right:28px}
4503
- .perf-hist-row-err{background:rgba(220,38,38,0.04)}
4504
- .perf-hist-row-err:hover{background:rgba(220,38,38,0.08)}
4505
- .perf-hist-row-hl{background:rgba(37,99,235,0.1);margin:0 -28px;padding-left:28px;padding-right:28px;border-left:3px solid #4ade80}
4506
- .perf-hist-row-hl.perf-hist-row-err{background:rgba(220,38,38,0.12);border-left-color:#f87171}
4507
- .perf-col{flex-shrink:0;border-right:1px solid var(--border-subtle);padding-right:16px;margin-right:16px}
4508
- .perf-col:last-child{border-right:none;padding-right:0;margin-right:0}
4509
- .perf-col-date{width:100px;color:var(--text-dim)}
4510
- .perf-col-health{width:60px;display:flex;align-items:center}
4511
- .perf-col-avg{width:70px;color:var(--text)}
4512
- .perf-col-status{width:50px;text-align:center}
4513
- .perf-col-qpr{width:60px;text-align:right;color:var(--text-dim)}
4409
+ .perf-hist-row-hl{background:rgba(37,99,235,0.1) !important;border-left:3px solid #4ade80}
4410
+
4411
+ /* Callers section */
4412
+ .perf-callers{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
4413
+ .perf-callers-list{display:flex;flex-direction:column;gap:0}
4414
+ .perf-caller-row{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
4415
+ .perf-caller-row:last-child{border-bottom:none}
4416
+ .perf-caller-name{flex:1;font-weight:500;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4417
+ .perf-caller-count{color:var(--text-muted);font-size:11px;flex-shrink:0}
4418
+ .perf-caller-avg{color:var(--text-dim);font-size:11px;flex-shrink:0}
4419
+
4420
+ /* Query breakdown section */
4421
+ .perf-queries{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
4422
+ .perf-queries-loading{font-size:11px;color:var(--text-muted);font-family:var(--mono)}
4423
+ .perf-queries-list{display:flex;flex-direction:column;gap:0}
4424
+ .perf-query-row{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
4425
+ .perf-query-row:last-child{border-bottom:none}
4426
+ .perf-query-label{flex:1;font-weight:500;color:var(--accent);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4427
+ .perf-query-avg{color:var(--text-muted);font-size:11px;flex-shrink:0}
4428
+ .perf-query-count{color:var(--text-dim);font-size:11px;flex-shrink:0}
4429
+
4430
+ /* Session trends */
4431
+ .perf-trends{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
4432
+ .perf-trends-list{display:flex;flex-direction:column;gap:0}
4433
+ .perf-trend-row{display:flex;align-items:center;gap:14px;padding:8px 12px;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
4434
+ .perf-trend-row:last-child{border-bottom:none}
4435
+ .perf-trend-current{background:var(--bg-muted);border-radius:var(--radius-sm);font-weight:600}
4436
+ .perf-trend-time{width:80px;color:var(--text-dim);font-size:11px;flex-shrink:0}
4437
+ .perf-trend-p95{flex-shrink:0}
4438
+ .perf-trend-reqs{color:var(--text-muted);font-size:11px;flex-shrink:0}
4439
+ .perf-trend-errs{font-size:11px;flex-shrink:0}
4440
+ .perf-trend-arrow{font-size:10px;font-weight:600;flex-shrink:0}
4441
+ .perf-trend-slower{color:var(--red)}
4442
+ .perf-trend-faster{color:var(--green)}
4514
4443
  `;
4515
4444
  }
4516
4445
  var init_graph = __esm({
@@ -4526,9 +4455,10 @@ function getOverviewStyles() {
4526
4455
  .ov-container{padding:24px 28px}
4527
4456
 
4528
4457
  /* Summary banner */
4529
- .ov-summary{display:flex;gap:24px;padding:16px 20px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:24px;flex-wrap:wrap;box-shadow:var(--shadow-sm)}
4530
- .ov-stat{display:flex;flex-direction:column;gap:2px}
4531
- .ov-stat-value{font-size:19px;font-weight:700;font-family:var(--mono);color:var(--text)}
4458
+ .ov-summary{display:flex;gap:10px;margin-bottom:24px;flex-wrap:wrap}
4459
+ .ov-stat{display:flex;flex-direction:column;gap:4px;flex:1;min-width:100px;padding:16px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-sm);transition:box-shadow var(--transition, .15s ease)}
4460
+ .ov-stat:hover{box-shadow:var(--shadow-md)}
4461
+ .ov-stat-value{font-size:22px;font-weight:700;font-family:var(--mono);color:var(--text);line-height:1.2}
4532
4462
  .ov-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
4533
4463
 
4534
4464
  /* Section header */
@@ -4537,8 +4467,8 @@ function getOverviewStyles() {
4537
4467
 
4538
4468
  /* Insight cards */
4539
4469
  .ov-cards{display:flex;flex-direction:column;gap:8px}
4540
- .ov-card{display:flex;align-items:flex-start;gap:14px;padding:14px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;transition:all .15s;box-shadow:var(--shadow-sm)}
4541
- .ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md)}
4470
+ .ov-card{display:flex;align-items:flex-start;gap:14px;padding:16px 20px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;transition:all var(--transition, .15s ease);box-shadow:var(--shadow-sm)}
4471
+ .ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md);transform:translateY(-1px)}
4542
4472
  .ov-card-icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:10px;border-radius:50%;margin-top:2px}
4543
4473
  .ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
4544
4474
  .ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
@@ -4547,6 +4477,7 @@ function getOverviewStyles() {
4547
4477
  .ov-card-body{flex:1;min-width:0}
4548
4478
  .ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
4549
4479
  .ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
4480
+ .ov-card-detail{font-size:11px;font-family:var(--mono);color:var(--text-muted);margin-top:6px;padding:8px 10px;background:var(--bg-muted);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);line-height:1.5}
4550
4481
  .ov-card-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
4551
4482
  .ov-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;margin-top:2px;font-family:var(--mono);transition:transform .15s}
4552
4483
 
@@ -4842,7 +4773,7 @@ function getTimelineStyles() {
4842
4773
  }
4843
4774
  `;
4844
4775
  }
4845
- var init_timeline2 = __esm({
4776
+ var init_timeline = __esm({
4846
4777
  "src/dashboard/styles/timeline.ts"() {
4847
4778
  "use strict";
4848
4779
  }
@@ -4862,7 +4793,7 @@ var init_styles = __esm({
4862
4793
  init_graph();
4863
4794
  init_overview();
4864
4795
  init_security2();
4865
- init_timeline2();
4796
+ init_timeline();
4866
4797
  }
4867
4798
  });
4868
4799
 
@@ -4915,7 +4846,7 @@ var init_page = __esm({
4915
4846
  });
4916
4847
 
4917
4848
  // src/telemetry/config.ts
4918
- import { homedir as homedir2 } from "os";
4849
+ import { homedir as homedir2, platform } from "os";
4919
4850
  import { join as join3 } from "path";
4920
4851
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
4921
4852
  import { randomUUID as randomUUID5 } from "crypto";
@@ -4930,11 +4861,17 @@ function readConfig() {
4930
4861
  function writeConfig(config) {
4931
4862
  try {
4932
4863
  if (!existsSync5(CONFIG_DIR))
4933
- mkdirSync3(CONFIG_DIR, { recursive: true, mode: DIR_MODE_OWNER_ONLY });
4934
- writeFileSync3(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
4935
- mode: FILE_MODE_OWNER_ONLY
4936
- });
4937
- } catch {
4864
+ mkdirSync3(CONFIG_DIR, { recursive: true, ...IS_WINDOWS ? {} : { mode: DIR_MODE_OWNER_ONLY } });
4865
+ writeFileSync3(
4866
+ CONFIG_PATH,
4867
+ JSON.stringify(config, null, 2) + "\n",
4868
+ IS_WINDOWS ? {} : { mode: FILE_MODE_OWNER_ONLY }
4869
+ );
4870
+ } catch (err) {
4871
+ if (process.env.BRAKIT_DEBUG) {
4872
+ process.stderr.write(`[brakit] config write failed: ${err?.message ?? err}
4873
+ `);
4874
+ }
4938
4875
  }
4939
4876
  }
4940
4877
  function getOrCreateConfig() {
@@ -4956,11 +4893,12 @@ function isTelemetryEnabled() {
4956
4893
  cachedEnabled = readConfig()?.telemetry ?? true;
4957
4894
  return cachedEnabled;
4958
4895
  }
4959
- var CONFIG_DIR, CONFIG_PATH, cachedEnabled;
4960
- var init_config = __esm({
4896
+ var IS_WINDOWS, CONFIG_DIR, CONFIG_PATH, cachedEnabled;
4897
+ var init_config2 = __esm({
4961
4898
  "src/telemetry/config.ts"() {
4962
4899
  "use strict";
4963
- init_network();
4900
+ init_features();
4901
+ IS_WINDOWS = platform() === "win32";
4964
4902
  CONFIG_DIR = join3(homedir2(), ".brakit");
4965
4903
  CONFIG_PATH = join3(CONFIG_DIR, "config.json");
4966
4904
  cachedEnabled = null;
@@ -4968,9 +4906,49 @@ var init_config = __esm({
4968
4906
  });
4969
4907
 
4970
4908
  // src/telemetry/index.ts
4971
- import { platform, release, arch } from "os";
4909
+ import { platform as platform2, release, arch } from "os";
4972
4910
  import { spawn } from "child_process";
4911
+ function commonProperties() {
4912
+ return {
4913
+ brakit_version: VERSION,
4914
+ node_version: process.version,
4915
+ os: `${platform2()}-${release()}`,
4916
+ arch: arch(),
4917
+ $lib: "brakit",
4918
+ $process_person_profile: false,
4919
+ $geoip_disable: true
4920
+ };
4921
+ }
4922
+ function sendToPosthog(event, properties) {
4923
+ if (!isTelemetryEnabled()) return;
4924
+ const config = getOrCreateConfig();
4925
+ const payload = {
4926
+ api_key: POSTHOG_KEY,
4927
+ event,
4928
+ distinct_id: config.anonymousId,
4929
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4930
+ properties: { ...commonProperties(), ...properties }
4931
+ };
4932
+ try {
4933
+ const body = JSON.stringify(payload);
4934
+ const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
4935
+ const child = spawn(
4936
+ process.execPath,
4937
+ [
4938
+ "-e",
4939
+ `fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
4940
+ ],
4941
+ { detached: true, stdio: "ignore" }
4942
+ );
4943
+ child.unref();
4944
+ } catch {
4945
+ }
4946
+ }
4947
+ function trackEvent(event, properties) {
4948
+ sendToPosthog(event, { sdk: "node", ...properties });
4949
+ }
4973
4950
  function initSession(framework, packageManager, isCustomCommand, adapters) {
4951
+ getOrCreateConfig();
4974
4952
  session.startTime = Date.now();
4975
4953
  session.framework = framework;
4976
4954
  session.packageManager = packageManager;
@@ -4990,7 +4968,25 @@ function recordTabViewed(tab) {
4990
4968
  session.tabsViewed.add(tab);
4991
4969
  }
4992
4970
  function recordDashboardOpened() {
4971
+ if (session.dashboardOpened) return;
4993
4972
  session.dashboardOpened = true;
4973
+ session.dashboardOpenedAt = Date.now();
4974
+ trackEvent(TELEMETRY_EVENT_DASHBOARD_VIEWED, {
4975
+ time_to_dashboard_ms: session.startTime > 0 ? Date.now() - session.startTime : null,
4976
+ request_count_at_open: session.requestCount
4977
+ });
4978
+ }
4979
+ function recordSetupCompleted(info) {
4980
+ session.frameworkCandidates = info.frameworkCandidates;
4981
+ session.adaptersFailed = info.adaptersFailed;
4982
+ session.setupDurationMs = info.setupDurationMs;
4983
+ session.setupSucceeded = true;
4984
+ }
4985
+ function recordFirstRequest() {
4986
+ if (!session.firstRequestAt) session.firstRequestAt = Date.now();
4987
+ }
4988
+ function recordExitReason(reason) {
4989
+ if (session.exitReason === "unknown") session.exitReason = reason;
4994
4990
  }
4995
4991
  function speedBucket(ms) {
4996
4992
  if (ms === 0) return "none";
@@ -5001,12 +4997,11 @@ function speedBucket(ms) {
5001
4997
  }
5002
4998
  return `>${t[t.length - 1]}ms`;
5003
4999
  }
5004
- function trackSession(registry) {
5000
+ function trackSession(services) {
5005
5001
  if (!isTelemetryEnabled()) return;
5006
5002
  const isFirstSession = readConfig() === null;
5007
- const config = getOrCreateConfig();
5008
- const metricsStore = registry.get("metrics-store");
5009
- const analysisEngine = registry.get("analysis-engine");
5003
+ const metricsStore = services.metricsStore;
5004
+ const analysisEngine = services.analysisEngine;
5010
5005
  const live = metricsStore.getLiveEndpoints();
5011
5006
  const insights = analysisEngine.getInsights();
5012
5007
  const findings = analysisEngine.getFindings();
@@ -5018,64 +5013,49 @@ function trackSession(registry) {
5018
5013
  totalDuration += ep.summary.p95Ms * ep.summary.totalRequests;
5019
5014
  if (ep.summary.p95Ms > slowestP95) slowestP95 = ep.summary.p95Ms;
5020
5015
  }
5021
- const payload = {
5022
- api_key: POSTHOG_KEY,
5023
- event: "session",
5024
- distinct_id: config.anonymousId,
5025
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5026
- properties: {
5027
- brakit_version: VERSION,
5028
- node_version: process.version,
5029
- os: `${platform()}-${release()}`,
5030
- arch: arch(),
5031
- framework: session.framework,
5032
- package_manager: session.packageManager,
5033
- is_custom_command: session.isCustomCommand,
5034
- first_session: isFirstSession,
5035
- adapters_detected: session.adapters,
5036
- request_count: session.requestCount,
5037
- error_count: registry.get("error-store").getAll().length,
5038
- query_count: registry.get("query-store").getAll().length,
5039
- fetch_count: registry.get("fetch-store").getAll().length,
5040
- insight_count: insights.length,
5041
- finding_count: findings.length,
5042
- insight_types: [...session.insightTypes],
5043
- rules_triggered: [...session.rulesTriggered],
5044
- endpoint_count: live.length,
5045
- avg_duration_ms: totalRequests > 0 ? Math.round(totalDuration / totalRequests) : 0,
5046
- slowest_endpoint_bucket: speedBucket(slowestP95),
5047
- tabs_viewed: [...session.tabsViewed],
5048
- dashboard_opened: session.dashboardOpened,
5049
- explain_used: session.explainUsed,
5050
- session_duration_s: Math.round((Date.now() - session.startTime) / 1e3),
5051
- $lib: "brakit",
5052
- $process_person_profile: false,
5053
- $geoip_disable: true
5054
- }
5055
- };
5056
- try {
5057
- const body = JSON.stringify(payload);
5058
- const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
5059
- const child = spawn(
5060
- process.execPath,
5061
- [
5062
- "-e",
5063
- `fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
5064
- ],
5065
- { detached: true, stdio: "ignore" }
5066
- );
5067
- child.unref();
5068
- } catch {
5069
- }
5016
+ const now = Date.now();
5017
+ sendToPosthog(TELEMETRY_EVENT_SESSION, {
5018
+ sdk: "node",
5019
+ framework: session.framework,
5020
+ package_manager: session.packageManager,
5021
+ is_custom_command: session.isCustomCommand,
5022
+ first_session: isFirstSession,
5023
+ adapters_detected: session.adapters,
5024
+ request_count: session.requestCount,
5025
+ error_count: services.errorStore.getAll().length,
5026
+ query_count: services.queryStore.getAll().length,
5027
+ fetch_count: services.fetchStore.getAll().length,
5028
+ insight_count: insights.length,
5029
+ finding_count: findings.length,
5030
+ insight_types: [...session.insightTypes],
5031
+ rules_triggered: [...session.rulesTriggered],
5032
+ endpoint_count: live.length,
5033
+ avg_duration_ms: totalRequests > 0 ? Math.round(totalDuration / totalRequests) : 0,
5034
+ slowest_endpoint_bucket: speedBucket(slowestP95),
5035
+ tabs_viewed: [...session.tabsViewed],
5036
+ dashboard_opened: session.dashboardOpened,
5037
+ explain_used: session.explainUsed,
5038
+ session_duration_s: Math.round((now - session.startTime) / 1e3),
5039
+ // Enhanced fields
5040
+ setup_succeeded: session.setupSucceeded,
5041
+ setup_duration_ms: session.setupDurationMs,
5042
+ framework_detection_candidates: session.frameworkCandidates,
5043
+ adapters_failed: session.adaptersFailed,
5044
+ time_to_first_request_ms: session.firstRequestAt ? session.firstRequestAt - session.startTime : null,
5045
+ time_to_dashboard_ms: session.dashboardOpenedAt ? session.dashboardOpenedAt - session.startTime : null,
5046
+ exit_reason: session.exitReason
5047
+ });
5048
+ getOrCreateConfig();
5070
5049
  }
5071
5050
  var POSTHOG_KEY, session;
5072
- var init_telemetry2 = __esm({
5051
+ var init_telemetry = __esm({
5073
5052
  "src/telemetry/index.ts"() {
5074
5053
  "use strict";
5075
5054
  init_src();
5055
+ init_config2();
5056
+ init_labels();
5076
5057
  init_config();
5077
- init_telemetry();
5078
- init_config();
5058
+ init_config2();
5079
5059
  POSTHOG_KEY = "phc_E9TwydCGnSfPLIUhNxChpeg32TSowjk31KiPhnLPP0x";
5080
5060
  session = {
5081
5061
  startTime: 0,
@@ -5088,7 +5068,14 @@ var init_telemetry2 = __esm({
5088
5068
  rulesTriggered: /* @__PURE__ */ new Set(),
5089
5069
  tabsViewed: /* @__PURE__ */ new Set(),
5090
5070
  dashboardOpened: false,
5091
- explainUsed: false
5071
+ explainUsed: false,
5072
+ frameworkCandidates: [],
5073
+ adaptersFailed: [],
5074
+ setupDurationMs: 0,
5075
+ setupSucceeded: false,
5076
+ firstRequestAt: 0,
5077
+ dashboardOpenedAt: 0,
5078
+ exitReason: "unknown"
5092
5079
  };
5093
5080
  }
5094
5081
  });
@@ -5097,32 +5084,30 @@ var init_telemetry2 = __esm({
5097
5084
  function isDashboardRequest(url) {
5098
5085
  return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
5099
5086
  }
5100
- function createDashboardHandler(registry) {
5101
- const metricsStore = registry.get("metrics-store");
5087
+ function createDashboardHandler(services) {
5088
+ const metricsStore = services.metricsStore;
5102
5089
  const routes = {
5103
- [DASHBOARD_API_REQUESTS]: createRequestsHandler(registry),
5104
- [DASHBOARD_API_EVENTS]: createSSEHandler(registry),
5105
- [DASHBOARD_API_FLOWS]: createFlowsHandler(registry),
5106
- [DASHBOARD_API_CLEAR]: createClearHandler(registry),
5107
- [DASHBOARD_API_LOGS]: createLogsHandler(registry),
5108
- [DASHBOARD_API_FETCHES]: createFetchesHandler(registry),
5109
- [DASHBOARD_API_ERRORS]: createErrorsHandler(registry),
5110
- [DASHBOARD_API_QUERIES]: createQueriesHandler(registry),
5090
+ [DASHBOARD_API_REQUESTS]: createRequestsHandler(services),
5091
+ [DASHBOARD_API_EVENTS]: createSSEHandler(services),
5092
+ [DASHBOARD_API_FLOWS]: createFlowsHandler(services),
5093
+ [DASHBOARD_API_CLEAR]: createClearHandler(services),
5094
+ [DASHBOARD_API_LOGS]: createLogsHandler(services),
5095
+ [DASHBOARD_API_FETCHES]: createFetchesHandler(services),
5096
+ [DASHBOARD_API_ERRORS]: createErrorsHandler(services),
5097
+ [DASHBOARD_API_QUERIES]: createQueriesHandler(services),
5111
5098
  [DASHBOARD_API_METRICS]: createMetricsHandler(metricsStore),
5112
5099
  [DASHBOARD_API_METRICS_LIVE]: createLiveMetricsHandler(metricsStore),
5113
- [DASHBOARD_API_INGEST]: createIngestHandler(registry),
5114
- [DASHBOARD_API_ACTIVITY]: createActivityHandler(registry)
5100
+ [DASHBOARD_API_INGEST]: createIngestHandler(services),
5101
+ [DASHBOARD_API_ACTIVITY]: createActivityHandler(services)
5115
5102
  };
5116
- if (registry.has("issue-store")) {
5117
- const issueStore = registry.get("issue-store");
5118
- routes[DASHBOARD_API_INSIGHTS] = createIssuesHandler(issueStore);
5119
- routes[DASHBOARD_API_SECURITY] = createIssuesHandler(issueStore);
5120
- routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(issueStore);
5121
- routes[DASHBOARD_API_FINDINGS_REPORT] = createIssuesReportHandler(
5122
- issueStore,
5123
- registry.get("event-bus")
5124
- );
5125
- }
5103
+ const issueStore = services.issueStore;
5104
+ routes[DASHBOARD_API_INSIGHTS] = createIssuesHandler(issueStore);
5105
+ routes[DASHBOARD_API_SECURITY] = createIssuesHandler(issueStore);
5106
+ routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(issueStore);
5107
+ routes[DASHBOARD_API_FINDINGS_REPORT] = createIssuesReportHandler(
5108
+ issueStore,
5109
+ services.bus
5110
+ );
5126
5111
  routes[DASHBOARD_API_TAB] = (req, res) => {
5127
5112
  const raw = (req.url ?? "").split("tab=")[1];
5128
5113
  if (raw) {
@@ -5133,7 +5118,7 @@ function createDashboardHandler(registry) {
5133
5118
  res.end();
5134
5119
  };
5135
5120
  return (req, res, config) => {
5136
- const path = (req.url ?? "/").split("?")[0];
5121
+ const path = stripQueryString(req.url ?? "/");
5137
5122
  const handler = routes[path];
5138
5123
  if (handler) {
5139
5124
  handler(req, res);
@@ -5151,13 +5136,14 @@ function createDashboardHandler(registry) {
5151
5136
  var init_router = __esm({
5152
5137
  "src/dashboard/router.ts"() {
5153
5138
  "use strict";
5139
+ init_endpoint();
5154
5140
  init_constants();
5155
- init_http();
5141
+ init_labels();
5156
5142
  init_api();
5157
5143
  init_issues();
5158
5144
  init_sse();
5159
5145
  init_page();
5160
- init_telemetry2();
5146
+ init_telemetry();
5161
5147
  }
5162
5148
  });
5163
5149
 
@@ -5198,51 +5184,6 @@ var init_event_bus = __esm({
5198
5184
  }
5199
5185
  });
5200
5186
 
5201
- // src/core/service-registry.ts
5202
- var ServiceRegistry;
5203
- var init_service_registry = __esm({
5204
- "src/core/service-registry.ts"() {
5205
- "use strict";
5206
- ServiceRegistry = class {
5207
- constructor() {
5208
- this.services = /* @__PURE__ */ new Map();
5209
- }
5210
- register(name, service) {
5211
- this.services.set(name, service);
5212
- }
5213
- get(name) {
5214
- const service = this.services.get(name);
5215
- if (!service) throw new Error(`Service "${name}" not registered`);
5216
- return service;
5217
- }
5218
- has(name) {
5219
- return this.services.has(name);
5220
- }
5221
- };
5222
- }
5223
- });
5224
-
5225
- // src/utils/static-patterns.ts
5226
- function isStaticPath(urlPath) {
5227
- return STATIC_PATTERNS.some((p) => p.test(urlPath));
5228
- }
5229
- var STATIC_PATTERNS;
5230
- var init_static_patterns = __esm({
5231
- "src/utils/static-patterns.ts"() {
5232
- "use strict";
5233
- STATIC_PATTERNS = [
5234
- /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
5235
- /^\/favicon/,
5236
- /^\/node_modules\//,
5237
- // Framework-specific static/internal paths
5238
- /^\/_next\//,
5239
- /^\/__nextjs/,
5240
- /^\/@vite\//,
5241
- /^\/__vite/
5242
- ];
5243
- }
5244
- });
5245
-
5246
5187
  // src/store/request-store.ts
5247
5188
  function flattenHeaders(headers2) {
5248
5189
  const flat = {};
@@ -5258,6 +5199,7 @@ var init_request_store = __esm({
5258
5199
  "use strict";
5259
5200
  init_constants();
5260
5201
  init_static_patterns();
5202
+ init_endpoint();
5261
5203
  RequestStore = class {
5262
5204
  constructor(maxEntries = MAX_REQUEST_ENTRIES) {
5263
5205
  this.maxEntries = maxEntries;
@@ -5266,7 +5208,7 @@ var init_request_store = __esm({
5266
5208
  }
5267
5209
  capture(input) {
5268
5210
  const url = input.url;
5269
- const path = url.split("?")[0];
5211
+ const path = stripQueryString(url);
5270
5212
  let requestBodyStr = null;
5271
5213
  if (input.requestBody && input.requestBody.length > 0) {
5272
5214
  requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
@@ -5291,7 +5233,8 @@ var init_request_store = __esm({
5291
5233
  startedAt: input.startTime,
5292
5234
  durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
5293
5235
  responseSize: input.responseBody?.length ?? 0,
5294
- isStatic: isStaticPath(path)
5236
+ isStatic: isStaticPath(path),
5237
+ isHealthCheck: isHealthCheckPath(path)
5295
5238
  };
5296
5239
  this.requests.push(entry);
5297
5240
  if (this.requests.length > this.maxEntries) {
@@ -5368,50 +5311,6 @@ var init_telemetry_store = __esm({
5368
5311
  }
5369
5312
  });
5370
5313
 
5371
- // src/store/fetch-store.ts
5372
- var FetchStore;
5373
- var init_fetch_store = __esm({
5374
- "src/store/fetch-store.ts"() {
5375
- "use strict";
5376
- init_telemetry_store();
5377
- FetchStore = class extends TelemetryStore {
5378
- };
5379
- }
5380
- });
5381
-
5382
- // src/store/log-store.ts
5383
- var LogStore;
5384
- var init_log_store = __esm({
5385
- "src/store/log-store.ts"() {
5386
- "use strict";
5387
- init_telemetry_store();
5388
- LogStore = class extends TelemetryStore {
5389
- };
5390
- }
5391
- });
5392
-
5393
- // src/store/error-store.ts
5394
- var ErrorStore;
5395
- var init_error_store = __esm({
5396
- "src/store/error-store.ts"() {
5397
- "use strict";
5398
- init_telemetry_store();
5399
- ErrorStore = class extends TelemetryStore {
5400
- };
5401
- }
5402
- });
5403
-
5404
- // src/store/query-store.ts
5405
- var QueryStore;
5406
- var init_query_store = __esm({
5407
- "src/store/query-store.ts"() {
5408
- "use strict";
5409
- init_telemetry_store();
5410
- QueryStore = class extends TelemetryStore {
5411
- };
5412
- }
5413
- });
5414
-
5415
5314
  // src/utils/math.ts
5416
5315
  function percentile(values, p) {
5417
5316
  if (values.length === 0) return 0;
@@ -5458,6 +5357,17 @@ var init_metrics_store = __esm({
5458
5357
  this.dirty = false;
5459
5358
  this.accumulators = /* @__PURE__ */ new Map();
5460
5359
  this.pendingPoints = /* @__PURE__ */ new Map();
5360
+ /**
5361
+ * Compute the adaptive performance baseline for an endpoint.
5362
+ * Returns the median p95 across historical sessions, or null when
5363
+ * there isn't enough data to establish a meaningful baseline.
5364
+ */
5365
+ /**
5366
+ * Cached baselines — invalidated on flush (when sessions change) and
5367
+ * on new request recordings (when pending points grow). Avoids recomputing
5368
+ * on every getLiveEndpoints() API call.
5369
+ */
5370
+ this.baselineCache = /* @__PURE__ */ new Map();
5461
5371
  this.data = { version: 1, endpoints: [] };
5462
5372
  }
5463
5373
  start() {
@@ -5482,7 +5392,7 @@ var init_metrics_store = __esm({
5482
5392
  this.flush(true);
5483
5393
  }
5484
5394
  recordRequest(req, metrics) {
5485
- if (req.isStatic) return;
5395
+ if (req.isStatic || req.isHealthCheck) return;
5486
5396
  this.dirty = true;
5487
5397
  const key = getEndpointKey(req.method, req.path);
5488
5398
  let acc = this.accumulators.get(key);
@@ -5531,6 +5441,38 @@ var init_metrics_store = __esm({
5531
5441
  getEndpoint(endpoint) {
5532
5442
  return this.endpointIndex.get(endpoint);
5533
5443
  }
5444
+ getEndpointBaseline(endpoint) {
5445
+ const pending2 = this.pendingPoints.get(endpoint);
5446
+ const pointCount = pending2?.length ?? 0;
5447
+ const cached = this.baselineCache.get(endpoint);
5448
+ if (cached && cached.pointCount === pointCount) return cached.value;
5449
+ const value = this.computeBaseline(endpoint, pending2);
5450
+ this.baselineCache.set(endpoint, { value, pointCount });
5451
+ return value;
5452
+ }
5453
+ computeBaseline(endpoint, pending2) {
5454
+ const ep = this.endpointIndex.get(endpoint);
5455
+ if (ep && ep.sessions.length >= BASELINE_MIN_SESSIONS) {
5456
+ const validSessions = ep.sessions.filter(
5457
+ (s) => s.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION
5458
+ );
5459
+ if (validSessions.length >= BASELINE_MIN_SESSIONS) {
5460
+ const p95s = validSessions.map((s) => s.p95DurationMs).sort((a, b) => a - b);
5461
+ return p95s[Math.floor(p95s.length / 2)];
5462
+ }
5463
+ }
5464
+ if (ep && ep.sessions.length === 1) {
5465
+ const session2 = ep.sessions[0];
5466
+ if (session2.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION) {
5467
+ return session2.p95DurationMs;
5468
+ }
5469
+ }
5470
+ if (pending2 && pending2.length >= BASELINE_PENDING_POINTS_MIN) {
5471
+ const warmDurations = pending2.slice(1).map((r) => r.durationMs).sort((a, b) => a - b);
5472
+ return warmDurations[Math.floor(warmDurations.length / 2)];
5473
+ }
5474
+ return null;
5475
+ }
5534
5476
  getLiveEndpoints() {
5535
5477
  const merged = /* @__PURE__ */ new Map();
5536
5478
  for (const ep of this.data.endpoints) {
@@ -5545,27 +5487,35 @@ var init_metrics_store = __esm({
5545
5487
  const endpoints = [];
5546
5488
  for (const [endpoint, requests] of merged) {
5547
5489
  if (requests.length === 0) continue;
5548
- const durations = requests.map((r) => r.durationMs);
5490
+ const warmRequests = requests.length > 1 ? requests.slice(1) : requests;
5491
+ const warmDurations = warmRequests.map((r) => r.durationMs);
5549
5492
  const errors = requests.filter((r) => isErrorStatus(r.statusCode)).length;
5550
- const totalQueries = requests.reduce((s, r) => s + r.queryCount, 0);
5551
- const totalQueryTime = requests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
5552
- const totalFetchTime = requests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
5553
- const n = requests.length;
5554
- const avgDurationMs = Math.round(durations.reduce((s, d) => s + d, 0) / n);
5493
+ const totalQueries = warmRequests.reduce((s, r) => s + r.queryCount, 0);
5494
+ const totalQueryTime = warmRequests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
5495
+ const totalFetchTime = warmRequests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
5496
+ const n = warmRequests.length;
5497
+ const avgDurationMs = Math.round(warmDurations.reduce((s, d) => s + d, 0) / n);
5555
5498
  const avgQueryTimeMs = Math.round(totalQueryTime / n);
5556
5499
  const avgFetchTimeMs = Math.round(totalFetchTime / n);
5500
+ const p95Ms = percentile(warmDurations, 0.95);
5501
+ const medianMs = percentile(warmDurations, 0.5);
5502
+ const epData = this.endpointIndex.get(endpoint);
5557
5503
  endpoints.push({
5558
5504
  endpoint,
5559
5505
  requests,
5560
5506
  summary: {
5561
- p95Ms: percentile(durations, 0.95),
5562
- errorRate: errors / n,
5507
+ p95Ms,
5508
+ medianMs,
5509
+ errorRate: errors / requests.length,
5510
+ // Error rate uses ALL requests
5563
5511
  avgQueryCount: Math.round(totalQueries / n),
5564
- totalRequests: n,
5512
+ totalRequests: requests.length,
5565
5513
  avgQueryTimeMs,
5566
5514
  avgFetchTimeMs,
5567
5515
  avgAppTimeMs: Math.max(0, avgDurationMs - avgQueryTimeMs - avgFetchTimeMs)
5568
- }
5516
+ },
5517
+ sessions: epData?.sessions,
5518
+ baselineP95Ms: this.getEndpointBaseline(endpoint)
5569
5519
  });
5570
5520
  }
5571
5521
  endpoints.sort((a, b) => b.summary.p95Ms - a.summary.p95Ms);
@@ -5576,6 +5526,7 @@ var init_metrics_store = __esm({
5576
5526
  this.endpointIndex.clear();
5577
5527
  this.accumulators.clear();
5578
5528
  this.pendingPoints.clear();
5529
+ this.baselineCache.clear();
5579
5530
  this.dirty = false;
5580
5531
  this.persistence.remove();
5581
5532
  }
@@ -5617,6 +5568,7 @@ var init_metrics_store = __esm({
5617
5568
  epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
5618
5569
  }
5619
5570
  this.pendingPoints.clear();
5571
+ this.baselineCache.clear();
5620
5572
  if (!this.dirty) return;
5621
5573
  if (sync) {
5622
5574
  this.persistence.saveSync(this.data);
@@ -5710,10 +5662,6 @@ var init_store = __esm({
5710
5662
  "use strict";
5711
5663
  init_request_store();
5712
5664
  init_telemetry_store();
5713
- init_fetch_store();
5714
- init_log_store();
5715
- init_error_store();
5716
- init_query_store();
5717
5665
  init_metrics_store();
5718
5666
  init_persistence();
5719
5667
  }
@@ -5745,9 +5693,9 @@ function formatConsoleLine(issue, suffix) {
5745
5693
  }
5746
5694
  return line;
5747
5695
  }
5748
- function startTerminalInsights(registry, proxyPort) {
5749
- const bus = registry.get("event-bus");
5750
- const metricsStore = registry.get("metrics-store");
5696
+ function startTerminalInsights(services, proxyPort) {
5697
+ const bus = services.bus;
5698
+ const metricsStore = services.metricsStore;
5751
5699
  const printedKeys = /* @__PURE__ */ new Set();
5752
5700
  const resolvedKeys = /* @__PURE__ */ new Set();
5753
5701
  const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
@@ -5818,8 +5766,8 @@ var init_terminal = __esm({
5818
5766
  "use strict";
5819
5767
  init_src();
5820
5768
  init_constants();
5821
- init_limits();
5822
- init_severity();
5769
+ init_config();
5770
+ init_labels();
5823
5771
  SEVERITY_COLOR = {
5824
5772
  critical: pc.red,
5825
5773
  warning: pc.yellow,
@@ -5914,8 +5862,14 @@ function outgoingToIncoming(headers2) {
5914
5862
  }
5915
5863
  return result;
5916
5864
  }
5865
+ function getDecompressor(encoding) {
5866
+ if (encoding === CONTENT_ENCODING_GZIP) return gunzip;
5867
+ if (encoding === CONTENT_ENCODING_BR) return brotliDecompress;
5868
+ if (encoding === CONTENT_ENCODING_DEFLATE) return inflate;
5869
+ return null;
5870
+ }
5917
5871
  function decompressAsync(body, encoding) {
5918
- const decompressor = encoding === CONTENT_ENCODING_GZIP ? gunzip : encoding === CONTENT_ENCODING_BR ? brotliDecompress : encoding === CONTENT_ENCODING_DEFLATE ? inflate : null;
5872
+ const decompressor = getDecompressor(encoding);
5919
5873
  if (!decompressor) return Promise.resolve(body);
5920
5874
  return new Promise((resolve6) => {
5921
5875
  decompressor(body, (err, result) => {
@@ -5929,7 +5883,7 @@ function toBuffer(chunk) {
5929
5883
  if (typeof chunk === "string") return Buffer.from(chunk);
5930
5884
  return null;
5931
5885
  }
5932
- function captureInProcess(req, res, requestId, requestStore) {
5886
+ function captureInProcess(req, res, requestId, requestStore, isChild = false) {
5933
5887
  const startTime = performance.now();
5934
5888
  const method = req.method ?? "GET";
5935
5889
  const resChunks = [];
@@ -5975,30 +5929,32 @@ function captureInProcess(req, res, requestId, requestStore) {
5975
5929
  const responseHeaders = outgoingToIncoming(res.getHeaders());
5976
5930
  const responseContentType = String(res.getHeader("content-type") ?? "");
5977
5931
  const capturedChunks = resChunks.slice();
5978
- void (async () => {
5979
- try {
5980
- let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
5981
- if (body && encoding && !truncated) {
5982
- body = await decompressAsync(body, encoding);
5932
+ if (!isChild) {
5933
+ void (async () => {
5934
+ try {
5935
+ let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
5936
+ if (body && encoding && !truncated) {
5937
+ body = await decompressAsync(body, encoding);
5938
+ }
5939
+ requestStore.capture({
5940
+ requestId,
5941
+ method,
5942
+ url: req.url ?? "/",
5943
+ requestHeaders: req.headers,
5944
+ requestBody: null,
5945
+ statusCode,
5946
+ responseHeaders,
5947
+ responseBody: body,
5948
+ responseContentType,
5949
+ startTime,
5950
+ endTime,
5951
+ config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
5952
+ });
5953
+ } catch (e) {
5954
+ brakitDebug(`capture store: ${getErrorMessage(e)}`);
5983
5955
  }
5984
- requestStore.capture({
5985
- requestId,
5986
- method,
5987
- url: req.url ?? "/",
5988
- requestHeaders: req.headers,
5989
- requestBody: null,
5990
- statusCode,
5991
- responseHeaders,
5992
- responseBody: body,
5993
- responseContentType,
5994
- startTime,
5995
- endTime,
5996
- config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
5997
- });
5998
- } catch (e) {
5999
- brakitDebug(`capture store: ${getErrorMessage(e)}`);
6000
- }
6001
- })();
5956
+ })();
5957
+ }
6002
5958
  return result;
6003
5959
  };
6004
5960
  }
@@ -6046,13 +6002,15 @@ function installInterceptor(deps) {
6046
6002
  deps.handleDashboard(req, res, deps.config);
6047
6003
  return true;
6048
6004
  }
6049
- const requestId = randomUUID8();
6005
+ const propagated = req.headers[BRAKIT_REQUEST_ID_HEADER];
6006
+ const requestId = propagated ?? randomUUID8();
6007
+ const isChild = propagated !== void 0;
6050
6008
  const ctx = {
6051
6009
  requestId,
6052
6010
  url,
6053
6011
  method: req.method ?? "GET"
6054
6012
  };
6055
- captureInProcess(req, res, requestId, deps.requestStore);
6013
+ captureInProcess(req, res, requestId, deps.requestStore, isChild);
6056
6014
  return storage.run(
6057
6015
  ctx,
6058
6016
  () => original.apply(this, [event, ...args])
@@ -6075,7 +6033,8 @@ var init_interceptor = __esm({
6075
6033
  init_safe_wrap();
6076
6034
  init_guard();
6077
6035
  init_capture();
6078
- init_http();
6036
+ init_labels();
6037
+ init_constants();
6079
6038
  originalEmit = null;
6080
6039
  }
6081
6040
  });
@@ -6093,18 +6052,12 @@ function setup() {
6093
6052
  initPromise = doSetup();
6094
6053
  return initPromise;
6095
6054
  }
6096
- function createStores(bus, registry) {
6055
+ function createStores(bus) {
6097
6056
  const requestStore = new RequestStore();
6098
- const fetchStore = new FetchStore();
6099
- const logStore = new LogStore();
6100
- const errorStore = new ErrorStore();
6101
- const queryStore = new QueryStore();
6102
- registry.register("event-bus", bus);
6103
- registry.register("request-store", requestStore);
6104
- registry.register("fetch-store", fetchStore);
6105
- registry.register("log-store", logStore);
6106
- registry.register("error-store", errorStore);
6107
- registry.register("query-store", queryStore);
6057
+ const fetchStore = new TelemetryStore();
6058
+ const logStore = new TelemetryStore();
6059
+ const errorStore = new TelemetryStore();
6060
+ const queryStore = new TelemetryStore();
6108
6061
  bus.on("telemetry:fetch", (data) => fetchStore.add(data));
6109
6062
  bus.on("telemetry:query", (data) => queryStore.add(data));
6110
6063
  bus.on("telemetry:log", (data) => logStore.add(data));
@@ -6124,31 +6077,33 @@ function installHooks(bus) {
6124
6077
  adapterRegistry.patchAll(telemetryEmit);
6125
6078
  const cwd = process.cwd();
6126
6079
  let framework = "unknown";
6080
+ let frameworkCandidates = [];
6127
6081
  try {
6128
6082
  const pkg = JSON.parse(
6129
- // readFileSync is acceptable here — runs once at startup
6130
6083
  __require("fs").readFileSync(resolve5(cwd, "package.json"), "utf-8")
6131
6084
  );
6132
6085
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
6133
6086
  framework = detectFrameworkFromDeps(allDeps);
6087
+ frameworkCandidates = KNOWN_DEPENDENCY_NAMES.filter((dep) => dep in allDeps);
6134
6088
  } catch {
6135
6089
  }
6136
6090
  return {
6137
6091
  framework,
6138
- adapterNames: adapterRegistry.getActive().map((a) => a.name)
6092
+ adapterNames: adapterRegistry.getActive().map((a) => a.name),
6093
+ adaptersFailed: [...adapterRegistry.getFailed()],
6094
+ frameworkCandidates
6139
6095
  };
6140
6096
  }
6141
- function startAnalysis(registry, stores, dataDir) {
6142
- const bus = registry.get("event-bus");
6097
+ function startAnalysis(bus, stores, dataDir, services) {
6143
6098
  const metricsStore = new MetricsStore(new FileMetricsPersistence(dataDir));
6144
6099
  metricsStore.start();
6145
- registry.register("metrics-store", metricsStore);
6146
6100
  const issueStore = new IssueStore(dataDir);
6147
6101
  issueStore.start();
6148
- registry.register("issue-store", issueStore);
6149
- const analysisEngine = new AnalysisEngine(registry);
6102
+ services.metricsStore = metricsStore;
6103
+ services.issueStore = issueStore;
6104
+ const analysisEngine = new AnalysisEngine(services);
6150
6105
  analysisEngine.start();
6151
- registry.register("analysis-engine", analysisEngine);
6106
+ services.analysisEngine = analysisEngine;
6152
6107
  bus.on("request:completed", (req) => {
6153
6108
  const queries = stores.queryStore.getByRequest(req.id);
6154
6109
  const fetches = stores.fetchStore.getByRequest(req.id);
@@ -6160,7 +6115,7 @@ function startAnalysis(registry, stores, dataDir) {
6160
6115
  });
6161
6116
  return { analysisEngine, metricsStore, issueStore };
6162
6117
  }
6163
- function registerLifecycle(registry, stores, services, cwd) {
6118
+ function registerLifecycle(allServices, stores, services, cwd) {
6164
6119
  let telemetrySent = false;
6165
6120
  const sendTelemetry = () => {
6166
6121
  if (telemetrySent) return;
@@ -6172,7 +6127,7 @@ function registerLifecycle(registry, stores, services, cwd) {
6172
6127
  recordRulesTriggered(
6173
6128
  services.analysisEngine.getFindings().map((f) => f.rule)
6174
6129
  );
6175
- trackSession(registry);
6130
+ trackSession(allServices);
6176
6131
  };
6177
6132
  let teardownCalled = false;
6178
6133
  const runTeardown = () => {
@@ -6191,7 +6146,14 @@ function registerLifecycle(registry, stores, services, cwd) {
6191
6146
  }
6192
6147
  };
6193
6148
  health.setTeardown(runTeardown);
6149
+ process.on("SIGINT", () => {
6150
+ recordExitReason(EXIT_REASON_SIGINT);
6151
+ });
6152
+ process.on("SIGTERM", () => {
6153
+ recordExitReason(EXIT_REASON_SIGTERM);
6154
+ });
6194
6155
  process.on("beforeExit", () => {
6156
+ recordExitReason(EXIT_REASON_CLEAN);
6195
6157
  sendTelemetry();
6196
6158
  });
6197
6159
  process.on("exit", () => {
@@ -6199,22 +6161,36 @@ function registerLifecycle(registry, stores, services, cwd) {
6199
6161
  });
6200
6162
  }
6201
6163
  async function doSetup() {
6164
+ const setupStart = Date.now();
6202
6165
  brakitDebug(`[setup] doSetup called at ${(/* @__PURE__ */ new Date()).toISOString()}`);
6203
6166
  const bus = new EventBus();
6204
- const registry = new ServiceRegistry();
6205
6167
  const cwd = process.cwd();
6206
- const stores = createStores(bus, registry);
6207
- const { framework, adapterNames } = installHooks(bus);
6168
+ const stores = createStores(bus);
6169
+ const services = {
6170
+ bus,
6171
+ ...stores
6172
+ };
6173
+ const { framework, adapterNames, adaptersFailed, frameworkCandidates } = installHooks(bus);
6208
6174
  initSession(framework, detectPackageManagerSync(cwd), false, adapterNames);
6175
+ const setupDurationMs = Date.now() - setupStart;
6176
+ recordSetupCompleted({ frameworkCandidates, adaptersFailed, setupDurationMs });
6177
+ trackEvent(TELEMETRY_EVENT_SETUP_COMPLETED, {
6178
+ framework,
6179
+ framework_detection_candidates: frameworkCandidates,
6180
+ adapters_detected: adapterNames,
6181
+ adapters_failed: adaptersFailed,
6182
+ hooks_installed: ["fetch", "console", "error"],
6183
+ setup_duration_ms: setupDurationMs
6184
+ });
6209
6185
  const dataDir = getProjectDataDir(cwd);
6210
- const services = startAnalysis(registry, stores, dataDir);
6186
+ const analysisServices = startAnalysis(bus, stores, dataDir, services);
6211
6187
  const config = {
6212
6188
  proxyPort: 0,
6213
6189
  targetPort: 0,
6214
6190
  showStatic: false,
6215
6191
  maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
6216
6192
  };
6217
- const handleDashboard = createDashboardHandler(registry);
6193
+ const handleDashboard = createDashboardHandler(services);
6218
6194
  installInterceptor({
6219
6195
  handleDashboard,
6220
6196
  config,
@@ -6222,6 +6198,11 @@ async function doSetup() {
6222
6198
  onFirstRequest(port) {
6223
6199
  setBrakitPort(port);
6224
6200
  brakitDebug(`[setup] onFirstRequest fired, port=${port}`);
6201
+ recordFirstRequest();
6202
+ trackEvent(TELEMETRY_EVENT_FIRST_REQUEST, {
6203
+ port,
6204
+ time_to_first_request_ms: Date.now() - setupStart
6205
+ });
6225
6206
  void (async () => {
6226
6207
  try {
6227
6208
  const dir = resolve5(cwd, METRICS_DIR);
@@ -6247,14 +6228,14 @@ async function doSetup() {
6247
6228
  brakitDebug(`port file write failed: ${getErrorMessage(err)}`);
6248
6229
  }
6249
6230
  })();
6250
- startTerminalInsights(registry, port);
6231
+ startTerminalInsights(services, port);
6251
6232
  process.stdout.write(
6252
6233
  ` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
6253
6234
  `
6254
6235
  );
6255
6236
  }
6256
6237
  });
6257
- registerLifecycle(registry, stores, services, cwd);
6238
+ registerLifecycle(services, stores, analysisServices, cwd);
6258
6239
  }
6259
6240
  var initPromise;
6260
6241
  var init_setup = __esm({
@@ -6266,12 +6247,8 @@ var init_setup = __esm({
6266
6247
  init_adapters();
6267
6248
  init_router();
6268
6249
  init_event_bus();
6269
- init_service_registry();
6270
6250
  init_request_store();
6271
- init_fetch_store();
6272
- init_log_store();
6273
- init_error_store();
6274
- init_query_store();
6251
+ init_telemetry_store();
6275
6252
  init_store();
6276
6253
  init_issue_store();
6277
6254
  init_engine();
@@ -6284,7 +6261,7 @@ var init_setup = __esm({
6284
6261
  init_type_guards();
6285
6262
  init_fs();
6286
6263
  init_project();
6287
- init_telemetry2();
6264
+ init_telemetry();
6288
6265
  initPromise = null;
6289
6266
  }
6290
6267
  });