brakit 0.8.7 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
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,50 @@ 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"]);
149
93
  }
150
94
  });
151
95
 
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"() {
96
+ // src/constants/labels.ts
97
+ 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;
98
+ var init_labels = __esm({
99
+ "src/constants/labels.ts"() {
156
100
  "use strict";
101
+ DASHBOARD_PREFIX = "/__brakit";
102
+ DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
103
+ DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
104
+ DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
105
+ DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
106
+ DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
107
+ DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
108
+ DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
109
+ DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
110
+ DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
111
+ DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
112
+ DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
113
+ DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
114
+ DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
115
+ DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
116
+ DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
117
+ DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
118
+ DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
119
+ VALID_TABS_TUPLE = [
120
+ "overview",
121
+ "actions",
122
+ "requests",
123
+ "fetches",
124
+ "queries",
125
+ "errors",
126
+ "logs",
127
+ "performance",
128
+ "security"
129
+ ];
130
+ VALID_TABS = new Set(VALID_TABS_TUPLE);
157
131
  BRAKIT_REQUEST_ID_HEADER = "x-brakit-request-id";
158
132
  BRAKIT_FETCH_ID_HEADER = "x-brakit-fetch-id";
159
133
  SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
@@ -164,13 +138,53 @@ var init_headers = __esm({
164
138
  "x-api-key",
165
139
  "x-auth-token"
166
140
  ]);
141
+ HTTP_OK = 200;
142
+ HTTP_NO_CONTENT = 204;
143
+ HTTP_BAD_REQUEST = 400;
144
+ HTTP_NOT_FOUND = 404;
145
+ HTTP_METHOD_NOT_ALLOWED = 405;
146
+ HTTP_PAYLOAD_TOO_LARGE = 413;
147
+ HTTP_INTERNAL_ERROR = 500;
148
+ SECURITY_HEADERS = {
149
+ "x-content-type-options": "nosniff",
150
+ "x-frame-options": "DENY",
151
+ "referrer-policy": "no-referrer",
152
+ "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
153
+ };
154
+ CONTENT_ENCODING_GZIP = "gzip";
155
+ CONTENT_ENCODING_BR = "br";
156
+ CONTENT_ENCODING_DEFLATE = "deflate";
157
+ SEVERITY_ICON = {
158
+ critical: "\u2717",
159
+ warning: "\u26A0",
160
+ info: "\u2139"
161
+ };
162
+ SSE_EVENT_FETCH = "fetch";
163
+ SSE_EVENT_LOG = "log";
164
+ SSE_EVENT_ERROR = "error_event";
165
+ SSE_EVENT_QUERY = "query";
166
+ SSE_EVENT_ISSUES = "issues";
167
+ SDK_EVENT_REQUEST = "request";
168
+ SDK_EVENT_DB_QUERY = "db.query";
169
+ SDK_EVENT_FETCH = "fetch";
170
+ SDK_EVENT_LOG = "log";
171
+ SDK_EVENT_ERROR = "error";
172
+ SDK_EVENT_AUTH_CHECK = "auth.check";
173
+ POSTHOG_HOST = "https://us.i.posthog.com";
174
+ POSTHOG_CAPTURE_PATH = "/i/v0/e/";
175
+ POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
176
+ SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
177
+ TIMELINE_FETCH = "fetch";
178
+ TIMELINE_LOG = "log";
179
+ TIMELINE_ERROR = "error";
180
+ TIMELINE_QUERY = "query";
167
181
  }
168
182
  });
169
183
 
170
- // src/constants/network.ts
184
+ // src/constants/features.ts
171
185
  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"() {
186
+ var init_features = __esm({
187
+ "src/constants/features.ts"() {
174
188
  "use strict";
175
189
  CLOUD_SIGNALS = [
176
190
  "VERCEL",
@@ -203,112 +217,13 @@ var init_network = __esm({
203
217
  }
204
218
  });
205
219
 
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
220
  // src/constants/index.ts
294
221
  var init_constants = __esm({
295
222
  "src/constants/index.ts"() {
296
223
  "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();
224
+ init_config();
225
+ init_labels();
226
+ init_features();
312
227
  }
313
228
  });
314
229
 
@@ -377,7 +292,7 @@ function setupFetchHook(emit) {
377
292
  statusCode: msg.response.statusCode ?? 0,
378
293
  durationMs: Math.round(performance.now() - info.startTime),
379
294
  parentRequestId: info.parentRequestId,
380
- timestamp: Date.now()
295
+ timestamp: performance.now()
381
296
  }
382
297
  });
383
298
  });
@@ -407,7 +322,7 @@ function setupConsoleHook(emit) {
407
322
  const ctx = getRequestContext();
408
323
  if (!ctx) return;
409
324
  const message = format(...args);
410
- const timestamp = Date.now();
325
+ const timestamp = performance.now();
411
326
  const parentRequestId = ctx.requestId;
412
327
  if (level === "error") {
413
328
  const errorArg = args.find((a) => a instanceof Error);
@@ -474,7 +389,7 @@ function createCaptureError(emit) {
474
389
  message: error.message,
475
390
  stack: error.stack ?? "",
476
391
  parentRequestId: ctx?.requestId ?? null,
477
- timestamp: Date.now()
392
+ timestamp: performance.now()
478
393
  }
479
394
  });
480
395
  };
@@ -538,65 +453,28 @@ var init_adapter_registry = __esm({
538
453
  }
539
454
  });
540
455
 
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
456
  // src/instrument/adapters/normalize.ts
563
457
  function normalizeSQL(sql) {
564
458
  if (!sql) return { op: "OTHER", table: "" };
565
459
  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
- }
460
+ const keyword = trimmed.split(/\s+/, 1)[0].toUpperCase();
461
+ const op = VALID_OPS.has(keyword) ? keyword : "OTHER";
462
+ const table = trimmed.match(TABLE_RE)?.[1] ?? "";
463
+ return { op, table };
585
464
  }
586
465
  function normalizePrismaOp(operation) {
587
466
  return PRISMA_OP_MAP[operation] ?? "OTHER";
588
467
  }
589
468
  function normalizeQueryParams(sql) {
590
469
  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;
470
+ return sql.replace(SQL_PARAM_MARKER, "?").replace(SQL_STRING_LITERAL, "?").replace(SQL_NUMBER_LITERAL, "?");
595
471
  }
