brakit 0.10.0 → 0.10.2

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.
@@ -15,7 +15,7 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // src/constants/config.ts
18
- var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS, MAX_JSON_BODY_BYTES, ANALYSIS_DEBOUNCE_MS, ISSUE_ID_HASH_LENGTH, ISSUES_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER, PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_API_LIMIT, MAX_OBJECT_SCAN_DEPTH, MAX_UNIQUE_ENDPOINTS, MAX_ACCUMULATOR_ENTRIES, ISSUE_PRUNE_TTL_MS, FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, CLEAN_HITS_FOR_RESOLUTION, STALE_ISSUE_TTL_MS, STRICT_MODE_MAX_GAP_MS, BASELINE_MIN_SESSIONS, BASELINE_MIN_REQUESTS_PER_SESSION, BASELINE_PENDING_POINTS_MIN, METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS, SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS, VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES, TELEMETRY_EVENT_SETUP_COMPLETED, TELEMETRY_EVENT_FIRST_REQUEST, TELEMETRY_EVENT_DASHBOARD_VIEWED, TELEMETRY_EVENT_SESSION, TELEMETRY_EVENT_GRAPH_FEATURE, EXIT_REASON_CLEAN, EXIT_REASON_SIGINT, EXIT_REASON_SIGTERM, DETAIL_PREVIEW_LENGTH, KNOWN_DEPENDENCY_NAMES;
18
+ var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, 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, DETAIL_PREVIEW_LENGTH;
19
19
  var init_config = __esm({
20
20
  "src/constants/config.ts"() {
21
21
  "use strict";
@@ -25,7 +25,6 @@ var init_config = __esm({
25
25
  MAX_TELEMETRY_ENTRIES = 1e3;
26
26
  MAX_TAB_NAME_LENGTH = 32;
27
27
  MAX_INGEST_BYTES = 10485760;
28
- TERMINAL_TRUNCATE_LENGTH = 80;
29
28
  SENSITIVE_MASK_MIN_LENGTH = 8;
30
29
  SENSITIVE_MASK_VISIBLE_CHARS = 4;
31
30
  MAX_JSON_BODY_BYTES = 65536;
@@ -90,36 +89,117 @@ var init_config = __esm({
90
89
  VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
91
90
  VALID_ISSUE_CATEGORIES = /* @__PURE__ */ new Set(["security", "performance", "reliability"]);
92
91
  VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
93
- TELEMETRY_EVENT_SETUP_COMPLETED = "setup_completed";
94
- TELEMETRY_EVENT_FIRST_REQUEST = "first_request";
95
- TELEMETRY_EVENT_DASHBOARD_VIEWED = "dashboard_viewed";
96
- TELEMETRY_EVENT_SESSION = "session";
97
- TELEMETRY_EVENT_GRAPH_FEATURE = "graph_feature";
98
- EXIT_REASON_CLEAN = "clean";
99
- EXIT_REASON_SIGINT = "sigint";
100
- EXIT_REASON_SIGTERM = "sigterm";
101
92
  DETAIL_PREVIEW_LENGTH = 120;
93
+ }
94
+ });
95
+
96
+ // src/constants/detection.ts
97
+ var NODE_MODULES_SEGMENT, INSTALLED_HOOKS, KNOWN_DEPENDENCY_NAMES, KNOWN_CONFIG_FILES, KNOWN_DEPENDENCY_SET;
98
+ var init_detection = __esm({
99
+ "src/constants/detection.ts"() {
100
+ "use strict";
101
+ NODE_MODULES_SEGMENT = "/node_modules/";
102
+ INSTALLED_HOOKS = ["fetch", "console", "error"];
102
103
  KNOWN_DEPENDENCY_NAMES = [
104
+ // -- Frameworks (meta) --
103
105
  "next",
104
106
  "@remix-run/dev",
105
107
  "nuxt",
106
- "vite",
107
108
  "astro",
109
+ // -- Frameworks (backend) --
110
+ "@nestjs/core",
111
+ "@adonisjs/core",
112
+ "sails",
108
113
  "express",
109
114
  "fastify",
110
115
  "hono",
111
116
  "koa",
112
- "nest",
117
+ "@hapi/hapi",
118
+ "elysia",
119
+ "h3",
120
+ "nitro",
121
+ "@trpc/server",
122
+ // -- Bundlers --
123
+ "vite",
124
+ // -- ORM / query builders --
113
125
  "prisma",
126
+ "@prisma/client",
114
127
  "drizzle-orm",
115
128
  "typeorm",
116
- "sequelize"
129
+ "sequelize",
130
+ "mongoose",
131
+ "kysely",
132
+ "knex",
133
+ "@mikro-orm/core",
134
+ "objection",
135
+ // -- DB drivers --
136
+ "pg",
137
+ "mysql2",
138
+ "mongodb",
139
+ "better-sqlite3",
140
+ "@libsql/client",
141
+ "@planetscale/database",
142
+ "ioredis",
143
+ "redis",
144
+ // -- Auth --
145
+ "lucia",
146
+ "next-auth",
147
+ "@auth/core",
148
+ "passport",
149
+ // -- Queues / messaging --
150
+ "bullmq",
151
+ "amqplib",
152
+ "kafkajs",
153
+ // -- Validation --
154
+ "zod",
155
+ "joi",
156
+ "yup",
157
+ "arktype",
158
+ "valibot",
159
+ // -- HTTP clients --
160
+ "axios",
161
+ "got",
162
+ "ky",
163
+ "undici",
164
+ // -- Realtime --
165
+ "socket.io",
166
+ "ws",
167
+ // -- CSS / styling --
168
+ "tailwindcss",
169
+ // -- Testing --
170
+ "vitest",
171
+ "jest",
172
+ "mocha",
173
+ // -- Runtime indicators --
174
+ "bun-types",
175
+ "@types/bun"
117
176
  ];
177
+ KNOWN_CONFIG_FILES = {
178
+ "next.config.js": "nextjs",
179
+ "next.config.mjs": "nextjs",
180
+ "next.config.ts": "nextjs",
181
+ "nuxt.config.ts": "nuxt",
182
+ "nuxt.config.js": "nuxt",
183
+ "astro.config.mjs": "astro",
184
+ "astro.config.ts": "astro",
185
+ "vite.config.ts": "vite",
186
+ "vite.config.js": "vite",
187
+ "drizzle.config.ts": "drizzle-orm",
188
+ "drizzle.config.js": "drizzle-orm",
189
+ "prisma/schema.prisma": "prisma",
190
+ "knexfile.js": "knex",
191
+ "knexfile.ts": "knex",
192
+ "mikro-orm.config.ts": "@mikro-orm/core",
193
+ "nest-cli.json": "@nestjs/core",
194
+ "tailwind.config.js": "tailwindcss",
195
+ "tailwind.config.ts": "tailwindcss"
196
+ };
197
+ KNOWN_DEPENDENCY_SET = new Set(KNOWN_DEPENDENCY_NAMES);
118
198
  }
119
199
  });
120
200
 
121
201
  // src/constants/labels.ts
122
- 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, DASHBOARD_API_GRAPH, 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;
202
+ 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, DASHBOARD_API_GRAPH, 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, 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, SDK_EVENT_HELLO, TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY, UNICODE_ARROW, UNICODE_EM_DASH, UNICODE_CHECK_MARK;
123
203
  var init_labels = __esm({
124
204
  "src/constants/labels.ts"() {
125
205
  "use strict";
@@ -145,14 +225,10 @@ var init_labels = __esm({
145
225
  VALID_TABS_TUPLE = [
146
226
  "overview",
147
227
  "actions",
148
- "requests",
149
- "fetches",
150
- "queries",
151
- "errors",
152
- "logs",
228
+ "insights",
153
229
  "performance",
154
- "security",
155
- "graph"
230
+ "graph",
231
+ "explorer"
156
232
  ];
157
233
  VALID_TABS = new Set(VALID_TABS_TUPLE);
158
234
  BRAKIT_REQUEST_ID_HEADER = "x-brakit-request-id";
@@ -181,11 +257,6 @@ var init_labels = __esm({
181
257
  CONTENT_ENCODING_GZIP = "gzip";
182
258
  CONTENT_ENCODING_BR = "br";
183
259
  CONTENT_ENCODING_DEFLATE = "deflate";
184
- SEVERITY_ICON = {
185
- critical: "\u2717",
186
- warning: "\u26A0",
187
- info: "\u2139"
188
- };
189
260
  SSE_EVENT_FETCH = "fetch";
190
261
  SSE_EVENT_LOG = "log";
191
262
  SSE_EVENT_ERROR = "error_event";
@@ -197,14 +268,14 @@ var init_labels = __esm({
197
268
  SDK_EVENT_LOG = "log";
198
269
  SDK_EVENT_ERROR = "error";
199
270
  SDK_EVENT_AUTH_CHECK = "auth.check";
200
- POSTHOG_HOST = "https://us.i.posthog.com";
201
- POSTHOG_CAPTURE_PATH = "/i/v0/e/";
202
- POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
203
- SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
271
+ SDK_EVENT_HELLO = "sdk.hello";
204
272
  TIMELINE_FETCH = "fetch";
205
273
  TIMELINE_LOG = "log";
206
274
  TIMELINE_ERROR = "error";
207
275
  TIMELINE_QUERY = "query";
276
+ UNICODE_ARROW = "\u2192";
277
+ UNICODE_EM_DASH = "\u2014";
278
+ UNICODE_CHECK_MARK = "\u2713";
208
279
  }
209
280
  });
210
281
 
@@ -244,13 +315,36 @@ var init_features = __esm({
244
315
  }
245
316
  });
246
317
 
318
+ // src/constants/telemetry.ts
319
+ var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, TELEMETRY_EVENT_SETUP_COMPLETED, TELEMETRY_EVENT_FIRST_REQUEST, TELEMETRY_EVENT_DASHBOARD_VIEWED, TELEMETRY_EVENT_SESSION, TELEMETRY_EVENT_GRAPH_FEATURE, TELEMETRY_SDK_NAME, EXIT_REASON_CLEAN, EXIT_REASON_SIGINT, EXIT_REASON_SIGTERM, SPEED_BUCKET_THRESHOLDS;
320
+ var init_telemetry = __esm({
321
+ "src/constants/telemetry.ts"() {
322
+ "use strict";
323
+ POSTHOG_HOST = "https://us.i.posthog.com";
324
+ POSTHOG_CAPTURE_PATH = "/i/v0/e/";
325
+ POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
326
+ TELEMETRY_EVENT_SETUP_COMPLETED = "setup_completed";
327
+ TELEMETRY_EVENT_FIRST_REQUEST = "first_request";
328
+ TELEMETRY_EVENT_DASHBOARD_VIEWED = "dashboard_viewed";
329
+ TELEMETRY_EVENT_SESSION = "session";
330
+ TELEMETRY_EVENT_GRAPH_FEATURE = "graph_feature";
331
+ TELEMETRY_SDK_NAME = "node";
332
+ EXIT_REASON_CLEAN = "clean";
333
+ EXIT_REASON_SIGINT = "sigint";
334
+ EXIT_REASON_SIGTERM = "sigterm";
335
+ SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
336
+ }
337
+ });
338
+
247
339
  // src/constants/index.ts
248
340
  var init_constants = __esm({
249
341
  "src/constants/index.ts"() {
250
342
  "use strict";
251
343
  init_config();
344
+ init_detection();
252
345
  init_labels();
253
346
  init_features();
347
+ init_telemetry();
254
348
  }
255
349
  });
256
350
 
@@ -339,6 +433,214 @@ var init_fetch = __esm({
339
433
  }
340
434
  });
341
435
 
436
+ // src/utils/log.ts
437
+ function brakitWarn(message) {
438
+ process.stderr.write(`${PREFIX} ${message}
439
+ `);
440
+ }
441
+ function brakitDebug(message) {
442
+ if (process.env.DEBUG_BRAKIT) {
443
+ process.stderr.write(`${PREFIX}:debug ${message}
444
+ `);
445
+ }
446
+ }
447
+ var PREFIX;
448
+ var init_log = __esm({
449
+ "src/utils/log.ts"() {
450
+ "use strict";
451
+ PREFIX = "[brakit]";
452
+ }
453
+ });
454
+
455
+ // src/utils/type-guards.ts
456
+ function isString(val) {
457
+ return typeof val === "string";
458
+ }
459
+ function isNumber(val) {
460
+ return typeof val === "number" && !isNaN(val);
461
+ }
462
+ function isBoolean(val) {
463
+ return typeof val === "boolean";
464
+ }
465
+ function isThenable(value) {
466
+ return value != null && typeof value.then === "function";
467
+ }
468
+ function getErrorMessage(err) {
469
+ if (err instanceof Error) return err.message;
470
+ if (typeof err === "string") return err;
471
+ return String(err);
472
+ }
473
+ function isValidIssueState(val) {
474
+ return typeof val === "string" && VALID_ISSUE_STATES.has(val);
475
+ }
476
+ function isValidIssueCategory(val) {
477
+ return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
478
+ }
479
+ function isValidAiFixStatus(val) {
480
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
481
+ }
482
+ function validateIssuesData(parsed) {
483
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
484
+ const obj = parsed;
485
+ if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
486
+ return parsed;
487
+ }
488
+ return null;
489
+ }
490
+ function validateMetricsData(parsed) {
491
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
492
+ const obj = parsed;
493
+ if (obj.version === 1 && Array.isArray(obj.endpoints)) {
494
+ return parsed;
495
+ }
496
+ return null;
497
+ }
498
+ var init_type_guards = __esm({
499
+ "src/utils/type-guards.ts"() {
500
+ "use strict";
501
+ init_config();
502
+ }
503
+ });
504
+
505
+ // src/runtime/capture.ts
506
+ import { gunzip, brotliDecompress, inflate } from "zlib";
507
+ function outgoingToIncoming(headers2) {
508
+ const result = {};
509
+ for (const [key, value] of Object.entries(headers2)) {
510
+ if (value === void 0) continue;
511
+ if (Array.isArray(value)) {
512
+ result[key] = value.map(String);
513
+ } else {
514
+ result[key] = String(value);
515
+ }
516
+ }
517
+ return result;
518
+ }
519
+ function getDecompressor(encoding) {
520
+ if (encoding === CONTENT_ENCODING_GZIP) return gunzip;
521
+ if (encoding === CONTENT_ENCODING_BR) return brotliDecompress;
522
+ if (encoding === CONTENT_ENCODING_DEFLATE) return inflate;
523
+ return null;
524
+ }
525
+ function decompressAsync(body, encoding) {
526
+ const decompressor = getDecompressor(encoding);
527
+ if (!decompressor) return Promise.resolve(body);
528
+ return new Promise((resolve6) => {
529
+ decompressor(body, (err, result) => {
530
+ resolve6(err ? body : result);
531
+ });
532
+ });
533
+ }
534
+ function toBuffer(chunk) {
535
+ if (Buffer.isBuffer(chunk)) return chunk;
536
+ if (chunk instanceof Uint8Array) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
537
+ if (typeof chunk === "string") return Buffer.from(chunk);
538
+ return null;
539
+ }
540
+ function drainPendingCaptures() {
541
+ if (pendingCaptures === 0) return Promise.resolve();
542
+ return new Promise((resolve6) => {
543
+ drainResolvers.push(resolve6);
544
+ });
545
+ }
546
+ function onCaptureSettled() {
547
+ pendingCaptures--;
548
+ if (pendingCaptures === 0 && drainResolvers.length > 0) {
549
+ const resolvers = drainResolvers;
550
+ drainResolvers = [];
551
+ for (const resolve6 of resolvers) resolve6();
552
+ }
553
+ }
554
+ function captureInProcess(req, res, requestId, requestStore, isChild = false) {
555
+ const startTime = performance.now();
556
+ const method = req.method ?? "GET";
557
+ const resChunks = [];
558
+ let resSize = 0;
559
+ const originalWrite = res.write;
560
+ const originalEnd = res.end;
561
+ let truncated = false;
562
+ res.write = function(...args) {
563
+ try {
564
+ const chunk = args[0];
565
+ if (chunk != null && typeof chunk !== "function") {
566
+ if (resSize < DEFAULT_MAX_BODY_CAPTURE) {
567
+ const buf = toBuffer(chunk);
568
+ if (buf) {
569
+ resChunks.push(buf);
570
+ resSize += buf.length;
571
+ }
572
+ } else {
573
+ truncated = true;
574
+ }
575
+ }
576
+ } catch (e) {
577
+ brakitDebug(`capture write: ${getErrorMessage(e)}`);
578
+ }
579
+ return originalWrite.apply(this, args);
580
+ };
581
+ res.end = function(...args) {
582
+ try {
583
+ const chunk = typeof args[0] !== "function" ? args[0] : void 0;
584
+ if (chunk != null && resSize < DEFAULT_MAX_BODY_CAPTURE) {
585
+ const buf = toBuffer(chunk);
586
+ if (buf) {
587
+ resChunks.push(buf);
588
+ }
589
+ }
590
+ } catch (e) {
591
+ brakitDebug(`capture end: ${getErrorMessage(e)}`);
592
+ }
593
+ const result = originalEnd.apply(this, args);
594
+ const endTime = performance.now();
595
+ const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
596
+ const statusCode = res.statusCode;
597
+ const responseHeaders = outgoingToIncoming(res.getHeaders());
598
+ const responseContentType = String(res.getHeader("content-type") ?? "");
599
+ const capturedChunks = resChunks.slice();
600
+ if (!isChild) {
601
+ pendingCaptures++;
602
+ void (async () => {
603
+ try {
604
+ let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
605
+ if (body && encoding && !truncated) {
606
+ body = await decompressAsync(body, encoding);
607
+ }
608
+ requestStore.capture({
609
+ requestId,
610
+ method,
611
+ url: req.url ?? "/",
612
+ requestHeaders: req.headers,
613
+ requestBody: null,
614
+ statusCode,
615
+ responseHeaders,
616
+ responseBody: body,
617
+ responseContentType,
618
+ startTime,
619
+ endTime,
620
+ config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
621
+ });
622
+ } catch (e) {
623
+ brakitDebug(`capture store: ${getErrorMessage(e)}`);
624
+ } finally {
625
+ onCaptureSettled();
626
+ }
627
+ })();
628
+ }
629
+ return result;
630
+ };
631
+ }
632
+ var pendingCaptures, drainResolvers;
633
+ var init_capture = __esm({
634
+ "src/runtime/capture.ts"() {
635
+ "use strict";
636
+ init_constants();
637
+ init_log();
638
+ init_type_guards();
639
+ pendingCaptures = 0;
640
+ drainResolvers = [];
641
+ }
642
+ });
643
+
342
644
  // src/instrument/hooks/console.ts
343
645
  import { format } from "util";
344
646
  function setupConsoleHook(emit) {
@@ -539,92 +841,23 @@ var init_normalize = __esm({
539
841
  }
540
842
  });
541
843
 
542
- // src/utils/type-guards.ts
543
- function isString(val) {
544
- return typeof val === "string";
844
+ // src/instrument/adapters/shared.ts
845
+ import { createRequire } from "module";
846
+ function tryRequire(id) {
847
+ try {
848
+ return appRequire(id);
849
+ } catch {
850
+ return null;
851
+ }
545
852
  }
546
- function isNumber(val) {
547
- return typeof val === "number" && !isNaN(val);
853
+ function getActiveRequestId() {
854
+ return getRequestContext()?.requestId ?? null;
548
855
  }
549
- function isBoolean(val) {
550
- return typeof val === "boolean";
551
- }
552
- function isThenable(value) {
553
- return value != null && typeof value.then === "function";
554
- }
555
- function getErrorMessage(err) {
556
- if (err instanceof Error) return err.message;
557
- if (typeof err === "string") return err;
558
- return String(err);
559
- }
560
- function isValidIssueState(val) {
561
- return typeof val === "string" && VALID_ISSUE_STATES.has(val);
562
- }
563
- function isValidIssueCategory(val) {
564
- return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
565
- }
566
- function isValidAiFixStatus(val) {
567
- return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
568
- }
569
- function validateIssuesData(parsed) {
570
- if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
571
- const obj = parsed;
572
- if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
573
- return parsed;
574
- }
575
- return null;
576
- }
577
- function validateMetricsData(parsed) {
578
- if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
579
- const obj = parsed;
580
- if (obj.version === 1 && Array.isArray(obj.endpoints)) {
581
- return parsed;
582
- }
583
- return null;
584
- }
585
- var init_type_guards = __esm({
586
- "src/utils/type-guards.ts"() {
587
- "use strict";
588
- init_config();
589
- }
590
- });
591
-
592
- // src/utils/log.ts
593
- function brakitWarn(message) {
594
- process.stderr.write(`${PREFIX} ${message}
595
- `);
596
- }
597
- function brakitDebug(message) {
598
- if (process.env.DEBUG_BRAKIT) {
599
- process.stderr.write(`${PREFIX}:debug ${message}
600
- `);
601
- }
602
- }
603
- var PREFIX;
604
- var init_log = __esm({
605
- "src/utils/log.ts"() {
606
- "use strict";
607
- PREFIX = "[brakit]";
608
- }
609
- });
610
-
611
- // src/instrument/adapters/shared.ts
612
- import { createRequire } from "module";
613
- function tryRequire(id) {
614
- try {
615
- return appRequire(id);
616
- } catch {
617
- return null;
618
- }
619
- }
620
- function getActiveRequestId() {
621
- return getRequestContext()?.requestId ?? null;
622
- }
623
- function getPrototype(lib, className) {
624
- const defaultExport = lib.default;
625
- const cls = defaultExport?.[className] ?? lib[className];
626
- if (!cls || typeof cls !== "function") return null;
627
- return cls.prototype ?? null;
856
+ function getPrototype(lib, className) {
857
+ const defaultExport = lib.default;
858
+ const cls = defaultExport?.[className] ?? lib[className];
859
+ if (!cls || typeof cls !== "function") return null;
860
+ return cls.prototype ?? null;
628
861
  }
629
862
  function buildQueryEvent(config, sql, op, table, start, requestId, rowCount) {
630
863
  return {
@@ -1834,554 +2067,219 @@ var init_sdk_event_parser = __esm({
1834
2067
  }
1835
2068
  });
1836
2069
 
1837
- // src/dashboard/api/ingest.ts
1838
- function isBrakitBatch(msg) {
1839
- return typeof msg === "object" && msg !== null && "_brakit" in msg && msg._brakit === true && !("version" in msg);
2070
+ // src/telemetry/config.ts
2071
+ import { homedir, platform } from "os";
2072
+ import { join } from "path";
2073
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2074
+ import { randomUUID as randomUUID5 } from "crypto";
2075
+ function isValidTelemetryConfig(value) {
2076
+ return typeof value === "object" && value !== null && typeof value.telemetry === "boolean" && typeof value.anonymousId === "string" && value.anonymousId.length > 0;
1840
2077
  }
1841
- function isSDKPayload(msg) {
1842
- return typeof msg === "object" && msg !== null && "_brakit" in msg && "version" in msg && typeof msg.version === "number";
2078
+ function readConfig() {
2079
+ try {
2080
+ if (!existsSync(CONFIG_PATH)) return null;
2081
+ const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
2082
+ return isValidTelemetryConfig(parsed) ? parsed : null;
2083
+ } catch {
2084
+ return null;
2085
+ }
1843
2086
  }
1844
- function createIngestHandler(services) {
1845
- const routeEvent = (event) => {
1846
- switch (event.type) {
1847
- case TIMELINE_FETCH:
1848
- services.fetchStore.add(event.data);
1849
- break;
1850
- case TIMELINE_LOG:
1851
- services.logStore.add(event.data);
1852
- break;
1853
- case TIMELINE_ERROR:
1854
- services.errorStore.add(event.data);
1855
- break;
1856
- case TIMELINE_QUERY:
1857
- services.queryStore.add(event.data);
1858
- break;
1859
- }
1860
- };
1861
- const queryStore = services.queryStore;
1862
- const fetchStore = services.fetchStore;
1863
- const logStore = services.logStore;
1864
- const errorStore = services.errorStore;
1865
- const requestStore = services.requestStore;
1866
- const stores = {
1867
- addQuery: (data) => queryStore.add(data),
1868
- addFetch: (data) => fetchStore.add(data),
1869
- addLog: (data) => logStore.add(data),
1870
- addError: (data) => errorStore.add(data),
1871
- addRequest: (data) => requestStore.add(data)
1872
- };
1873
- return (req, res) => {
1874
- if (req.method !== "POST") {
1875
- sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1876
- return;
2087
+ function writeConfig(config) {
2088
+ try {
2089
+ if (!existsSync(CONFIG_DIR))
2090
+ mkdirSync(CONFIG_DIR, { recursive: true, ...IS_WINDOWS ? {} : { mode: DIR_MODE_OWNER_ONLY } });
2091
+ writeFileSync(
2092
+ CONFIG_PATH,
2093
+ JSON.stringify(config, null, 2) + "\n",
2094
+ IS_WINDOWS ? {} : { mode: FILE_MODE_OWNER_ONLY }
2095
+ );
2096
+ } catch (err) {
2097
+ if (process.env.BRAKIT_DEBUG) {
2098
+ process.stderr.write(`[brakit] config write failed: ${err?.message ?? err}
2099
+ `);
1877
2100
  }
1878
- const chunks = [];
1879
- let totalSize = 0;
1880
- req.on("data", (chunk) => {
1881
- totalSize += chunk.length;
1882
- if (totalSize > MAX_INGEST_BYTES) {
1883
- sendJson(req, res, HTTP_PAYLOAD_TOO_LARGE, { error: "Payload too large" });
1884
- req.destroy();
1885
- return;
1886
- }
1887
- chunks.push(chunk);
1888
- });
1889
- req.on("end", () => {
1890
- if (res.headersSent) return;
1891
- try {
1892
- const body = JSON.parse(Buffer.concat(chunks).toString());
1893
- if (isSDKPayload(body)) {
1894
- for (const event of body.events) {
1895
- routeSDKEvent(event, stores);
1896
- }
1897
- res.writeHead(HTTP_NO_CONTENT);
1898
- res.end();
1899
- return;
1900
- }
1901
- if (isBrakitBatch(body)) {
1902
- for (const event of body.events) {
1903
- routeEvent(event);
1904
- }
1905
- res.writeHead(HTTP_NO_CONTENT);
1906
- res.end();
1907
- return;
1908
- }
1909
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid batch" });
1910
- } catch {
1911
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid JSON" });
1912
- }
1913
- });
1914
- req.on("error", () => {
1915
- if (!res.headersSent) {
1916
- res.writeHead(HTTP_BAD_REQUEST);
1917
- res.end();
1918
- }
1919
- });
1920
- };
2101
+ }
1921
2102
  }
1922
- var init_ingest = __esm({
1923
- "src/dashboard/api/ingest.ts"() {
2103
+ function getOrCreateConfig() {
2104
+ const existing = readConfig();
2105
+ if (existing) return existing;
2106
+ const config = { telemetry: true, anonymousId: randomUUID5() };
2107
+ writeConfig(config);
2108
+ return config;
2109
+ }
2110
+ function isTelemetryEnabled() {
2111
+ if (cachedEnabled !== null) return cachedEnabled;
2112
+ const env = process.env.BRAKIT_TELEMETRY;
2113
+ if (env !== void 0) {
2114
+ const normalized = env.toLowerCase().trim();
2115
+ cachedEnabled = normalized !== "false" && normalized !== "0" && normalized !== "off";
2116
+ return cachedEnabled;
2117
+ }
2118
+ cachedEnabled = readConfig()?.telemetry ?? true;
2119
+ return cachedEnabled;
2120
+ }
2121
+ var IS_WINDOWS, CONFIG_DIR, CONFIG_PATH, cachedEnabled;
2122
+ var init_config2 = __esm({
2123
+ "src/telemetry/config.ts"() {
1924
2124
  "use strict";
1925
- init_config();
1926
- init_labels();
1927
- init_shared2();
1928
- init_sdk_event_parser();
2125
+ init_features();
2126
+ IS_WINDOWS = platform() === "win32";
2127
+ CONFIG_DIR = join(homedir(), ".brakit");
2128
+ CONFIG_PATH = join(CONFIG_DIR, "config.json");
2129
+ cachedEnabled = null;
1929
2130
  }
1930
2131
  });
1931
2132
 
1932
- // src/dashboard/api/metrics.ts
1933
- function createMetricsHandler(metricsStore) {
1934
- return (req, res) => {
1935
- if (!requireGet(req, res)) return;
1936
- const url = parseRequestUrl(req);
1937
- const endpoint = url.searchParams.get("endpoint");
1938
- if (endpoint) {
1939
- const ep = metricsStore.getEndpoint(endpoint);
1940
- sendJson(req, res, HTTP_OK, { endpoints: ep ? [ep] : [] });
1941
- return;
2133
+ // src/utils/fs.ts
2134
+ import { access, readFile, writeFile } from "fs/promises";
2135
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2136
+ import { createHash } from "crypto";
2137
+ import { homedir as homedir2 } from "os";
2138
+ import { resolve, join as join2 } from "path";
2139
+ function getProjectDataDir(projectRoot) {
2140
+ const absolute = resolve(projectRoot);
2141
+ const hash = createHash("sha256").update(absolute).digest("hex").slice(0, PROJECT_HASH_LENGTH);
2142
+ return join2(homedir2(), ".brakit", "projects", hash);
2143
+ }
2144
+ async function fileExists(path) {
2145
+ try {
2146
+ await access(path);
2147
+ return true;
2148
+ } catch {
2149
+ return false;
2150
+ }
2151
+ }
2152
+ function ensureGitignore(dir, entry) {
2153
+ try {
2154
+ const gitignorePath = resolve(dir, "../.gitignore");
2155
+ if (existsSync2(gitignorePath)) {
2156
+ const content = readFileSync2(gitignorePath, "utf-8");
2157
+ if (content.split("\n").some((l) => l.trim() === entry)) return;
2158
+ writeFileSync2(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
2159
+ } else {
2160
+ writeFileSync2(gitignorePath, entry + "\n");
1942
2161
  }
1943
- sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
1944
- };
2162
+ } catch (err) {
2163
+ brakitDebug(`ensureGitignore failed: ${getErrorMessage(err)}`);
2164
+ }
1945
2165
  }
1946
- var init_metrics = __esm({
1947
- "src/dashboard/api/metrics.ts"() {
2166
+ async function ensureGitignoreAsync(dir, entry) {
2167
+ try {
2168
+ const gitignorePath = resolve(dir, "../.gitignore");
2169
+ if (await fileExists(gitignorePath)) {
2170
+ const content = await readFile(gitignorePath, "utf-8");
2171
+ if (content.split("\n").some((l) => l.trim() === entry)) return;
2172
+ await writeFile(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
2173
+ } else {
2174
+ await writeFile(gitignorePath, entry + "\n");
2175
+ }
2176
+ } catch (err) {
2177
+ brakitDebug(`ensureGitignoreAsync failed: ${getErrorMessage(err)}`);
2178
+ }
2179
+ }
2180
+ var init_fs = __esm({
2181
+ "src/utils/fs.ts"() {
1948
2182
  "use strict";
1949
- init_shared2();
1950
- init_labels();
2183
+ init_config();
2184
+ init_log();
2185
+ init_type_guards();
1951
2186
  }
1952
2187
  });
1953
2188
 
1954
- // src/dashboard/api/metrics-live.ts
1955
- function createLiveMetricsHandler(metricsStore) {
1956
- return (req, res) => {
1957
- if (!requireGet(req, res)) return;
1958
- sendJson(req, res, 200, { endpoints: metricsStore.getLiveEndpoints() });
1959
- };
1960
- }
1961
- var init_metrics_live = __esm({
1962
- "src/dashboard/api/metrics-live.ts"() {
2189
+ // src/utils/atomic-writer.ts
2190
+ import {
2191
+ writeFileSync as writeFileSync3,
2192
+ existsSync as existsSync3,
2193
+ mkdirSync as mkdirSync3,
2194
+ renameSync
2195
+ } from "fs";
2196
+ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
2197
+ var AtomicWriter;
2198
+ var init_atomic_writer = __esm({
2199
+ "src/utils/atomic-writer.ts"() {
1963
2200
  "use strict";
1964
- init_shared2();
1965
- }
1966
- });
1967
-
1968
- // src/dashboard/api/activity.ts
1969
- function buildTimeline(services, requestId) {
1970
- const fetches = services.fetchStore.getByRequest(requestId);
1971
- const logs = services.logStore.getByRequest(requestId);
1972
- const errors = services.errorStore.getByRequest(requestId);
1973
- const queries = services.queryStore.getByRequest(requestId);
1974
- const timeline = [];
1975
- for (const fetch of fetches)
1976
- timeline.push({ type: TIMELINE_FETCH, timestamp: fetch.timestamp, data: fetch });
1977
- for (const log of logs)
1978
- timeline.push({ type: TIMELINE_LOG, timestamp: log.timestamp, data: log });
1979
- for (const error of errors)
1980
- timeline.push({ type: TIMELINE_ERROR, timestamp: error.timestamp, data: error });
1981
- for (const query of queries)
1982
- timeline.push({ type: TIMELINE_QUERY, timestamp: query.timestamp, data: query });
1983
- timeline.sort((a, b) => a.timestamp - b.timestamp);
1984
- return {
1985
- total: timeline.length,
1986
- timeline,
1987
- counts: {
1988
- fetches: fetches.length,
1989
- logs: logs.length,
1990
- errors: errors.length,
1991
- queries: queries.length
1992
- }
1993
- };
1994
- }
1995
- function createActivityHandler(services) {
1996
- return (req, res) => {
1997
- if (!requireGet(req, res)) return;
1998
- try {
1999
- const url = parseRequestUrl(req);
2000
- const requestId = url.searchParams.get("requestId");
2001
- const requestIds = url.searchParams.get("requestIds");
2002
- if (!requestId && !requestIds) {
2003
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId or requestIds parameter required" });
2004
- return;
2201
+ init_fs();
2202
+ init_log();
2203
+ init_type_guards();
2204
+ AtomicWriter = class {
2205
+ constructor(opts) {
2206
+ this.opts = opts;
2207
+ this.writing = false;
2208
+ this.pendingContent = null;
2209
+ this.tmpPath = opts.filePath + ".tmp";
2005
2210
  }
2006
- if (requestId) {
2007
- const result = buildTimeline(services, requestId);
2008
- sendJson(req, res, HTTP_OK, { requestId, ...result });
2009
- return;
2211
+ writeSync(content) {
2212
+ try {
2213
+ this.ensureDir();
2214
+ writeFileSync3(this.tmpPath, content);
2215
+ renameSync(this.tmpPath, this.opts.filePath);
2216
+ } catch (err) {
2217
+ brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2218
+ }
2010
2219
  }
2011
- const ids = (requestIds || "").split(",").filter(Boolean).slice(0, MAX_BATCH_IDS);
2012
- const activities = {};
2013
- for (const id of ids) {
2014
- activities[id] = buildTimeline(services, id);
2220
+ async writeAsync(content) {
2221
+ if (this.writing) {
2222
+ this.pendingContent = content;
2223
+ return;
2224
+ }
2225
+ this.writing = true;
2226
+ try {
2227
+ await this.ensureDirAsync();
2228
+ await writeFile2(this.tmpPath, content);
2229
+ await rename(this.tmpPath, this.opts.filePath);
2230
+ } catch (err) {
2231
+ brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2232
+ } finally {
2233
+ this.writing = false;
2234
+ if (this.pendingContent !== null) {
2235
+ const next = this.pendingContent;
2236
+ this.pendingContent = null;
2237
+ this.writeAsync(next).catch(() => {
2238
+ });
2239
+ }
2240
+ }
2015
2241
  }
2016
- sendJson(req, res, HTTP_OK, { requestIds: ids, activities });
2017
- } catch (err) {
2018
- brakitDebug(`activity handler error: ${err}`);
2019
- if (!res.headersSent) {
2020
- sendJson(req, res, HTTP_INTERNAL_ERROR, { error: "Internal error" });
2242
+ ensureDir() {
2243
+ if (!existsSync3(this.opts.dir)) {
2244
+ mkdirSync3(this.opts.dir, { recursive: true });
2245
+ if (this.opts.gitignoreEntry) {
2246
+ ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
2247
+ }
2248
+ }
2021
2249
  }
2022
- }
2023
- };
2024
- }
2025
- var MAX_BATCH_IDS;
2026
- var init_activity = __esm({
2027
- "src/dashboard/api/activity.ts"() {
2028
- "use strict";
2029
- init_shared2();
2030
- init_labels();
2031
- init_log();
2032
- MAX_BATCH_IDS = 50;
2033
- }
2034
- });
2035
-
2036
- // src/dashboard/api/index.ts
2037
- var init_api = __esm({
2038
- "src/dashboard/api/index.ts"() {
2039
- "use strict";
2040
- init_handlers();
2041
- init_ingest();
2042
- init_metrics();
2043
- init_metrics_live();
2044
- init_activity();
2250
+ async ensureDirAsync() {
2251
+ if (!await fileExists(this.opts.dir)) {
2252
+ await mkdir(this.opts.dir, { recursive: true });
2253
+ if (this.opts.gitignoreEntry) {
2254
+ await ensureGitignoreAsync(this.opts.dir, this.opts.gitignoreEntry);
2255
+ }
2256
+ }
2257
+ }
2258
+ };
2045
2259
  }
2046
2260
  });
2047
2261
 
2048
- // src/dashboard/api/issues.ts
2049
- function createIssuesHandler(issueStore) {
2050
- return (req, res) => {
2051
- if (!requireGet(req, res)) return;
2052
- const url = parseRequestUrl(req);
2053
- const stateParam = url.searchParams.get("state");
2054
- const categoryParam = url.searchParams.get("category");
2055
- let issues;
2056
- if (stateParam && isValidIssueState(stateParam)) {
2057
- issues = issueStore.getByState(stateParam);
2058
- } else if (categoryParam && isValidIssueCategory(categoryParam)) {
2059
- issues = issueStore.getByCategory(categoryParam);
2060
- } else {
2061
- issues = issueStore.getAll();
2062
- }
2063
- sendJson(req, res, HTTP_OK, { issues });
2064
- };
2065
- }
2066
- function createFindingsHandler(issueStore) {
2067
- return (req, res) => {
2068
- if (!requireGet(req, res)) return;
2069
- const url = parseRequestUrl(req);
2070
- const stateParam = url.searchParams.get("state");
2071
- let issues;
2072
- if (stateParam && isValidIssueState(stateParam)) {
2073
- issues = issueStore.getByState(stateParam);
2074
- } else {
2075
- issues = issueStore.getAll();
2076
- }
2077
- sendJson(req, res, HTTP_OK, {
2078
- total: issues.length,
2079
- findings: issues
2080
- });
2081
- };
2082
- }
2083
- function createIssuesReportHandler(issueStore, eventBus) {
2084
- return async (req, res) => {
2085
- if (req.method !== "POST") {
2086
- sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
2087
- return;
2088
- }
2089
- const body = await readJsonBody(req, res);
2090
- if (!body) return;
2091
- const { findingId, status, notes } = body;
2092
- if (!findingId || typeof findingId !== "string") {
2093
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "findingId is required" });
2094
- return;
2095
- }
2096
- if (!isValidAiFixStatus(status)) {
2097
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "status must be 'fixed' or 'wont_fix'" });
2098
- return;
2099
- }
2100
- if (!notes || typeof notes !== "string") {
2101
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "notes is required" });
2102
- return;
2103
- }
2104
- if (issueStore.reportFix(findingId, status, notes)) {
2105
- eventBus.emit("issues:changed", issueStore.getAll());
2106
- sendJson(req, res, HTTP_OK, { ok: true });
2107
- return;
2108
- }
2109
- sendJson(req, res, HTTP_NOT_FOUND, { error: "Finding not found" });
2110
- };
2262
+ // src/utils/issue-id.ts
2263
+ import { createHash as createHash2 } from "crypto";
2264
+ function computeIssueId(issue) {
2265
+ const stableDesc = issue.desc.replace(/\d[\d,.]*\s*\w*/g, "#");
2266
+ const key = `${issue.rule}:${issue.endpoint ?? "global"}:${stableDesc}`;
2267
+ return createHash2("sha256").update(key).digest("hex").slice(0, ISSUE_ID_HASH_LENGTH);
2111
2268
  }
2112
- var init_issues = __esm({
2113
- "src/dashboard/api/issues.ts"() {
2269
+ var init_issue_id = __esm({
2270
+ "src/utils/issue-id.ts"() {
2114
2271
  "use strict";
2115
- init_shared2();
2116
- init_type_guards();
2117
- init_labels();
2272
+ init_config();
2118
2273
  }
2119
2274
  });
2120
2275
 
2121
- // src/dashboard/api/graph.ts
2122
- function createGraphHandler(services) {
2123
- return (req, res) => {
2124
- if (!requireGet(req, res)) return;
2125
- const url = parseRequestUrl(req);
2126
- const rawCluster = url.searchParams.get("cluster") ?? void 0;
2127
- const rawNode = url.searchParams.get("node") ?? void 0;
2128
- const rawLevel = url.searchParams.get("level") ?? void 0;
2129
- const rawGrouping = url.searchParams.get("grouping") ?? void 0;
2130
- const cluster = rawCluster && rawCluster.length <= MAX_PARAM_LENGTH ? rawCluster : void 0;
2131
- const node = rawNode && rawNode.length <= MAX_PARAM_LENGTH ? rawNode : void 0;
2132
- const level = rawLevel && VALID_LEVELS.has(rawLevel) ? rawLevel : void 0;
2133
- const grouping = rawGrouping && VALID_GROUPINGS.has(rawGrouping) ? rawGrouping : void 0;
2134
- const { graphBuilder, metricsStore } = services;
2135
- graphBuilder.enrichWithMetrics((endpointKey) => {
2136
- const metrics = metricsStore.getEndpoint(endpointKey);
2137
- if (!metrics || metrics.sessions.length === 0) return void 0;
2138
- const latest = metrics.sessions[metrics.sessions.length - 1];
2139
- return latest.p95DurationMs;
2140
- });
2141
- const data = graphBuilder.getApiResponse({ cluster, node, level, grouping });
2142
- sendJson(req, res, HTTP_OK, data);
2143
- };
2144
- }
2145
- var VALID_LEVELS, VALID_GROUPINGS, MAX_PARAM_LENGTH;
2146
- var init_graph = __esm({
2147
- "src/dashboard/api/graph.ts"() {
2148
- "use strict";
2149
- init_labels();
2150
- init_shared2();
2151
- VALID_LEVELS = /* @__PURE__ */ new Set(["endpoints", "clusters"]);
2152
- VALID_GROUPINGS = /* @__PURE__ */ new Set(["path", "auth-boundary", "data-domain"]);
2153
- MAX_PARAM_LENGTH = 200;
2154
- }
2155
- });
2156
-
2157
- // src/dashboard/sse.ts
2158
- function createSSEHandler(services) {
2159
- const clients = /* @__PURE__ */ new Set();
2160
- function broadcast(eventType, data) {
2161
- if (clients.size === 0) return;
2162
- const frame = eventType ? `event: ${eventType}
2163
- data: ${data}
2164
-
2165
- ` : `data: ${data}
2166
-
2167
- `;
2168
- for (const client of clients) {
2169
- if (client.res.destroyed) {
2170
- clients.delete(client);
2171
- continue;
2172
- }
2173
- try {
2174
- client.res.write(frame);
2175
- } catch {
2176
- clients.delete(client);
2177
- }
2178
- }
2179
- }
2180
- const bus = services.bus;
2181
- bus.on("request:completed", (r) => broadcast(null, JSON.stringify(r)));
2182
- bus.on("telemetry:fetch", (e) => broadcast(SSE_EVENT_FETCH, JSON.stringify(e)));
2183
- bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
2184
- bus.on("telemetry:error", (e) => broadcast(SSE_EVENT_ERROR, JSON.stringify(e)));
2185
- bus.on("telemetry:query", (e) => broadcast(SSE_EVENT_QUERY, JSON.stringify(e)));
2186
- bus.on("analysis:updated", ({ issues }) => {
2187
- broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
2188
- });
2189
- bus.on("issues:changed", (issues) => {
2190
- broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
2191
- });
2192
- return (req, res) => {
2193
- const headers2 = {
2194
- "content-type": "text/event-stream",
2195
- "cache-control": "no-cache",
2196
- connection: "keep-alive"
2197
- };
2198
- const corsOrigin = getCorsOrigin(req);
2199
- if (corsOrigin) {
2200
- headers2["access-control-allow-origin"] = corsOrigin;
2201
- }
2202
- res.writeHead(HTTP_OK, headers2);
2203
- res.write(":ok\n\n");
2204
- const heartbeat = setInterval(() => {
2205
- if (res.destroyed) {
2206
- clearInterval(heartbeat);
2207
- clients.delete(client);
2208
- return;
2209
- }
2210
- try {
2211
- res.write(":heartbeat\n\n");
2212
- } catch {
2213
- clearInterval(heartbeat);
2214
- clients.delete(client);
2215
- }
2216
- }, SSE_HEARTBEAT_INTERVAL_MS);
2217
- heartbeat.unref();
2218
- const client = { res, heartbeat };
2219
- clients.add(client);
2220
- req.on("close", () => {
2221
- clearInterval(heartbeat);
2222
- clients.delete(client);
2223
- });
2224
- };
2225
- }
2226
- var init_sse = __esm({
2227
- "src/dashboard/sse.ts"() {
2228
- "use strict";
2229
- init_constants();
2230
- init_labels();
2231
- init_shared2();
2232
- }
2233
- });
2234
-
2235
- // src/utils/fs.ts
2236
- import { access, readFile, writeFile } from "fs/promises";
2237
- import { existsSync, readFileSync, writeFileSync } from "fs";
2238
- import { createHash } from "crypto";
2239
- import { homedir } from "os";
2240
- import { resolve, join } from "path";
2241
- function getProjectDataDir(projectRoot) {
2242
- const absolute = resolve(projectRoot);
2243
- const hash = createHash("sha256").update(absolute).digest("hex").slice(0, PROJECT_HASH_LENGTH);
2244
- return join(homedir(), ".brakit", "projects", hash);
2245
- }
2246
- async function fileExists(path) {
2247
- try {
2248
- await access(path);
2249
- return true;
2250
- } catch {
2251
- return false;
2252
- }
2253
- }
2254
- function ensureGitignore(dir, entry) {
2255
- try {
2256
- const gitignorePath = resolve(dir, "../.gitignore");
2257
- if (existsSync(gitignorePath)) {
2258
- const content = readFileSync(gitignorePath, "utf-8");
2259
- if (content.split("\n").some((l) => l.trim() === entry)) return;
2260
- writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
2261
- } else {
2262
- writeFileSync(gitignorePath, entry + "\n");
2263
- }
2264
- } catch (err) {
2265
- brakitDebug(`ensureGitignore failed: ${getErrorMessage(err)}`);
2266
- }
2267
- }
2268
- async function ensureGitignoreAsync(dir, entry) {
2269
- try {
2270
- const gitignorePath = resolve(dir, "../.gitignore");
2271
- if (await fileExists(gitignorePath)) {
2272
- const content = await readFile(gitignorePath, "utf-8");
2273
- if (content.split("\n").some((l) => l.trim() === entry)) return;
2274
- await writeFile(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
2275
- } else {
2276
- await writeFile(gitignorePath, entry + "\n");
2277
- }
2278
- } catch (err) {
2279
- brakitDebug(`ensureGitignoreAsync failed: ${getErrorMessage(err)}`);
2280
- }
2281
- }
2282
- var init_fs = __esm({
2283
- "src/utils/fs.ts"() {
2284
- "use strict";
2285
- init_config();
2286
- init_log();
2287
- init_type_guards();
2288
- }
2289
- });
2290
-
2291
- // src/utils/atomic-writer.ts
2292
- import {
2293
- writeFileSync as writeFileSync2,
2294
- existsSync as existsSync2,
2295
- mkdirSync as mkdirSync2,
2296
- renameSync
2297
- } from "fs";
2298
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
2299
- var AtomicWriter;
2300
- var init_atomic_writer = __esm({
2301
- "src/utils/atomic-writer.ts"() {
2302
- "use strict";
2303
- init_fs();
2304
- init_log();
2305
- init_type_guards();
2306
- AtomicWriter = class {
2307
- constructor(opts) {
2308
- this.opts = opts;
2309
- this.writing = false;
2310
- this.pendingContent = null;
2311
- this.tmpPath = opts.filePath + ".tmp";
2312
- }
2313
- writeSync(content) {
2314
- try {
2315
- this.ensureDir();
2316
- writeFileSync2(this.tmpPath, content);
2317
- renameSync(this.tmpPath, this.opts.filePath);
2318
- } catch (err) {
2319
- brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2320
- }
2321
- }
2322
- async writeAsync(content) {
2323
- if (this.writing) {
2324
- this.pendingContent = content;
2325
- return;
2326
- }
2327
- this.writing = true;
2328
- try {
2329
- await this.ensureDirAsync();
2330
- await writeFile2(this.tmpPath, content);
2331
- await rename(this.tmpPath, this.opts.filePath);
2332
- } catch (err) {
2333
- brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2334
- } finally {
2335
- this.writing = false;
2336
- if (this.pendingContent !== null) {
2337
- const next = this.pendingContent;
2338
- this.pendingContent = null;
2339
- this.writeAsync(next).catch(() => {
2340
- });
2341
- }
2342
- }
2343
- }
2344
- ensureDir() {
2345
- if (!existsSync2(this.opts.dir)) {
2346
- mkdirSync2(this.opts.dir, { recursive: true });
2347
- if (this.opts.gitignoreEntry) {
2348
- ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
2349
- }
2350
- }
2351
- }
2352
- async ensureDirAsync() {
2353
- if (!await fileExists(this.opts.dir)) {
2354
- await mkdir(this.opts.dir, { recursive: true });
2355
- if (this.opts.gitignoreEntry) {
2356
- await ensureGitignoreAsync(this.opts.dir, this.opts.gitignoreEntry);
2357
- }
2358
- }
2359
- }
2360
- };
2361
- }
2362
- });
2363
-
2364
- // src/utils/issue-id.ts
2365
- import { createHash as createHash2 } from "crypto";
2366
- function computeIssueId(issue) {
2367
- const stableDesc = issue.desc.replace(/\d[\d,.]*\s*\w*/g, "#");
2368
- const key = `${issue.rule}:${issue.endpoint ?? "global"}:${stableDesc}`;
2369
- return createHash2("sha256").update(key).digest("hex").slice(0, ISSUE_ID_HASH_LENGTH);
2370
- }
2371
- var init_issue_id = __esm({
2372
- "src/utils/issue-id.ts"() {
2373
- "use strict";
2374
- init_config();
2375
- }
2376
- });
2377
-
2378
- // src/store/issue-store.ts
2379
- import { readFile as readFile2 } from "fs/promises";
2380
- import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
2381
- import { resolve as resolve2 } from "path";
2382
- var IssueStore;
2383
- var init_issue_store = __esm({
2384
- "src/store/issue-store.ts"() {
2276
+ // src/store/issue-store.ts
2277
+ import { readFile as readFile2 } from "fs/promises";
2278
+ import { readFileSync as readFileSync3, existsSync as existsSync4, unlinkSync } from "fs";
2279
+ import { resolve as resolve2 } from "path";
2280
+ var IssueStore;
2281
+ var init_issue_store = __esm({
2282
+ "src/store/issue-store.ts"() {
2385
2283
  "use strict";
2386
2284
  init_fs();
2387
2285
  init_config();
@@ -2426,7 +2324,7 @@ var init_issue_store = __esm({
2426
2324
  existing.occurrences++;
2427
2325
  existing.issue = issue;
2428
2326
  existing.cleanHitsSinceLastSeen = 0;
2429
- if (existing.state === "resolved" || existing.state === "stale") {
2327
+ if (existing.aiStatus !== "wont_fix" && (existing.state === "resolved" || existing.state === "stale")) {
2430
2328
  existing.state = "regressed";
2431
2329
  existing.resolvedAt = null;
2432
2330
  }
@@ -2452,10 +2350,11 @@ var init_issue_store = __esm({
2452
2350
  return stateful;
2453
2351
  }
2454
2352
  /**
2455
- * Reconcile issues against the current analysis results using evidence-based resolution.
2456
- *
2457
- * @param currentIssueIds - IDs of issues detected in the current analysis cycle
2458
- * @param activeEndpoints - Endpoints that had requests in the current cycle
2353
+ * Evidence-based reconciliation: for each active issue whose endpoint had
2354
+ * traffic but the issue was NOT re-detected, increment cleanHitsSinceLastSeen.
2355
+ * After CLEAN_HITS_FOR_RESOLUTION consecutive clean cycles, auto-resolve.
2356
+ * Issues on endpoints with no recent traffic are marked stale after STALE_ISSUE_TTL_MS.
2357
+ * Resolved and stale issues are pruned after their respective TTLs expire.
2459
2358
  */
2460
2359
  reconcile(currentIssueIds, activeEndpoints) {
2461
2360
  const now = Date.now();
@@ -2523,7 +2422,7 @@ var init_issue_store = __esm({
2523
2422
  this.issues.clear();
2524
2423
  this.dirty = false;
2525
2424
  try {
2526
- if (existsSync3(this.issuesPath)) {
2425
+ if (existsSync4(this.issuesPath)) {
2527
2426
  unlinkSync(this.issuesPath);
2528
2427
  }
2529
2428
  } catch {
@@ -2545,17 +2444,23 @@ var init_issue_store = __esm({
2545
2444
  /** Sync load for tests only — not used in production paths. */
2546
2445
  loadSync() {
2547
2446
  try {
2548
- if (existsSync3(this.issuesPath)) {
2549
- const raw = readFileSync2(this.issuesPath, "utf-8");
2447
+ if (existsSync4(this.issuesPath)) {
2448
+ const raw = readFileSync3(this.issuesPath, "utf-8");
2550
2449
  this.hydrate(raw);
2551
2450
  }
2552
2451
  } catch (err) {
2553
2452
  brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
2554
2453
  }
2555
2454
  }
2556
- /** Parse and populate issues from a raw JSON string. */
2557
2455
  hydrate(raw) {
2558
- const validated = validateIssuesData(JSON.parse(raw));
2456
+ let parsed;
2457
+ try {
2458
+ parsed = JSON.parse(raw);
2459
+ } catch (err) {
2460
+ brakitDebug(`IssueStore: corrupt JSON in issues file, starting fresh: ${err}`);
2461
+ return;
2462
+ }
2463
+ const validated = validateIssuesData(parsed);
2559
2464
  if (!validated) return;
2560
2465
  for (const issue of validated.issues) {
2561
2466
  this.issues.set(issue.issueId, issue);
@@ -2568,8 +2473,12 @@ var init_issue_store = __esm({
2568
2473
  }
2569
2474
  flushSync() {
2570
2475
  if (!this.dirty) return;
2571
- this.writer.writeSync(this.serialize());
2572
- this.dirty = false;
2476
+ try {
2477
+ this.writer.writeSync(this.serialize());
2478
+ this.dirty = false;
2479
+ } catch (err) {
2480
+ brakitDebug(`IssueStore: flush failed, will retry: ${err}`);
2481
+ }
2573
2482
  }
2574
2483
  serialize() {
2575
2484
  const data = {
@@ -2584,19 +2493,26 @@ var init_issue_store = __esm({
2584
2493
 
2585
2494
  // src/detect/project.ts
2586
2495
  import { readFile as readFile3, readdir } from "fs/promises";
2587
- import { existsSync as existsSync4 } from "fs";
2588
- import { join as join2, relative } from "path";
2496
+ import { existsSync as existsSync5 } from "fs";
2497
+ import { join as join3, relative } from "path";
2589
2498
  function detectFrameworkFromDeps(allDeps) {
2590
2499
  for (const f of FRAMEWORKS) {
2591
2500
  if (allDeps[f.dep]) return f.name;
2592
2501
  }
2593
2502
  return "unknown";
2594
2503
  }
2595
- function detectPackageManagerSync(rootDir) {
2596
- if (existsSync4(join2(rootDir, "bun.lockb")) || existsSync4(join2(rootDir, "bun.lock"))) return "bun";
2597
- if (existsSync4(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
2598
- if (existsSync4(join2(rootDir, "yarn.lock"))) return "yarn";
2599
- if (existsSync4(join2(rootDir, "package-lock.json"))) return "npm";
2504
+ function detectConfigFiles(rootDir) {
2505
+ const found = /* @__PURE__ */ new Set();
2506
+ for (const [file, label] of Object.entries(KNOWN_CONFIG_FILES)) {
2507
+ if (existsSync5(join3(rootDir, file))) found.add(label);
2508
+ }
2509
+ return [...found];
2510
+ }
2511
+ function detectPackageManager(rootDir) {
2512
+ if (existsSync5(join3(rootDir, "bun.lockb")) || existsSync5(join3(rootDir, "bun.lock"))) return "bun";
2513
+ if (existsSync5(join3(rootDir, "pnpm-lock.yaml"))) return "pnpm";
2514
+ if (existsSync5(join3(rootDir, "yarn.lock"))) return "yarn";
2515
+ if (existsSync5(join3(rootDir, "package-lock.json"))) return "npm";
2600
2516
  return "unknown";
2601
2517
  }
2602
2518
  var FRAMEWORKS;
@@ -2604,12 +2520,24 @@ var init_project = __esm({
2604
2520
  "src/detect/project.ts"() {
2605
2521
  "use strict";
2606
2522
  init_fs();
2523
+ init_detection();
2607
2524
  FRAMEWORKS = [
2525
+ // Meta-frameworks first (they bundle Express/Vite internally)
2608
2526
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
2609
2527
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
2610
2528
  { name: "nuxt", dep: "nuxt", devCmd: "nuxt dev", bin: "nuxt", defaultPort: 3e3, devArgs: ["dev", "--port"] },
2611
- { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] },
2612
- { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
2529
+ { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] },
2530
+ { name: "nestjs", dep: "@nestjs/core", devCmd: "nest start", bin: "nest", defaultPort: 3e3, devArgs: ["--watch"] },
2531
+ { name: "adonis", dep: "@adonisjs/core", devCmd: "node ace serve", bin: "ace", defaultPort: 3333, devArgs: ["serve", "--watch"] },
2532
+ { name: "sails", dep: "sails", devCmd: "sails lift", bin: "sails", defaultPort: 1337, devArgs: ["lift"] },
2533
+ // Server frameworks
2534
+ { name: "hono", dep: "hono", devCmd: "node", bin: "node", defaultPort: 3e3 },
2535
+ { name: "fastify", dep: "fastify", devCmd: "node", bin: "node", defaultPort: 3e3 },
2536
+ { name: "koa", dep: "koa", devCmd: "node", bin: "node", defaultPort: 3e3 },
2537
+ { name: "hapi", dep: "@hapi/hapi", devCmd: "node", bin: "node", defaultPort: 3e3 },
2538
+ { name: "express", dep: "express", devCmd: "node", bin: "node", defaultPort: 3e3 },
2539
+ // Bundlers (last — likely used alongside a framework above)
2540
+ { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] }
2613
2541
  ];
2614
2542
  }
2615
2543
  });
@@ -3345,14 +3273,14 @@ var init_disposable = __esm({
3345
3273
  "use strict";
3346
3274
  SubscriptionBag = class {
3347
3275
  constructor() {
3348
- this.items = [];
3276
+ this.items = /* @__PURE__ */ new Set();
3349
3277
  }
3350
3278
  add(teardown) {
3351
- this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
3279
+ this.items.add(typeof teardown === "function" ? { dispose: teardown } : teardown);
3352
3280
  }
3353
3281
  dispose() {
3354
3282
  for (const d of this.items) d.dispose();
3355
- this.items.length = 0;
3283
+ this.items.clear();
3356
3284
  }
3357
3285
  };
3358
3286
  }
@@ -3967,275 +3895,936 @@ var init_pattern_rules = __esm({
3967
3895
  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."
3968
3896
  });
3969
3897
  }
3970
- return insights;
3898
+ return insights;
3899
+ }
3900
+ };
3901
+ crossEndpointRule = {
3902
+ id: "cross-endpoint",
3903
+ check(ctx) {
3904
+ const insights = [];
3905
+ const queryMap = /* @__PURE__ */ new Map();
3906
+ const allEndpoints = /* @__PURE__ */ new Set();
3907
+ for (const [reqId, reqQueries] of ctx.queriesByReq) {
3908
+ const req = ctx.reqById.get(reqId);
3909
+ if (!req) continue;
3910
+ const endpoint = getEndpointKey(req.method, req.path);
3911
+ allEndpoints.add(endpoint);
3912
+ const seenInReq = /* @__PURE__ */ new Set();
3913
+ for (const query of reqQueries) {
3914
+ const shape = getQueryShape(query);
3915
+ let entry = queryMap.get(shape);
3916
+ if (!entry) {
3917
+ entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: query };
3918
+ queryMap.set(shape, entry);
3919
+ }
3920
+ entry.count++;
3921
+ if (!seenInReq.has(shape)) {
3922
+ seenInReq.add(shape);
3923
+ entry.endpoints.add(endpoint);
3924
+ }
3925
+ }
3926
+ }
3927
+ if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
3928
+ for (const [, queryMetric] of queryMap) {
3929
+ if (queryMetric.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
3930
+ if (queryMetric.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
3931
+ const coveragePct = Math.round(queryMetric.endpoints.size / allEndpoints.size * 100);
3932
+ if (coveragePct < CROSS_ENDPOINT_PCT) continue;
3933
+ const info = getQueryInfo(queryMetric.first);
3934
+ const label = info.op + (info.table ? ` ${info.table}` : "");
3935
+ insights.push({
3936
+ severity: "warning",
3937
+ type: "cross-endpoint",
3938
+ title: "Repeated Query Across Endpoints",
3939
+ desc: `${label} runs on ${queryMetric.endpoints.size} of ${allEndpoints.size} endpoints (${coveragePct}%).`,
3940
+ hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
3941
+ detail: `Endpoints: ${[...queryMetric.endpoints].slice(0, 5).join(", ")}${queryMetric.endpoints.size > 5 ? ` +${queryMetric.endpoints.size - 5} more` : ""}. Total: ${queryMetric.count} executions.`
3942
+ });
3943
+ }
3944
+ }
3945
+ return insights;
3946
+ }
3947
+ };
3948
+ }
3949
+ });
3950
+
3951
+ // src/analysis/insights/rules/security.ts
3952
+ var securityRule;
3953
+ var init_security = __esm({
3954
+ "src/analysis/insights/rules/security.ts"() {
3955
+ "use strict";
3956
+ securityRule = {
3957
+ id: "security",
3958
+ check(ctx) {
3959
+ if (!ctx.securityFindings) return [];
3960
+ return ctx.securityFindings.map((finding) => ({
3961
+ severity: finding.severity,
3962
+ type: "security",
3963
+ title: finding.title,
3964
+ desc: finding.desc,
3965
+ hint: finding.hint,
3966
+ detail: finding.detail
3967
+ }));
3968
+ }
3969
+ };
3970
+ }
3971
+ });
3972
+
3973
+ // src/analysis/insights/rules/index.ts
3974
+ var init_rules2 = __esm({
3975
+ "src/analysis/insights/rules/index.ts"() {
3976
+ "use strict";
3977
+ init_query_rules();
3978
+ init_response_rules();
3979
+ init_reliability_rules();
3980
+ init_pattern_rules();
3981
+ init_security();
3982
+ }
3983
+ });
3984
+
3985
+ // src/analysis/insights/index.ts
3986
+ function createDefaultInsightRunner() {
3987
+ const runner = new InsightRunner();
3988
+ runner.register(n1Rule);
3989
+ runner.register(crossEndpointRule);
3990
+ runner.register(redundantQueryRule);
3991
+ runner.register(errorRule);
3992
+ runner.register(errorHotspotRule);
3993
+ runner.register(duplicateRule);
3994
+ runner.register(slowRule);
3995
+ runner.register(queryHeavyRule);
3996
+ runner.register(selectStarRule);
3997
+ runner.register(highRowsRule);
3998
+ runner.register(responseOverfetchRule);
3999
+ runner.register(largeResponseRule);
4000
+ runner.register(regressionRule);
4001
+ runner.register(securityRule);
4002
+ return runner;
4003
+ }
4004
+ function computeInsights(ctx) {
4005
+ return createDefaultInsightRunner().run(ctx);
4006
+ }
4007
+ var init_insights = __esm({
4008
+ "src/analysis/insights/index.ts"() {
4009
+ "use strict";
4010
+ init_runner();
4011
+ init_runner();
4012
+ init_rules2();
4013
+ }
4014
+ });
4015
+
4016
+ // src/analysis/insights.ts
4017
+ var init_insights2 = __esm({
4018
+ "src/analysis/insights.ts"() {
4019
+ "use strict";
4020
+ init_insights();
4021
+ }
4022
+ });
4023
+
4024
+ // src/analysis/issue-mappers.ts
4025
+ function categorizeInsight(type) {
4026
+ if (type === "security") return "security";
4027
+ if (type === "error" || type === "error-hotspot") return "reliability";
4028
+ return "performance";
4029
+ }
4030
+ function insightToIssue(insight) {
4031
+ return {
4032
+ category: categorizeInsight(insight.type),
4033
+ rule: insight.type,
4034
+ severity: insight.severity,
4035
+ title: insight.title,
4036
+ desc: insight.desc,
4037
+ hint: insight.hint,
4038
+ detail: insight.detail,
4039
+ endpoint: extractEndpointFromDesc(insight.desc) ?? void 0
4040
+ };
4041
+ }
4042
+ function securityFindingToIssue(finding) {
4043
+ return {
4044
+ category: "security",
4045
+ rule: finding.rule,
4046
+ severity: finding.severity,
4047
+ title: finding.title,
4048
+ desc: finding.desc,
4049
+ hint: finding.hint,
4050
+ detail: finding.detail,
4051
+ endpoint: finding.endpoint
4052
+ };
4053
+ }
4054
+ var init_issue_mappers = __esm({
4055
+ "src/analysis/issue-mappers.ts"() {
4056
+ "use strict";
4057
+ init_endpoint();
4058
+ }
4059
+ });
4060
+
4061
+ // src/analysis/engine.ts
4062
+ var AnalysisEngine;
4063
+ var init_engine = __esm({
4064
+ "src/analysis/engine.ts"() {
4065
+ "use strict";
4066
+ init_config();
4067
+ init_disposable();
4068
+ init_group();
4069
+ init_rules();
4070
+ init_insights2();
4071
+ init_issue_mappers();
4072
+ init_issue_id();
4073
+ init_prepare();
4074
+ AnalysisEngine = class {
4075
+ constructor(services, debounceMs = ANALYSIS_DEBOUNCE_MS) {
4076
+ this.services = services;
4077
+ this.debounceMs = debounceMs;
4078
+ this.cachedInsights = [];
4079
+ this.cachedFindings = [];
4080
+ this.debounceTimer = null;
4081
+ this.subs = new SubscriptionBag();
4082
+ this.scanner = createDefaultScanner();
4083
+ }
4084
+ start() {
4085
+ const bus = this.services.bus;
4086
+ this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
4087
+ this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
4088
+ this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
4089
+ this.subs.add(bus.on("telemetry:log", () => this.scheduleRecompute()));
4090
+ }
4091
+ stop() {
4092
+ this.subs.dispose();
4093
+ if (this.debounceTimer) {
4094
+ clearTimeout(this.debounceTimer);
4095
+ this.debounceTimer = null;
4096
+ }
4097
+ }
4098
+ getInsights() {
4099
+ return this.cachedInsights;
4100
+ }
4101
+ getFindings() {
4102
+ return this.cachedFindings;
4103
+ }
4104
+ scheduleRecompute() {
4105
+ if (this.debounceTimer) return;
4106
+ this.debounceTimer = setTimeout(() => {
4107
+ this.debounceTimer = null;
4108
+ this.recompute();
4109
+ }, this.debounceMs);
4110
+ }
4111
+ recompute() {
4112
+ const allRequests = this.services.requestStore.getAll();
4113
+ const queries = this.services.queryStore.getAll();
4114
+ const errors = this.services.errorStore.getAll();
4115
+ const logs = this.services.logStore.getAll();
4116
+ const fetches = this.services.fetchStore.getAll();
4117
+ const requests = keepRecentPerEndpoint(allRequests);
4118
+ const flows = groupRequestsIntoFlows(requests);
4119
+ this.cachedFindings = this.scanner.scan({ requests, logs });
4120
+ this.cachedInsights = computeInsights({
4121
+ requests,
4122
+ queries,
4123
+ errors,
4124
+ flows,
4125
+ fetches,
4126
+ previousMetrics: this.services.metricsStore.getAll(),
4127
+ securityFindings: this.cachedFindings
4128
+ });
4129
+ const issueStore = this.services.issueStore;
4130
+ const currentIssueIds = /* @__PURE__ */ new Set();
4131
+ for (const finding of this.cachedFindings) {
4132
+ const issue = securityFindingToIssue(finding);
4133
+ issueStore.upsert(issue, "passive");
4134
+ currentIssueIds.add(computeIssueId(issue));
4135
+ }
4136
+ for (const insight of this.cachedInsights) {
4137
+ const issue = insightToIssue(insight);
4138
+ issueStore.upsert(issue, "passive");
4139
+ currentIssueIds.add(computeIssueId(issue));
4140
+ }
4141
+ const activeEndpoints = extractActiveEndpoints(allRequests);
4142
+ issueStore.reconcile(currentIssueIds, activeEndpoints);
4143
+ const update = {
4144
+ insights: this.cachedInsights,
4145
+ findings: this.cachedFindings,
4146
+ issues: issueStore.getAll()
4147
+ };
4148
+ this.services.bus.emit("analysis:updated", update);
4149
+ }
4150
+ };
4151
+ }
4152
+ });
4153
+
4154
+ // src/index.ts
4155
+ var VERSION;
4156
+ var init_src = __esm({
4157
+ "src/index.ts"() {
4158
+ "use strict";
4159
+ init_issue_store();
4160
+ init_project();
4161
+ init_adapter_registry();
4162
+ init_rules();
4163
+ init_engine();
4164
+ init_insights2();
4165
+ init_insights();
4166
+ VERSION = "0.10.2";
4167
+ }
4168
+ });
4169
+
4170
+ // src/telemetry/transport.ts
4171
+ import { platform as platform2, release, arch } from "os";
4172
+ import { spawn } from "child_process";
4173
+ function commonProperties() {
4174
+ return {
4175
+ brakit_version: VERSION,
4176
+ node_version: process.version,
4177
+ os: `${platform2()}-${release()}`,
4178
+ arch: arch(),
4179
+ $lib: "brakit",
4180
+ $process_person_profile: false,
4181
+ $geoip_disable: true
4182
+ };
4183
+ }
4184
+ function sendToPosthog(event, properties) {
4185
+ if (!isTelemetryEnabled() || !POSTHOG_KEY) return;
4186
+ const config = getOrCreateConfig();
4187
+ const payload = {
4188
+ api_key: POSTHOG_KEY,
4189
+ event,
4190
+ distinct_id: config.anonymousId,
4191
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4192
+ properties: { ...commonProperties(), ...properties }
4193
+ };
4194
+ try {
4195
+ const serializedPayload = JSON.stringify(payload);
4196
+ const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
4197
+ const child = spawn(
4198
+ process.execPath,
4199
+ [
4200
+ "-e",
4201
+ `fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(serializedPayload)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
4202
+ ],
4203
+ { detached: true, stdio: "ignore" }
4204
+ );
4205
+ child.unref();
4206
+ } catch (err) {
4207
+ brakitDebug(`telemetry send failed: ${err}`);
4208
+ }
4209
+ }
4210
+ function trackEvent(event, properties) {
4211
+ sendToPosthog(event, { sdk: TELEMETRY_SDK_NAME, ...properties });
4212
+ }
4213
+ var POSTHOG_KEY;
4214
+ var init_transport = __esm({
4215
+ "src/telemetry/transport.ts"() {
4216
+ "use strict";
4217
+ init_src();
4218
+ init_telemetry();
4219
+ init_config2();
4220
+ init_log();
4221
+ POSTHOG_KEY = "phc_E9TwydCGnSfPLIUhNxChpeg32TSowjk31KiPhnLPP0x";
4222
+ }
4223
+ });
4224
+
4225
+ // src/telemetry/session.ts
4226
+ function speedBucket(ms) {
4227
+ if (ms === 0) return "none";
4228
+ const t = SPEED_BUCKET_THRESHOLDS;
4229
+ if (ms < t[0]) return `<${t[0]}ms`;
4230
+ for (let i = 1; i < t.length; i++) {
4231
+ if (ms < t[i]) return `${t[i - 1]}-${t[i]}ms`;
4232
+ }
4233
+ return `>${t[t.length - 1]}ms`;
4234
+ }
4235
+ var defaultTransport, Session;
4236
+ var init_session = __esm({
4237
+ "src/telemetry/session.ts"() {
4238
+ "use strict";
4239
+ init_telemetry();
4240
+ init_config2();
4241
+ init_transport();
4242
+ defaultTransport = { send: sendToPosthog, track: trackEvent };
4243
+ Session = class {
4244
+ constructor(transport = defaultTransport) {
4245
+ // ── Setup phase ──
4246
+ this.startTime = 0;
4247
+ this.framework = "unknown";
4248
+ this.packageManager = "";
4249
+ this.isCustomCommand = false;
4250
+ this.adapters = [];
4251
+ this.setupDurationMs = 0;
4252
+ this.setupSucceeded = false;
4253
+ // ── Detection ──
4254
+ this.detectedDependencies = [];
4255
+ this.adaptersFailed = [];
4256
+ this.configFilesDetected = [];
4257
+ this.jsRuntime = "node";
4258
+ this.loadedPackages = [];
4259
+ // ── Python SDK ──
4260
+ this.pythonConnected = false;
4261
+ this.pythonFramework = "unknown";
4262
+ this.pythonAdapters = [];
4263
+ this.pythonVersion = "";
4264
+ // ── Runtime accumulation ──
4265
+ this.requestCount = 0;
4266
+ this.insightTypes = /* @__PURE__ */ new Set();
4267
+ this.rulesTriggered = /* @__PURE__ */ new Set();
4268
+ this.tabsViewed = /* @__PURE__ */ new Set();
4269
+ this.dashboardOpened = false;
4270
+ this.explainUsed = false;
4271
+ this.firstRequestAt = 0;
4272
+ this.dashboardOpenedAt = 0;
4273
+ this.exitReason = "unknown";
4274
+ this.transport = transport;
4275
+ }
4276
+ // ── Setup phase ──
4277
+ init(framework, packageManager, isCustomCommand, adapters) {
4278
+ getOrCreateConfig();
4279
+ this.startTime = Date.now();
4280
+ this.framework = framework;
4281
+ this.packageManager = packageManager;
4282
+ this.isCustomCommand = isCustomCommand;
4283
+ this.adapters = adapters;
4284
+ }
4285
+ recordSetup(detection, durationMs) {
4286
+ this.detectedDependencies = detection.detectedDependencies;
4287
+ this.adaptersFailed = detection.adaptersFailed;
4288
+ this.configFilesDetected = detection.configFilesDetected;
4289
+ this.jsRuntime = detection.jsRuntime;
4290
+ this.setupDurationMs = durationMs;
4291
+ this.setupSucceeded = true;
4292
+ }
4293
+ // ── Runtime events ──
4294
+ recordFirstRequest(loadedPackages) {
4295
+ if (!this.firstRequestAt) this.firstRequestAt = Date.now();
4296
+ this.loadedPackages = loadedPackages;
4297
+ }
4298
+ recordPythonStack(info) {
4299
+ this.pythonConnected = true;
4300
+ this.pythonFramework = info.framework;
4301
+ this.pythonAdapters = info.adapters;
4302
+ this.pythonVersion = info.pythonVersion;
4303
+ }
4304
+ recordDashboardOpened() {
4305
+ if (this.dashboardOpened) return;
4306
+ this.dashboardOpened = true;
4307
+ this.dashboardOpenedAt = Date.now();
4308
+ this.transport.track(TELEMETRY_EVENT_DASHBOARD_VIEWED, {
4309
+ time_to_dashboard_ms: this.startTime > 0 ? Date.now() - this.startTime : null,
4310
+ request_count_at_open: this.requestCount
4311
+ });
4312
+ }
4313
+ recordTabViewed(tab) {
4314
+ this.tabsViewed.add(tab);
4315
+ }
4316
+ recordExplainUsed() {
4317
+ this.explainUsed = true;
4318
+ }
4319
+ recordExitReason(reason) {
4320
+ if (this.exitReason === "unknown") this.exitReason = reason;
4321
+ }
4322
+ // ── Pre-flush snapshot from services ──
4323
+ recordCounts(requestCount, insightTypes, rulesTriggered) {
4324
+ this.requestCount = requestCount;
4325
+ for (const t of insightTypes) this.insightTypes.add(t);
4326
+ for (const r of rulesTriggered) this.rulesTriggered.add(r);
4327
+ }
4328
+ // ── Serialization ──
4329
+ /** Build the full PostHog session payload. Single source of truth for all fields. */
4330
+ toPostHogPayload(services) {
4331
+ const metricsStore = services.metricsStore;
4332
+ const analysisEngine = services.analysisEngine;
4333
+ const live = metricsStore.getLiveEndpoints();
4334
+ const insights = analysisEngine.getInsights();
4335
+ const findings = analysisEngine.getFindings();
4336
+ let totalRequests = 0;
4337
+ let totalDuration = 0;
4338
+ let slowestP95 = 0;
4339
+ for (const ep of live) {
4340
+ totalRequests += ep.summary.totalRequests;
4341
+ totalDuration += ep.summary.p95Ms * ep.summary.totalRequests;
4342
+ if (ep.summary.p95Ms > slowestP95) slowestP95 = ep.summary.p95Ms;
4343
+ }
4344
+ const now = Date.now();
4345
+ return {
4346
+ sdk: TELEMETRY_SDK_NAME,
4347
+ // Stack detection
4348
+ framework: this.framework,
4349
+ package_manager: this.packageManager,
4350
+ js_runtime: this.jsRuntime,
4351
+ framework_detection_candidates: this.detectedDependencies,
4352
+ config_files_detected: this.configFilesDetected,
4353
+ loaded_packages: this.loadedPackages,
4354
+ loaded_package_count: this.loadedPackages.length,
4355
+ adapters_detected: this.adapters,
4356
+ adapters_failed: this.adaptersFailed,
4357
+ // Python SDK
4358
+ python_connected: this.pythonConnected,
4359
+ python_framework: this.pythonFramework,
4360
+ python_adapters: this.pythonAdapters,
4361
+ python_version: this.pythonVersion,
4362
+ // Session metadata
4363
+ is_custom_command: this.isCustomCommand,
4364
+ first_session: readConfig() === null,
4365
+ setup_succeeded: this.setupSucceeded,
4366
+ setup_duration_ms: this.setupDurationMs,
4367
+ session_duration_s: Math.ceil((now - this.startTime) / 1e3),
4368
+ session_duration_ms: now - this.startTime,
4369
+ exit_reason: this.exitReason,
4370
+ // Usage
4371
+ request_count: this.requestCount,
4372
+ error_count: services.errorStore.getAll().length,
4373
+ query_count: services.queryStore.getAll().length,
4374
+ fetch_count: services.fetchStore.getAll().length,
4375
+ endpoint_count: live.length,
4376
+ avg_duration_ms: totalRequests > 0 ? Math.round(totalDuration / totalRequests) : 0,
4377
+ slowest_endpoint_bucket: speedBucket(slowestP95),
4378
+ // Analysis
4379
+ insight_count: insights.length,
4380
+ finding_count: findings.length,
4381
+ insight_types: [...this.insightTypes],
4382
+ rules_triggered: [...this.rulesTriggered],
4383
+ // Dashboard engagement
4384
+ tabs_viewed: [...this.tabsViewed],
4385
+ dashboard_opened: this.dashboardOpened,
4386
+ explain_used: this.explainUsed,
4387
+ // Timing
4388
+ time_to_first_request_ms: this.firstRequestAt ? this.firstRequestAt - this.startTime : null,
4389
+ time_to_dashboard_ms: this.dashboardOpenedAt ? this.dashboardOpenedAt - this.startTime : null
4390
+ };
4391
+ }
4392
+ flush(services) {
4393
+ this.transport.send(TELEMETRY_EVENT_SESSION, this.toPostHogPayload(services));
4394
+ getOrCreateConfig();
3971
4395
  }
3972
4396
  };
3973
- crossEndpointRule = {
3974
- id: "cross-endpoint",
3975
- check(ctx) {
3976
- const insights = [];
3977
- const queryMap = /* @__PURE__ */ new Map();
3978
- const allEndpoints = /* @__PURE__ */ new Set();
3979
- for (const [reqId, reqQueries] of ctx.queriesByReq) {
3980
- const req = ctx.reqById.get(reqId);
3981
- if (!req) continue;
3982
- const endpoint = getEndpointKey(req.method, req.path);
3983
- allEndpoints.add(endpoint);
3984
- const seenInReq = /* @__PURE__ */ new Set();
3985
- for (const query of reqQueries) {
3986
- const shape = getQueryShape(query);
3987
- let entry = queryMap.get(shape);
3988
- if (!entry) {
3989
- entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: query };
3990
- queryMap.set(shape, entry);
3991
- }
3992
- entry.count++;
3993
- if (!seenInReq.has(shape)) {
3994
- seenInReq.add(shape);
3995
- entry.endpoints.add(endpoint);
4397
+ }
4398
+ });
4399
+
4400
+ // src/telemetry/index.ts
4401
+ function recordGraphFeature(feature, detail) {
4402
+ trackEvent(TELEMETRY_EVENT_GRAPH_FEATURE, {
4403
+ feature,
4404
+ ...detail ? { detail } : {}
4405
+ });
4406
+ }
4407
+ function flushSession(services) {
4408
+ if (!isTelemetryEnabled()) return;
4409
+ session.flush(services);
4410
+ }
4411
+ var session;
4412
+ var init_telemetry2 = __esm({
4413
+ "src/telemetry/index.ts"() {
4414
+ "use strict";
4415
+ init_config2();
4416
+ init_transport();
4417
+ init_session();
4418
+ init_session();
4419
+ init_telemetry();
4420
+ init_config2();
4421
+ init_transport();
4422
+ session = new Session();
4423
+ }
4424
+ });
4425
+
4426
+ // src/dashboard/api/ingest.ts
4427
+ function isBrakitBatch(msg) {
4428
+ return typeof msg === "object" && msg !== null && "_brakit" in msg && msg._brakit === true && !("version" in msg);
4429
+ }
4430
+ function isSDKPayload(msg) {
4431
+ return typeof msg === "object" && msg !== null && "_brakit" in msg && "version" in msg && typeof msg.version === "number";
4432
+ }
4433
+ function createIngestHandler(services) {
4434
+ const routeEvent = (event) => {
4435
+ switch (event.type) {
4436
+ case TIMELINE_FETCH:
4437
+ bus.emit("telemetry:fetch", event.data);
4438
+ break;
4439
+ case TIMELINE_LOG:
4440
+ bus.emit("telemetry:log", event.data);
4441
+ break;
4442
+ case TIMELINE_ERROR:
4443
+ bus.emit("telemetry:error", event.data);
4444
+ break;
4445
+ case TIMELINE_QUERY:
4446
+ bus.emit("telemetry:query", event.data);
4447
+ break;
4448
+ }
4449
+ };
4450
+ const { bus, requestStore } = services;
4451
+ const stores = {
4452
+ addQuery: (data) => bus.emit("telemetry:query", data),
4453
+ addFetch: (data) => bus.emit("telemetry:fetch", data),
4454
+ addLog: (data) => bus.emit("telemetry:log", data),
4455
+ addError: (data) => bus.emit("telemetry:error", data),
4456
+ addRequest: (data) => requestStore.add(data)
4457
+ };
4458
+ return (req, res) => {
4459
+ if (req.method !== "POST") {
4460
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
4461
+ return;
4462
+ }
4463
+ const chunks = [];
4464
+ let totalSize = 0;
4465
+ req.on("data", (chunk) => {
4466
+ totalSize += chunk.length;
4467
+ if (totalSize > MAX_INGEST_BYTES) {
4468
+ sendJson(req, res, HTTP_PAYLOAD_TOO_LARGE, { error: "Payload too large" });
4469
+ req.destroy();
4470
+ return;
4471
+ }
4472
+ chunks.push(chunk);
4473
+ });
4474
+ req.on("end", () => {
4475
+ if (res.headersSent) return;
4476
+ try {
4477
+ const body = JSON.parse(Buffer.concat(chunks).toString());
4478
+ if (isSDKPayload(body)) {
4479
+ for (const event of body.events) {
4480
+ if (event.type === SDK_EVENT_HELLO) {
4481
+ const d = event.data;
4482
+ session.recordPythonStack({
4483
+ framework: String(d.framework ?? "unknown"),
4484
+ adapters: Array.isArray(d.adapters) ? d.adapters.map(String) : [],
4485
+ pythonVersion: String(d.pythonVersion ?? "unknown")
4486
+ });
4487
+ continue;
3996
4488
  }
4489
+ routeSDKEvent(event, stores);
3997
4490
  }
4491
+ res.writeHead(HTTP_NO_CONTENT);
4492
+ res.end();
4493
+ return;
3998
4494
  }
3999
- if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
4000
- for (const [, queryMetric] of queryMap) {
4001
- if (queryMetric.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
4002
- if (queryMetric.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
4003
- const coveragePct = Math.round(queryMetric.endpoints.size / allEndpoints.size * 100);
4004
- if (coveragePct < CROSS_ENDPOINT_PCT) continue;
4005
- const info = getQueryInfo(queryMetric.first);
4006
- const label = info.op + (info.table ? ` ${info.table}` : "");
4007
- insights.push({
4008
- severity: "warning",
4009
- type: "cross-endpoint",
4010
- title: "Repeated Query Across Endpoints",
4011
- desc: `${label} runs on ${queryMetric.endpoints.size} of ${allEndpoints.size} endpoints (${coveragePct}%).`,
4012
- hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
4013
- detail: `Endpoints: ${[...queryMetric.endpoints].slice(0, 5).join(", ")}${queryMetric.endpoints.size > 5 ? ` +${queryMetric.endpoints.size - 5} more` : ""}. Total: ${queryMetric.count} executions.`
4014
- });
4495
+ if (isBrakitBatch(body)) {
4496
+ for (const event of body.events) {
4497
+ routeEvent(event);
4015
4498
  }
4499
+ res.writeHead(HTTP_NO_CONTENT);
4500
+ res.end();
4501
+ return;
4016
4502
  }
4017
- return insights;
4503
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid batch" });
4504
+ } catch {
4505
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid JSON" });
4018
4506
  }
4019
- };
4507
+ });
4508
+ req.on("error", () => {
4509
+ if (!res.headersSent) {
4510
+ res.writeHead(HTTP_BAD_REQUEST);
4511
+ res.end();
4512
+ }
4513
+ });
4514
+ };
4515
+ }
4516
+ var init_ingest = __esm({
4517
+ "src/dashboard/api/ingest.ts"() {
4518
+ "use strict";
4519
+ init_config();
4520
+ init_labels();
4521
+ init_shared2();
4522
+ init_sdk_event_parser();
4523
+ init_labels();
4524
+ init_telemetry2();
4020
4525
  }
4021
4526
  });
4022
4527
 
4023
- // src/analysis/insights/rules/security.ts
4024
- var securityRule;
4025
- var init_security = __esm({
4026
- "src/analysis/insights/rules/security.ts"() {
4528
+ // src/dashboard/api/metrics.ts
4529
+ function createMetricsHandler(metricsStore) {
4530
+ return (req, res) => {
4531
+ if (!requireGet(req, res)) return;
4532
+ const url = parseRequestUrl(req);
4533
+ const endpoint = url.searchParams.get("endpoint");
4534
+ if (endpoint) {
4535
+ const ep = metricsStore.getEndpoint(endpoint);
4536
+ sendJson(req, res, HTTP_OK, { endpoints: ep ? [ep] : [] });
4537
+ return;
4538
+ }
4539
+ sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
4540
+ };
4541
+ }
4542
+ var init_metrics = __esm({
4543
+ "src/dashboard/api/metrics.ts"() {
4027
4544
  "use strict";
4028
- securityRule = {
4029
- id: "security",
4030
- check(ctx) {
4031
- if (!ctx.securityFindings) return [];
4032
- return ctx.securityFindings.map((finding) => ({
4033
- severity: finding.severity,
4034
- type: "security",
4035
- title: finding.title,
4036
- desc: finding.desc,
4037
- hint: finding.hint,
4038
- detail: finding.detail
4039
- }));
4040
- }
4041
- };
4545
+ init_shared2();
4546
+ init_labels();
4042
4547
  }
4043
4548
  });
4044
4549
 
4045
- // src/analysis/insights/rules/index.ts
4046
- var init_rules2 = __esm({
4047
- "src/analysis/insights/rules/index.ts"() {
4550
+ // src/dashboard/api/metrics-live.ts
4551
+ function createLiveMetricsHandler(metricsStore) {
4552
+ return (req, res) => {
4553
+ if (!requireGet(req, res)) return;
4554
+ sendJson(req, res, 200, { endpoints: metricsStore.getLiveEndpoints() });
4555
+ };
4556
+ }
4557
+ var init_metrics_live = __esm({
4558
+ "src/dashboard/api/metrics-live.ts"() {
4048
4559
  "use strict";
4049
- init_query_rules();
4050
- init_response_rules();
4051
- init_reliability_rules();
4052
- init_pattern_rules();
4053
- init_security();
4560
+ init_shared2();
4054
4561
  }
4055
4562
  });
4056
4563
 
4057
- // src/analysis/insights/index.ts
4058
- function createDefaultInsightRunner() {
4059
- const runner = new InsightRunner();
4060
- runner.register(n1Rule);
4061
- runner.register(crossEndpointRule);
4062
- runner.register(redundantQueryRule);
4063
- runner.register(errorRule);
4064
- runner.register(errorHotspotRule);
4065
- runner.register(duplicateRule);
4066
- runner.register(slowRule);
4067
- runner.register(queryHeavyRule);
4068
- runner.register(selectStarRule);
4069
- runner.register(highRowsRule);
4070
- runner.register(responseOverfetchRule);
4071
- runner.register(largeResponseRule);
4072
- runner.register(regressionRule);
4073
- runner.register(securityRule);
4074
- return runner;
4564
+ // src/dashboard/api/activity.ts
4565
+ function buildTimeline(services, requestId) {
4566
+ const fetches = services.fetchStore.getByRequest(requestId);
4567
+ const logs = services.logStore.getByRequest(requestId);
4568
+ const errors = services.errorStore.getByRequest(requestId);
4569
+ const queries = services.queryStore.getByRequest(requestId);
4570
+ const timeline = [];
4571
+ for (const fetch of fetches)
4572
+ timeline.push({ type: TIMELINE_FETCH, timestamp: fetch.timestamp, data: fetch });
4573
+ for (const log of logs)
4574
+ timeline.push({ type: TIMELINE_LOG, timestamp: log.timestamp, data: log });
4575
+ for (const error of errors)
4576
+ timeline.push({ type: TIMELINE_ERROR, timestamp: error.timestamp, data: error });
4577
+ for (const query of queries)
4578
+ timeline.push({ type: TIMELINE_QUERY, timestamp: query.timestamp, data: query });
4579
+ timeline.sort((a, b) => a.timestamp - b.timestamp);
4580
+ return {
4581
+ total: timeline.length,
4582
+ timeline,
4583
+ counts: {
4584
+ fetches: fetches.length,
4585
+ logs: logs.length,
4586
+ errors: errors.length,
4587
+ queries: queries.length
4588
+ }
4589
+ };
4075
4590
  }
4076
- function computeInsights(ctx) {
4077
- return createDefaultInsightRunner().run(ctx);
4591
+ function createActivityHandler(services) {
4592
+ return (req, res) => {
4593
+ if (!requireGet(req, res)) return;
4594
+ try {
4595
+ const url = parseRequestUrl(req);
4596
+ const requestId = url.searchParams.get("requestId");
4597
+ const requestIds = url.searchParams.get("requestIds");
4598
+ if (!requestId && !requestIds) {
4599
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId or requestIds parameter required" });
4600
+ return;
4601
+ }
4602
+ if (requestId) {
4603
+ const result = buildTimeline(services, requestId);
4604
+ sendJson(req, res, HTTP_OK, { requestId, ...result });
4605
+ return;
4606
+ }
4607
+ const ids = (requestIds || "").split(",").filter(Boolean).slice(0, MAX_BATCH_IDS);
4608
+ const activities = {};
4609
+ for (const id of ids) {
4610
+ activities[id] = buildTimeline(services, id);
4611
+ }
4612
+ sendJson(req, res, HTTP_OK, { requestIds: ids, activities });
4613
+ } catch (err) {
4614
+ brakitDebug(`activity handler error: ${err}`);
4615
+ if (!res.headersSent) {
4616
+ sendJson(req, res, HTTP_INTERNAL_ERROR, { error: "Internal error" });
4617
+ }
4618
+ }
4619
+ };
4078
4620
  }
4079
- var init_insights = __esm({
4080
- "src/analysis/insights/index.ts"() {
4621
+ var MAX_BATCH_IDS;
4622
+ var init_activity = __esm({
4623
+ "src/dashboard/api/activity.ts"() {
4081
4624
  "use strict";
4082
- init_runner();
4083
- init_runner();
4084
- init_rules2();
4625
+ init_shared2();
4626
+ init_labels();
4627
+ init_log();
4628
+ MAX_BATCH_IDS = 50;
4085
4629
  }
4086
4630
  });
4087
4631
 
4088
- // src/analysis/insights.ts
4089
- var init_insights2 = __esm({
4090
- "src/analysis/insights.ts"() {
4632
+ // src/dashboard/api/index.ts
4633
+ var init_api = __esm({
4634
+ "src/dashboard/api/index.ts"() {
4091
4635
  "use strict";
4092
- init_insights();
4636
+ init_handlers();
4637
+ init_ingest();
4638
+ init_metrics();
4639
+ init_metrics_live();
4640
+ init_activity();
4093
4641
  }
4094
4642
  });
4095
4643
 
4096
- // src/analysis/issue-mappers.ts
4097
- function categorizeInsight(type) {
4098
- if (type === "security") return "security";
4099
- if (type === "error" || type === "error-hotspot") return "reliability";
4100
- return "performance";
4644
+ // src/dashboard/api/issues.ts
4645
+ function createIssuesHandler(issueStore) {
4646
+ return (req, res) => {
4647
+ if (!requireGet(req, res)) return;
4648
+ const url = parseRequestUrl(req);
4649
+ const stateParam = url.searchParams.get("state");
4650
+ const categoryParam = url.searchParams.get("category");
4651
+ let issues;
4652
+ if (stateParam && isValidIssueState(stateParam)) {
4653
+ issues = issueStore.getByState(stateParam);
4654
+ } else if (categoryParam && isValidIssueCategory(categoryParam)) {
4655
+ issues = issueStore.getByCategory(categoryParam);
4656
+ } else {
4657
+ issues = issueStore.getAll();
4658
+ }
4659
+ sendJson(req, res, HTTP_OK, { issues });
4660
+ };
4101
4661
  }
4102
- function insightToIssue(insight) {
4103
- return {
4104
- category: categorizeInsight(insight.type),
4105
- rule: insight.type,
4106
- severity: insight.severity,
4107
- title: insight.title,
4108
- desc: insight.desc,
4109
- hint: insight.hint,
4110
- detail: insight.detail,
4111
- endpoint: extractEndpointFromDesc(insight.desc) ?? void 0
4662
+ function createFindingsHandler(issueStore) {
4663
+ return (req, res) => {
4664
+ if (!requireGet(req, res)) return;
4665
+ const url = parseRequestUrl(req);
4666
+ const stateParam = url.searchParams.get("state");
4667
+ let issues;
4668
+ if (stateParam && isValidIssueState(stateParam)) {
4669
+ issues = issueStore.getByState(stateParam);
4670
+ } else {
4671
+ issues = issueStore.getAll();
4672
+ }
4673
+ sendJson(req, res, HTTP_OK, {
4674
+ total: issues.length,
4675
+ findings: issues
4676
+ });
4112
4677
  };
4113
4678
  }
4114
- function securityFindingToIssue(finding) {
4115
- return {
4116
- category: "security",
4117
- rule: finding.rule,
4118
- severity: finding.severity,
4119
- title: finding.title,
4120
- desc: finding.desc,
4121
- hint: finding.hint,
4122
- detail: finding.detail,
4123
- endpoint: finding.endpoint
4679
+ function createIssuesReportHandler(issueStore, eventBus) {
4680
+ return async (req, res) => {
4681
+ if (req.method !== "POST") {
4682
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
4683
+ return;
4684
+ }
4685
+ const body = await readJsonBody(req, res);
4686
+ if (!body) return;
4687
+ const { findingId, status, notes } = body;
4688
+ if (!findingId || typeof findingId !== "string") {
4689
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "findingId is required" });
4690
+ return;
4691
+ }
4692
+ if (!isValidAiFixStatus(status)) {
4693
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "status must be 'fixed' or 'wont_fix'" });
4694
+ return;
4695
+ }
4696
+ if (!notes || typeof notes !== "string") {
4697
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "notes is required" });
4698
+ return;
4699
+ }
4700
+ if (issueStore.reportFix(findingId, status, notes)) {
4701
+ eventBus.emit("issues:changed", issueStore.getAll());
4702
+ sendJson(req, res, HTTP_OK, { ok: true });
4703
+ return;
4704
+ }
4705
+ sendJson(req, res, HTTP_NOT_FOUND, { error: "Finding not found" });
4124
4706
  };
4125
4707
  }
4126
- var init_issue_mappers = __esm({
4127
- "src/analysis/issue-mappers.ts"() {
4708
+ var init_issues = __esm({
4709
+ "src/dashboard/api/issues.ts"() {
4128
4710
  "use strict";
4129
- init_endpoint();
4711
+ init_shared2();
4712
+ init_type_guards();
4713
+ init_labels();
4130
4714
  }
4131
4715
  });
4132
4716
 
4133
- // src/analysis/engine.ts
4134
- var AnalysisEngine;
4135
- var init_engine = __esm({
4136
- "src/analysis/engine.ts"() {
4717
+ // src/dashboard/api/graph.ts
4718
+ function createGraphHandler(services) {
4719
+ return (req, res) => {
4720
+ if (!requireGet(req, res)) return;
4721
+ const url = parseRequestUrl(req);
4722
+ const rawCluster = url.searchParams.get("cluster") ?? void 0;
4723
+ const rawNode = url.searchParams.get("node") ?? void 0;
4724
+ const rawLevel = url.searchParams.get("level") ?? void 0;
4725
+ const rawGrouping = url.searchParams.get("grouping") ?? void 0;
4726
+ const cluster = rawCluster && rawCluster.length <= MAX_PARAM_LENGTH ? rawCluster : void 0;
4727
+ const node = rawNode && rawNode.length <= MAX_PARAM_LENGTH ? rawNode : void 0;
4728
+ const level = rawLevel && VALID_LEVELS.has(rawLevel) ? rawLevel : void 0;
4729
+ const grouping = rawGrouping && VALID_GROUPINGS.has(rawGrouping) ? rawGrouping : void 0;
4730
+ const { graphBuilder, metricsStore } = services;
4731
+ graphBuilder.enrichWithMetrics((endpointKey) => {
4732
+ const metrics = metricsStore.getEndpoint(endpointKey);
4733
+ if (!metrics || metrics.sessions.length === 0) return void 0;
4734
+ const latest = metrics.sessions[metrics.sessions.length - 1];
4735
+ return latest.p95DurationMs;
4736
+ });
4737
+ const data = graphBuilder.getApiResponse({ cluster, node, level, grouping });
4738
+ sendJson(req, res, HTTP_OK, data);
4739
+ };
4740
+ }
4741
+ var VALID_LEVELS, VALID_GROUPINGS, MAX_PARAM_LENGTH;
4742
+ var init_graph = __esm({
4743
+ "src/dashboard/api/graph.ts"() {
4137
4744
  "use strict";
4138
- init_config();
4139
- init_disposable();
4140
- init_group();
4141
- init_rules();
4142
- init_insights2();
4143
- init_issue_mappers();
4144
- init_issue_id();
4145
- init_prepare();
4146
- AnalysisEngine = class {
4147
- constructor(services, debounceMs = ANALYSIS_DEBOUNCE_MS) {
4148
- this.services = services;
4149
- this.debounceMs = debounceMs;
4150
- this.cachedInsights = [];
4151
- this.cachedFindings = [];
4152
- this.debounceTimer = null;
4153
- this.subs = new SubscriptionBag();
4154
- this.scanner = createDefaultScanner();
4155
- }
4156
- start() {
4157
- const bus = this.services.bus;
4158
- this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
4159
- this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
4160
- this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
4161
- this.subs.add(bus.on("telemetry:log", () => this.scheduleRecompute()));
4162
- }
4163
- stop() {
4164
- this.subs.dispose();
4165
- if (this.debounceTimer) {
4166
- clearTimeout(this.debounceTimer);
4167
- this.debounceTimer = null;
4168
- }
4169
- }
4170
- getInsights() {
4171
- return this.cachedInsights;
4172
- }
4173
- getFindings() {
4174
- return this.cachedFindings;
4175
- }
4176
- scheduleRecompute() {
4177
- if (this.debounceTimer) return;
4178
- this.debounceTimer = setTimeout(() => {
4179
- this.debounceTimer = null;
4180
- this.recompute();
4181
- }, this.debounceMs);
4182
- }
4183
- recompute() {
4184
- const allRequests = this.services.requestStore.getAll();
4185
- const queries = this.services.queryStore.getAll();
4186
- const errors = this.services.errorStore.getAll();
4187
- const logs = this.services.logStore.getAll();
4188
- const fetches = this.services.fetchStore.getAll();
4189
- const requests = keepRecentPerEndpoint(allRequests);
4190
- const flows = groupRequestsIntoFlows(requests);
4191
- this.cachedFindings = this.scanner.scan({ requests, logs });
4192
- this.cachedInsights = computeInsights({
4193
- requests,
4194
- queries,
4195
- errors,
4196
- flows,
4197
- fetches,
4198
- previousMetrics: this.services.metricsStore.getAll(),
4199
- securityFindings: this.cachedFindings
4200
- });
4201
- const issueStore = this.services.issueStore;
4202
- const currentIssueIds = /* @__PURE__ */ new Set();
4203
- for (const finding of this.cachedFindings) {
4204
- const issue = securityFindingToIssue(finding);
4205
- issueStore.upsert(issue, "passive");
4206
- currentIssueIds.add(computeIssueId(issue));
4207
- }
4208
- for (const insight of this.cachedInsights) {
4209
- const issue = insightToIssue(insight);
4210
- issueStore.upsert(issue, "passive");
4211
- currentIssueIds.add(computeIssueId(issue));
4212
- }
4213
- const activeEndpoints = extractActiveEndpoints(allRequests);
4214
- issueStore.reconcile(currentIssueIds, activeEndpoints);
4215
- const update = {
4216
- insights: this.cachedInsights,
4217
- findings: this.cachedFindings,
4218
- issues: issueStore.getAll()
4219
- };
4220
- this.services.bus.emit("analysis:updated", update);
4221
- }
4222
- };
4745
+ init_labels();
4746
+ init_shared2();
4747
+ VALID_LEVELS = /* @__PURE__ */ new Set(["endpoints", "clusters"]);
4748
+ VALID_GROUPINGS = /* @__PURE__ */ new Set(["path", "auth-boundary", "data-domain"]);
4749
+ MAX_PARAM_LENGTH = 200;
4223
4750
  }
4224
4751
  });
4225
4752
 
4226
- // src/index.ts
4227
- var VERSION;
4228
- var init_src = __esm({
4229
- "src/index.ts"() {
4753
+ // src/dashboard/sse.ts
4754
+ function createSSEHandler(services) {
4755
+ const clients = /* @__PURE__ */ new Set();
4756
+ function broadcast(eventType, data) {
4757
+ if (clients.size === 0) return;
4758
+ const frame = eventType ? `event: ${eventType}
4759
+ data: ${data}
4760
+
4761
+ ` : `data: ${data}
4762
+
4763
+ `;
4764
+ for (const client of clients) {
4765
+ if (client.res.destroyed) {
4766
+ clients.delete(client);
4767
+ continue;
4768
+ }
4769
+ try {
4770
+ client.res.write(frame);
4771
+ } catch {
4772
+ clients.delete(client);
4773
+ }
4774
+ }
4775
+ }
4776
+ const bus = services.bus;
4777
+ bus.on("request:completed", (r) => broadcast(null, JSON.stringify(r)));
4778
+ bus.on("telemetry:fetch", (e) => broadcast(SSE_EVENT_FETCH, JSON.stringify(e)));
4779
+ bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
4780
+ bus.on("telemetry:error", (e) => broadcast(SSE_EVENT_ERROR, JSON.stringify(e)));
4781
+ bus.on("telemetry:query", (e) => broadcast(SSE_EVENT_QUERY, JSON.stringify(e)));
4782
+ bus.on("analysis:updated", ({ issues }) => {
4783
+ broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
4784
+ });
4785
+ bus.on("issues:changed", (issues) => {
4786
+ broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
4787
+ });
4788
+ return (req, res) => {
4789
+ const headers2 = {
4790
+ "content-type": "text/event-stream",
4791
+ "cache-control": "no-cache",
4792
+ connection: "keep-alive"
4793
+ };
4794
+ const corsOrigin = getCorsOrigin(req);
4795
+ if (corsOrigin) {
4796
+ headers2["access-control-allow-origin"] = corsOrigin;
4797
+ }
4798
+ res.writeHead(HTTP_OK, headers2);
4799
+ res.write(":ok\n\n");
4800
+ const heartbeat = setInterval(() => {
4801
+ if (res.destroyed) {
4802
+ clearInterval(heartbeat);
4803
+ clients.delete(client);
4804
+ return;
4805
+ }
4806
+ try {
4807
+ res.write(":heartbeat\n\n");
4808
+ } catch {
4809
+ clearInterval(heartbeat);
4810
+ clients.delete(client);
4811
+ }
4812
+ }, SSE_HEARTBEAT_INTERVAL_MS);
4813
+ heartbeat.unref();
4814
+ const client = { res, heartbeat };
4815
+ clients.add(client);
4816
+ req.on("close", () => {
4817
+ clearInterval(heartbeat);
4818
+ clients.delete(client);
4819
+ });
4820
+ };
4821
+ }
4822
+ var init_sse = __esm({
4823
+ "src/dashboard/sse.ts"() {
4230
4824
  "use strict";
4231
- init_issue_store();
4232
- init_project();
4233
- init_adapter_registry();
4234
- init_rules();
4235
- init_engine();
4236
- init_insights2();
4237
- init_insights();
4238
- VERSION = "0.10.0";
4825
+ init_constants();
4826
+ init_labels();
4827
+ init_shared2();
4239
4828
  }
4240
4829
  });
4241
4830
 
@@ -4255,7 +4844,10 @@ function getBaseStyles() {
4255
4844
  --red:#dc2626;
4256
4845
  --cyan:#0891b2;
4257
4846
  --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);
4258
- --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);
4847
+ --amber-bg:rgba(217,119,6,0.08);--amber-border:rgba(217,119,6,0.15);
4848
+ --red-bg:rgba(220,38,38,0.08);--red-border:rgba(220,38,38,0.2);
4849
+ --blue-bg:rgba(37,99,235,0.08);--cyan-bg:rgba(8,145,178,0.07);
4850
+ --accent-bg:rgba(99,102,241,0.08);
4259
4851
  --sidebar-width:232px;--header-height:52px;
4260
4852
  --radius:8px;--radius-sm:6px;
4261
4853
  --shadow-sm:0 1px 3px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.03);
@@ -4312,6 +4904,7 @@ function getLayoutStyles() {
4312
4904
  .sidebar-logo .logo-version{font-weight:400;font-size:11px;color:var(--text-muted);margin-left:8px;letter-spacing:0}
4313
4905
  .sidebar-nav{padding:12px;flex:1}
4314
4906
  .sidebar-section{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);padding:16px 12px 8px}
4907
+ .sidebar-divider{height:1px;background:var(--border-subtle);margin:8px 12px}
4315
4908
  .sidebar-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:var(--radius);color:var(--text-dim);font-size:14px;font-weight:500;cursor:pointer;transition:all .15s;border:none;background:transparent;width:100%;text-align:left;font-family:var(--sans)}
4316
4909
  .sidebar-item:hover{background:var(--bg-hover);color:var(--text)}
4317
4910
  .sidebar-item.active{background:var(--bg-active);color:var(--accent)}
@@ -4345,7 +4938,7 @@ function getLayoutStyles() {
4345
4938
  /* Content */
4346
4939
  .main-content{flex:1;overflow-y:auto}
4347
4940
  bk-dashboard{display:contents}
4348
- bk-overview-view,bk-flows-view,bk-requests-view,bk-fetches-view,bk-queries-view,bk-errors-view,bk-logs-view,bk-security-view,bk-performance-view,bk-timeline-panel,bk-empty-state{display:block}
4941
+ bk-overview-view,bk-flows-view,bk-requests-view,bk-fetches-view,bk-queries-view,bk-errors-view,bk-logs-view,bk-security-view,bk-performance-view,bk-explorer-view,bk-insights-view,bk-timeline-panel,bk-empty-state{display:block}
4349
4942
  bk-graph-view{display:block}
4350
4943
  bk-method-badge,bk-status-pill,bk-duration-label,bk-copy-button{display:inline-flex;flex-shrink:0}
4351
4944
  bk-stat-card{display:inline-flex}
@@ -4732,53 +5325,25 @@ var init_graph2 = __esm({
4732
5325
  // src/dashboard/styles/overview.ts
4733
5326
  function getOverviewStyles() {
4734
5327
  return `
4735
- /* Overview */
4736
- .ov-container{padding:24px 28px}
4737
-
4738
- /* Summary banner */
4739
- .ov-summary{display:flex;gap:10px;margin-bottom:24px;flex-wrap:wrap}
4740
- .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)}
4741
- .ov-stat:hover{box-shadow:var(--shadow-md)}
4742
- .ov-stat-value{font-size:22px;font-weight:700;font-family:var(--mono);color:var(--text);line-height:1.2}
4743
- .ov-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
4744
-
4745
- /* Section header */
4746
- .ov-section-title{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:12px;display:flex;align-items:center;gap:8px}
4747
- .ov-issue-count{font-size:11px;font-family:var(--mono);color:var(--text-dim);background:var(--bg-muted);border:1px solid var(--border);padding:1px 8px;border-radius:10px}
4748
-
4749
- /* Insight cards */
4750
- .ov-cards{display:flex;flex-direction:column;gap:8px}
4751
- .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)}
4752
- .ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md);transform:translateY(-1px)}
4753
- .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}
4754
- .ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
4755
- .ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
4756
- .ov-card-icon.info{background:rgba(37,99,235,.08);color:var(--blue)}
4757
- .ov-card-icon.resolved{background:var(--green-bg);color:var(--green)}
4758
- .ov-card-body{flex:1;min-width:0}
4759
- .ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
4760
- .ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
4761
- .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}
4762
- .ov-card-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
4763
- .ov-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;margin-top:2px;font-family:var(--mono);transition:transform .15s}
4764
-
4765
- /* Expanded card */
4766
- .ov-card.expanded{border-color:var(--border-light);box-shadow:var(--shadow-md)}
4767
- .ov-card-expand{display:none;margin-top:10px;padding-top:10px;border-top:1px solid var(--border)}
4768
- .ov-card-hint{font-size:12px;color:var(--text-dim);line-height:1.5;margin-bottom:10px}
4769
- .ov-card-link{font-size:12px;font-weight:600;color:var(--blue);cursor:pointer;display:inline-block;padding:4px 0}
4770
- .ov-card-link:hover{text-decoration:underline}
4771
- .ov-detail-label{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
4772
- .ov-detail-item{font-size:12px;color:var(--text);font-family:var(--mono);padding:2px 0}
4773
-
4774
- /* All-clear banner */
4775
- .ov-clear{display:flex;align-items:center;gap:12px;padding:16px 20px;background:var(--green-bg-subtle);border:1px solid var(--green-border);border-radius:var(--radius);color:var(--green);font-size:13px;font-weight:500}
4776
- .ov-clear-icon{font-size:16px}
5328
+ .ov-container{padding:28px}
4777
5329
 
4778
- /* Resolved section */
4779
- .ov-resolved-title{margin-top:24px}
4780
- .ov-card-resolved{opacity:.7;border-color:var(--green-border);cursor:default}
4781
- .ov-card-resolved:hover{opacity:1;box-shadow:var(--shadow-sm)}
5330
+ .ov-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
5331
+
5332
+ .ov-card-nav{display:flex;align-items:center;gap:16px;padding:20px 24px;background:var(--bg-card);border:1px solid var(--border);border-radius:12px;cursor:pointer;transition:all .18s ease;box-shadow:0 1px 3px rgba(0,0,0,.04)}
5333
+ .ov-card-nav:hover{border-color:var(--border-light);box-shadow:0 4px 16px rgba(0,0,0,.06);transform:translateY(-2px)}
5334
+ .ov-card-nav:active{transform:translateY(0)}
5335
+
5336
+ .ov-card-empty{opacity:.55}
5337
+ .ov-card-empty:hover{opacity:.8}
5338
+
5339
+ .ov-card-icon-lg{font-size:22px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;flex-shrink:0;background:var(--bg-muted);border-radius:10px;color:var(--text-muted)}
5340
+
5341
+ .ov-card-content{flex:1;min-width:0}
5342
+ .ov-card-headline{font-size:16px;font-weight:700;color:var(--text);font-family:var(--mono);line-height:1.3}
5343
+ .ov-card-context{font-size:12px;color:var(--text-muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5344
+
5345
+ .ov-card-arrow{font-size:16px;color:var(--text-muted);opacity:0;transition:opacity .15s,transform .15s;flex-shrink:0}
5346
+ .ov-card-nav:hover .ov-card-arrow{opacity:1;transform:translateX(2px)}
4782
5347
  `;
4783
5348
  }
4784
5349
  var init_overview = __esm({
@@ -5158,325 +5723,181 @@ function getGraphViewStyles() {
5158
5723
  .graph-detail-empty{font-size:12px;color:var(--text-muted);padding:16px;text-align:center}
5159
5724
 
5160
5725
  /* Pulse animation for critical security badges */
5161
- @keyframes graph-pulse{0%,100%{opacity:1}50%{opacity:0.5}}
5162
- .graph-pulse{animation:graph-pulse 2s ease-in-out infinite}
5163
-
5164
- /* Flow edge animation */
5165
- @keyframes graph-flow-dash{to{stroke-dashoffset:-24}}
5166
- .graph-flow-edge{animation:graph-flow-dash 1s linear infinite}
5167
- `;
5168
- }
5169
- var init_graph_view = __esm({
5170
- "src/dashboard/styles/graph-view.ts"() {
5171
- "use strict";
5172
- }
5173
- });
5174
-
5175
- // src/dashboard/styles.ts
5176
- function getStyles() {
5177
- return getBaseStyles() + getLayoutStyles() + getFlowStyles() + getRequestStyles() + getPerformanceStyles() + getOverviewStyles() + getSecurityStyles() + getTimelineStyles() + getGraphViewStyles();
5178
- }
5179
- var init_styles = __esm({
5180
- "src/dashboard/styles.ts"() {
5181
- "use strict";
5182
- init_base();
5183
- init_layout();
5184
- init_flows();
5185
- init_requests();
5186
- init_graph2();
5187
- init_overview();
5188
- init_security2();
5189
- init_timeline();
5190
- init_graph_view();
5191
- }
5192
- });
5193
-
5194
- // src/dashboard/layout.ts
5195
- function getLayoutHtml() {
5196
- return `<bk-dashboard></bk-dashboard>`;
5197
- }
5198
- var init_layout2 = __esm({
5199
- "src/dashboard/layout.ts"() {
5200
- "use strict";
5201
- }
5202
- });
5203
-
5204
- // src/dashboard/page.ts
5205
- import { readFileSync as readFileSync3 } from "fs";
5206
- import { resolve as resolve3, dirname } from "path";
5207
- import { fileURLToPath } from "url";
5208
- function getClientBundle() {
5209
- if (clientBundle) return clientBundle;
5210
- const bundlePath = resolve3(__dirname, "../dashboard-client.global.js");
5211
- clientBundle = readFileSync3(bundlePath, "utf-8");
5212
- return clientBundle;
5213
- }
5214
- function getDashboardHtml(config) {
5215
- return `<!DOCTYPE html>
5216
- <html lang="en">
5217
- <head>
5218
- <meta charset="UTF-8">
5219
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
5220
- <title>brakit</title>
5221
- <style>${getStyles()}</style>
5222
- </head>
5223
- <body>
5224
- ${getLayoutHtml()}
5225
- <script>window.__BRAKIT_CONFIG__={port:${config.proxyPort},version:"${VERSION}"};</script>
5226
- <script>${getClientBundle()}</script>
5227
- </body>
5228
- </html>`;
5229
- }
5230
- var __dirname, clientBundle;
5231
- var init_page = __esm({
5232
- "src/dashboard/page.ts"() {
5233
- "use strict";
5234
- init_src();
5235
- init_styles();
5236
- init_layout2();
5237
- __dirname = dirname(fileURLToPath(import.meta.url));
5238
- clientBundle = null;
5239
- }
5240
- });
5241
-
5242
- // src/telemetry/config.ts
5243
- import { homedir as homedir2, platform } from "os";
5244
- import { join as join3 } from "path";
5245
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
5246
- import { randomUUID as randomUUID5 } from "crypto";
5247
- function readConfig() {
5248
- try {
5249
- if (!existsSync5(CONFIG_PATH)) return null;
5250
- return JSON.parse(readFileSync4(CONFIG_PATH, "utf-8"));
5251
- } catch {
5252
- return null;
5253
- }
5254
- }
5255
- function writeConfig(config) {
5256
- try {
5257
- if (!existsSync5(CONFIG_DIR))
5258
- mkdirSync3(CONFIG_DIR, { recursive: true, ...IS_WINDOWS ? {} : { mode: DIR_MODE_OWNER_ONLY } });
5259
- writeFileSync3(
5260
- CONFIG_PATH,
5261
- JSON.stringify(config, null, 2) + "\n",
5262
- IS_WINDOWS ? {} : { mode: FILE_MODE_OWNER_ONLY }
5263
- );
5264
- } catch (err) {
5265
- if (process.env.BRAKIT_DEBUG) {
5266
- process.stderr.write(`[brakit] config write failed: ${err?.message ?? err}
5267
- `);
5268
- }
5269
- }
5270
- }
5271
- function getOrCreateConfig() {
5272
- const existing = readConfig();
5273
- if (existing && typeof existing.telemetry === "boolean" && existing.anonymousId) {
5274
- return existing;
5275
- }
5276
- const config = { telemetry: true, anonymousId: randomUUID5() };
5277
- writeConfig(config);
5278
- return config;
5279
- }
5280
- function isTelemetryEnabled() {
5281
- if (cachedEnabled !== null) return cachedEnabled;
5282
- const env = process.env.BRAKIT_TELEMETRY;
5283
- if (env !== void 0) {
5284
- cachedEnabled = env !== "false" && env !== "0" && env !== "off";
5285
- return cachedEnabled;
5286
- }
5287
- cachedEnabled = readConfig()?.telemetry ?? true;
5288
- return cachedEnabled;
5289
- }
5290
- var IS_WINDOWS, CONFIG_DIR, CONFIG_PATH, cachedEnabled;
5291
- var init_config2 = __esm({
5292
- "src/telemetry/config.ts"() {
5293
- "use strict";
5294
- init_features();
5295
- IS_WINDOWS = platform() === "win32";
5296
- CONFIG_DIR = join3(homedir2(), ".brakit");
5297
- CONFIG_PATH = join3(CONFIG_DIR, "config.json");
5298
- cachedEnabled = null;
5299
- }
5300
- });
5301
-
5302
- // src/telemetry/index.ts
5303
- import { platform as platform2, release, arch } from "os";
5304
- import { spawn } from "child_process";
5305
- function commonProperties() {
5306
- return {
5307
- brakit_version: VERSION,
5308
- node_version: process.version,
5309
- os: `${platform2()}-${release()}`,
5310
- arch: arch(),
5311
- $lib: "brakit",
5312
- $process_person_profile: false,
5313
- $geoip_disable: true
5314
- };
5315
- }
5316
- function sendToPosthog(event, properties) {
5317
- if (!isTelemetryEnabled()) return;
5318
- const config = getOrCreateConfig();
5319
- const payload = {
5320
- api_key: POSTHOG_KEY,
5321
- event,
5322
- distinct_id: config.anonymousId,
5323
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5324
- properties: { ...commonProperties(), ...properties }
5325
- };
5326
- try {
5327
- const body = JSON.stringify(payload);
5328
- const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
5329
- const child = spawn(
5330
- process.execPath,
5331
- [
5332
- "-e",
5333
- `fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
5334
- ],
5335
- { detached: true, stdio: "ignore" }
5336
- );
5337
- child.unref();
5338
- } catch {
5339
- }
5340
- }
5341
- function trackEvent(event, properties) {
5342
- sendToPosthog(event, { sdk: "node", ...properties });
5343
- }
5344
- function initSession(framework, packageManager, isCustomCommand, adapters) {
5345
- getOrCreateConfig();
5346
- session.startTime = Date.now();
5347
- session.framework = framework;
5348
- session.packageManager = packageManager;
5349
- session.isCustomCommand = isCustomCommand;
5350
- session.adapters = adapters;
5351
- }
5352
- function recordRequestCount(count) {
5353
- session.requestCount = count;
5354
- }
5355
- function recordInsightTypes(types) {
5356
- for (const t of types) session.insightTypes.add(t);
5357
- }
5358
- function recordRulesTriggered(rules) {
5359
- for (const r of rules) session.rulesTriggered.add(r);
5360
- }
5361
- function recordTabViewed(tab) {
5362
- session.tabsViewed.add(tab);
5363
- }
5364
- function recordDashboardOpened() {
5365
- if (session.dashboardOpened) return;
5366
- session.dashboardOpened = true;
5367
- session.dashboardOpenedAt = Date.now();
5368
- trackEvent(TELEMETRY_EVENT_DASHBOARD_VIEWED, {
5369
- time_to_dashboard_ms: session.startTime > 0 ? Date.now() - session.startTime : null,
5370
- request_count_at_open: session.requestCount
5371
- });
5726
+ @keyframes graph-pulse{0%,100%{opacity:1}50%{opacity:0.5}}
5727
+ .graph-pulse{animation:graph-pulse 2s ease-in-out infinite}
5728
+
5729
+ /* Flow edge animation */
5730
+ @keyframes graph-flow-dash{to{stroke-dashoffset:-24}}
5731
+ .graph-flow-edge{animation:graph-flow-dash 1s linear infinite}
5732
+ `;
5372
5733
  }
5373
- function recordGraphFeature(feature, detail) {
5374
- trackEvent(TELEMETRY_EVENT_GRAPH_FEATURE, {
5375
- feature,
5376
- ...detail ? { detail } : {}
5377
- });
5734
+ var init_graph_view = __esm({
5735
+ "src/dashboard/styles/graph-view.ts"() {
5736
+ "use strict";
5737
+ }
5738
+ });
5739
+
5740
+ // src/dashboard/styles/explorer.ts
5741
+ function getExplorerStyles() {
5742
+ return `
5743
+ /* Explorer sub-tabs */
5744
+ .explorer-tabs{display:flex;gap:0;border-bottom:1px solid var(--border);padding:0 28px;background:var(--bg);position:sticky;top:0;z-index:2}
5745
+ .explorer-tab{padding:10px 16px;font-size:13px;font-weight:500;color:var(--text-muted);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:6px;font-family:var(--sans);white-space:nowrap}
5746
+ .explorer-tab:hover{color:var(--text)}
5747
+ .explorer-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
5748
+ .explorer-tab-count{font-size:11px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:1px 6px;border-radius:8px}
5749
+ .explorer-tab.active .explorer-tab-count{color:var(--accent);background:var(--accent-bg)}
5750
+ `;
5378
5751
  }
5379
- function recordSetupCompleted(info) {
5380
- session.frameworkCandidates = info.frameworkCandidates;
5381
- session.adaptersFailed = info.adaptersFailed;
5382
- session.setupDurationMs = info.setupDurationMs;
5383
- session.setupSucceeded = true;
5752
+ var init_explorer = __esm({
5753
+ "src/dashboard/styles/explorer.ts"() {
5754
+ "use strict";
5755
+ }
5756
+ });
5757
+
5758
+ // src/dashboard/styles/insights.ts
5759
+ function getInsightsStyles() {
5760
+ return `
5761
+ /* Insights filter chips */
5762
+ .insights-filters{display:flex;gap:6px;padding:16px 28px;border-bottom:1px solid var(--border);background:var(--bg);position:sticky;top:0;z-index:2}
5763
+ .insights-chip{font-size:12px;font-weight:500;padding:5px 14px;border:1px solid var(--border);border-radius:20px;background:var(--bg);color:var(--text-muted);cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:5px;font-family:var(--sans)}
5764
+ .insights-chip:hover{border-color:var(--text-muted);color:var(--text)}
5765
+ .insights-chip.active{background:var(--accent);color:white;border-color:var(--accent)}
5766
+ .insights-chip-count{font-size:10px;font-family:var(--mono);background:rgba(0,0,0,.08);padding:1px 5px;border-radius:8px}
5767
+ .insights-chip.active .insights-chip-count{background:rgba(255,255,255,.25)}
5768
+
5769
+ /* Summary bar */
5770
+ .insights-summary{display:flex;gap:14px;padding:10px 28px;font-size:12px;font-weight:500;border-bottom:1px solid var(--border)}
5771
+ .insights-summary-stat{display:flex;align-items:center;gap:4px}
5772
+ .insights-summary-stat.critical{color:var(--red)}
5773
+ .insights-summary-stat.warning{color:var(--amber)}
5774
+ .insights-summary-stat.resolved{color:var(--green)}
5775
+
5776
+ /* AI hint */
5777
+ .insights-ai-hint{font-size:11px;color:var(--text-muted);padding:0 0 12px}
5778
+ .insights-ai-hint code{background:var(--bg-muted);padding:2px 6px;border-radius:4px;font-family:var(--mono);font-size:11px}
5779
+
5780
+ /* Insights card list */
5781
+ .insights-list{padding:16px 28px}
5782
+
5783
+ .insights-empty{display:flex;align-items:center;gap:10px;padding:24px;color:var(--green);font-size:14px;font-weight:500}
5784
+ .insights-empty-icon{font-size:18px}
5785
+
5786
+ .insights-card{display:flex;align-items:flex-start;gap:12px;padding:12px 16px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .15s;margin-bottom:6px}
5787
+ .insights-card:hover{border-color:var(--border-light);box-shadow:0 2px 8px rgba(0,0,0,.04)}
5788
+ .insights-card.expanded{border-color:var(--border-light);box-shadow:0 2px 8px rgba(0,0,0,.04)}
5789
+ .insights-card.resolved{opacity:.55}
5790
+ .insights-card.resolved:hover{opacity:.8}
5791
+
5792
+ .insights-card-left{flex-shrink:0;padding-top:2px}
5793
+ .insights-sev{width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:10px;border-radius:50%}
5794
+ .insights-sev.critical{background:var(--red-bg);color:var(--red)}
5795
+ .insights-sev.warning{background:var(--amber-bg);color:var(--amber)}
5796
+ .insights-sev.info{background:var(--blue-bg);color:var(--blue)}
5797
+
5798
+ .insights-card-body{flex:1;min-width:0}
5799
+ .insights-card-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:3px}
5800
+ .insights-card-title{font-size:13px;font-weight:600;color:var(--text)}
5801
+ .insights-card-title.resolved{text-decoration:line-through;color:var(--text-muted)}
5802
+ .insights-card-cat{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);background:var(--bg-muted);padding:1px 6px;border-radius:4px}
5803
+ .insights-card-count{font-size:11px;font-family:var(--mono);color:var(--text-muted)}
5804
+ .insights-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
5805
+ .insights-card-detail{font-size:11px;font-family:var(--mono);color:var(--text-muted);margin-top:6px;padding:6px 10px;background:var(--bg-muted);border:1px solid var(--border-subtle);border-radius:6px;line-height:1.5}
5806
+ .insights-card-progress{font-size:11px;color:var(--text-muted);margin-top:4px;font-family:var(--mono)}
5807
+ .insights-card-hint{font-size:12px;color:var(--text-dim);line-height:1.6;margin-top:8px;padding-top:8px;border-top:1px solid var(--border)}
5808
+
5809
+ .insights-badge-regressed{font-size:9px;font-weight:700;color:var(--red);background:var(--red-bg);padding:1px 6px;border-radius:4px}
5810
+ .insights-badge-verifying{font-size:9px;font-weight:700;color:var(--amber);background:var(--amber-bg);padding:1px 6px;border-radius:4px}
5811
+ .insights-badge-resolved{font-size:9px;font-weight:700;color:var(--green);background:var(--green-bg);padding:1px 6px;border-radius:4px}
5812
+
5813
+ .insights-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;padding-top:2px;font-family:var(--mono);transition:transform .15s}
5814
+
5815
+ .insights-section{display:flex;align-items:center;gap:8px;padding:14px 0 8px;margin-top:4px;font-size:12px;font-weight:700;color:var(--text);text-transform:uppercase;letter-spacing:.5px;border-top:1px solid var(--border);user-select:none}
5816
+ .insights-section:first-child{border-top:none;margin-top:0}
5817
+ .insights-section-icon{font-size:11px;width:16px;text-align:center}
5818
+ .insights-section-count{font-size:11px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:1px 7px;border-radius:8px;font-weight:500}
5819
+ .insights-section-regressed{color:var(--red)}
5820
+ .insights-section-regressed .insights-section-count{color:var(--red);background:var(--red-bg)}
5821
+ .insights-section-verifying{color:var(--amber)}
5822
+ .insights-section-verifying .insights-section-count{color:var(--amber);background:var(--amber-bg)}
5823
+ .insights-section-resolved{color:var(--green)}
5824
+ .insights-section-resolved .insights-section-count{color:var(--green);background:var(--green-bg)}
5825
+ .insights-section-dismissed{color:var(--text-muted);cursor:pointer}
5826
+ .insights-section-dismissed:hover{color:var(--text)}
5827
+ `;
5384
5828
  }
5385
- function recordFirstRequest() {
5386
- if (!session.firstRequestAt) session.firstRequestAt = Date.now();
5829
+ var init_insights3 = __esm({
5830
+ "src/dashboard/styles/insights.ts"() {
5831
+ "use strict";
5832
+ }
5833
+ });
5834
+
5835
+ // src/dashboard/styles.ts
5836
+ function getStyles() {
5837
+ return getBaseStyles() + getLayoutStyles() + getFlowStyles() + getRequestStyles() + getPerformanceStyles() + getOverviewStyles() + getSecurityStyles() + getTimelineStyles() + getGraphViewStyles() + getExplorerStyles() + getInsightsStyles();
5387
5838
  }
5388
- function recordExitReason(reason) {
5389
- if (session.exitReason === "unknown") session.exitReason = reason;
5839
+ var init_styles = __esm({
5840
+ "src/dashboard/styles.ts"() {
5841
+ "use strict";
5842
+ init_base();
5843
+ init_layout();
5844
+ init_flows();
5845
+ init_requests();
5846
+ init_graph2();
5847
+ init_overview();
5848
+ init_security2();
5849
+ init_timeline();
5850
+ init_graph_view();
5851
+ init_explorer();
5852
+ init_insights3();
5853
+ }
5854
+ });
5855
+
5856
+ // src/dashboard/layout.ts
5857
+ function getLayoutHtml() {
5858
+ return `<bk-dashboard></bk-dashboard>`;
5390
5859
  }
5391
- function speedBucket(ms) {
5392
- if (ms === 0) return "none";
5393
- const t = SPEED_BUCKET_THRESHOLDS;
5394
- if (ms < t[0]) return `<${t[0]}ms`;
5395
- for (let i = 1; i < t.length; i++) {
5396
- if (ms < t[i]) return `${t[i - 1]}-${t[i]}ms`;
5860
+ var init_layout2 = __esm({
5861
+ "src/dashboard/layout.ts"() {
5862
+ "use strict";
5397
5863
  }
5398
- return `>${t[t.length - 1]}ms`;
5864
+ });
5865
+
5866
+ // src/dashboard/page.ts
5867
+ import { readFileSync as readFileSync4 } from "fs";
5868
+ import { resolve as resolve3, dirname } from "path";
5869
+ import { fileURLToPath } from "url";
5870
+ function getClientBundle() {
5871
+ if (clientBundle) return clientBundle;
5872
+ const bundlePath = resolve3(__dirname, "../dashboard-client.global.js");
5873
+ clientBundle = readFileSync4(bundlePath, "utf-8");
5874
+ return clientBundle;
5399
5875
  }
5400
- function trackSession(services) {
5401
- if (!isTelemetryEnabled()) return;
5402
- const isFirstSession = readConfig() === null;
5403
- const metricsStore = services.metricsStore;
5404
- const analysisEngine = services.analysisEngine;
5405
- const live = metricsStore.getLiveEndpoints();
5406
- const insights = analysisEngine.getInsights();
5407
- const findings = analysisEngine.getFindings();
5408
- let totalRequests = 0;
5409
- let totalDuration = 0;
5410
- let slowestP95 = 0;
5411
- for (const ep of live) {
5412
- totalRequests += ep.summary.totalRequests;
5413
- totalDuration += ep.summary.p95Ms * ep.summary.totalRequests;
5414
- if (ep.summary.p95Ms > slowestP95) slowestP95 = ep.summary.p95Ms;
5415
- }
5416
- const now = Date.now();
5417
- sendToPosthog(TELEMETRY_EVENT_SESSION, {
5418
- sdk: "node",
5419
- framework: session.framework,
5420
- package_manager: session.packageManager,
5421
- is_custom_command: session.isCustomCommand,
5422
- first_session: isFirstSession,
5423
- adapters_detected: session.adapters,
5424
- request_count: session.requestCount,
5425
- error_count: services.errorStore.getAll().length,
5426
- query_count: services.queryStore.getAll().length,
5427
- fetch_count: services.fetchStore.getAll().length,
5428
- insight_count: insights.length,
5429
- finding_count: findings.length,
5430
- insight_types: [...session.insightTypes],
5431
- rules_triggered: [...session.rulesTriggered],
5432
- endpoint_count: live.length,
5433
- avg_duration_ms: totalRequests > 0 ? Math.round(totalDuration / totalRequests) : 0,
5434
- slowest_endpoint_bucket: speedBucket(slowestP95),
5435
- tabs_viewed: [...session.tabsViewed],
5436
- dashboard_opened: session.dashboardOpened,
5437
- explain_used: session.explainUsed,
5438
- session_duration_s: Math.round((now - session.startTime) / 1e3),
5439
- // Enhanced fields
5440
- setup_succeeded: session.setupSucceeded,
5441
- setup_duration_ms: session.setupDurationMs,
5442
- framework_detection_candidates: session.frameworkCandidates,
5443
- adapters_failed: session.adaptersFailed,
5444
- time_to_first_request_ms: session.firstRequestAt ? session.firstRequestAt - session.startTime : null,
5445
- time_to_dashboard_ms: session.dashboardOpenedAt ? session.dashboardOpenedAt - session.startTime : null,
5446
- exit_reason: session.exitReason
5447
- });
5448
- getOrCreateConfig();
5876
+ function getDashboardHtml(config) {
5877
+ return `<!DOCTYPE html>
5878
+ <html lang="en">
5879
+ <head>
5880
+ <meta charset="UTF-8">
5881
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
5882
+ <title>brakit</title>
5883
+ <style>${getStyles()}</style>
5884
+ </head>
5885
+ <body>
5886
+ ${getLayoutHtml()}
5887
+ <script>window.__BRAKIT_CONFIG__={port:${config.proxyPort},version:"${VERSION}"};</script>
5888
+ <script>${getClientBundle()}</script>
5889
+ </body>
5890
+ </html>`;
5449
5891
  }
5450
- var POSTHOG_KEY, session;
5451
- var init_telemetry = __esm({
5452
- "src/telemetry/index.ts"() {
5892
+ var __dirname, clientBundle;
5893
+ var init_page = __esm({
5894
+ "src/dashboard/page.ts"() {
5453
5895
  "use strict";
5454
5896
  init_src();
5455
- init_config2();
5456
- init_labels();
5457
- init_config();
5458
- init_config2();
5459
- POSTHOG_KEY = "phc_E9TwydCGnSfPLIUhNxChpeg32TSowjk31KiPhnLPP0x";
5460
- session = {
5461
- startTime: 0,
5462
- framework: "",
5463
- packageManager: "",
5464
- isCustomCommand: false,
5465
- adapters: [],
5466
- requestCount: 0,
5467
- insightTypes: /* @__PURE__ */ new Set(),
5468
- rulesTriggered: /* @__PURE__ */ new Set(),
5469
- tabsViewed: /* @__PURE__ */ new Set(),
5470
- dashboardOpened: false,
5471
- explainUsed: false,
5472
- frameworkCandidates: [],
5473
- adaptersFailed: [],
5474
- setupDurationMs: 0,
5475
- setupSucceeded: false,
5476
- firstRequestAt: 0,
5477
- dashboardOpenedAt: 0,
5478
- exitReason: "unknown"
5479
- };
5897
+ init_styles();
5898
+ init_layout2();
5899
+ __dirname = dirname(fileURLToPath(import.meta.url));
5900
+ clientBundle = null;
5480
5901
  }
5481
5902
  });
5482
5903
 
@@ -5514,7 +5935,7 @@ function createDashboardHandler(services) {
5514
5935
  const url = new URL(req.url ?? "/", "http://localhost");
5515
5936
  const tab = url.searchParams.get("tab");
5516
5937
  if (tab && tab.length <= MAX_TAB_NAME_LENGTH && VALID_TABS.has(tab)) {
5517
- recordTabViewed(tab);
5938
+ session.recordTabViewed(tab);
5518
5939
  }
5519
5940
  const event = url.searchParams.get("event");
5520
5941
  if (event && event.length <= MAX_TAB_NAME_LENGTH) {
@@ -5532,7 +5953,7 @@ function createDashboardHandler(services) {
5532
5953
  handler(req, res);
5533
5954
  return;
5534
5955
  }
5535
- if (isTelemetryEnabled()) recordDashboardOpened();
5956
+ if (isTelemetryEnabled()) session.recordDashboardOpened();
5536
5957
  res.writeHead(HTTP_OK, {
5537
5958
  "content-type": "text/html; charset=utf-8",
5538
5959
  "cache-control": "no-cache",
@@ -5552,7 +5973,7 @@ var init_router = __esm({
5552
5973
  init_graph();
5553
5974
  init_sse();
5554
5975
  init_page();
5555
- init_telemetry();
5976
+ init_telemetry2();
5556
5977
  }
5557
5978
  });
5558
5979
 
@@ -5691,7 +6112,7 @@ var init_telemetry_store = __esm({
5691
6112
  constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
5692
6113
  this.maxEntries = maxEntries;
5693
6114
  this.entries = [];
5694
- this.listeners = [];
6115
+ this.listeners = /* @__PURE__ */ new Set();
5695
6116
  }
5696
6117
  add(data) {
5697
6118
  const entry = { id: randomUUID6(), ...data };
@@ -5710,11 +6131,10 @@ var init_telemetry_store = __esm({
5710
6131
  this.entries.length = 0;
5711
6132
  }
5712
6133
  onEntry(fn) {
5713
- this.listeners.push(fn);
6134
+ this.listeners.add(fn);
5714
6135
  }
5715
6136
  offEntry(fn) {
5716
- const idx = this.listeners.indexOf(fn);
5717
- if (idx !== -1) this.listeners.splice(idx, 1);
6137
+ this.listeners.delete(fn);
5718
6138
  }
5719
6139
  };
5720
6140
  }
@@ -6697,54 +7117,34 @@ import pc from "picocolors";
6697
7117
  function print(line) {
6698
7118
  process.stdout.write(line + "\n");
6699
7119
  }
6700
- function severityIcon(severity) {
6701
- return SEVERITY_COLOR[severity](SEVERITY_ICON[severity]);
6702
- }
6703
- function colorTitle(severity, text) {
6704
- const color = SEVERITY_COLOR[severity];
6705
- return severity === "info" ? color(text) : color(pc.bold(text));
6706
- }
6707
- function truncate(s, max = TERMINAL_TRUNCATE_LENGTH) {
6708
- return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
6709
- }
6710
- function formatConsoleLine(issue, suffix) {
6711
- const icon = severityIcon(issue.severity);
6712
- const title = colorTitle(issue.severity, issue.title);
6713
- const desc = pc.dim(truncate(issue.desc) + (suffix ?? ""));
6714
- let line = ` ${icon} ${title} \u2014 ${desc}`;
6715
- if (issue.detail) {
6716
- line += `
6717
- ${pc.dim("\u2514 " + issue.detail)}`;
6718
- }
6719
- return line;
7120
+ function pluralize(n, word) {
7121
+ return n === 1 ? `${n} ${word}` : `${n} ${word}s`;
6720
7122
  }
6721
7123
  function startTerminalInsights(services, proxyPort) {
6722
7124
  const bus = services.bus;
6723
- const metricsStore = services.metricsStore;
6724
7125
  const printedKeys = /* @__PURE__ */ new Set();
6725
7126
  const resolvedKeys = /* @__PURE__ */ new Set();
6726
- const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
7127
+ const dashUrl = `http://localhost:${proxyPort}${DASHBOARD_PREFIX}`;
7128
+ const prefix = ` ${pc.magenta(pc.bold("brakit"))} ${pc.dim(UNICODE_ARROW)}`;
7129
+ const actionHint = `${pc.underline(dashUrl)} ${pc.dim("or run")} ${pc.bold('"Fix brakit findings"')} ${pc.dim("in your AI tool")}`;
6727
7130
  return bus.on("analysis:updated", ({ issues }) => {
6728
- const newLines = [];
6729
- const resolvedLines = [];
6730
- const regressedLines = [];
7131
+ let newCount = 0;
7132
+ let resolvedCount = 0;
7133
+ let regressedCount = 0;
6731
7134
  for (const si of issues) {
7135
+ if (si.aiStatus === "wont_fix") continue;
6732
7136
  if (si.state === "resolved") {
6733
7137
  if (resolvedKeys.has(si.issueId)) continue;
6734
7138
  resolvedKeys.add(si.issueId);
6735
7139
  printedKeys.delete(si.issueId);
6736
- const title = pc.green(pc.bold(`\u2713 ${si.issue.title}`));
6737
- const desc = pc.dim(truncate(si.issue.desc));
6738
- resolvedLines.push(` ${title} \u2014 ${desc} ${pc.green("resolved")}`);
7140
+ resolvedCount++;
6739
7141
  continue;
6740
7142
  }
6741
7143
  if (si.state === "regressed") {
6742
7144
  if (!printedKeys.has(si.issueId)) {
6743
7145
  printedKeys.add(si.issueId);
6744
7146
  resolvedKeys.delete(si.issueId);
6745
- const title = pc.red(pc.bold(`\u26A0 ${si.issue.title}`));
6746
- const desc = pc.dim(truncate(si.issue.desc));
6747
- regressedLines.push(` ${title} \u2014 ${desc} ${pc.red("regressed")}`);
7147
+ regressedCount++;
6748
7148
  }
6749
7149
  continue;
6750
7150
  }
@@ -6752,52 +7152,27 @@ function startTerminalInsights(services, proxyPort) {
6752
7152
  if (si.issue.severity === "info") continue;
6753
7153
  if (printedKeys.has(si.issueId)) continue;
6754
7154
  printedKeys.add(si.issueId);
6755
- let suffix;
6756
- if (si.issue.rule === "slow") {
6757
- const endpoint = si.issue.endpoint;
6758
- if (endpoint) {
6759
- const ep = metricsStore.getEndpoint(endpoint);
6760
- if (ep && ep.sessions.length > 1) {
6761
- const prev = ep.sessions[ep.sessions.length - 2];
6762
- suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
6763
- }
6764
- }
6765
- }
6766
- newLines.push(formatConsoleLine(si.issue, suffix));
7155
+ newCount++;
6767
7156
  }
6768
- if (newLines.length > 0) {
6769
- print("");
6770
- for (const line of newLines) print(line);
7157
+ if (newCount > 0) {
6771
7158
  print("");
6772
- print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.dim("Dashboard:")} ${pc.underline(`http://${dashUrl}`)} ${pc.dim("or ask your AI:")} ${pc.bold('"Fix brakit findings"')}`);
7159
+ print(`${prefix} ${pc.yellow(pluralize(newCount, "new issue"))} ${pc.dim(UNICODE_EM_DASH)} ${actionHint}`);
6773
7160
  }
6774
- if (regressedLines.length > 0) {
7161
+ if (regressedCount > 0) {
6775
7162
  print("");
6776
- for (const line of regressedLines) print(line);
6777
- print("");
6778
- print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.red("Issues came back after being resolved!")}`);
7163
+ print(`${prefix} ${pc.red(pluralize(regressedCount, "issue") + " regressed")} ${pc.dim(UNICODE_EM_DASH)} ${actionHint}`);
6779
7164
  }
6780
- if (resolvedLines.length > 0) {
6781
- print("");
6782
- for (const line of resolvedLines) print(line);
7165
+ if (resolvedCount > 0) {
6783
7166
  print("");
6784
- print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.green("Issues fixed!")}`);
7167
+ print(`${prefix} ${pc.green(pluralize(resolvedCount, "issue") + " resolved " + UNICODE_CHECK_MARK)}`);
6785
7168
  }
6786
7169
  });
6787
7170
  }
6788
- var SEVERITY_COLOR;
6789
7171
  var init_terminal = __esm({
6790
7172
  "src/output/terminal.ts"() {
6791
7173
  "use strict";
6792
7174
  init_src();
6793
7175
  init_constants();
6794
- init_config();
6795
- init_labels();
6796
- SEVERITY_COLOR = {
6797
- critical: pc.red,
6798
- warning: pc.yellow,
6799
- info: pc.dim
6800
- };
6801
7176
  }
6802
7177
  });
6803
7178
 
@@ -6873,125 +7248,6 @@ var init_guard = __esm({
6873
7248
  }
6874
7249
  });
6875
7250
 
6876
- // src/runtime/capture.ts
6877
- import { gunzip, brotliDecompress, inflate } from "zlib";
6878
- function outgoingToIncoming(headers2) {
6879
- const result = {};
6880
- for (const [key, value] of Object.entries(headers2)) {
6881
- if (value === void 0) continue;
6882
- if (Array.isArray(value)) {
6883
- result[key] = value.map(String);
6884
- } else {
6885
- result[key] = String(value);
6886
- }
6887
- }
6888
- return result;
6889
- }
6890
- function getDecompressor(encoding) {
6891
- if (encoding === CONTENT_ENCODING_GZIP) return gunzip;
6892
- if (encoding === CONTENT_ENCODING_BR) return brotliDecompress;
6893
- if (encoding === CONTENT_ENCODING_DEFLATE) return inflate;
6894
- return null;
6895
- }
6896
- function decompressAsync(body, encoding) {
6897
- const decompressor = getDecompressor(encoding);
6898
- if (!decompressor) return Promise.resolve(body);
6899
- return new Promise((resolve6) => {
6900
- decompressor(body, (err, result) => {
6901
- resolve6(err ? body : result);
6902
- });
6903
- });
6904
- }
6905
- function toBuffer(chunk) {
6906
- if (Buffer.isBuffer(chunk)) return chunk;
6907
- if (chunk instanceof Uint8Array) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
6908
- if (typeof chunk === "string") return Buffer.from(chunk);
6909
- return null;
6910
- }
6911
- function captureInProcess(req, res, requestId, requestStore, isChild = false) {
6912
- const startTime = performance.now();
6913
- const method = req.method ?? "GET";
6914
- const resChunks = [];
6915
- let resSize = 0;
6916
- const originalWrite = res.write;
6917
- const originalEnd = res.end;
6918
- let truncated = false;
6919
- res.write = function(...args) {
6920
- try {
6921
- const chunk = args[0];
6922
- if (chunk != null && typeof chunk !== "function") {
6923
- if (resSize < DEFAULT_MAX_BODY_CAPTURE) {
6924
- const buf = toBuffer(chunk);
6925
- if (buf) {
6926
- resChunks.push(buf);
6927
- resSize += buf.length;
6928
- }
6929
- } else {
6930
- truncated = true;
6931
- }
6932
- }
6933
- } catch (e) {
6934
- brakitDebug(`capture write: ${getErrorMessage(e)}`);
6935
- }
6936
- return originalWrite.apply(this, args);
6937
- };
6938
- res.end = function(...args) {
6939
- try {
6940
- const chunk = typeof args[0] !== "function" ? args[0] : void 0;
6941
- if (chunk != null && resSize < DEFAULT_MAX_BODY_CAPTURE) {
6942
- const buf = toBuffer(chunk);
6943
- if (buf) {
6944
- resChunks.push(buf);
6945
- }
6946
- }
6947
- } catch (e) {
6948
- brakitDebug(`capture end: ${getErrorMessage(e)}`);
6949
- }
6950
- const result = originalEnd.apply(this, args);
6951
- const endTime = performance.now();
6952
- const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
6953
- const statusCode = res.statusCode;
6954
- const responseHeaders = outgoingToIncoming(res.getHeaders());
6955
- const responseContentType = String(res.getHeader("content-type") ?? "");
6956
- const capturedChunks = resChunks.slice();
6957
- if (!isChild) {
6958
- void (async () => {
6959
- try {
6960
- let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
6961
- if (body && encoding && !truncated) {
6962
- body = await decompressAsync(body, encoding);
6963
- }
6964
- requestStore.capture({
6965
- requestId,
6966
- method,
6967
- url: req.url ?? "/",
6968
- requestHeaders: req.headers,
6969
- requestBody: null,
6970
- statusCode,
6971
- responseHeaders,
6972
- responseBody: body,
6973
- responseContentType,
6974
- startTime,
6975
- endTime,
6976
- config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
6977
- });
6978
- } catch (e) {
6979
- brakitDebug(`capture store: ${getErrorMessage(e)}`);
6980
- }
6981
- })();
6982
- }
6983
- return result;
6984
- };
6985
- }
6986
- var init_capture = __esm({
6987
- "src/runtime/capture.ts"() {
6988
- "use strict";
6989
- init_constants();
6990
- init_log();
6991
- init_type_guards();
6992
- }
6993
- });
6994
-
6995
7251
  // src/runtime/interceptor.ts
6996
7252
  import http from "http";
6997
7253
  import { randomUUID as randomUUID8 } from "crypto";
@@ -7064,13 +7320,46 @@ var init_interceptor = __esm({
7064
7320
  }
7065
7321
  });
7066
7322
 
7323
+ // src/detect/runtime.ts
7324
+ function detectJsRuntime() {
7325
+ if (globalThis.Bun) return "bun";
7326
+ if (globalThis.Deno) return "deno";
7327
+ return "node";
7328
+ }
7329
+ function extractLoadedPackages() {
7330
+ try {
7331
+ const cache = __require.cache;
7332
+ if (!cache) return [];
7333
+ const seen = /* @__PURE__ */ new Set();
7334
+ const segment = NODE_MODULES_SEGMENT;
7335
+ const segmentWin = segment.replaceAll("/", "\\");
7336
+ for (const key of Object.keys(cache)) {
7337
+ const nm = Math.max(key.lastIndexOf(segment), key.lastIndexOf(segmentWin));
7338
+ if (nm === -1) continue;
7339
+ const after = key.slice(nm + segment.length);
7340
+ const parts = after.split(/[/\\]/);
7341
+ const pkgName = parts[0].startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
7342
+ if (KNOWN_DEPENDENCY_SET.has(pkgName)) seen.add(pkgName);
7343
+ }
7344
+ return [...seen];
7345
+ } catch {
7346
+ return [];
7347
+ }
7348
+ }
7349
+ var init_runtime = __esm({
7350
+ "src/detect/runtime.ts"() {
7351
+ "use strict";
7352
+ init_detection();
7353
+ }
7354
+ });
7355
+
7067
7356
  // src/runtime/setup.ts
7068
7357
  var setup_exports = {};
7069
7358
  __export(setup_exports, {
7070
7359
  setup: () => setup
7071
7360
  });
7072
7361
  import { readFile as readFile5, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
7073
- import { existsSync as existsSync7, unlinkSync as unlinkSync3 } from "fs";
7362
+ import { existsSync as existsSync7, unlinkSync as unlinkSync3, readFileSync as readFileSync6 } from "fs";
7074
7363
  import { resolve as resolve5 } from "path";
7075
7364
  function setup() {
7076
7365
  if (initPromise) return initPromise;
@@ -7102,21 +7391,25 @@ function installHooks(bus) {
7102
7391
  adapterRegistry.patchAll(telemetryEmit);
7103
7392
  const cwd = process.cwd();
7104
7393
  let framework = "unknown";
7105
- let frameworkCandidates = [];
7394
+ let detectedDependencies = [];
7395
+ let configFilesDetected = [];
7106
7396
  try {
7107
7397
  const pkg = JSON.parse(
7108
- __require("fs").readFileSync(resolve5(cwd, "package.json"), "utf-8")
7398
+ readFileSync6(resolve5(cwd, "package.json"), "utf-8")
7109
7399
  );
7110
7400
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7111
7401
  framework = detectFrameworkFromDeps(allDeps);
7112
- frameworkCandidates = KNOWN_DEPENDENCY_NAMES.filter((dep) => dep in allDeps);
7402
+ detectedDependencies = KNOWN_DEPENDENCY_NAMES.filter((dep) => dep in allDeps);
7403
+ configFilesDetected = detectConfigFiles(cwd);
7113
7404
  } catch {
7114
7405
  }
7115
7406
  return {
7116
7407
  framework,
7117
7408
  adapterNames: adapterRegistry.getActive().map((a) => a.name),
7118
7409
  adaptersFailed: [...adapterRegistry.getFailed()],
7119
- frameworkCandidates
7410
+ detectedDependencies,
7411
+ configFilesDetected,
7412
+ jsRuntime: detectJsRuntime()
7120
7413
  };
7121
7414
  }
7122
7415
  function startAnalysis(bus, stores, dataDir, services) {
@@ -7145,14 +7438,12 @@ function registerLifecycle(allServices, stores, services, cwd) {
7145
7438
  const sendTelemetry = () => {
7146
7439
  if (telemetrySent) return;
7147
7440
  telemetrySent = true;
7148
- recordRequestCount(stores.requestStore.getAll().length);
7149
- recordInsightTypes(
7150
- services.analysisEngine.getInsights().map((i) => i.type)
7151
- );
7152
- recordRulesTriggered(
7441
+ session.recordCounts(
7442
+ stores.requestStore.getAll().length,
7443
+ services.analysisEngine.getInsights().map((i) => i.type),
7153
7444
  services.analysisEngine.getFindings().map((f) => f.rule)
7154
7445
  );
7155
- trackSession(allServices);
7446
+ flushSession(allServices);
7156
7447
  };
7157
7448
  let teardownCalled = false;
7158
7449
  const runTeardown = () => {
@@ -7172,13 +7463,14 @@ function registerLifecycle(allServices, stores, services, cwd) {
7172
7463
  };
7173
7464
  health.setTeardown(runTeardown);
7174
7465
  process.on("SIGINT", () => {
7175
- recordExitReason(EXIT_REASON_SIGINT);
7466
+ session.recordExitReason(EXIT_REASON_SIGINT);
7176
7467
  });
7177
7468
  process.on("SIGTERM", () => {
7178
- recordExitReason(EXIT_REASON_SIGTERM);
7469
+ session.recordExitReason(EXIT_REASON_SIGTERM);
7179
7470
  });
7180
- process.on("beforeExit", () => {
7181
- recordExitReason(EXIT_REASON_CLEAN);
7471
+ process.on("beforeExit", async () => {
7472
+ await drainPendingCaptures();
7473
+ session.recordExitReason(EXIT_REASON_CLEAN);
7182
7474
  sendTelemetry();
7183
7475
  });
7184
7476
  process.on("exit", () => {
@@ -7195,17 +7487,19 @@ async function doSetup() {
7195
7487
  bus,
7196
7488
  ...stores
7197
7489
  };
7198
- const { framework, adapterNames, adaptersFailed, frameworkCandidates } = installHooks(bus);
7199
- initSession(framework, detectPackageManagerSync(cwd), false, adapterNames);
7490
+ const detection = installHooks(bus);
7491
+ session.init(detection.framework, detectPackageManager(cwd), false, detection.adapterNames);
7200
7492
  const setupDurationMs = Date.now() - setupStart;
7201
- recordSetupCompleted({ frameworkCandidates, adaptersFailed, setupDurationMs });
7493
+ session.recordSetup(detection, setupDurationMs);
7202
7494
  trackEvent(TELEMETRY_EVENT_SETUP_COMPLETED, {
7203
- framework,
7204
- framework_detection_candidates: frameworkCandidates,
7205
- adapters_detected: adapterNames,
7206
- adapters_failed: adaptersFailed,
7207
- hooks_installed: ["fetch", "console", "error"],
7208
- setup_duration_ms: setupDurationMs
7495
+ framework: detection.framework,
7496
+ framework_detection_candidates: detection.detectedDependencies,
7497
+ adapters_detected: detection.adapterNames,
7498
+ adapters_failed: detection.adaptersFailed,
7499
+ hooks_installed: [...INSTALLED_HOOKS],
7500
+ setup_duration_ms: setupDurationMs,
7501
+ config_files_detected: detection.configFilesDetected,
7502
+ js_runtime: detection.jsRuntime
7209
7503
  });
7210
7504
  const graphBuilder = new GraphBuilder(bus, stores.requestStore);
7211
7505
  graphBuilder.start();
@@ -7226,10 +7520,13 @@ async function doSetup() {
7226
7520
  onFirstRequest(port) {
7227
7521
  setBrakitPort(port);
7228
7522
  brakitDebug(`[setup] onFirstRequest fired, port=${port}`);
7229
- recordFirstRequest();
7523
+ const loadedPackages = extractLoadedPackages();
7524
+ session.recordFirstRequest(loadedPackages);
7230
7525
  trackEvent(TELEMETRY_EVENT_FIRST_REQUEST, {
7231
7526
  port,
7232
- time_to_first_request_ms: Date.now() - setupStart
7527
+ time_to_first_request_ms: Date.now() - setupStart,
7528
+ loaded_packages: loadedPackages,
7529
+ loaded_package_count: loadedPackages.length
7233
7530
  });
7234
7531
  void (async () => {
7235
7532
  try {
@@ -7270,6 +7567,7 @@ var init_setup = __esm({
7270
7567
  "src/runtime/setup.ts"() {
7271
7568
  "use strict";
7272
7569
  init_fetch();
7570
+ init_capture();
7273
7571
  init_console();
7274
7572
  init_errors();
7275
7573
  init_adapters();
@@ -7284,13 +7582,15 @@ var init_setup = __esm({
7284
7582
  init_terminal();
7285
7583
  init_src();
7286
7584
  init_constants();
7585
+ init_detection();
7287
7586
  init_health();
7288
7587
  init_interceptor();
7289
7588
  init_log();
7290
7589
  init_type_guards();
7291
7590
  init_fs();
7292
7591
  init_project();
7293
- init_telemetry();
7592
+ init_runtime();
7593
+ init_telemetry2();
7294
7594
  initPromise = null;
7295
7595
  }
7296
7596
  });