596
- var PRISMA_OP_MAP;
472
+ var VALID_OPS, TABLE_RE, PRISMA_OP_MAP, SQL_PARAM_MARKER, SQL_STRING_LITERAL, SQL_NUMBER_LITERAL;
597
473
  var init_normalize = __esm({
598
474
  "src/instrument/adapters/normalize.ts"() {
599
475
  "use strict";
476
+ VALID_OPS = /* @__PURE__ */ new Set(["SELECT", "INSERT", "UPDATE", "DELETE"]);
477
+ TABLE_RE = /(?:FROM|INTO|UPDATE)\s+(?:"?\w+"?\.)?"?(\w+)"?/i;
600
478
  PRISMA_OP_MAP = {
601
479
  findUnique: "SELECT",
602
480
  findUniqueOrThrow: "SELECT",
@@ -615,79 +493,199 @@ var init_normalize = __esm({
615
493
  delete: "DELETE",
616
494
  deleteMany: "DELETE"
617
495
  };
496
+ SQL_PARAM_MARKER = /\$\d+/g;
497
+ SQL_STRING_LITERAL = /'[^']*'/g;
498
+ SQL_NUMBER_LITERAL = /\b\d+(\.\d+)?\b/g;
618
499
  }
619
500
  });
620
501
 
621
- // src/instrument/adapters/pg.ts
622
- var origQuery, proto, pgAdapter;
623
- var init_pg = __esm({
624
- "src/instrument/adapters/pg.ts"() {
502
+ // src/utils/type-guards.ts
503
+ function isString(val) {
504
+ return typeof val === "string";
505
+ }
506
+ function isNumber(val) {
507
+ return typeof val === "number" && !isNaN(val);
508
+ }
509
+ function isBoolean(val) {
510
+ return typeof val === "boolean";
511
+ }
512
+ function isThenable(value) {
513
+ return value != null && typeof value.then === "function";
514
+ }
515
+ function getErrorMessage(err) {
516
+ if (err instanceof Error) return err.message;
517
+ if (typeof err === "string") return err;
518
+ return String(err);
519
+ }
520
+ function isValidIssueState(val) {
521
+ return typeof val === "string" && VALID_ISSUE_STATES.has(val);
522
+ }
523
+ function isValidIssueCategory(val) {
524
+ return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
525
+ }
526
+ function isValidAiFixStatus(val) {
527
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
528
+ }
529
+ function validateIssuesData(parsed) {
530
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
531
+ const obj = parsed;
532
+ if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
533
+ return parsed;
534
+ }
535
+ return null;
536
+ }
537
+ function validateMetricsData(parsed) {
538
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
539
+ const obj = parsed;
540
+ if (obj.version === 1 && Array.isArray(obj.endpoints)) {
541
+ return parsed;
542
+ }
543
+ return null;
544
+ }
545
+ var init_type_guards = __esm({
546
+ "src/utils/type-guards.ts"() {
625
547
  "use strict";
626
- init_shared();
548
+ init_config();
549
+ }
550
+ });
551
+
552
+ // src/utils/log.ts
553
+ function brakitWarn(message) {
554
+ process.stderr.write(`${PREFIX} ${message}
555
+ `);
556
+ }
557
+ function brakitDebug(message) {
558
+ if (process.env.DEBUG_BRAKIT) {
559
+ process.stderr.write(`${PREFIX}:debug ${message}
560
+ `);
561
+ }
562
+ }
563
+ var PREFIX;
564
+ var init_log = __esm({
565
+ "src/utils/log.ts"() {
566
+ "use strict";
567
+ PREFIX = "[brakit]";
568
+ }
569
+ });
570
+
571
+ // src/instrument/adapters/shared.ts
572
+ import { createRequire } from "module";
573
+ function tryRequire(id) {
574
+ try {
575
+ return appRequire(id);
576
+ } catch {
577
+ return null;
578
+ }
579
+ }
580
+ function getActiveRequestId() {
581
+ return getRequestContext()?.requestId ?? null;
582
+ }
583
+ function getPrototype(lib, className) {
584
+ const defaultExport = lib.default;
585
+ const cls = defaultExport?.[className] ?? lib[className];
586
+ if (!cls || typeof cls !== "function") return null;
587
+ return cls.prototype ?? null;
588
+ }
589
+ function buildQueryEvent(config, sql, op, table, start, requestId, rowCount) {
590
+ return {
591
+ type: "query",
592
+ data: {
593
+ driver: config.driver,
594
+ source: config.driver,
595
+ sql,
596
+ normalizedOp: op,
597
+ table,
598
+ durationMs: Math.round(performance.now() - start),
599
+ rowCount: rowCount ?? void 0,
600
+ parentRequestId: requestId,
601
+ timestamp: performance.now()
602
+ }
603
+ };
604
+ }
605
+ function wrapQueryMethod(original, emit, config) {
606
+ return function(...args) {
607
+ const sql = config.extractSql(args);
608
+ const start = performance.now();
609
+ const requestId = getActiveRequestId();
610
+ const { op, table } = normalizeSQL(sql ?? "");
611
+ const emitQuery = (result2) => {
612
+ const rowCount = config.extractRowCount?.(result2);
613
+ emit(buildQueryEvent(config, sql, op, table, start, requestId, rowCount));
614
+ };
615
+ const lastIdx = args.length - 1;
616
+ if (lastIdx >= 0 && typeof args[lastIdx] === "function") {
617
+ const originalCallback = args[lastIdx];
618
+ args[lastIdx] = function(...callbackArgs) {
619
+ emitQuery(callbackArgs[1]);
620
+ return originalCallback.apply(this, callbackArgs);
621
+ };
622
+ return original.apply(this, args);
623
+ }
624
+ const result = original.apply(this, args);
625
+ if (isThenable(result)) {
626
+ return result.then((res) => {
627
+ try {
628
+ emitQuery(res);
629
+ } catch (e) {
630
+ brakitDebug(`query telemetry: ${getErrorMessage(e)}`);
631
+ }
632
+ return res;
633
+ });
634
+ }
635
+ if (config.supportsEventEmitter && result && typeof result.on === "function") {
636
+ result.on(
637
+ "end",
638
+ (res) => emitQuery(res)
639
+ );
640
+ return result;
641
+ }
642
+ return result;
643
+ };
644
+ }
645
+ var appRequire;
646
+ var init_shared = __esm({
647
+ "src/instrument/adapters/shared.ts"() {
648
+ "use strict";
649
+ init_context();
627
650
  init_normalize();
651
+ init_type_guards();
652
+ init_log();
653
+ appRequire = createRequire(process.cwd() + "/index.js");
654
+ }
655
+ });
656
+
657
+ // src/instrument/adapters/pg.ts
658
+ var origQuery, proto, pgConfig, pgAdapter;
659
+ var init_pg = __esm({
660
+ "src/instrument/adapters/pg.ts"() {
661
+ "use strict";
662
+ init_shared();
628
663
  origQuery = null;
629
664
  proto = null;
665
+ pgConfig = {
666
+ driver: "pg",
667
+ extractSql: (args) => {
668
+ const q = args[0];
669
+ if (typeof q === "string") return q;
670
+ if (typeof q === "object" && q !== null && "text" in q) return q.text;
671
+ return void 0;
672
+ },
673
+ extractRowCount: (result) => result?.rowCount,
674
+ supportsEventEmitter: true
675
+ };
630
676
  pgAdapter = {
631
677
  name: "pg",
632
678
  detect() {
633
679
  return tryRequire("pg") !== null;
634
680
  },
681
+ /** Monkeypatches pg's Client prototype to intercept database queries and emit telemetry events. */
635
682
  patch(emit) {
636
683
  const pg = tryRequire("pg");
637
684
  if (!pg) return;
638
- const Client = pg.default?.Client ?? pg.Client;
639
- if (!Client || typeof Client !== "function") return;
640
- proto = Client.prototype ?? null;
685
+ proto = getPrototype(pg, "Client");
641
686
  if (!proto?.query) return;
642
687
  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
- };
688
+ proto.query = wrapQueryMethod(origQuery, emit, pgConfig);
691
689
  },
692
690
  unpatch() {
693
691
  if (proto && origQuery) {
@@ -701,70 +699,34 @@ var init_pg = __esm({
701
699
  });
702
700
 
703
701
  // src/instrument/adapters/mysql2.ts
704
- var originals2, proto2, mysql2Adapter;
702
+ var PATCHED_METHODS, originals2, proto2, mysql2Config, mysql2Adapter;
705
703
  var init_mysql2 = __esm({
706
704
  "src/instrument/adapters/mysql2.ts"() {
707
705
  "use strict";
708
706
  init_shared();
709
- init_normalize();
707
+ PATCHED_METHODS = ["query", "execute"];
710
708
  originals2 = /* @__PURE__ */ new Map();
711
709
  proto2 = null;
710
+ mysql2Config = {
711
+ driver: "mysql2",
712
+ extractSql: (args) => typeof args[0] === "string" ? args[0] : void 0
713
+ };
712
714
  mysql2Adapter = {
713
715
  name: "mysql2",
714
716
  detect() {
715
717
  return tryRequire("mysql2") !== null;
716
718
  },
719
+ /** Monkeypatches mysql2's Connection prototype to intercept database queries and emit telemetry events. */
717
720
  patch(emit) {
718
721
  const mysql2 = tryRequire("mysql2");
719
722
  if (!mysql2) return;
720
- proto2 = mysql2.Connection?.prototype ?? null;
723
+ proto2 = getPrototype(mysql2, "Connection");
721
724
  if (!proto2) return;
722
- for (const method of ["query", "execute"]) {
725
+ for (const method of PATCHED_METHODS) {
723
726
  const orig = proto2[method];
724
727
  if (typeof orig !== "function") continue;
725
728
  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
- };
729
+ proto2[method] = wrapQueryMethod(orig, emit, mysql2Config);
768
730
  }
769
731
  },
770
732
  unpatch() {
@@ -797,9 +759,8 @@ var init_prisma = __esm({
797
759
  patch(emit) {
798
760
  const prismaModule = tryRequire("@prisma/client");
799
761
  if (!prismaModule) return;
800
- const PrismaClient = prismaModule.default?.PrismaClient ?? prismaModule.PrismaClient;
801
- if (!PrismaClient || typeof PrismaClient !== "function") return;
802
- prismaProto = PrismaClient.prototype;
762
+ prismaProto = getPrototype(prismaModule, "PrismaClient");
763
+ if (!prismaProto) return;
803
764
  origConnect = prismaProto.$connect;
804
765
  if (typeof origConnect !== "function") return;
805
766
  const saved = origConnect;
@@ -815,7 +776,7 @@ var init_prisma = __esm({
815
776
  args: opArgs,
816
777
  query
817
778
  }) {
818
- const requestId = captureRequestId();
779
+ const requestId = getActiveRequestId();
819
780
  const start = performance.now();
820
781
  const result = await query(opArgs);
821
782
  emit({
@@ -829,7 +790,7 @@ var init_prisma = __esm({
829
790
  table: model,
830
791
  durationMs: Math.round(performance.now() - start),
831
792
  parentRequestId: requestId,
832
- timestamp: Date.now()
793
+ timestamp: performance.now()
833
794
  }
834
795
  });
835
796
  return result;
@@ -874,24 +835,35 @@ var init_adapters = __esm({
874
835
  }
875
836
  });
876
837
 
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"() {
838
+ // src/utils/endpoint.ts
839
+ function isDynamicSegment(segment) {
840
+ return UUID_RE.test(segment) || NUMERIC_ID_RE.test(segment) || HEX_HASH_RE.test(segment) || ALPHA_TOKEN_RE.test(segment);
841
+ }
842
+ function normalizePath(path) {
843
+ const qIdx = path.indexOf("?");
844
+ const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
845
+ return pathname.split("/").map((seg) => seg && isDynamicSegment(seg) ? DYNAMIC_SEGMENT_PLACEHOLDER : seg).join("/");
846
+ }
847
+ function getEndpointKey(method, path) {
848
+ return `${method} ${normalizePath(path)}`;
849
+ }
850
+ function extractEndpointFromDesc(desc) {
851
+ return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
852
+ }
853
+ function stripQueryString(path) {
854
+ const i = path.indexOf("?");
855
+ return i === -1 ? path : path.slice(0, i);
856
+ }
857
+ var UUID_RE, NUMERIC_ID_RE, HEX_HASH_RE, ALPHA_TOKEN_RE, DYNAMIC_SEGMENT_PLACEHOLDER, ENDPOINT_PREFIX_RE;
858
+ var init_endpoint = __esm({
859
+ "src/utils/endpoint.ts"() {
881
860
  "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
- };
861
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
862
+ NUMERIC_ID_RE = /^\d+$/;
863
+ HEX_HASH_RE = /^[0-9a-f]{12,}$/i;
864
+ ALPHA_TOKEN_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/;
865
+ DYNAMIC_SEGMENT_PLACEHOLDER = ":id";
866
+ ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
895
867
  }
896
868
  });
897
869
 
@@ -915,6 +887,7 @@ var init_http_status = __esm({
915
887
  function detectCategory(req) {
916
888
  const { method, url, statusCode, responseHeaders } = req;
917
889
  if (req.isStatic) return "static";
890
+ if (req.isHealthCheck) return "health-check";
918
891
  if (statusCode === 307 && (url.includes("__clerk_handshake") || url.includes("__clerk_db_jwt"))) {
919
892
  return "auth-handshake";
920
893
  }
@@ -1079,20 +1052,41 @@ var init_label = __esm({
1079
1052
  });
1080
1053
 
1081
1054
  // src/analysis/transforms.ts
1082
- function markDuplicates(requests) {
1055
+ function isDuplicateCandidate(req) {
1056
+ return DUPLICATE_CATEGORIES.has(req.category);
1057
+ }
1058
+ function buildRequestKey(req) {
1059
+ return `${req.method} ${stripQueryString(getEffectivePath(req))}`;
1060
+ }
1061
+ function isStrictModePattern(requests, counts) {
1062
+ if (counts.size === 0 || ![...counts.values()].every((c) => c === 2)) {
1063
+ return false;
1064
+ }
1065
+ const firstByKey = /* @__PURE__ */ new Map();
1066
+ for (const req of requests) {
1067
+ if (!isDuplicateCandidate(req)) continue;
1068
+ const key = buildRequestKey(req);
1069
+ const first = firstByKey.get(key);
1070
+ if (!first) {
1071
+ firstByKey.set(key, req);
1072
+ } else if (Math.abs(req.startedAt - first.startedAt) > STRICT_MODE_MAX_GAP_MS) {
1073
+ return false;
1074
+ }
1075
+ }
1076
+ return true;
1077
+ }
1078
+ function flagDuplicateRequests(requests) {
1083
1079
  const counts = /* @__PURE__ */ new Map();
1084
1080
  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]}`;
1081
+ if (!isDuplicateCandidate(req)) continue;
1082
+ const key = buildRequestKey(req);
1088
1083
  counts.set(key, (counts.get(key) ?? 0) + 1);
1089
1084
  }
1090
- const isStrictMode = counts.size > 0 && [...counts.values()].every((c) => c === 2);
1085
+ const isStrictMode = isStrictModePattern(requests, counts);
1091
1086
  const seen = /* @__PURE__ */ new Set();
1092
1087
  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]}`;
1088
+ if (!isDuplicateCandidate(req)) continue;
1089
+ const key = buildRequestKey(req);
1096
1090
  if (seen.has(key)) {
1097
1091
  if (isStrictMode) {
1098
1092
  req.isStrictModeDupe = true;
@@ -1104,20 +1098,20 @@ function markDuplicates(requests) {
1104
1098
  }
1105
1099
  }
1106
1100
  }
1107
- function collapsePolling(requests) {
1101
+ function mergePollingSequences(requests) {
1108
1102
  const result = [];
1109
1103
  let i = 0;
1110
1104
  while (i < requests.length) {
1111
1105
  const current = requests[i];
1112
- const currentEffective = getEffectivePath(current).split("?")[0];
1106
+ const currentEffective = stripQueryString(getEffectivePath(current));
1113
1107
  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++;
1108
+ let nextIndex = i + 1;
1109
+ while (nextIndex < requests.length && requests[nextIndex].method === "GET" && stripQueryString(getEffectivePath(requests[nextIndex])) === currentEffective) {
1110
+ nextIndex++;
1117
1111
  }
1118
- const count = j - i;
1112
+ const count = nextIndex - i;
1119
1113
  if (count >= MIN_POLLING_SEQUENCE) {
1120
- const last = requests[j - 1];
1114
+ const last = requests[nextIndex - 1];
1121
1115
  const pollingDuration = last.startedAt + last.durationMs - current.startedAt;
1122
1116
  const endpointName = prettifyEndpoint(currentEffective);
1123
1117
  result.push({
@@ -1128,7 +1122,7 @@ function collapsePolling(requests) {
1128
1122
  pollingDurationMs: pollingDuration,
1129
1123
  isDuplicate: false
1130
1124
  });
1131
- i = j;
1125
+ i = nextIndex;
1132
1126
  continue;
1133
1127
  }
1134
1128
  }
@@ -1141,18 +1135,18 @@ function formatDurationLabel(ms) {
1141
1135
  if (ms < 1e3) return `${ms}ms`;
1142
1136
  return `${(ms / 1e3).toFixed(1)}s`;
1143
1137
  }
1144
- function detectWarnings(requests) {
1138
+ function collectRequestWarnings(requests) {
1145
1139
  const warnings = [];
1146
1140
  const duplicateCount = requests.filter((r) => r.isDuplicate).length;
1147
1141
  if (duplicateCount > 0) {
1148
1142
  const unique = new Set(
1149
- requests.filter((r) => r.isDuplicate).map((r) => `${r.method} ${getEffectivePath(r).split("?")[0]}`)
1143
+ requests.filter((r) => r.isDuplicate).map((r) => buildRequestKey(r))
1150
1144
  );
1151
1145
  const endpoints = unique.size;
1152
1146
  const sameData = requests.filter((r) => r.isDuplicate).every((r) => {
1153
- const key = `${r.method} ${getEffectivePath(r).split("?")[0]}`;
1147
+ const key = buildRequestKey(r);
1154
1148
  const first = requests.find(
1155
- (o) => !o.isDuplicate && `${o.method} ${getEffectivePath(o).split("?")[0]}` === key
1149
+ (o) => !o.isDuplicate && buildRequestKey(o) === key
1156
1150
  );
1157
1151
  return first && first.responseBody === r.responseBody;
1158
1152
  });
@@ -1173,18 +1167,30 @@ function detectWarnings(requests) {
1173
1167
  }
1174
1168
  return warnings;
1175
1169
  }
1170
+ var DUPLICATE_CATEGORIES;
1176
1171
  var init_transforms = __esm({
1177
1172
  "src/analysis/transforms.ts"() {
1178
1173
  "use strict";
1179
1174
  init_constants();
1175
+ init_config();
1180
1176
  init_categorize();
1181
1177
  init_label();
1182
1178
  init_http_status();
1179
+ init_endpoint();
1180
+ DUPLICATE_CATEGORIES = /* @__PURE__ */ new Set(["data-fetch", "auth-check"]);
1183
1181
  }
1184
1182
  });
1185
1183
 
1186
1184
  // src/analysis/group.ts
1187
1185
  import { randomUUID as randomUUID3 } from "crypto";
1186
+ function shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, startedAt) {
1187
+ if (currentRequests.length === 0) return false;
1188
+ const sourcePage = labeled.sourcePage;
1189
+ const isNewPage = sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
1190
+ const isTimeGap = startedAt - lastEndTime > FLOW_GAP_MS;
1191
+ const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
1192
+ return isNewPage || isTimeGap || isPageLoad;
1193
+ }
1188
1194
  function groupRequestsIntoFlows(requests) {
1189
1195
  if (requests.length === 0) return [];
1190
1196
  const flows = [];
@@ -1195,17 +1201,12 @@ function groupRequestsIntoFlows(requests) {
1195
1201
  if (req.path.startsWith(DASHBOARD_PREFIX)) continue;
1196
1202
  const labeled = labelRequest(req);
1197
1203
  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)) {
1204
+ if (shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, req.startedAt)) {
1204
1205
  flows.push(buildFlow(currentRequests));
1205
1206
  currentRequests = [];
1206
1207
  }
1207
1208
  currentRequests.push(labeled);
1208
- currentSourcePage = sourcePage ?? currentSourcePage;
1209
+ currentSourcePage = labeled.sourcePage ?? currentSourcePage;
1209
1210
  lastEndTime = Math.max(lastEndTime, req.startedAt + req.durationMs);
1210
1211
  }
1211
1212
  if (currentRequests.length > 0) {
@@ -1214,8 +1215,8 @@ function groupRequestsIntoFlows(requests) {
1214
1215
  return flows;
1215
1216
  }
1216
1217
  function buildFlow(rawRequests) {
1217
- markDuplicates(rawRequests);
1218
- const requests = collapsePolling(rawRequests);
1218
+ flagDuplicateRequests(rawRequests);
1219
+ const requests = mergePollingSequences(rawRequests);
1219
1220
  const first = requests[0];
1220
1221
  const startTime = first.startedAt;
1221
1222
  const endTime = Math.max(
@@ -1234,7 +1235,7 @@ function buildFlow(rawRequests) {
1234
1235
  startTime,
1235
1236
  totalDurationMs: Math.round(endTime - startTime),
1236
1237
  hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
1237
- warnings: detectWarnings(rawRequests),
1238
+ warnings: collectRequestWarnings(rawRequests),
1238
1239
  sourcePage,
1239
1240
  redundancyPct
1240
1241
  };
@@ -1246,20 +1247,20 @@ function getDominantSourcePage(requests) {
1246
1247
  counts.set(req.sourcePage, (counts.get(req.sourcePage) ?? 0) + 1);
1247
1248
  }
1248
1249
  }
1249
- let best = "";
1250
- let bestCount = 0;
1250
+ let mostCommonPage = "";
1251
+ let highestCount = 0;
1251
1252
  for (const [page, count] of counts) {
1252
- if (count > bestCount) {
1253
- best = page;
1254
- bestCount = count;
1253
+ if (count > highestCount) {
1254
+ mostCommonPage = page;
1255
+ highestCount = count;
1255
1256
  }
1256
1257
  }
1257
- return best || requests[0]?.path?.split("?")[0] || "/";
1258
+ return mostCommonPage || (requests[0]?.path ? stripQueryString(requests[0].path) : "") || "/";
1258
1259
  }
1259
1260
  function deriveFlowLabel(requests, sourcePage) {
1260
1261
  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
1262
  if (trigger.category === "page-load" || trigger.category === "navigation") {
1262
- const pageName = prettifyPageName(trigger.path.split("?")[0]);
1263
+ const pageName = prettifyPageName(stripQueryString(trigger.path));
1263
1264
  return `${pageName} Page`;
1264
1265
  }
1265
1266
  if (trigger.category === "api-call") {
@@ -1288,6 +1289,7 @@ var init_group = __esm({
1288
1289
  "use strict";
1289
1290
  init_constants();
1290
1291
  init_http_status();
1292
+ init_endpoint();
1291
1293
  init_label();
1292
1294
  init_categorize();
1293
1295
  init_transforms();
@@ -1385,12 +1387,28 @@ var init_shared2 = __esm({
1385
1387
  "src/dashboard/api/shared.ts"() {
1386
1388
  "use strict";
1387
1389
  init_constants();
1388
- init_limits();
1389
- init_http();
1390
+ init_config();
1391
+ init_labels();
1390
1392
  }
1391
1393
  });
1392
1394
 
1393
1395
  // src/dashboard/api/handlers.ts
1396
+ function filterByStatusRange(requests, statusStr) {
1397
+ if (statusStr.endsWith("xx")) {
1398
+ const prefix = parseInt(statusStr[0], 10);
1399
+ return requests.filter(
1400
+ (r) => Math.floor(r.statusCode / 100) === prefix
1401
+ );
1402
+ }
1403
+ const code = parseInt(statusStr, 10);
1404
+ return requests.filter((r) => r.statusCode === code);
1405
+ }
1406
+ function filterBySearch(requests, searchQuery) {
1407
+ const lower = searchQuery.toLowerCase();
1408
+ return requests.filter(
1409
+ (r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
1410
+ );
1411
+ }
1394
1412
  function sanitizeRequest(r) {
1395
1413
  return {
1396
1414
  ...r,
@@ -1398,7 +1416,7 @@ function sanitizeRequest(r) {
1398
1416
  responseHeaders: maskSensitiveHeaders(r.responseHeaders)
1399
1417
  };
1400
1418
  }
1401
- function createRequestsHandler(registry) {
1419
+ function createRequestsHandler(services) {
1402
1420
  return (req, res) => {
1403
1421
  if (!requireGet(req, res)) return;
1404
1422
  const url = parseRequestUrl(req);
@@ -1408,26 +1426,15 @@ function createRequestsHandler(registry) {
1408
1426
  const rawLimit = parseInt(url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT), 10);
1409
1427
  const limit = Math.min(Math.max(rawLimit || DEFAULT_API_LIMIT, 1), MAX_API_LIMIT);
1410
1428
  const offset = Math.max(parseInt(url.searchParams.get("offset") ?? "0", 10) || 0, 0);
1411
- let results = [...registry.get("request-store").getAll()].reverse();
1429
+ let results = [...services.requestStore.getAll()].reverse();
1412
1430
  if (method) {
1413
1431
  results = results.filter((r) => r.method === method.toUpperCase());
1414
1432
  }
1415
1433
  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
- }
1434
+ results = filterByStatusRange(results, status);
1425
1435
  }
1426
1436
  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
- );
1437
+ results = filterBySearch(results, search);
1431
1438
  }
1432
1439
  const total = results.length;
1433
1440
  results = results.slice(offset, offset + limit);
@@ -1435,96 +1442,84 @@ function createRequestsHandler(registry) {
1435
1442
  sendJson(req, res, HTTP_OK, { total, requests: sanitized });
1436
1443
  };
1437
1444
  }
1438
- function createFlowsHandler(registry) {
1445
+ function createFlowsHandler(services) {
1439
1446
  return (req, res) => {
1440
1447
  if (!requireGet(req, res)) return;
1441
- const flows = groupRequestsIntoFlows(registry.get("request-store").getAll()).reverse().map((flow) => ({
1448
+ const flows = groupRequestsIntoFlows(services.requestStore.getAll()).reverse().map((flow) => ({
1442
1449
  ...flow,
1443
1450
  requests: flow.requests.map(sanitizeRequest)
1444
1451
  }));
1445
1452
  sendJson(req, res, HTTP_OK, { total: flows.length, flows });
1446
1453
  };
1447
1454
  }
1448
- function createClearHandler(registry) {
1455
+ function createClearHandler(services) {
1449
1456
  return (req, res) => {
1450
1457
  if (req.method !== "POST") {
1451
1458
  sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1452
1459
  return;
1453
1460
  }
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);
1461
+ services.requestStore.clear();
1462
+ services.fetchStore.clear();
1463
+ services.logStore.clear();
1464
+ services.errorStore.clear();
1465
+ services.queryStore.clear();
1466
+ services.metricsStore.reset();
1467
+ services.issueStore.clear();
1468
+ services.bus.emit("store:cleared", void 0);
1462
1469
  sendJson(req, res, HTTP_OK, { cleared: true });
1463
1470
  };
1464
1471
  }
1465
- function createFetchesHandler(registry) {
1466
- return (req, res) => handleTelemetryGet(req, res, registry.get("fetch-store"));
1472
+ function createFetchesHandler(services) {
1473
+ return (req, res) => handleTelemetryGet(req, res, services.fetchStore);
1467
1474
  }
1468
- function createLogsHandler(registry) {
1469
- return (req, res) => handleTelemetryGet(req, res, registry.get("log-store"));
1475
+ function createLogsHandler(services) {
1476
+ return (req, res) => handleTelemetryGet(req, res, services.logStore);
1470
1477
  }
1471
- function createErrorsHandler(registry) {
1472
- return (req, res) => handleTelemetryGet(req, res, registry.get("error-store"));
1478
+ function createErrorsHandler(services) {
1479
+ return (req, res) => handleTelemetryGet(req, res, services.errorStore);
1473
1480
  }
1474
- function createQueriesHandler(registry) {
1475
- return (req, res) => handleTelemetryGet(req, res, registry.get("query-store"));
1481
+ function createQueriesHandler(services) {
1482
+ return (req, res) => handleTelemetryGet(req, res, services.queryStore);
1476
1483
  }
1477
1484
  var init_handlers = __esm({
1478
1485
  "src/dashboard/api/handlers.ts"() {
1479
1486
  "use strict";
1480
1487
  init_group();
1481
1488
  init_constants();
1482
- init_http();
1489
+ init_labels();
1483
1490
  init_shared2();
1484
1491
  }
1485
1492
  });
1486
1493
 
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;
1494
+ // src/utils/static-patterns.ts
1495
+ function isStaticPath(urlPath) {
1496
+ return STATIC_PATTERNS.some((p) => p.test(urlPath));
1516
1497
  }
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;
1498
+ function isHealthCheckPath(urlPath) {
1499
+ return HEALTH_CHECK_PATTERNS.some((p) => p.test(urlPath));
1522
1500
  }
1523
- var init_type_guards = __esm({
1524
- "src/utils/type-guards.ts"() {
1501
+ var STATIC_PATTERNS, HEALTH_CHECK_PATTERNS;
1502
+ var init_static_patterns = __esm({
1503
+ "src/utils/static-patterns.ts"() {
1525
1504
  "use strict";
1526
- init_lifecycle();
1527
- init_limits();
1505
+ STATIC_PATTERNS = [
1506
+ /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
1507
+ /^\/favicon/,
1508
+ /^\/node_modules\//,
1509
+ // Framework-specific static/internal paths
1510
+ /^\/_next\//,
1511
+ /^\/__nextjs/,
1512
+ /^\/@vite\//,
1513
+ /^\/__vite/
1514
+ ];
1515
+ HEALTH_CHECK_PATTERNS = [
1516
+ /^\/health(z|check)?$/i,
1517
+ /^\/ping$/i,
1518
+ /^\/(ready|readiness|liveness)$/i,
1519
+ /^\/status$/i,
1520
+ /^\/__health$/i,
1521
+ /^\/api\/health(z|check)?$/i
1522
+ ];
1528
1523
  }
1529
1524
  });
1530
1525
 
@@ -1545,8 +1540,8 @@ function numOrUndef(val) {
1545
1540
  function headers(val) {
1546
1541
  if (val && typeof val === "object" && !Array.isArray(val)) {
1547
1542
  const result = {};
1548
- for (const [k, v] of Object.entries(val)) {
1549
- if (typeof v === "string") result[k] = v;
1543
+ for (const [key, value] of Object.entries(val)) {
1544
+ if (typeof value === "string") result[key] = value;
1550
1545
  }
1551
1546
  return result;
1552
1547
  }
@@ -1610,7 +1605,7 @@ function parseRequestEvent(data, ts) {
1610
1605
  id: str(data.id, randomUUID4()),
1611
1606
  method: str(data.method, "GET"),
1612
1607
  url,
1613
- path: url.split("?")[0],
1608
+ path: stripQueryString(url),
1614
1609
  headers: headers(data.headers),
1615
1610
  requestBody: isString(data.requestBody) ? data.requestBody : null,
1616
1611
  statusCode: num(data.statusCode, 200),
@@ -1619,7 +1614,8 @@ function parseRequestEvent(data, ts) {
1619
1614
  startedAt: ts,
1620
1615
  durationMs: num(data.durationMs, 0),
1621
1616
  responseSize: num(data.responseSize, 0),
1622
- isStatic: isBoolean(data.isStatic) ? data.isStatic : false
1617
+ isStatic: isBoolean(data.isStatic) ? data.isStatic : false,
1618
+ isHealthCheck: isBoolean(data.isHealthCheck) ? data.isHealthCheck : isHealthCheckPath(stripQueryString(url))
1623
1619
  };
1624
1620
  }
1625
1621
  function routeSDKEvent(event, stores) {
@@ -1653,7 +1649,9 @@ var init_sdk_event_parser = __esm({
1653
1649
  "src/dashboard/api/sdk-event-parser.ts"() {
1654
1650
  "use strict";
1655
1651
  init_type_guards();
1656
- init_sdk_events();
1652
+ init_labels();
1653
+ init_static_patterns();
1654
+ init_endpoint();
1657
1655
  LOG_LEVEL_MAP = {
1658
1656
  debug: "debug",
1659
1657
  info: "info",
@@ -1673,28 +1671,28 @@ function isBrakitBatch(msg) {
1673
1671
  function isSDKPayload(msg) {
1674
1672
  return typeof msg === "object" && msg !== null && "_brakit" in msg && "version" in msg && typeof msg.version === "number";
1675
1673
  }
1676
- function createIngestHandler(registry) {
1674
+ function createIngestHandler(services) {
1677
1675
  const routeEvent = (event) => {
1678
1676
  switch (event.type) {
1679
1677
  case TIMELINE_FETCH:
1680
- registry.get("fetch-store").add(event.data);
1678
+ services.fetchStore.add(event.data);
1681
1679
  break;
1682
1680
  case TIMELINE_LOG:
1683
- registry.get("log-store").add(event.data);
1681
+ services.logStore.add(event.data);
1684
1682
  break;
1685
1683
  case TIMELINE_ERROR:
1686
- registry.get("error-store").add(event.data);
1684
+ services.errorStore.add(event.data);
1687
1685
  break;
1688
1686
  case TIMELINE_QUERY:
1689
- registry.get("query-store").add(event.data);
1687
+ services.queryStore.add(event.data);
1690
1688
  break;
1691
1689
  }
1692
1690
  };
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");
1691
+ const queryStore = services.queryStore;
1692
+ const fetchStore = services.fetchStore;
1693
+ const logStore = services.logStore;
1694
+ const errorStore = services.errorStore;
1695
+ const requestStore = services.requestStore;
1698
1696
  const stores = {
1699
1697
  addQuery: (data) => queryStore.add(data),
1700
1698
  addFetch: (data) => fetchStore.add(data),
@@ -1754,9 +1752,8 @@ function createIngestHandler(registry) {
1754
1752
  var init_ingest = __esm({
1755
1753
  "src/dashboard/api/ingest.ts"() {
1756
1754
  "use strict";
1757
- init_limits();
1758
- init_http();
1759
- init_timeline();
1755
+ init_config();
1756
+ init_labels();
1760
1757
  init_shared2();
1761
1758
  init_sdk_event_parser();
1762
1759
  }
@@ -1776,11 +1773,11 @@ function createMetricsHandler(metricsStore) {
1776
1773
  sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
1777
1774
  };
1778
1775
  }
1779
- var init_metrics2 = __esm({
1776
+ var init_metrics = __esm({
1780
1777
  "src/dashboard/api/metrics.ts"() {
1781
1778
  "use strict";
1782
1779
  init_shared2();
1783
- init_http();
1780
+ init_labels();
1784
1781
  }
1785
1782
  });
1786
1783
 
@@ -1798,61 +1795,55 @@ var init_metrics_live = __esm({
1798
1795
  }
1799
1796
  });
1800
1797
 
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
1798
  // src/dashboard/api/activity.ts
1821
- function createActivityHandler(registry) {
1799
+ function buildTimeline(services, requestId) {
1800
+ const fetches = services.fetchStore.getByRequest(requestId);
1801
+ const logs = services.logStore.getByRequest(requestId);
1802
+ const errors = services.errorStore.getByRequest(requestId);
1803
+ const queries = services.queryStore.getByRequest(requestId);
1804
+ const timeline = [];
1805
+ for (const fetch of fetches)
1806
+ timeline.push({ type: TIMELINE_FETCH, timestamp: fetch.timestamp, data: fetch });
1807
+ for (const log of logs)
1808
+ timeline.push({ type: TIMELINE_LOG, timestamp: log.timestamp, data: log });
1809
+ for (const error of errors)
1810
+ timeline.push({ type: TIMELINE_ERROR, timestamp: error.timestamp, data: error });
1811
+ for (const query of queries)
1812
+ timeline.push({ type: TIMELINE_QUERY, timestamp: query.timestamp, data: query });
1813
+ timeline.sort((a, b) => a.timestamp - b.timestamp);
1814
+ return {
1815
+ total: timeline.length,
1816
+ timeline,
1817
+ counts: {
1818
+ fetches: fetches.length,
1819
+ logs: logs.length,
1820
+ errors: errors.length,
1821
+ queries: queries.length
1822
+ }
1823
+ };
1824
+ }
1825
+ function createActivityHandler(services) {
1822
1826
  return (req, res) => {
1823
1827
  if (!requireGet(req, res)) return;
1824
1828
  try {
1825
1829
  const url = parseRequestUrl(req);
1826
1830
  const requestId = url.searchParams.get("requestId");
1827
- if (!requestId) {
1828
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId parameter required" });
1831
+ const requestIds = url.searchParams.get("requestIds");
1832
+ if (!requestId && !requestIds) {
1833
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId or requestIds parameter required" });
1829
1834
  return;
1830
1835
  }
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
- });
1836
+ if (requestId) {
1837
+ const result = buildTimeline(services, requestId);
1838
+ sendJson(req, res, HTTP_OK, { requestId, ...result });
1839
+ return;
1840
+ }
1841
+ const ids = (requestIds || "").split(",").filter(Boolean).slice(0, MAX_BATCH_IDS);
1842
+ const activities = {};
1843
+ for (const id of ids) {
1844
+ activities[id] = buildTimeline(services, id);
1845
+ }
1846
+ sendJson(req, res, HTTP_OK, { requestIds: ids, activities });
1856
1847
  } catch (err) {
1857
1848
  brakitDebug(`activity handler error: ${err}`);
1858
1849
  if (!res.headersSent) {
@@ -1861,13 +1852,14 @@ function createActivityHandler(registry) {
1861
1852
  }
1862
1853
  };
1863
1854
  }
1855
+ var MAX_BATCH_IDS;
1864
1856
  var init_activity = __esm({
1865
1857
  "src/dashboard/api/activity.ts"() {
1866
1858
  "use strict";
1867
1859
  init_shared2();
1868
- init_http();
1869
- init_timeline();
1860
+ init_labels();
1870
1861
  init_log();
1862
+ MAX_BATCH_IDS = 50;
1871
1863
  }
1872
1864
  });
1873
1865
 
@@ -1877,7 +1869,7 @@ var init_api = __esm({
1877
1869
  "use strict";
1878
1870
  init_handlers();
1879
1871
  init_ingest();
1880
- init_metrics2();
1872
+ init_metrics();
1881
1873
  init_metrics_live();
1882
1874
  init_activity();
1883
1875
  }
@@ -1952,25 +1944,12 @@ var init_issues = __esm({
1952
1944
  "use strict";
1953
1945
  init_shared2();
1954
1946
  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";
1947
+ init_labels();
1969
1948
  }
1970
1949
  });
1971
1950
 
1972
1951
  // src/dashboard/sse.ts
1973
- function createSSEHandler(registry) {
1952
+ function createSSEHandler(services) {
1974
1953
  const clients = /* @__PURE__ */ new Set();
1975
1954
  function broadcast(eventType, data) {
1976
1955
  if (clients.size === 0) return;
@@ -1992,7 +1971,7 @@ data: ${data}
1992
1971
  }
1993
1972
  }
1994
1973
  }
1995
- const bus = registry.get("event-bus");
1974
+ const bus = services.bus;
1996
1975
  bus.on("request:completed", (r) => broadcast(null, JSON.stringify(r)));
1997
1976
  bus.on("telemetry:fetch", (e) => broadcast(SSE_EVENT_FETCH, JSON.stringify(e)));
1998
1977
  bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
@@ -2042,8 +2021,7 @@ var init_sse = __esm({
2042
2021
  "src/dashboard/sse.ts"() {
2043
2022
  "use strict";
2044
2023
  init_constants();
2045
- init_http();
2046
- init_events();
2024
+ init_labels();
2047
2025
  init_shared2();
2048
2026
  }
2049
2027
  });
@@ -2098,7 +2076,7 @@ async function ensureGitignoreAsync(dir, entry) {
2098
2076
  var init_fs = __esm({
2099
2077
  "src/utils/fs.ts"() {
2100
2078
  "use strict";
2101
- init_limits();
2079
+ init_config();
2102
2080
  init_log();
2103
2081
  init_type_guards();
2104
2082
  }
@@ -2187,7 +2165,7 @@ function computeIssueId(issue) {
2187
2165
  var init_issue_id = __esm({
2188
2166
  "src/utils/issue-id.ts"() {
2189
2167
  "use strict";
2190
- init_limits();
2168
+ init_config();
2191
2169
  }
2192
2170
  });
2193
2171
 
@@ -2200,10 +2178,7 @@ var init_issue_store = __esm({
2200
2178
  "src/store/issue-store.ts"() {
2201
2179
  "use strict";
2202
2180
  init_fs();
2203
- init_metrics();
2204
- init_limits();
2205
- init_thresholds();
2206
- init_limits();
2181
+ init_config();
2207
2182
  init_atomic_writer();
2208
2183
  init_log();
2209
2184
  init_type_guards();
@@ -2446,7 +2421,7 @@ function unwrapResponse(parsed) {
2446
2421
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
2447
2422
  const obj = parsed;
2448
2423
  const keys = Object.keys(obj);
2449
- if (keys.length > 3) return parsed;
2424
+ if (keys.length > MAX_WRAPPER_KEYS) return parsed;
2450
2425
  let best = null;
2451
2426
  let bestSize = 0;
2452
2427
  for (const key of keys) {
@@ -2464,10 +2439,12 @@ function unwrapResponse(parsed) {
2464
2439
  }
2465
2440
  return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
2466
2441
  }
2442
+ var MAX_WRAPPER_KEYS;
2467
2443
  var init_response = __esm({
2468
2444
  "src/utils/response.ts"() {
2469
2445
  "use strict";
2470
- init_thresholds();
2446
+ init_config();
2447
+ MAX_WRAPPER_KEYS = 3;
2471
2448
  }
2472
2449
  });
2473
2450
 
@@ -2505,92 +2482,154 @@ var init_patterns = __esm({
2505
2482
  }
2506
2483
  });
2507
2484
 
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));
2516
- }
2517
- return found;
2485
+ // src/utils/collections.ts
2486
+ function getOrCreate(map, key, create) {
2487
+ let value = map.get(key);
2488
+ if (value === void 0) {
2489
+ value = create();
2490
+ map.set(key, value);
2518
2491
  }
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);
2523
- }
2524
- if (typeof val === "object" && val !== null) {
2525
- found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
2492
+ return value;
2493
+ }
2494
+ function deduplicateFindings(items, extract) {
2495
+ const seen = /* @__PURE__ */ new Map();
2496
+ const findings = [];
2497
+ for (const item of items) {
2498
+ const result = extract(item);
2499
+ if (!result) continue;
2500
+ const existing = seen.get(result.key);
2501
+ if (existing) {
2502
+ existing.count++;
2503
+ continue;
2526
2504
  }
2505
+ seen.set(result.key, result.finding);
2506
+ findings.push(result.finding);
2527
2507
  }
2528
- return found;
2508
+ return findings;
2529
2509
  }
2530
- var exposedSecretRule;
2531
- var init_exposed_secret = __esm({
2532
- "src/analysis/rules/exposed-secret.ts"() {
2533
- "use strict";
2534
- init_patterns();
2535
- init_limits();
2536
- init_http_status();
2537
- exposedSecretRule = {
2538
- id: "exposed-secret",
2539
- severity: "critical",
2540
- name: "Exposed Secret in Response",
2541
- hint: RULE_HINTS["exposed-secret"],
2542
- 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
2566
- };
2567
- seen.set(dedupKey, finding);
2568
- findings.push(finding);
2569
- }
2570
- return findings;
2571
- }
2510
+ function groupBy(items, keyFn) {
2511
+ const map = /* @__PURE__ */ new Map();
2512
+ for (const item of items) {
2513
+ const key = keyFn(item);
2514
+ if (key == null) continue;
2515
+ let arr = map.get(key);
2516
+ if (!arr) {
2517
+ arr = [];
2518
+ map.set(key, arr);
2519
+ }
2520
+ arr.push(item);
2521
+ }
2522
+ return map;
2523
+ }
2524
+ var init_collections = __esm({
2525
+ "src/utils/collections.ts"() {
2526
+ "use strict";
2527
+ }
2528
+ });
2529
+
2530
+ // src/utils/object-scan.ts
2531
+ function walkObject(obj, visitor, options) {
2532
+ const opts = { ...DEFAULTS, ...options };
2533
+ walk(obj, visitor, opts, 0);
2534
+ }
2535
+ function walk(obj, visitor, opts, depth) {
2536
+ if (depth >= opts.maxDepth) return;
2537
+ if (!obj || typeof obj !== "object") return;
2538
+ if (Array.isArray(obj)) {
2539
+ for (let i = 0; i < Math.min(obj.length, opts.arrayLimit); i++) {
2540
+ walk(obj[i], visitor, opts, depth + 1);
2541
+ }
2542
+ return;
2543
+ }
2544
+ for (const key of Object.keys(obj)) {
2545
+ const val = obj[key];
2546
+ visitor(key, val, depth);
2547
+ if (typeof val === "object" && val !== null) {
2548
+ walk(val, visitor, opts, depth + 1);
2549
+ }
2550
+ }
2551
+ }
2552
+ function collectFromObject(obj, match, options) {
2553
+ const results = [];
2554
+ walkObject(obj, (key, value) => {
2555
+ const result = match(key, value);
2556
+ if (result !== null) results.push(result);
2557
+ }, options);
2558
+ return results;
2559
+ }
2560
+ var DEFAULTS;
2561
+ var init_object_scan = __esm({
2562
+ "src/utils/object-scan.ts"() {
2563
+ "use strict";
2564
+ init_config();
2565
+ DEFAULTS = {
2566
+ maxDepth: MAX_OBJECT_SCAN_DEPTH,
2567
+ arrayLimit: SECRET_SCAN_ARRAY_LIMIT
2572
2568
  };
2573
2569
  }
2574
2570
  });
2575
2571
 
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"() {
2572
+ // src/analysis/rules/auth-rules.ts
2573
+ function findSecretKeys(obj) {
2574
+ return collectFromObject(
2575
+ obj,
2576
+ (key, val) => SECRET_KEYS.test(key) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val) ? key : null
2577
+ );
2578
+ }
2579
+ function isFrameworkResponse(request) {
2580
+ if (isRedirect(request.statusCode)) return true;
2581
+ if (request.path?.startsWith("/__")) return true;
2582
+ if (request.responseHeaders?.["x-middleware-rewrite"]) return true;
2583
+ return false;
2584
+ }
2585
+ var exposedSecretRule, tokenInUrlRule, insecureCookieRule, corsCredentialsRule;
2586
+ var init_auth_rules = __esm({
2587
+ "src/analysis/rules/auth-rules.ts"() {
2580
2588
  "use strict";
2581
2589
  init_patterns();
2590
+ init_config();
2591
+ init_http_status();
2592
+ init_collections();
2593
+ init_object_scan();
2594
+ exposedSecretRule = {
2595
+ id: "exposed-secret",
2596
+ severity: "critical",
2597
+ name: "Exposed Secret in Response",
2598
+ hint: RULE_HINTS["exposed-secret"],
2599
+ check(ctx) {
2600
+ return deduplicateFindings(ctx.requests, (request) => {
2601
+ if (isErrorStatus(request.statusCode)) return null;
2602
+ const parsed = ctx.parsedBodies.response.get(request.id);
2603
+ if (!parsed) return null;
2604
+ const keys = findSecretKeys(parsed);
2605
+ if (keys.length === 0) return null;
2606
+ const ep = `${request.method} ${request.path}`;
2607
+ return {
2608
+ key: `${ep}:${keys.sort().join(",")}`,
2609
+ finding: {
2610
+ severity: "critical",
2611
+ rule: "exposed-secret",
2612
+ title: "Exposed Secret in Response",
2613
+ desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
2614
+ hint: this.hint,
2615
+ detail: `Exposed fields: ${keys.join(", ")}. ${keys.length} unmasked secret value${keys.length !== 1 ? "s" : ""} in response body.`,
2616
+ endpoint: ep,
2617
+ count: 1
2618
+ }
2619
+ };
2620
+ });
2621
+ }
2622
+ };
2582
2623
  tokenInUrlRule = {
2583
2624
  id: "token-in-url",
2584
2625
  severity: "critical",
2585
2626
  name: "Auth Token in URL",
2586
2627
  hint: RULE_HINTS["token-in-url"],
2587
2628
  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("&");
2629
+ return deduplicateFindings(ctx.requests, (request) => {
2630
+ const qIdx = request.url.indexOf("?");
2631
+ if (qIdx === -1) return null;
2632
+ const params = request.url.substring(qIdx + 1).split("&");
2594
2633
  const flagged = [];
2595
2634
  for (const param of params) {
2596
2635
  const [name, ...rest] = param.split("=");
@@ -2600,222 +2639,64 @@ var init_token_in_url = __esm({
2600
2639
  flagged.push(name);
2601
2640
  }
2602
2641
  }
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 = {
2642
+ if (flagged.length === 0) return null;
2643
+ const ep = `${request.method} ${request.path}`;
2644
+ return {
2645
+ key: `${ep}:${flagged.sort().join(",")}`,
2646
+ finding: {
2703
2647
  severity: "critical",
2704
- rule: "error-info-leak",
2705
- title: "Sensitive Data in Error Response",
2706
- desc: `${ep} \u2014 error response exposes ${p.label}`,
2648
+ rule: "token-in-url",
2649
+ title: "Auth Token in URL",
2650
+ desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
2707
2651
  hint: this.hint,
2652
+ detail: `Parameters in URL: ${flagged.join(", ")}. Auth tokens in URLs are logged by proxies, browsers, and CDNs.`,
2708
2653
  endpoint: ep,
2709
2654
  count: 1
2710
- };
2711
- seen.set(dedupKey, finding);
2712
- findings.push(finding);
2713
- }
2714
- }
2715
- return findings;
2655
+ }
2656
+ };
2657
+ });
2716
2658
  }
2717
2659
  };
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
2660
  insecureCookieRule = {
2735
2661
  id: "insecure-cookie",
2736
2662
  severity: "warning",
2737
2663
  name: "Insecure Cookie",
2738
2664
  hint: RULE_HINTS["insecure-cookie"],
2739
2665
  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"];
2666
+ const cookieEntries = [];
2667
+ for (const request of ctx.requests) {
2668
+ if (!request.responseHeaders) continue;
2669
+ if (isFrameworkResponse(request)) continue;
2670
+ const setCookie = request.responseHeaders["set-cookie"];
2746
2671
  if (!setCookie) continue;
2747
2672
  const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
2748
2673
  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 = {
2674
+ cookieEntries.push({ cookie });
2675
+ }
2676
+ }
2677
+ return deduplicateFindings(cookieEntries, ({ cookie }) => {
2678
+ const cookieName = cookie.trim().split("=")[0].trim();
2679
+ const lower = cookie.toLowerCase();
2680
+ const issues = [];
2681
+ if (!lower.includes("httponly")) issues.push("HttpOnly");
2682
+ if (!lower.includes("samesite")) issues.push("SameSite");
2683
+ if (issues.length === 0) return null;
2684
+ return {
2685
+ key: `${cookieName}:${issues.join(",")}`,
2686
+ finding: {
2762
2687
  severity: "warning",
2763
2688
  rule: "insecure-cookie",
2764
2689
  title: "Insecure Cookie",
2765
2690
  desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
2766
2691
  hint: this.hint,
2692
+ 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
2693
  endpoint: cookieName,
2768
2694
  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
- }];
2695
+ }
2696
+ };
2697
+ });
2808
2698
  }
2809
2699
  };
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
2700
  corsCredentialsRule = {
2820
2701
  id: "cors-credentials",
2821
2702
  severity: "warning",
@@ -2824,12 +2705,12 @@ var init_cors_credentials = __esm({
2824
2705
  check(ctx) {
2825
2706
  const findings = [];
2826
2707
  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"];
2708
+ for (const request of ctx.requests) {
2709
+ if (!request.responseHeaders) continue;
2710
+ const origin = request.responseHeaders["access-control-allow-origin"];
2711
+ const creds = request.responseHeaders["access-control-allow-credentials"];
2831
2712
  if (origin !== "*" || creds !== "true") continue;
2832
- const ep = `${r.method} ${r.path}`;
2713
+ const ep = `${request.method} ${request.path}`;
2833
2714
  if (seen.has(ep)) continue;
2834
2715
  seen.add(ep);
2835
2716
  findings.push({
@@ -2848,25 +2729,13 @@ var init_cors_credentials = __esm({
2848
2729
  }
2849
2730
  });
2850
2731
 
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;
2732
+ // src/analysis/rules/data-rules.ts
2733
+ function findEmails(obj) {
2734
+ return collectFromObject(
2735
+ obj,
2736
+ (_key, val) => typeof val === "string" && EMAIL_RE.test(val) ? val : null,
2737
+ { arrayLimit: PII_SCAN_ARRAY_LIMIT }
2738
+ );
2870
2739
  }
2871
2740
  function topLevelFieldCount(obj) {
2872
2741
  if (Array.isArray(obj)) {
@@ -2939,14 +2808,107 @@ function detectPII(method, reqBody, resBody) {
2939
2808
  const target = unwrapResponse(resBody);
2940
2809
  return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
2941
2810
  }
2942
- var WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
2943
- var init_response_pii_leak = __esm({
2944
- "src/analysis/rules/response-pii-leak.ts"() {
2811
+ var stackTraceLeakRule, CRITICAL_PATTERNS, errorInfoLeakRule, sensitiveLogsRule, WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
2812
+ var init_data_rules = __esm({
2813
+ "src/analysis/rules/data-rules.ts"() {
2945
2814
  "use strict";
2815
+ init_collections();
2946
2816
  init_patterns();
2947
2817
  init_response();
2948
- init_limits();
2818
+ init_config();
2949
2819
  init_http_status();
2820
+ init_object_scan();
2821
+ stackTraceLeakRule = {
2822
+ id: "stack-trace-leak",
2823
+ severity: "critical",
2824
+ name: "Stack Trace Leaked to Client",
2825
+ hint: RULE_HINTS["stack-trace-leak"],
2826
+ check(ctx) {
2827
+ return deduplicateFindings(ctx.requests, (request) => {
2828
+ if (!request.responseBody) return null;
2829
+ if (!STACK_TRACE_RE.test(request.responseBody)) return null;
2830
+ const ep = `${request.method} ${request.path}`;
2831
+ const firstLine = request.responseBody.split("\n").find((l) => STACK_TRACE_RE.test(l))?.trim() ?? "";
2832
+ return {
2833
+ key: ep,
2834
+ finding: {
2835
+ severity: "critical",
2836
+ rule: "stack-trace-leak",
2837
+ title: "Stack Trace Leaked to Client",
2838
+ desc: `${ep} \u2014 response exposes internal stack trace`,
2839
+ hint: this.hint,
2840
+ detail: firstLine ? `Stack trace: ${firstLine.slice(0, 120)}` : void 0,
2841
+ endpoint: ep,
2842
+ count: 1
2843
+ }
2844
+ };
2845
+ });
2846
+ }
2847
+ };
2848
+ CRITICAL_PATTERNS = [
2849
+ { re: DB_CONN_RE, label: "database connection string" },
2850
+ { re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
2851
+ { re: SECRET_VAL_RE, label: "secret value" }
2852
+ ];
2853
+ errorInfoLeakRule = {
2854
+ id: "error-info-leak",
2855
+ severity: "critical",
2856
+ name: "Sensitive Data in Error Response",
2857
+ hint: RULE_HINTS["error-info-leak"],
2858
+ check(ctx) {
2859
+ const entries = [];
2860
+ for (const request of ctx.requests) {
2861
+ if (request.statusCode < 400) continue;
2862
+ if (!request.responseBody) continue;
2863
+ if (request.responseHeaders["x-nextjs-error"] || request.responseHeaders["x-nextjs-matched-path"]) continue;
2864
+ const ep = `${request.method} ${request.path}`;
2865
+ for (const pattern of CRITICAL_PATTERNS) {
2866
+ if (pattern.re.test(request.responseBody)) {
2867
+ entries.push({ ep, pattern, body: request.responseBody });
2868
+ }
2869
+ }
2870
+ }
2871
+ return deduplicateFindings(entries, ({ ep, pattern }) => {
2872
+ return {
2873
+ key: `${ep}:${pattern.label}`,
2874
+ finding: {
2875
+ severity: "critical",
2876
+ rule: "error-info-leak",
2877
+ title: "Sensitive Data in Error Response",
2878
+ desc: `${ep} \u2014 error response exposes ${pattern.label}`,
2879
+ hint: this.hint,
2880
+ detail: `Detected: ${pattern.label} in error response body`,
2881
+ endpoint: ep,
2882
+ count: 1
2883
+ }
2884
+ };
2885
+ });
2886
+ }
2887
+ };
2888
+ sensitiveLogsRule = {
2889
+ id: "sensitive-logs",
2890
+ severity: "warning",
2891
+ name: "Sensitive Data in Logs",
2892
+ hint: RULE_HINTS["sensitive-logs"],
2893
+ check(ctx) {
2894
+ let count = 0;
2895
+ for (const log of ctx.logs) {
2896
+ if (!log.message) continue;
2897
+ if (log.message.startsWith("[brakit]")) continue;
2898
+ if (LOG_SECRET_RE.test(log.message)) count++;
2899
+ }
2900
+ if (count === 0) return [];
2901
+ return [{
2902
+ severity: "warning",
2903
+ rule: "sensitive-logs",
2904
+ title: "Sensitive Data in Logs",
2905
+ desc: `Console output contains secret/token values \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
2906
+ hint: this.hint,
2907
+ endpoint: "console",
2908
+ count
2909
+ }];
2910
+ }
2911
+ };
2950
2912
  WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
2951
2913
  REASON_LABELS = {
2952
2914
  echo: "echoes back PII from the request body",
@@ -2960,35 +2922,33 @@ var init_response_pii_leak = __esm({
2960
2922
  name: "PII Leak in Response",
2961
2923
  hint: RULE_HINTS["response-pii-leak"],
2962
2924
  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
2925
+ return deduplicateFindings(ctx.requests, (request) => {
2926
+ if (isErrorStatus(request.statusCode)) return null;
2927
+ if (SELF_SERVICE_PATH.test(request.path)) return null;
2928
+ const resJson = ctx.parsedBodies.response.get(request.id);
2929
+ if (!resJson) return null;
2930
+ const reqJson = ctx.parsedBodies.request.get(request.id) ?? null;
2931
+ const detection = detectPII(request.method, reqJson, resJson);
2932
+ if (!detection) return null;
2933
+ const ep = `${request.method} ${request.path}`;
2934
+ const fieldCount = topLevelFieldCount(resJson);
2935
+ const detailParts = [`Pattern: ${REASON_LABELS[detection.reason]}`];
2936
+ if (detection.emailCount > 0) detailParts.push(`${detection.emailCount} email${detection.emailCount !== 1 ? "s" : ""} detected`);
2937
+ if (fieldCount > 0) detailParts.push(`${fieldCount} fields per record`);
2938
+ return {
2939
+ key: ep,
2940
+ finding: {
2941
+ severity: "warning",
2942
+ rule: "response-pii-leak",
2943
+ title: "PII Leak in Response",
2944
+ desc: `${ep} \u2014 exposes PII in response`,
2945
+ hint: this.hint,
2946
+ detail: detailParts.join(". "),
2947
+ endpoint: ep,
2948
+ count: 1
2949
+ }
2987
2950
  };
2988
- seen.set(ep, finding);
2989
- findings.push(finding);
2990
- }
2991
- return findings;
2951
+ });
2992
2952
  }
2993
2953
  };
2994
2954
  }
@@ -2998,14 +2958,14 @@ var init_response_pii_leak = __esm({
2998
2958
  function buildBodyCache(requests) {
2999
2959
  const response = /* @__PURE__ */ new Map();
3000
2960
  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);
2961
+ for (const req of requests) {
2962
+ if (req.responseBody) {
2963
+ const parsed = tryParseJson(req.responseBody);
2964
+ if (parsed != null) response.set(req.id, parsed);
3005
2965
  }
3006
- if (r.requestBody) {
3007
- const parsed = tryParseJson(r.requestBody);
3008
- if (parsed != null) request.set(r.id, parsed);
2966
+ if (req.requestBody) {
2967
+ const parsed = tryParseJson(req.requestBody);
2968
+ if (parsed != null) request.set(req.id, parsed);
3009
2969
  }
3010
2970
  }
3011
2971
  return { response, request };
@@ -3027,14 +2987,10 @@ var init_scanner = __esm({
3027
2987
  "src/analysis/rules/scanner.ts"() {
3028
2988
  "use strict";
3029
2989
  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();
2990
+ init_log();
2991
+ init_type_guards();
2992
+ init_auth_rules();
2993
+ init_data_rules();
3038
2994
  SecurityScanner = class {
3039
2995
  constructor() {
3040
2996
  this.rules = [];
@@ -3051,7 +3007,8 @@ var init_scanner = __esm({
3051
3007
  for (const rule of this.rules) {
3052
3008
  try {
3053
3009
  findings.push(...rule.check(ctx));
3054
- } catch {
3010
+ } catch (e) {
3011
+ brakitDebug(`rule ${rule.id} failed: ${getErrorMessage(e)}`);
3055
3012
  }
3056
3013
  }
3057
3014
  return findings;
@@ -3068,76 +3025,28 @@ var init_rules = __esm({
3068
3025
  "src/analysis/rules/index.ts"() {
3069
3026
  "use strict";
3070
3027
  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();
3028
+ init_auth_rules();
3029
+ init_data_rules();
3079
3030
  }
3080
- });
3081
-
3082
- // src/core/disposable.ts
3083
- var SubscriptionBag;
3084
- var init_disposable = __esm({
3085
- "src/core/disposable.ts"() {
3086
- "use strict";
3087
- SubscriptionBag = class {
3088
- constructor() {
3089
- this.items = [];
3090
- }
3091
- add(teardown) {
3092
- this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
3093
- }
3094
- dispose() {
3095
- for (const d of this.items) d.dispose();
3096
- this.items.length = 0;
3097
- }
3098
- };
3099
- }
3100
- });
3101
-
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"() {
3031
+ });
3032
+
3033
+ // src/core/disposable.ts
3034
+ var SubscriptionBag;
3035
+ var init_disposable = __esm({
3036
+ "src/core/disposable.ts"() {
3138
3037
  "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+)/;
3038
+ SubscriptionBag = class {
3039
+ constructor() {
3040
+ this.items = [];
3041
+ }
3042
+ add(teardown) {
3043
+ this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
3044
+ }
3045
+ dispose() {
3046
+ for (const d of this.items) d.dispose();
3047
+ this.items.length = 0;
3048
+ }
3049
+ };
3141
3050
  }
3142
3051
  });
3143
3052
 
@@ -3161,7 +3070,7 @@ var init_query_helpers = __esm({
3161
3070
  });
3162
3071
 
3163
3072
  // src/analysis/insights/prepare.ts
3164
- function createEndpointGroup() {
3073
+ function emptyEndpointGroup() {
3165
3074
  return {
3166
3075
  total: 0,
3167
3076
  errors: 0,
@@ -3173,16 +3082,12 @@ function createEndpointGroup() {
3173
3082
  queryShapeDurations: /* @__PURE__ */ new Map()
3174
3083
  };
3175
3084
  }
3176
- function windowByEndpoint(requests) {
3085
+ function keepRecentPerEndpoint(requests) {
3177
3086
  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);
3087
+ for (const request of requests) {
3088
+ const endpointKey = getEndpointKey(request.method, request.path);
3089
+ const list = getOrCreate(byEndpoint, endpointKey, () => []);
3090
+ list.push(request);
3186
3091
  }
3187
3092
  const windowed = [];
3188
3093
  for (const [, reqs] of byEndpoint) {
@@ -3190,54 +3095,67 @@ function windowByEndpoint(requests) {
3190
3095
  }
3191
3096
  return windowed;
3192
3097
  }
3098
+ function filterUserRequests(requests) {
3099
+ return requests.filter(
3100
+ (request) => !request.isStatic && !request.isHealthCheck && (!request.path || !request.path.startsWith(DASHBOARD_PREFIX))
3101
+ );
3102
+ }
3193
3103
  function extractActiveEndpoints(requests) {
3194
3104
  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
- }
3105
+ for (const request of filterUserRequests(requests)) {
3106
+ endpoints.add(getEndpointKey(request.method, request.path));
3199
3107
  }
3200
3108
  return endpoints;
3201
3109
  }
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);
3110
+ function aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq) {
3210
3111
  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);
3112
+ for (const request of recent) {
3113
+ const endpointKey = getEndpointKey(request.method, request.path);
3114
+ const group = getOrCreate(endpointGroups, endpointKey, emptyEndpointGroup);
3115
+ group.total++;
3116
+ if (isErrorStatus(request.statusCode)) group.errors++;
3117
+ group.totalDuration += request.durationMs;
3118
+ group.totalSize += request.responseSize ?? 0;
3119
+ const reqQueries = queriesByReq.get(request.id) ?? [];
3120
+ group.queryCount += reqQueries.length;
3121
+ for (const query of reqQueries) {
3122
+ group.totalQueryTimeMs += query.durationMs;
3123
+ const shape = getQueryShape(query);
3124
+ const info = getQueryInfo(query);
3125
+ const shapeDuration = getOrCreate(group.queryShapeDurations, shape, () => ({
3126
+ totalMs: 0,
3127
+ count: 0,
3128
+ label: info.op + (info.table ? ` ${info.table}` : "")
3129
+ }));
3130
+ shapeDuration.totalMs += query.durationMs;
3131
+ shapeDuration.count++;
3217
3132
  }
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++;
3133
+ const reqFetches = fetchesByReq.get(request.id) ?? [];
3134
+ for (const fetch of reqFetches) {
3135
+ group.totalFetchTimeMs += fetch.durationMs;
3235
3136
  }
3236
- const reqFetches = fetchesByReq.get(r.id) ?? [];
3237
- for (const f of reqFetches) {
3238
- g.totalFetchTimeMs += f.durationMs;
3137
+ }
3138
+ return endpointGroups;
3139
+ }
3140
+ function collectStrictModeDupeIds(ctx) {
3141
+ const ids = /* @__PURE__ */ new Set();
3142
+ for (const flow of ctx.flows) {
3143
+ for (const req of flow.requests) {
3144
+ if (req.isStrictModeDupe) ids.add(req.id);
3239
3145
  }
3240
3146
  }
3147
+ return ids;
3148
+ }
3149
+ function buildInsightContext(ctx) {
3150
+ const strictModeDupeIds = collectStrictModeDupeIds(ctx);
3151
+ const nonStatic = filterUserRequests(ctx.requests).filter((req) => !strictModeDupeIds.has(req.id));
3152
+ const filteredQueries = strictModeDupeIds.size > 0 ? ctx.queries.filter((q) => !q.parentRequestId || !strictModeDupeIds.has(q.parentRequestId)) : ctx.queries;
3153
+ const filteredFetches = strictModeDupeIds.size > 0 ? ctx.fetches.filter((f) => !f.parentRequestId || !strictModeDupeIds.has(f.parentRequestId)) : ctx.fetches;
3154
+ const queriesByReq = groupBy(filteredQueries, (query) => query.parentRequestId);
3155
+ const fetchesByReq = groupBy(filteredFetches, (fetch) => fetch.parentRequestId);
3156
+ const reqById = new Map(nonStatic.map((request) => [request.id, request]));
3157
+ const recent = keepRecentPerEndpoint(nonStatic);
3158
+ const endpointGroups = aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq);
3241
3159
  return {
3242
3160
  ...ctx,
3243
3161
  nonStatic,
@@ -3254,7 +3172,7 @@ var init_prepare = __esm({
3254
3172
  init_endpoint();
3255
3173
  init_constants();
3256
3174
  init_http_status();
3257
- init_thresholds();
3175
+ init_config();
3258
3176
  init_query_helpers();
3259
3177
  }
3260
3178
  });
@@ -3265,6 +3183,8 @@ var init_runner = __esm({
3265
3183
  "src/analysis/insights/runner.ts"() {
3266
3184
  "use strict";
3267
3185
  init_prepare();
3186
+ init_log();
3187
+ init_type_guards();
3268
3188
  SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
3269
3189
  InsightRunner = class {
3270
3190
  constructor() {
@@ -3274,12 +3194,13 @@ var init_runner = __esm({
3274
3194
  this.rules.push(rule);
3275
3195
  }
3276
3196
  run(ctx) {
3277
- const prepared = prepareContext(ctx);
3197
+ const prepared = buildInsightContext(ctx);
3278
3198
  const insights = [];
3279
3199
  for (const rule of this.rules) {
3280
3200
  try {
3281
3201
  insights.push(...rule.check(prepared));
3282
- } catch {
3202
+ } catch (e) {
3203
+ brakitDebug(`insight rule ${rule.id} failed: ${getErrorMessage(e)}`);
3283
3204
  }
3284
3205
  }
3285
3206
  insights.sort(
@@ -3291,420 +3212,129 @@ var init_runner = __esm({
3291
3212
  }
3292
3213
  });
3293
3214
 
3294
- // src/analysis/insights/rules/n1.ts
3295
- var n1Rule;
3296
- var init_n1 = __esm({
3297
- "src/analysis/insights/rules/n1.ts"() {
3215
+ // src/analysis/insights/rules/query-rules.ts
3216
+ var n1Rule, redundantQueryRule, selectStarRule, highRowsRule, queryHeavyRule;
3217
+ var init_query_rules = __esm({
3218
+ "src/analysis/insights/rules/query-rules.ts"() {
3298
3219
  "use strict";
3299
3220
  init_query_helpers();
3300
3221
  init_endpoint();
3301
3222
  init_constants();
3223
+ init_patterns();
3302
3224
  n1Rule = {
3303
3225
  id: "n1",
3304
3226
  check(ctx) {
3305
3227
  const insights = [];
3306
- const seen = /* @__PURE__ */ new Set();
3228
+ const reportedKeys = /* @__PURE__ */ new Set();
3307
3229
  for (const [reqId, reqQueries] of ctx.queriesByReq) {
3308
3230
  const req = ctx.reqById.get(reqId);
3309
3231
  if (!req) continue;
3310
3232
  const endpoint = getEndpointKey(req.method, req.path);
3311
3233
  const shapeGroups = /* @__PURE__ */ new Map();
3312
- for (const q of reqQueries) {
3313
- const shape = getQueryShape(q);
3234
+ for (const query of reqQueries) {
3235
+ const shape = getQueryShape(query);
3314
3236
  let group = shapeGroups.get(shape);
3315
3237
  if (!group) {
3316
- group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: q };
3238
+ group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: query };
3317
3239
  shapeGroups.set(shape, group);
3318
3240
  }
3319
3241
  group.count++;
3320
- group.distinctSql.add(q.sql ?? shape);
3242
+ group.distinctSql.add(query.sql ?? shape);
3321
3243
  }
3322
- for (const [, sg] of shapeGroups) {
3323
- if (sg.count <= N1_QUERY_THRESHOLD || sg.distinctSql.size <= 1) continue;
3324
- const info = getQueryInfo(sg.first);
3244
+ for (const [, shapeGroup] of shapeGroups) {
3245
+ if (shapeGroup.count <= N1_QUERY_THRESHOLD || shapeGroup.distinctSql.size <= 1) continue;
3246
+ const info = getQueryInfo(shapeGroup.first);
3325
3247
  const key = `${endpoint}:${info.op}:${info.table || "unknown"}`;
3326
- if (seen.has(key)) continue;
3327
- seen.add(key);
3248
+ if (reportedKeys.has(key)) continue;
3249
+ reportedKeys.add(key);
3328
3250
  insights.push({
3329
3251
  severity: "critical",
3330
3252
  type: "n1",
3331
3253
  title: "N+1 Query Pattern",
3332
- desc: `${endpoint} runs ${sg.count}x ${info.op} ${info.table} with different params in a single request`,
3254
+ desc: `${endpoint} runs ${shapeGroup.count}x ${info.op} ${info.table} with different params in a single request`,
3333
3255
  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"
3335
- });
3336
- }
3337
- }
3338
- return insights;
3339
- }
3340
- };
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"
3256
+ detail: `${shapeGroup.count} queries with ${shapeGroup.distinctSql.size} distinct param variations. Example: ${[...shapeGroup.distinctSql][0]?.slice(0, 100) ?? info.op + " " + info.table}`
3393
3257
  });
3394
3258
  }
3395
3259
  }
3396
3260
  return insights;
3397
3261
  }
3398
3262
  };
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
3263
  redundantQueryRule = {
3411
3264
  id: "redundant-query",
3412
3265
  check(ctx) {
3413
3266
  const insights = [];
3414
- const seen = /* @__PURE__ */ new Set();
3267
+ const reportedKeys = /* @__PURE__ */ new Set();
3415
3268
  for (const [reqId, reqQueries] of ctx.queriesByReq) {
3416
3269
  const req = ctx.reqById.get(reqId);
3417
3270
  if (!req) continue;
3418
3271
  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);
3272
+ const identicalQueryMap = /* @__PURE__ */ new Map();
3273
+ for (const query of reqQueries) {
3274
+ if (!query.sql) continue;
3275
+ let entry = identicalQueryMap.get(query.sql);
3423
3276
  if (!entry) {
3424
- entry = { count: 0, first: q };
3425
- exact.set(q.sql, entry);
3277
+ entry = { count: 0, first: query };
3278
+ identicalQueryMap.set(query.sql, entry);
3426
3279
  }
3427
3280
  entry.count++;
3428
3281
  }
3429
- for (const [, e] of exact) {
3430
- if (e.count < REDUNDANT_QUERY_MIN_COUNT) continue;
3431
- const info = getQueryInfo(e.first);
3282
+ for (const [, entry] of identicalQueryMap) {
3283
+ if (entry.count < REDUNDANT_QUERY_MIN_COUNT) continue;
3284
+ const info = getQueryInfo(entry.first);
3432
3285
  const label = info.op + (info.table ? ` ${info.table}` : "");
3433
- const dedupKey = `${endpoint}:${label}`;
3434
- if (seen.has(dedupKey)) continue;
3435
- seen.add(dedupKey);
3286
+ const deduplicationKey = `${endpoint}:${label}`;
3287
+ if (reportedKeys.has(deduplicationKey)) continue;
3288
+ reportedKeys.add(deduplicationKey);
3436
3289
  insights.push({
3437
3290
  severity: "warning",
3438
3291
  type: "redundant-query",
3439
3292
  title: "Redundant Query",
3440
- desc: `${label} runs ${e.count}x with identical params in ${endpoint}.`,
3293
+ desc: `${label} runs ${entry.count}x with identical params in ${endpoint}.`,
3441
3294
  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"
3443
- });
3444
- }
3445
- }
3446
- return insights;
3447
- }
3448
- };
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"
3295
+ detail: entry.first.sql ? `Query: ${entry.first.sql.slice(0, 120)}` : void 0
3644
3296
  });
3645
3297
  }
3646
3298
  }
3647
3299
  return insights;
3648
3300
  }
3649
3301
  };
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
3302
  selectStarRule = {
3662
3303
  id: "select-star",
3663
3304
  check(ctx) {
3664
- const seen = /* @__PURE__ */ new Map();
3305
+ const tableCounts = /* @__PURE__ */ new Map();
3665
3306
  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);
3307
+ for (const query of reqQueries) {
3308
+ if (!query.sql) continue;
3309
+ const isSelectStar = SELECT_STAR_RE.test(query.sql.trim()) || SELECT_DOT_STAR_RE.test(query.sql);
3669
3310
  if (!isSelectStar) continue;
3670
- const info = getQueryInfo(q);
3671
- const key = info.table || "unknown";
3672
- seen.set(key, (seen.get(key) ?? 0) + 1);
3311
+ const info = getQueryInfo(query);
3312
+ const table = info.table || "unknown";
3313
+ tableCounts.set(table, (tableCounts.get(table) ?? 0) + 1);
3673
3314
  }
3674
3315
  }
3675
3316
  const insights = [];
3676
- for (const [table, count] of seen) {
3317
+ for (const [table, count] of tableCounts) {
3677
3318
  if (count < OVERFETCH_MIN_REQUESTS) continue;
3678
3319
  insights.push({
3679
3320
  severity: "warning",
3680
3321
  type: "select-star",
3681
3322
  title: "SELECT * Query",
3682
3323
  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"
3324
+ hint: "SELECT * fetches all columns including ones you don\u2019t need. Specify only required columns to reduce data transfer and memory usage."
3685
3325
  });
3686
3326
  }
3687
3327
  return insights;
3688
3328
  }
3689
3329
  };
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
3330
  highRowsRule = {
3701
3331
  id: "high-rows",
3702
3332
  check(ctx) {
3703
3333
  const seen = /* @__PURE__ */ new Map();
3704
3334
  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);
3335
+ for (const query of reqQueries) {
3336
+ if (!query.rowCount || query.rowCount <= HIGH_ROW_COUNT) continue;
3337
+ const info = getQueryInfo(query);
3708
3338
  const key = `${info.op} ${info.table || "unknown"}`;
3709
3339
  let entry = seen.get(key);
3710
3340
  if (!entry) {
@@ -3712,7 +3342,7 @@ var init_high_rows = __esm({
3712
3342
  seen.set(key, entry);
3713
3343
  }
3714
3344
  entry.count++;
3715
- if (q.rowCount > entry.max) entry.max = q.rowCount;
3345
+ if (query.rowCount > entry.max) entry.max = query.rowCount;
3716
3346
  }
3717
3347
  }
3718
3348
  const insights = [];
@@ -3723,39 +3353,81 @@ var init_high_rows = __esm({
3723
3353
  type: "high-rows",
3724
3354
  title: "Large Result Set",
3725
3355
  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"
3356
+ hint: "Fetching many rows slows responses and wastes memory. Add a LIMIT clause, implement pagination, or filter with a WHERE condition."
3728
3357
  });
3729
3358
  }
3730
3359
  return insights;
3731
3360
  }
3732
3361
  };
3362
+ queryHeavyRule = {
3363
+ id: "query-heavy",
3364
+ check(ctx) {
3365
+ const insights = [];
3366
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3367
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3368
+ const avgQueries = Math.round(group.queryCount / group.total);
3369
+ if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
3370
+ insights.push({
3371
+ severity: "warning",
3372
+ type: "query-heavy",
3373
+ title: "Query-Heavy Endpoint",
3374
+ desc: `${endpointKey} \u2014 avg ${avgQueries} queries/request`,
3375
+ hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches."
3376
+ });
3377
+ }
3378
+ }
3379
+ return insights;
3380
+ }
3381
+ };
3382
+ }
3383
+ });
3384
+
3385
+ // src/utils/format.ts
3386
+ function formatDuration(ms) {
3387
+ if (ms < 1e3) return `${ms}ms`;
3388
+ return `${(ms / 1e3).toFixed(1)}s`;
3389
+ }
3390
+ function formatSize(bytes) {
3391
+ if (bytes < 1024) return `${bytes}B`;
3392
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
3393
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
3394
+ }
3395
+ function pct(part, total) {
3396
+ return total > 0 ? Math.round(part / total * 100) : 0;
3397
+ }
3398
+ var init_format = __esm({
3399
+ "src/utils/format.ts"() {
3400
+ "use strict";
3733
3401
  }
3734
3402
  });
3735
3403
 
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"() {
3404
+ // src/analysis/insights/rules/response-rules.ts
3405
+ var responseOverfetchRule, largeResponseRule;
3406
+ var init_response_rules = __esm({
3407
+ "src/analysis/insights/rules/response-rules.ts"() {
3740
3408
  "use strict";
3741
3409
  init_endpoint();
3742
3410
  init_response();
3743
3411
  init_http_status();
3412
+ init_format();
3744
3413
  init_patterns();
3414
+ init_log();
3415
+ init_type_guards();
3745
3416
  init_constants();
3746
3417
  responseOverfetchRule = {
3747
3418
  id: "response-overfetch",
3748
3419
  check(ctx) {
3749
3420
  const insights = [];
3750
3421
  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;
3422
+ for (const request of ctx.nonStatic) {
3423
+ if (isErrorStatus(request.statusCode) || !request.responseBody) continue;
3424
+ const endpointKey = getEndpointKey(request.method, request.path);
3425
+ if (seen.has(endpointKey)) continue;
3755
3426
  let parsed;
3756
3427
  try {
3757
- parsed = JSON.parse(r.responseBody);
3758
- } catch {
3428
+ parsed = JSON.parse(request.responseBody);
3429
+ } catch (e) {
3430
+ brakitDebug(`json parse: ${getErrorMessage(e)}`);
3759
3431
  continue;
3760
3432
  }
3761
3433
  const target = unwrapResponse(parsed);
@@ -3778,45 +3450,33 @@ var init_response_overfetch = __esm({
3778
3450
  reasons.push(`${fields.length} fields returned`);
3779
3451
  }
3780
3452
  if (reasons.length > 0) {
3781
- seen.add(ep);
3453
+ seen.add(endpointKey);
3782
3454
  insights.push({
3783
3455
  severity: "info",
3784
3456
  type: "response-overfetch",
3785
3457
  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"
3458
+ desc: `${endpointKey} \u2014 ${reasons.join(", ")}`,
3459
+ 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
3460
  });
3790
3461
  }
3791
3462
  }
3792
3463
  return insights;
3793
3464
  }
3794
3465
  };
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
3466
  largeResponseRule = {
3806
3467
  id: "large-response",
3807
3468
  check(ctx) {
3808
3469
  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);
3470
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3471
+ if (group.total < OVERFETCH_MIN_REQUESTS) continue;
3472
+ const avgSize = Math.round(group.totalSize / group.total);
3812
3473
  if (avgSize > LARGE_RESPONSE_BYTES) {
3813
3474
  insights.push({
3814
3475
  severity: "info",
3815
3476
  type: "large-response",
3816
3477
  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"
3478
+ desc: `${endpointKey} \u2014 avg ${formatSize(avgSize)} response`,
3479
+ hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression."
3820
3480
  });
3821
3481
  }
3822
3482
  }
@@ -3826,13 +3486,66 @@ var init_large_response = __esm({
3826
3486
  }
3827
3487
  });
3828
3488
 
3829
- // src/analysis/insights/rules/regression.ts
3830
- var regressionRule;
3831
- var init_regression = __esm({
3832
- "src/analysis/insights/rules/regression.ts"() {
3489
+ // src/analysis/insights/rules/reliability-rules.ts
3490
+ function getAdaptiveSlowThreshold(endpointKey, previousMetrics) {
3491
+ if (!previousMetrics) return null;
3492
+ const ep = previousMetrics.find((m) => m.endpoint === endpointKey);
3493
+ if (!ep || ep.sessions.length < BASELINE_MIN_SESSIONS) return null;
3494
+ const valid = ep.sessions.filter((s) => s.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION);
3495
+ if (valid.length < BASELINE_MIN_SESSIONS) return null;
3496
+ const p95s = valid.map((s) => s.p95DurationMs).sort((a, b) => a - b);
3497
+ const medianP95 = p95s[Math.floor(p95s.length / 2)];
3498
+ return medianP95 * 2;
3499
+ }
3500
+ var errorRule, errorHotspotRule, regressionRule, slowRule;
3501
+ var init_reliability_rules = __esm({
3502
+ "src/analysis/insights/rules/reliability-rules.ts"() {
3833
3503
  "use strict";
3834
3504
  init_format();
3835
3505
  init_constants();
3506
+ errorRule = {
3507
+ id: "error",
3508
+ check(ctx) {
3509
+ if (ctx.errors.length === 0) return [];
3510
+ const insights = [];
3511
+ const groups = /* @__PURE__ */ new Map();
3512
+ for (const error of ctx.errors) {
3513
+ const name = error.name || "Error";
3514
+ groups.set(name, (groups.get(name) ?? 0) + 1);
3515
+ }
3516
+ for (const [name, cnt] of groups) {
3517
+ insights.push({
3518
+ severity: "critical",
3519
+ type: "error",
3520
+ title: "Unhandled Error",
3521
+ desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
3522
+ hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
3523
+ detail: ctx.errors.find((e) => e.name === name)?.message
3524
+ });
3525
+ }
3526
+ return insights;
3527
+ }
3528
+ };
3529
+ errorHotspotRule = {
3530
+ id: "error-hotspot",
3531
+ check(ctx) {
3532
+ const insights = [];
3533
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3534
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3535
+ const errorRate = Math.round(group.errors / group.total * 100);
3536
+ if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
3537
+ insights.push({
3538
+ severity: "critical",
3539
+ type: "error-hotspot",
3540
+ title: "Error Hotspot",
3541
+ desc: `${endpointKey} \u2014 ${errorRate}% error rate (${group.errors}/${group.total} requests)`,
3542
+ hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces."
3543
+ });
3544
+ }
3545
+ }
3546
+ return insights;
3547
+ }
3548
+ };
3836
3549
  regressionRule = {
3837
3550
  id: "regression",
3838
3551
  check(ctx) {
@@ -3851,8 +3564,7 @@ var init_regression = __esm({
3851
3564
  type: "regression",
3852
3565
  title: "Performance Regression",
3853
3566
  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"
3567
+ hint: "This endpoint is slower than the previous session. Check if recent code changes added queries or processing."
3856
3568
  });
3857
3569
  }
3858
3570
  if (prev.avgQueryCount > 0 && current.avgQueryCount > prev.avgQueryCount * QUERY_COUNT_REGRESSION_RATIO) {
@@ -3861,8 +3573,136 @@ var init_regression = __esm({
3861
3573
  type: "regression",
3862
3574
  title: "Query Count Regression",
3863
3575
  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"
3576
+ hint: "This endpoint is making more database queries than before. Check for new N+1 patterns or removed query optimizations."
3577
+ });
3578
+ }
3579
+ }
3580
+ return insights;
3581
+ }
3582
+ };
3583
+ slowRule = {
3584
+ id: "slow",
3585
+ check(ctx) {
3586
+ const insights = [];
3587
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3588
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3589
+ const avgMs = Math.round(group.totalDuration / group.total);
3590
+ const threshold = getAdaptiveSlowThreshold(endpointKey, ctx.previousMetrics);
3591
+ if (threshold === null || avgMs < threshold) continue;
3592
+ const avgQueryMs = Math.round(group.totalQueryTimeMs / group.total);
3593
+ const avgFetchMs = Math.round(group.totalFetchTimeMs / group.total);
3594
+ const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
3595
+ const parts = [];
3596
+ if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
3597
+ if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
3598
+ if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
3599
+ const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
3600
+ let detail;
3601
+ let slowestMs = 0;
3602
+ for (const [, shapeDuration] of group.queryShapeDurations) {
3603
+ const avgShapeMs = shapeDuration.totalMs / shapeDuration.count;
3604
+ if (avgShapeMs > slowestMs) {
3605
+ slowestMs = avgShapeMs;
3606
+ detail = `Slowest query: ${shapeDuration.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${shapeDuration.count}x)`;
3607
+ }
3608
+ }
3609
+ insights.push({
3610
+ severity: "warning",
3611
+ type: "slow",
3612
+ title: "Slow Endpoint",
3613
+ desc: `${endpointKey} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
3614
+ 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.",
3615
+ detail
3616
+ });
3617
+ }
3618
+ return insights;
3619
+ }
3620
+ };
3621
+ }
3622
+ });
3623
+
3624
+ // src/analysis/insights/rules/pattern-rules.ts
3625
+ var duplicateRule, crossEndpointRule;
3626
+ var init_pattern_rules = __esm({
3627
+ "src/analysis/insights/rules/pattern-rules.ts"() {
3628
+ "use strict";
3629
+ init_query_helpers();
3630
+ init_endpoint();
3631
+ init_constants();
3632
+ duplicateRule = {
3633
+ id: "duplicate",
3634
+ check(ctx) {
3635
+ const dupCounts = /* @__PURE__ */ new Map();
3636
+ const flowCount = /* @__PURE__ */ new Map();
3637
+ for (const flow of ctx.flows) {
3638
+ if (!flow.requests) continue;
3639
+ const seenInFlow = /* @__PURE__ */ new Set();
3640
+ for (const request of flow.requests) {
3641
+ if (!request.isDuplicate) continue;
3642
+ const deduplicationKey = `${request.method} ${request.label ?? request.path ?? request.url}`;
3643
+ dupCounts.set(deduplicationKey, (dupCounts.get(deduplicationKey) ?? 0) + 1);
3644
+ if (!seenInFlow.has(deduplicationKey)) {
3645
+ seenInFlow.add(deduplicationKey);
3646
+ flowCount.set(deduplicationKey, (flowCount.get(deduplicationKey) ?? 0) + 1);
3647
+ }
3648
+ }
3649
+ }
3650
+ const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
3651
+ const insights = [];
3652
+ for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
3653
+ const duplicate = dupEntries[i];
3654
+ insights.push({
3655
+ severity: "warning",
3656
+ type: "duplicate",
3657
+ title: "Duplicate API Call",
3658
+ desc: `${duplicate.key} loaded ${duplicate.count}x as duplicate across ${duplicate.flows} action${duplicate.flows !== 1 ? "s" : ""}`,
3659
+ 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."
3660
+ });
3661
+ }
3662
+ return insights;
3663
+ }
3664
+ };
3665
+ crossEndpointRule = {
3666
+ id: "cross-endpoint",
3667
+ check(ctx) {
3668
+ const insights = [];
3669
+ const queryMap = /* @__PURE__ */ new Map();
3670
+ const allEndpoints = /* @__PURE__ */ new Set();
3671
+ for (const [reqId, reqQueries] of ctx.queriesByReq) {
3672
+ const req = ctx.reqById.get(reqId);
3673
+ if (!req) continue;
3674
+ const endpoint = getEndpointKey(req.method, req.path);
3675
+ allEndpoints.add(endpoint);
3676
+ const seenInReq = /* @__PURE__ */ new Set();
3677
+ for (const query of reqQueries) {
3678
+ const shape = getQueryShape(query);
3679
+ let entry = queryMap.get(shape);
3680
+ if (!entry) {
3681
+ entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: query };
3682
+ queryMap.set(shape, entry);
3683
+ }
3684
+ entry.count++;
3685
+ if (!seenInReq.has(shape)) {
3686
+ seenInReq.add(shape);
3687
+ entry.endpoints.add(endpoint);
3688
+ }
3689
+ }
3690
+ }
3691
+ if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
3692
+ for (const [, queryMetric] of queryMap) {
3693
+ if (queryMetric.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
3694
+ if (queryMetric.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
3695
+ const coveragePct = Math.round(queryMetric.endpoints.size / allEndpoints.size * 100);
3696
+ if (coveragePct < CROSS_ENDPOINT_PCT) continue;
3697
+ const info = getQueryInfo(queryMetric.first);
3698
+ const label = info.op + (info.table ? ` ${info.table}` : "");
3699
+ insights.push({
3700
+ severity: "warning",
3701
+ type: "cross-endpoint",
3702
+ title: "Repeated Query Across Endpoints",
3703
+ desc: `${label} runs on ${queryMetric.endpoints.size} of ${allEndpoints.size} endpoints (${coveragePct}%).`,
3704
+ hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
3705
+ detail: `Endpoints: ${[...queryMetric.endpoints].slice(0, 5).join(", ")}${queryMetric.endpoints.size > 5 ? ` +${queryMetric.endpoints.size - 5} more` : ""}. Total: ${queryMetric.count} executions.`
3866
3706
  });
3867
3707
  }
3868
3708
  }
@@ -3881,13 +3721,13 @@ var init_security = __esm({
3881
3721
  id: "security",
3882
3722
  check(ctx) {
3883
3723
  if (!ctx.securityFindings) return [];
3884
- return ctx.securityFindings.map((f) => ({
3885
- severity: f.severity,
3724
+ return ctx.securityFindings.map((finding) => ({
3725
+ severity: finding.severity,
3886
3726
  type: "security",
3887
- title: f.title,
3888
- desc: f.desc,
3889
- hint: f.hint,
3890
- nav: "security"
3727
+ title: finding.title,
3728
+ desc: finding.desc,
3729
+ hint: finding.hint,
3730
+ detail: finding.detail
3891
3731
  }));
3892
3732
  }
3893
3733
  };
@@ -3898,19 +3738,10 @@ var init_security = __esm({
3898
3738
  var init_rules2 = __esm({
3899
3739
  "src/analysis/insights/rules/index.ts"() {
3900
3740
  "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();
3741
+ init_query_rules();
3742
+ init_response_rules();
3743
+ init_reliability_rules();
3744
+ init_pattern_rules();
3914
3745
  init_security();
3915
3746
  }
3916
3747
  });
@@ -3969,8 +3800,7 @@ function insightToIssue(insight) {
3969
3800
  desc: insight.desc,
3970
3801
  hint: insight.hint,
3971
3802
  detail: insight.detail,
3972
- endpoint: extractEndpointFromDesc(insight.desc) ?? void 0,
3973
- nav: insight.nav
3803
+ endpoint: extractEndpointFromDesc(insight.desc) ?? void 0
3974
3804
  };
3975
3805
  }
3976
3806
  function securityFindingToIssue(finding) {
@@ -3981,8 +3811,8 @@ function securityFindingToIssue(finding) {
3981
3811
  title: finding.title,
3982
3812
  desc: finding.desc,
3983
3813
  hint: finding.hint,
3984
- endpoint: finding.endpoint,
3985
- nav: "security"
3814
+ detail: finding.detail,
3815
+ endpoint: finding.endpoint
3986
3816
  };
3987
3817
  }
3988
3818
  var init_issue_mappers = __esm({
@@ -3997,7 +3827,7 @@ var AnalysisEngine;
3997
3827
  var init_engine = __esm({
3998
3828
  "src/analysis/engine.ts"() {
3999
3829
  "use strict";
4000
- init_limits();
3830
+ init_config();
4001
3831
  init_disposable();
4002
3832
  init_group();
4003
3833
  init_rules();
@@ -4006,8 +3836,8 @@ var init_engine = __esm({
4006
3836
  init_issue_id();
4007
3837
  init_prepare();
4008
3838
  AnalysisEngine = class {
4009
- constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
4010
- this.registry = registry;
3839
+ constructor(services, debounceMs = ANALYSIS_DEBOUNCE_MS) {
3840
+ this.services = services;
4011
3841
  this.debounceMs = debounceMs;
4012
3842
  this.cachedInsights = [];
4013
3843
  this.cachedFindings = [];
@@ -4016,7 +3846,7 @@ var init_engine = __esm({
4016
3846
  this.scanner = createDefaultScanner();
4017
3847
  }
4018
3848
  start() {
4019
- const bus = this.registry.get("event-bus");
3849
+ const bus = this.services.bus;
4020
3850
  this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
4021
3851
  this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
4022
3852
  this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
@@ -4043,12 +3873,12 @@ var init_engine = __esm({
4043
3873
  }, this.debounceMs);
4044
3874
  }
4045
3875
  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);
3876
+ const allRequests = this.services.requestStore.getAll();
3877
+ const queries = this.services.queryStore.getAll();
3878
+ const errors = this.services.errorStore.getAll();
3879
+ const logs = this.services.logStore.getAll();
3880
+ const fetches = this.services.fetchStore.getAll();
3881
+ const requests = keepRecentPerEndpoint(allRequests);
4052
3882
  const flows = groupRequestsIntoFlows(requests);
4053
3883
  this.cachedFindings = this.scanner.scan({ requests, logs });
4054
3884
  this.cachedInsights = computeInsights({
@@ -4057,33 +3887,29 @@ var init_engine = __esm({
4057
3887
  errors,
4058
3888
  flows,
4059
3889
  fetches,
4060
- previousMetrics: this.registry.get("metrics-store").getAll(),
3890
+ previousMetrics: this.services.metricsStore.getAll(),
4061
3891
  securityFindings: this.cachedFindings
4062
3892
  });
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);
3893
+ const issueStore = this.services.issueStore;
3894
+ const currentIssueIds = /* @__PURE__ */ new Set();
3895
+ for (const finding of this.cachedFindings) {
3896
+ const issue = securityFindingToIssue(finding);
3897
+ issueStore.upsert(issue, "passive");
3898
+ currentIssueIds.add(computeIssueId(issue));
4086
3899
  }
3900
+ for (const insight of this.cachedInsights) {
3901
+ const issue = insightToIssue(insight);
3902
+ issueStore.upsert(issue, "passive");
3903
+ currentIssueIds.add(computeIssueId(issue));
3904
+ }
3905
+ const activeEndpoints = extractActiveEndpoints(allRequests);
3906
+ issueStore.reconcile(currentIssueIds, activeEndpoints);
3907
+ const update = {
3908
+ insights: this.cachedInsights,
3909
+ findings: this.cachedFindings,
3910
+ issues: issueStore.getAll()
3911
+ };
3912
+ this.services.bus.emit("analysis:updated", update);
4087
3913
  }
4088
3914
  };
4089
3915
  }
@@ -4101,7 +3927,7 @@ var init_src = __esm({
4101
3927
  init_engine();
4102
3928
  init_insights2();
4103
3929
  init_insights();
4104
- VERSION = "0.8.7";
3930
+ VERSION = "0.9.0";
4105
3931
  }
4106
3932
  });
4107
3933
 
@@ -4121,11 +3947,13 @@ function getBaseStyles() {
4121
3947
  --red:#dc2626;
4122
3948
  --cyan:#0891b2;
4123
3949
  --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);
3950
+ --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
3951
  --sidebar-width:232px;--header-height:52px;
4125
3952
  --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);
3953
+ --shadow-sm:0 1px 3px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.03);
3954
+ --shadow-md:0 2px 6px rgba(0,0,0,0.08),0 1px 3px rgba(0,0,0,0.04);
3955
+ --shadow-lg:0 4px 16px rgba(0,0,0,0.1),0 2px 6px rgba(0,0,0,0.05);
3956
+ --transition:0.15s ease;
4129
3957
  --breakdown-db:#6366f1;--breakdown-fetch:#f59e0b;--breakdown-app:#94a3b8;
4130
3958
  --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
4131
3959
  --sans:Inter,system-ui,-apple-system,sans-serif;
@@ -4243,8 +4071,8 @@ function getFlowStyles() {
4243
4071
  .flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
4244
4072
  .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
4073
  .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)}
4074
+ .flow-badge-pill.badge-warn{background:var(--amber-bg);color:var(--amber)}
4075
+ .flow-badge-pill.badge-error{background:var(--red-bg);color:var(--red)}
4248
4076
  .flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
4249
4077
 
4250
4078
  /* Flow expand panel */
@@ -4263,23 +4091,23 @@ function getFlowStyles() {
4263
4091
  /* Method badges */
4264
4092
  .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
4093
  .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)}
4094
+ .method-badge-POST{background:var(--blue-bg);color:var(--blue)}
4095
+ .method-badge-PUT,.method-badge-PATCH{background:var(--amber-bg);color:var(--amber)}
4096
+ .method-badge-DELETE{background:var(--red-bg);color:var(--red)}
4269
4097
  .method-badge-HEAD,.method-badge-OPTIONS{background:var(--bg-muted);color:var(--text-muted)}
4270
4098
 
4271
4099
  /* Status pills */
4272
4100
  .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
4101
  .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)}
4102
+ .status-pill-3xx{background:var(--cyan-bg);color:var(--cyan)}
4103
+ .status-pill-4xx{background:var(--amber-bg);color:var(--amber)}
4104
+ .status-pill-5xx{background:var(--red-bg);color:var(--red)}
4277
4105
 
4278
4106
  .traffic-card-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;font-size:13px}
4279
4107
  .traffic-card-path.is-dup{color:var(--text-muted);font-weight:400}
4280
4108
  .traffic-card-dur{color:var(--text-muted);font-size:12px;flex-shrink:0}
4281
4109
  .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}
4110
+ .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
4111
 
4284
4112
  /* Body toggles */
4285
4113
  .traffic-body{padding:0;margin-top:8px}
@@ -4308,7 +4136,7 @@ function getFlowStyles() {
4308
4136
  .flow-subreq .subreq-label.is-dup{color:var(--text-muted);font-weight:400}
4309
4137
  .flow-subreq .subreq-status{flex-shrink:0}
4310
4138
  .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}
4139
+ .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
4140
  .flow-subreq-detail{display:none;padding:12px 0;border-bottom:1px solid var(--border-subtle)}
4313
4141
  .flow-subreq-detail.open{display:block}
4314
4142
 
@@ -4337,6 +4165,41 @@ function getFlowStyles() {
4337
4165
  /* Strict Mode duplicate banner */
4338
4166
  .strict-mode-dupe{opacity:0.55}
4339
4167
  .strict-mode-banner{font-size:11px;color:var(--text-muted);padding:6px 0 0;font-family:var(--mono)}
4168
+
4169
+ /* Flow detail tabs */
4170
+ .flow-detail-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--border)}
4171
+ .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}
4172
+ .flow-tab:hover{color:var(--text)}
4173
+ .flow-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
4174
+
4175
+ /* Waterfall chart \u2014 request bars on time axis, sub-events as text rows */
4176
+ .flow-waterfall{padding:0;font-family:var(--mono);font-size:11px}
4177
+ .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}
4178
+ .wf-rows{display:flex;flex-direction:column;gap:0}
4179
+
4180
+ /* Request group \u2014 request bar + its sub-events */
4181
+ .wf-request-group{border-bottom:1px solid var(--border-subtle);padding:2px 0}
4182
+ .wf-request-group:last-child{border-bottom:none}
4183
+
4184
+ /* Request row \u2014 label | bar on time axis | duration */
4185
+ .wf-req-row{display:flex;align-items:center;gap:0;height:24px;transition:background .1s}
4186
+ .wf-req-row:hover{background:var(--bg-hover)}
4187
+ .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}
4188
+ .wf-bar-track{flex:1;position:relative;height:14px;min-width:0;overflow:hidden}
4189
+ .wf-bar{position:absolute;top:1px;height:12px;border-radius:3px;opacity:0.8;min-width:3px}
4190
+ .wf-req-row:hover .wf-bar{opacity:1}
4191
+ .wf-req-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-muted);font-size:10px;padding-left:8px}
4192
+
4193
+ /* Sub-event rows \u2014 same layout as request rows: label | bar track | duration */
4194
+ .wf-sub-row{display:flex;align-items:center;gap:0;height:20px;transition:background .1s}
4195
+ .wf-sub-row:hover{background:var(--bg-hover)}
4196
+ .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}
4197
+ .wf-sub-dot{width:6px;height:6px;border-radius:2px;flex-shrink:0}
4198
+ .wf-sub-bar-sized{height:8px !important;top:3px !important;opacity:0.65}
4199
+ .wf-sub-row:hover .wf-sub-bar-sized{opacity:0.9}
4200
+ .wf-sub-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-dim);font-size:9px;padding-left:8px}
4201
+
4202
+ .wf-loading{color:var(--text-muted);padding:12px 0;font-size:11px;font-family:var(--mono)}
4340
4203
  `;
4341
4204
  }
4342
4205
  var init_flows = __esm({
@@ -4445,22 +4308,39 @@ function getPerformanceStyles() {
4445
4308
  .perf-badge-lg{padding:4px 12px;font-size:13px;border-radius:var(--radius-sm)}
4446
4309
  .perf-badge-sm{padding:1px 6px;font-size:9px}
4447
4310
 
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}
4311
+ /* Overview: summary cards */
4312
+ .perf-overview{padding:16px 28px}
4313
+ .perf-summary-row{display:flex;gap:8px;margin-bottom:16px}
4314
+ .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)}
4315
+ .perf-summary-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-family:var(--sans);font-weight:600}
4316
+ .perf-summary-value{font-size:20px;font-weight:700;font-family:var(--mono);color:var(--text)}
4317
+ .perf-summary-value-sm{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4318
+
4319
+ /* Shared table styles */
4320
+ .perf-table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px}
4321
+ .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}
4322
+ .perf-table tbody td{padding:11px 14px;border-bottom:1px solid var(--border-subtle);color:var(--text)}
4323
+ .perf-table-row{cursor:pointer;transition:background var(--transition, .15s ease)}
4324
+ .perf-table-row:hover{background:var(--bg-hover)}
4325
+ .perf-table tbody tr:last-child td{border-bottom:none}
4326
+ .perf-th-right{text-align:right !important}
4327
+ .perf-th-center{text-align:center !important}
4328
+ .perf-td-right{text-align:right}
4329
+ .perf-td-center{text-align:center}
4330
+ .perf-td-name{font-weight:600;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4331
+ .perf-td-muted{color:var(--text-dim)}
4332
+ .perf-row-err{background:var(--red-bg)}
4333
+ .perf-row-err:hover{background:rgba(220,38,38,0.1)}
4334
+
4335
+ /* Heat map table wrapper */
4336
+ .perf-heatmap{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm)}
4337
+ .perf-hm-p95{display:inline-flex;align-items:center;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;border:1px solid}
4338
+ .perf-hm-split-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;background:var(--bg-muted);width:100%;min-width:80px}
4460
4339
 
4461
4340
  /* Detail view */
4462
4341
  .perf-detail-header{padding:20px 28px 16px;border-bottom:1px solid var(--border-subtle)}
4463
4342
  .perf-detail-title{display:flex;align-items:center;gap:12px;font-size:17px;font-weight:600;color:var(--text);font-family:var(--mono)}
4343
+ .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
4344
  .perf-metric-row{display:flex;gap:4px;padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
4465
4345
  .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
4346
  .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 +4375,42 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
4495
4375
  .perf-canvas{border-radius:var(--radius);background:var(--bg-muted);border:1px solid var(--border)}
4496
4376
  .perf-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:10px}
4497
4377
 
4498
- /* Request history table */
4378
+ /* Request history */
4499
4379
  .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)}
4380
+ .perf-hist-row-hl{background:rgba(37,99,235,0.1) !important;border-left:3px solid #4ade80}
4381
+
4382
+ /* Callers section */
4383
+ .perf-callers{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
4384
+ .perf-callers-list{display:flex;flex-direction:column;gap:0}
4385
+ .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}
4386
+ .perf-caller-row:last-child{border-bottom:none}
4387
+ .perf-caller-name{flex:1;font-weight:500;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4388
+ .perf-caller-count{color:var(--text-muted);font-size:11px;flex-shrink:0}
4389
+ .perf-caller-avg{color:var(--text-dim);font-size:11px;flex-shrink:0}
4390
+
4391
+ /* Query breakdown section */
4392
+ .perf-queries{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
4393
+ .perf-queries-loading{font-size:11px;color:var(--text-muted);font-family:var(--mono)}
4394
+ .perf-queries-list{display:flex;flex-direction:column;gap:0}
4395
+ .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}
4396
+ .perf-query-row:last-child{border-bottom:none}
4397
+ .perf-query-label{flex:1;font-weight:500;color:var(--accent);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4398
+ .perf-query-avg{color:var(--text-muted);font-size:11px;flex-shrink:0}
4399
+ .perf-query-count{color:var(--text-dim);font-size:11px;flex-shrink:0}
4400
+
4401
+ /* Session trends */
4402
+ .perf-trends{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
4403
+ .perf-trends-list{display:flex;flex-direction:column;gap:0}
4404
+ .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}
4405
+ .perf-trend-row:last-child{border-bottom:none}
4406
+ .perf-trend-current{background:var(--bg-muted);border-radius:var(--radius-sm);font-weight:600}
4407
+ .perf-trend-time{width:80px;color:var(--text-dim);font-size:11px;flex-shrink:0}
4408
+ .perf-trend-p95{flex-shrink:0}
4409
+ .perf-trend-reqs{color:var(--text-muted);font-size:11px;flex-shrink:0}
4410
+ .perf-trend-errs{font-size:11px;flex-shrink:0}
4411
+ .perf-trend-arrow{font-size:10px;font-weight:600;flex-shrink:0}
4412
+ .perf-trend-slower{color:var(--red)}
4413
+ .perf-trend-faster{color:var(--green)}
4514
4414
  `;
4515
4415
  }
4516
4416
  var init_graph = __esm({
@@ -4526,9 +4426,10 @@ function getOverviewStyles() {
4526
4426
  .ov-container{padding:24px 28px}
4527
4427
 
4528
4428
  /* 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)}
4429
+ .ov-summary{display:flex;gap:10px;margin-bottom:24px;flex-wrap:wrap}
4430
+ .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)}
4431
+ .ov-stat:hover{box-shadow:var(--shadow-md)}
4432
+ .ov-stat-value{font-size:22px;font-weight:700;font-family:var(--mono);color:var(--text);line-height:1.2}
4532
4433
  .ov-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
4533
4434
 
4534
4435
  /* Section header */
@@ -4537,8 +4438,8 @@ function getOverviewStyles() {
4537
4438
 
4538
4439
  /* Insight cards */
4539
4440
  .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)}
4441
+ .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)}
4442
+ .ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md);transform:translateY(-1px)}
4542
4443
  .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
4444
  .ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
4544
4445
  .ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
@@ -4547,6 +4448,7 @@ function getOverviewStyles() {
4547
4448
  .ov-card-body{flex:1;min-width:0}
4548
4449
  .ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
4549
4450
  .ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
4451
+ .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
4452
  .ov-card-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
4551
4453
  .ov-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;margin-top:2px;font-family:var(--mono);transition:transform .15s}
4552
4454
 
@@ -4842,7 +4744,7 @@ function getTimelineStyles() {
4842
4744
  }
4843
4745
  `;
4844
4746
  }
4845
- var init_timeline2 = __esm({
4747
+ var init_timeline = __esm({
4846
4748
  "src/dashboard/styles/timeline.ts"() {
4847
4749
  "use strict";
4848
4750
  }
@@ -4862,7 +4764,7 @@ var init_styles = __esm({
4862
4764
  init_graph();
4863
4765
  init_overview();
4864
4766
  init_security2();
4865
- init_timeline2();
4767
+ init_timeline();
4866
4768
  }
4867
4769
  });
4868
4770
 
@@ -4957,10 +4859,10 @@ function isTelemetryEnabled() {
4957
4859
  return cachedEnabled;
4958
4860
  }
4959
4861
  var CONFIG_DIR, CONFIG_PATH, cachedEnabled;
4960
- var init_config = __esm({
4862
+ var init_config2 = __esm({
4961
4863
  "src/telemetry/config.ts"() {
4962
4864
  "use strict";
4963
- init_network();
4865
+ init_features();
4964
4866
  CONFIG_DIR = join3(homedir2(), ".brakit");
4965
4867
  CONFIG_PATH = join3(CONFIG_DIR, "config.json");
4966
4868
  cachedEnabled = null;
@@ -5001,12 +4903,12 @@ function speedBucket(ms) {
5001
4903
  }
5002
4904
  return `>${t[t.length - 1]}ms`;
5003
4905
  }
5004
- function trackSession(registry) {
4906
+ function trackSession(services) {
5005
4907
  if (!isTelemetryEnabled()) return;
5006
4908
  const isFirstSession = readConfig() === null;
5007
4909
  const config = getOrCreateConfig();
5008
- const metricsStore = registry.get("metrics-store");
5009
- const analysisEngine = registry.get("analysis-engine");
4910
+ const metricsStore = services.metricsStore;
4911
+ const analysisEngine = services.analysisEngine;
5010
4912
  const live = metricsStore.getLiveEndpoints();
5011
4913
  const insights = analysisEngine.getInsights();
5012
4914
  const findings = analysisEngine.getFindings();
@@ -5034,9 +4936,9 @@ function trackSession(registry) {
5034
4936
  first_session: isFirstSession,
5035
4937
  adapters_detected: session.adapters,
5036
4938
  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,
4939
+ error_count: services.errorStore.getAll().length,
4940
+ query_count: services.queryStore.getAll().length,
4941
+ fetch_count: services.fetchStore.getAll().length,
5040
4942
  insight_count: insights.length,
5041
4943
  finding_count: findings.length,
5042
4944
  insight_types: [...session.insightTypes],
@@ -5069,13 +4971,13 @@ function trackSession(registry) {
5069
4971
  }
5070
4972
  }
5071
4973
  var POSTHOG_KEY, session;
5072
- var init_telemetry2 = __esm({
4974
+ var init_telemetry = __esm({
5073
4975
  "src/telemetry/index.ts"() {
5074
4976
  "use strict";
5075
4977
  init_src();
5076
- init_config();
5077
- init_telemetry();
5078
- init_config();
4978
+ init_config2();
4979
+ init_labels();
4980
+ init_config2();
5079
4981
  POSTHOG_KEY = "phc_E9TwydCGnSfPLIUhNxChpeg32TSowjk31KiPhnLPP0x";
5080
4982
  session = {
5081
4983
  startTime: 0,
@@ -5097,32 +4999,30 @@ var init_telemetry2 = __esm({
5097
4999
  function isDashboardRequest(url) {
5098
5000
  return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
5099
5001
  }
5100
- function createDashboardHandler(registry) {
5101
- const metricsStore = registry.get("metrics-store");
5002
+ function createDashboardHandler(services) {
5003
+ const metricsStore = services.metricsStore;
5102
5004
  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),
5005
+ [DASHBOARD_API_REQUESTS]: createRequestsHandler(services),
5006
+ [DASHBOARD_API_EVENTS]: createSSEHandler(services),
5007
+ [DASHBOARD_API_FLOWS]: createFlowsHandler(services),
5008
+ [DASHBOARD_API_CLEAR]: createClearHandler(services),
5009
+ [DASHBOARD_API_LOGS]: createLogsHandler(services),
5010
+ [DASHBOARD_API_FETCHES]: createFetchesHandler(services),
5011
+ [DASHBOARD_API_ERRORS]: createErrorsHandler(services),
5012
+ [DASHBOARD_API_QUERIES]: createQueriesHandler(services),
5111
5013
  [DASHBOARD_API_METRICS]: createMetricsHandler(metricsStore),
5112
5014
  [DASHBOARD_API_METRICS_LIVE]: createLiveMetricsHandler(metricsStore),
5113
- [DASHBOARD_API_INGEST]: createIngestHandler(registry),
5114
- [DASHBOARD_API_ACTIVITY]: createActivityHandler(registry)
5015
+ [DASHBOARD_API_INGEST]: createIngestHandler(services),
5016
+ [DASHBOARD_API_ACTIVITY]: createActivityHandler(services)
5115
5017
  };
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
- }
5018
+ const issueStore = services.issueStore;
5019
+ routes[DASHBOARD_API_INSIGHTS] = createIssuesHandler(issueStore);
5020
+ routes[DASHBOARD_API_SECURITY] = createIssuesHandler(issueStore);
5021
+ routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(issueStore);
5022
+ routes[DASHBOARD_API_FINDINGS_REPORT] = createIssuesReportHandler(
5023
+ issueStore,
5024
+ services.bus
5025
+ );
5126
5026
  routes[DASHBOARD_API_TAB] = (req, res) => {
5127
5027
  const raw = (req.url ?? "").split("tab=")[1];
5128
5028
  if (raw) {
@@ -5133,7 +5033,7 @@ function createDashboardHandler(registry) {
5133
5033
  res.end();
5134
5034
  };
5135
5035
  return (req, res, config) => {
5136
- const path = (req.url ?? "/").split("?")[0];
5036
+ const path = stripQueryString(req.url ?? "/");
5137
5037
  const handler = routes[path];
5138
5038
  if (handler) {
5139
5039
  handler(req, res);
@@ -5151,13 +5051,14 @@ function createDashboardHandler(registry) {
5151
5051
  var init_router = __esm({
5152
5052
  "src/dashboard/router.ts"() {
5153
5053
  "use strict";
5054
+ init_endpoint();
5154
5055
  init_constants();
5155
- init_http();
5056
+ init_labels();
5156
5057
  init_api();
5157
5058
  init_issues();
5158
5059
  init_sse();
5159
5060
  init_page();
5160
- init_telemetry2();
5061
+ init_telemetry();
5161
5062
  }
5162
5063
  });
5163
5064
 
@@ -5198,51 +5099,6 @@ var init_event_bus = __esm({
5198
5099
  }
5199
5100
  });
5200
5101
 
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
5102
  // src/store/request-store.ts
5247
5103
  function flattenHeaders(headers2) {
5248
5104
  const flat = {};
@@ -5258,6 +5114,7 @@ var init_request_store = __esm({
5258
5114
  "use strict";
5259
5115
  init_constants();
5260
5116
  init_static_patterns();
5117
+ init_endpoint();
5261
5118
  RequestStore = class {
5262
5119
  constructor(maxEntries = MAX_REQUEST_ENTRIES) {
5263
5120
  this.maxEntries = maxEntries;
@@ -5266,7 +5123,7 @@ var init_request_store = __esm({
5266
5123
  }
5267
5124
  capture(input) {
5268
5125
  const url = input.url;
5269
- const path = url.split("?")[0];
5126
+ const path = stripQueryString(url);
5270
5127
  let requestBodyStr = null;
5271
5128
  if (input.requestBody && input.requestBody.length > 0) {
5272
5129
  requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
@@ -5291,7 +5148,8 @@ var init_request_store = __esm({
5291
5148
  startedAt: input.startTime,
5292
5149
  durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
5293
5150
  responseSize: input.responseBody?.length ?? 0,
5294
- isStatic: isStaticPath(path)
5151
+ isStatic: isStaticPath(path),
5152
+ isHealthCheck: isHealthCheckPath(path)
5295
5153
  };
5296
5154
  this.requests.push(entry);
5297
5155
  if (this.requests.length > this.maxEntries) {
@@ -5368,50 +5226,6 @@ var init_telemetry_store = __esm({
5368
5226
  }
5369
5227
  });
5370
5228
 
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
5229
  // src/utils/math.ts
5416
5230
  function percentile(values, p) {
5417
5231
  if (values.length === 0) return 0;
@@ -5458,6 +5272,17 @@ var init_metrics_store = __esm({
5458
5272
  this.dirty = false;
5459
5273
  this.accumulators = /* @__PURE__ */ new Map();
5460
5274
  this.pendingPoints = /* @__PURE__ */ new Map();
5275
+ /**
5276
+ * Compute the adaptive performance baseline for an endpoint.
5277
+ * Returns the median p95 across historical sessions, or null when
5278
+ * there isn't enough data to establish a meaningful baseline.
5279
+ */
5280
+ /**
5281
+ * Cached baselines — invalidated on flush (when sessions change) and
5282
+ * on new request recordings (when pending points grow). Avoids recomputing
5283
+ * on every getLiveEndpoints() API call.
5284
+ */
5285
+ this.baselineCache = /* @__PURE__ */ new Map();
5461
5286
  this.data = { version: 1, endpoints: [] };
5462
5287
  }
5463
5288
  start() {
@@ -5482,7 +5307,7 @@ var init_metrics_store = __esm({
5482
5307
  this.flush(true);
5483
5308
  }
5484
5309
  recordRequest(req, metrics) {
5485
- if (req.isStatic) return;
5310
+ if (req.isStatic || req.isHealthCheck) return;
5486
5311
  this.dirty = true;
5487
5312
  const key = getEndpointKey(req.method, req.path);
5488
5313
  let acc = this.accumulators.get(key);
@@ -5531,6 +5356,38 @@ var init_metrics_store = __esm({
5531
5356
  getEndpoint(endpoint) {
5532
5357
  return this.endpointIndex.get(endpoint);
5533
5358
  }
5359
+ getEndpointBaseline(endpoint) {
5360
+ const pending2 = this.pendingPoints.get(endpoint);
5361
+ const pointCount = pending2?.length ?? 0;
5362
+ const cached = this.baselineCache.get(endpoint);
5363
+ if (cached && cached.pointCount === pointCount) return cached.value;
5364
+ const value = this.computeBaseline(endpoint, pending2);
5365
+ this.baselineCache.set(endpoint, { value, pointCount });
5366
+ return value;
5367
+ }
5368
+ computeBaseline(endpoint, pending2) {
5369
+ const ep = this.endpointIndex.get(endpoint);
5370
+ if (ep && ep.sessions.length >= BASELINE_MIN_SESSIONS) {
5371
+ const validSessions = ep.sessions.filter(
5372
+ (s) => s.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION
5373
+ );
5374
+ if (validSessions.length >= BASELINE_MIN_SESSIONS) {
5375
+ const p95s = validSessions.map((s) => s.p95DurationMs).sort((a, b) => a - b);
5376
+ return p95s[Math.floor(p95s.length / 2)];
5377
+ }
5378
+ }
5379
+ if (ep && ep.sessions.length === 1) {
5380
+ const session2 = ep.sessions[0];
5381
+ if (session2.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION) {
5382
+ return session2.p95DurationMs;
5383
+ }
5384
+ }
5385
+ if (pending2 && pending2.length >= BASELINE_PENDING_POINTS_MIN) {
5386
+ const warmDurations = pending2.slice(1).map((r) => r.durationMs).sort((a, b) => a - b);
5387
+ return warmDurations[Math.floor(warmDurations.length / 2)];
5388
+ }
5389
+ return null;
5390
+ }
5534
5391
  getLiveEndpoints() {
5535
5392
  const merged = /* @__PURE__ */ new Map();
5536
5393
  for (const ep of this.data.endpoints) {
@@ -5545,27 +5402,35 @@ var init_metrics_store = __esm({
5545
5402
  const endpoints = [];
5546
5403
  for (const [endpoint, requests] of merged) {
5547
5404
  if (requests.length === 0) continue;
5548
- const durations = requests.map((r) => r.durationMs);
5405
+ const warmRequests = requests.length > 1 ? requests.slice(1) : requests;
5406
+ const warmDurations = warmRequests.map((r) => r.durationMs);
5549
5407
  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);
5408
+ const totalQueries = warmRequests.reduce((s, r) => s + r.queryCount, 0);
5409
+ const totalQueryTime = warmRequests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
5410
+ const totalFetchTime = warmRequests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
5411
+ const n = warmRequests.length;
5412
+ const avgDurationMs = Math.round(warmDurations.reduce((s, d) => s + d, 0) / n);
5555
5413
  const avgQueryTimeMs = Math.round(totalQueryTime / n);
5556
5414
  const avgFetchTimeMs = Math.round(totalFetchTime / n);
5415
+ const p95Ms = percentile(warmDurations, 0.95);
5416
+ const medianMs = percentile(warmDurations, 0.5);
5417
+ const epData = this.endpointIndex.get(endpoint);
5557
5418
  endpoints.push({
5558
5419
  endpoint,
5559
5420
  requests,
5560
5421
  summary: {
5561
- p95Ms: percentile(durations, 0.95),
5562
- errorRate: errors / n,
5422
+ p95Ms,
5423
+ medianMs,
5424
+ errorRate: errors / requests.length,
5425
+ // Error rate uses ALL requests
5563
5426
  avgQueryCount: Math.round(totalQueries / n),
5564
- totalRequests: n,
5427
+ totalRequests: requests.length,
5565
5428
  avgQueryTimeMs,
5566
5429
  avgFetchTimeMs,
5567
5430
  avgAppTimeMs: Math.max(0, avgDurationMs - avgQueryTimeMs - avgFetchTimeMs)
5568
- }
5431
+ },
5432
+ sessions: epData?.sessions,
5433
+ baselineP95Ms: this.getEndpointBaseline(endpoint)
5569
5434
  });
5570
5435
  }
5571
5436
  endpoints.sort((a, b) => b.summary.p95Ms - a.summary.p95Ms);
@@ -5576,6 +5441,7 @@ var init_metrics_store = __esm({
5576
5441
  this.endpointIndex.clear();
5577
5442
  this.accumulators.clear();
5578
5443
  this.pendingPoints.clear();
5444
+ this.baselineCache.clear();
5579
5445
  this.dirty = false;
5580
5446
  this.persistence.remove();
5581
5447
  }
@@ -5617,6 +5483,7 @@ var init_metrics_store = __esm({
5617
5483
  epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
5618
5484
  }
5619
5485
  this.pendingPoints.clear();
5486
+ this.baselineCache.clear();
5620
5487
  if (!this.dirty) return;
5621
5488
  if (sync) {
5622
5489
  this.persistence.saveSync(this.data);
@@ -5710,10 +5577,6 @@ var init_store = __esm({
5710
5577
  "use strict";
5711
5578
  init_request_store();
5712
5579
  init_telemetry_store();
5713
- init_fetch_store();
5714
- init_log_store();
5715
- init_error_store();
5716
- init_query_store();
5717
5580
  init_metrics_store();
5718
5581
  init_persistence();
5719
5582
  }
@@ -5745,9 +5608,9 @@ function formatConsoleLine(issue, suffix) {
5745
5608
  }
5746
5609
  return line;
5747
5610
  }
5748
- function startTerminalInsights(registry, proxyPort) {
5749
- const bus = registry.get("event-bus");
5750
- const metricsStore = registry.get("metrics-store");
5611
+ function startTerminalInsights(services, proxyPort) {
5612
+ const bus = services.bus;
5613
+ const metricsStore = services.metricsStore;
5751
5614
  const printedKeys = /* @__PURE__ */ new Set();
5752
5615
  const resolvedKeys = /* @__PURE__ */ new Set();
5753
5616
  const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
@@ -5818,8 +5681,8 @@ var init_terminal = __esm({
5818
5681
  "use strict";
5819
5682
  init_src();
5820
5683
  init_constants();
5821
- init_limits();
5822
- init_severity();
5684
+ init_config();
5685
+ init_labels();
5823
5686
  SEVERITY_COLOR = {
5824
5687
  critical: pc.red,
5825
5688
  warning: pc.yellow,
@@ -5914,8 +5777,14 @@ function outgoingToIncoming(headers2) {
5914
5777
  }
5915
5778
  return result;
5916
5779
  }
5780
+ function getDecompressor(encoding) {
5781
+ if (encoding === CONTENT_ENCODING_GZIP) return gunzip;
5782
+ if (encoding === CONTENT_ENCODING_BR) return brotliDecompress;
5783
+ if (encoding === CONTENT_ENCODING_DEFLATE) return inflate;
5784
+ return null;
5785
+ }
5917
5786
  function decompressAsync(body, encoding) {
5918
- const decompressor = encoding === CONTENT_ENCODING_GZIP ? gunzip : encoding === CONTENT_ENCODING_BR ? brotliDecompress : encoding === CONTENT_ENCODING_DEFLATE ? inflate : null;
5787
+ const decompressor = getDecompressor(encoding);
5919
5788
  if (!decompressor) return Promise.resolve(body);
5920
5789
  return new Promise((resolve6) => {
5921
5790
  decompressor(body, (err, result) => {
@@ -5929,7 +5798,7 @@ function toBuffer(chunk) {
5929
5798
  if (typeof chunk === "string") return Buffer.from(chunk);
5930
5799
  return null;
5931
5800
  }
5932
- function captureInProcess(req, res, requestId, requestStore) {
5801
+ function captureInProcess(req, res, requestId, requestStore, isChild = false) {
5933
5802
  const startTime = performance.now();
5934
5803
  const method = req.method ?? "GET";
5935
5804
  const resChunks = [];
@@ -5975,30 +5844,32 @@ function captureInProcess(req, res, requestId, requestStore) {
5975
5844
  const responseHeaders = outgoingToIncoming(res.getHeaders());
5976
5845
  const responseContentType = String(res.getHeader("content-type") ?? "");
5977
5846
  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);
5847
+ if (!isChild) {
5848
+ void (async () => {
5849
+ try {
5850
+ let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
5851
+ if (body && encoding && !truncated) {
5852
+ body = await decompressAsync(body, encoding);
5853
+ }
5854
+ requestStore.capture({
5855
+ requestId,
5856
+ method,
5857
+ url: req.url ?? "/",
5858
+ requestHeaders: req.headers,
5859
+ requestBody: null,
5860
+ statusCode,
5861
+ responseHeaders,
5862
+ responseBody: body,
5863
+ responseContentType,
5864
+ startTime,
5865
+ endTime,
5866
+ config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
5867
+ });
5868
+ } catch (e) {
5869
+ brakitDebug(`capture store: ${getErrorMessage(e)}`);
5983
5870
  }
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
- })();
5871
+ })();
5872
+ }
6002
5873
  return result;
6003
5874
  };
6004
5875
  }
@@ -6046,13 +5917,15 @@ function installInterceptor(deps) {
6046
5917
  deps.handleDashboard(req, res, deps.config);
6047
5918
  return true;
6048
5919
  }
6049
- const requestId = randomUUID8();
5920
+ const propagated = req.headers[BRAKIT_REQUEST_ID_HEADER];
5921
+ const requestId = propagated ?? randomUUID8();
5922
+ const isChild = propagated !== void 0;
6050
5923
  const ctx = {
6051
5924
  requestId,
6052
5925
  url,
6053
5926
  method: req.method ?? "GET"
6054
5927
  };
6055
- captureInProcess(req, res, requestId, deps.requestStore);
5928
+ captureInProcess(req, res, requestId, deps.requestStore, isChild);
6056
5929
  return storage.run(
6057
5930
  ctx,
6058
5931
  () => original.apply(this, [event, ...args])
@@ -6075,7 +5948,8 @@ var init_interceptor = __esm({
6075
5948
  init_safe_wrap();
6076
5949
  init_guard();
6077
5950
  init_capture();
6078
- init_http();
5951
+ init_labels();
5952
+ init_constants();
6079
5953
  originalEmit = null;
6080
5954
  }
6081
5955
  });
@@ -6093,18 +5967,12 @@ function setup() {
6093
5967
  initPromise = doSetup();
6094
5968
  return initPromise;
6095
5969
  }
6096
- function createStores(bus, registry) {
5970
+ function createStores(bus) {
6097
5971
  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);
5972
+ const fetchStore = new TelemetryStore();
5973
+ const logStore = new TelemetryStore();
5974
+ const errorStore = new TelemetryStore();
5975
+ const queryStore = new TelemetryStore();
6108
5976
  bus.on("telemetry:fetch", (data) => fetchStore.add(data));
6109
5977
  bus.on("telemetry:query", (data) => queryStore.add(data));
6110
5978
  bus.on("telemetry:log", (data) => logStore.add(data));
@@ -6138,17 +6006,16 @@ function installHooks(bus) {
6138
6006
  adapterNames: adapterRegistry.getActive().map((a) => a.name)
6139
6007
  };
6140
6008
  }
6141
- function startAnalysis(registry, stores, dataDir) {
6142
- const bus = registry.get("event-bus");
6009
+ function startAnalysis(bus, stores, dataDir, services) {
6143
6010
  const metricsStore = new MetricsStore(new FileMetricsPersistence(dataDir));
6144
6011
  metricsStore.start();
6145
- registry.register("metrics-store", metricsStore);
6146
6012
  const issueStore = new IssueStore(dataDir);
6147
6013
  issueStore.start();
6148
- registry.register("issue-store", issueStore);
6149
- const analysisEngine = new AnalysisEngine(registry);
6014
+ services.metricsStore = metricsStore;
6015
+ services.issueStore = issueStore;
6016
+ const analysisEngine = new AnalysisEngine(services);
6150
6017
  analysisEngine.start();
6151
- registry.register("analysis-engine", analysisEngine);
6018
+ services.analysisEngine = analysisEngine;
6152
6019
  bus.on("request:completed", (req) => {
6153
6020
  const queries = stores.queryStore.getByRequest(req.id);
6154
6021
  const fetches = stores.fetchStore.getByRequest(req.id);
@@ -6160,7 +6027,7 @@ function startAnalysis(registry, stores, dataDir) {
6160
6027
  });
6161
6028
  return { analysisEngine, metricsStore, issueStore };
6162
6029
  }
6163
- function registerLifecycle(registry, stores, services, cwd) {
6030
+ function registerLifecycle(allServices, stores, services, cwd) {
6164
6031
  let telemetrySent = false;
6165
6032
  const sendTelemetry = () => {
6166
6033
  if (telemetrySent) return;
@@ -6172,7 +6039,7 @@ function registerLifecycle(registry, stores, services, cwd) {
6172
6039
  recordRulesTriggered(
6173
6040
  services.analysisEngine.getFindings().map((f) => f.rule)
6174
6041
  );
6175
- trackSession(registry);
6042
+ trackSession(allServices);
6176
6043
  };
6177
6044
  let teardownCalled = false;
6178
6045
  const runTeardown = () => {
@@ -6201,20 +6068,23 @@ function registerLifecycle(registry, stores, services, cwd) {
6201
6068
  async function doSetup() {
6202
6069
  brakitDebug(`[setup] doSetup called at ${(/* @__PURE__ */ new Date()).toISOString()}`);
6203
6070
  const bus = new EventBus();
6204
- const registry = new ServiceRegistry();
6205
6071
  const cwd = process.cwd();
6206
- const stores = createStores(bus, registry);
6072
+ const stores = createStores(bus);
6073
+ const services = {
6074
+ bus,
6075
+ ...stores
6076
+ };
6207
6077
  const { framework, adapterNames } = installHooks(bus);
6208
6078
  initSession(framework, detectPackageManagerSync(cwd), false, adapterNames);
6209
6079
  const dataDir = getProjectDataDir(cwd);
6210
- const services = startAnalysis(registry, stores, dataDir);
6080
+ const analysisServices = startAnalysis(bus, stores, dataDir, services);
6211
6081
  const config = {
6212
6082
  proxyPort: 0,
6213
6083
  targetPort: 0,
6214
6084
  showStatic: false,
6215
6085
  maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
6216
6086
  };
6217
- const handleDashboard = createDashboardHandler(registry);
6087
+ const handleDashboard = createDashboardHandler(services);
6218
6088
  installInterceptor({
6219
6089
  handleDashboard,
6220
6090
  config,
@@ -6247,14 +6117,14 @@ async function doSetup() {
6247
6117
  brakitDebug(`port file write failed: ${getErrorMessage(err)}`);
6248
6118
  }
6249
6119
  })();
6250
- startTerminalInsights(registry, port);
6120
+ startTerminalInsights(services, port);
6251
6121
  process.stdout.write(
6252
6122
  ` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
6253
6123
  `
6254
6124
  );
6255
6125
  }
6256
6126
  });
6257
- registerLifecycle(registry, stores, services, cwd);
6127
+ registerLifecycle(services, stores, analysisServices, cwd);
6258
6128
  }
6259
6129
  var initPromise;
6260
6130
  var init_setup = __esm({
@@ -6266,12 +6136,8 @@ var init_setup = __esm({
6266
6136
  init_adapters();
6267
6137
  init_router();
6268
6138
  init_event_bus();
6269
- init_service_registry();
6270
6139
  init_request_store();
6271
- init_fetch_store();
6272
- init_log_store();
6273
- init_error_store();
6274
- init_query_store();
6140
+ init_telemetry_store();
6275
6141
  init_store();
6276
6142
  init_issue_store();
6277
6143
  init_engine();
@@ -6284,7 +6150,7 @@ var init_setup = __esm({
6284
6150
  init_type_guards();
6285
6151
  init_fs();
6286
6152
  init_project();
6287
- init_telemetry2();
6153
+ init_telemetry();
6288
6154
  initPromise = null;
6289
6155
  }
6290
6156
  });