brakit 0.10.1 → 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,21 +89,24 @@ 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) --
108
110
  "@nestjs/core",
109
111
  "@adonisjs/core",
110
112
  "sails",
@@ -113,16 +115,91 @@ var init_config = __esm({
113
115
  "hono",
114
116
  "koa",
115
117
  "@hapi/hapi",
118
+ "elysia",
119
+ "h3",
120
+ "nitro",
121
+ "@trpc/server",
122
+ // -- Bundlers --
123
+ "vite",
124
+ // -- ORM / query builders --
116
125
  "prisma",
126
+ "@prisma/client",
117
127
  "drizzle-orm",
118
128
  "typeorm",
119
- "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"
120
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);
121
198
  }
122
199
  });
123
200
 
124
201
  // src/constants/labels.ts
125
- 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;
126
203
  var init_labels = __esm({
127
204
  "src/constants/labels.ts"() {
128
205
  "use strict";
@@ -180,11 +257,6 @@ var init_labels = __esm({
180
257
  CONTENT_ENCODING_GZIP = "gzip";
181
258
  CONTENT_ENCODING_BR = "br";
182
259
  CONTENT_ENCODING_DEFLATE = "deflate";
183
- SEVERITY_ICON = {
184
- critical: "\u2717",
185
- warning: "\u26A0",
186
- info: "\u2139"
187
- };
188
260
  SSE_EVENT_FETCH = "fetch";
189
261
  SSE_EVENT_LOG = "log";
190
262
  SSE_EVENT_ERROR = "error_event";
@@ -196,14 +268,14 @@ var init_labels = __esm({
196
268
  SDK_EVENT_LOG = "log";
197
269
  SDK_EVENT_ERROR = "error";
198
270
  SDK_EVENT_AUTH_CHECK = "auth.check";
199
- POSTHOG_HOST = "https://us.i.posthog.com";
200
- POSTHOG_CAPTURE_PATH = "/i/v0/e/";
201
- POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
202
- SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
271
+ SDK_EVENT_HELLO = "sdk.hello";
203
272
  TIMELINE_FETCH = "fetch";
204
273
  TIMELINE_LOG = "log";
205
274
  TIMELINE_ERROR = "error";
206
275
  TIMELINE_QUERY = "query";
276
+ UNICODE_ARROW = "\u2192";
277
+ UNICODE_EM_DASH = "\u2014";
278
+ UNICODE_CHECK_MARK = "\u2713";
207
279
  }
208
280
  });
209
281
 
@@ -243,13 +315,36 @@ var init_features = __esm({
243
315
  }
244
316
  });
245
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
+
246
339
  // src/constants/index.ts
247
340
  var init_constants = __esm({
248
341
  "src/constants/index.ts"() {
249
342
  "use strict";
250
343
  init_config();
344
+ init_detection();
251
345
  init_labels();
252
346
  init_features();
347
+ init_telemetry();
253
348
  }
254
349
  });
255
350
 
@@ -1972,550 +2067,219 @@ var init_sdk_event_parser = __esm({
1972
2067
  }
1973
2068
  });
1974
2069
 
1975
- // src/dashboard/api/ingest.ts
1976
- function isBrakitBatch(msg) {
1977
- 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;
1978
2077
  }
1979
- function isSDKPayload(msg) {
1980
- 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
+ }
1981
2086
  }
1982
- function createIngestHandler(services) {
1983
- const routeEvent = (event) => {
1984
- switch (event.type) {
1985
- case TIMELINE_FETCH:
1986
- bus.emit("telemetry:fetch", event.data);
1987
- break;
1988
- case TIMELINE_LOG:
1989
- bus.emit("telemetry:log", event.data);
1990
- break;
1991
- case TIMELINE_ERROR:
1992
- bus.emit("telemetry:error", event.data);
1993
- break;
1994
- case TIMELINE_QUERY:
1995
- bus.emit("telemetry:query", event.data);
1996
- break;
1997
- }
1998
- };
1999
- const { bus, requestStore } = services;
2000
- const stores = {
2001
- addQuery: (data) => bus.emit("telemetry:query", data),
2002
- addFetch: (data) => bus.emit("telemetry:fetch", data),
2003
- addLog: (data) => bus.emit("telemetry:log", data),
2004
- addError: (data) => bus.emit("telemetry:error", data),
2005
- addRequest: (data) => requestStore.add(data)
2006
- };
2007
- return (req, res) => {
2008
- if (req.method !== "POST") {
2009
- sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
2010
- 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
+ `);
2011
2100
  }
2012
- const chunks = [];
2013
- let totalSize = 0;
2014
- req.on("data", (chunk) => {
2015
- totalSize += chunk.length;
2016
- if (totalSize > MAX_INGEST_BYTES) {
2017
- sendJson(req, res, HTTP_PAYLOAD_TOO_LARGE, { error: "Payload too large" });
2018
- req.destroy();
2019
- return;
2020
- }
2021
- chunks.push(chunk);
2022
- });
2023
- req.on("end", () => {
2024
- if (res.headersSent) return;
2025
- try {
2026
- const body = JSON.parse(Buffer.concat(chunks).toString());
2027
- if (isSDKPayload(body)) {
2028
- for (const event of body.events) {
2029
- routeSDKEvent(event, stores);
2030
- }
2031
- res.writeHead(HTTP_NO_CONTENT);
2032
- res.end();
2033
- return;
2034
- }
2035
- if (isBrakitBatch(body)) {
2036
- for (const event of body.events) {
2037
- routeEvent(event);
2038
- }
2039
- res.writeHead(HTTP_NO_CONTENT);
2040
- res.end();
2041
- return;
2042
- }
2043
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid batch" });
2044
- } catch {
2045
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid JSON" });
2046
- }
2047
- });
2048
- req.on("error", () => {
2049
- if (!res.headersSent) {
2050
- res.writeHead(HTTP_BAD_REQUEST);
2051
- res.end();
2052
- }
2053
- });
2054
- };
2101
+ }
2055
2102
  }
2056
- var init_ingest = __esm({
2057
- "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"() {
2058
2124
  "use strict";
2059
- init_config();
2060
- init_labels();
2061
- init_shared2();
2062
- 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;
2063
2130
  }
2064
2131
  });
2065
2132
 
2066
- // src/dashboard/api/metrics.ts
2067
- function createMetricsHandler(metricsStore) {
2068
- return (req, res) => {
2069
- if (!requireGet(req, res)) return;
2070
- const url = parseRequestUrl(req);
2071
- const endpoint = url.searchParams.get("endpoint");
2072
- if (endpoint) {
2073
- const ep = metricsStore.getEndpoint(endpoint);
2074
- sendJson(req, res, HTTP_OK, { endpoints: ep ? [ep] : [] });
2075
- 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");
2076
2161
  }
2077
- sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
2078
- };
2162
+ } catch (err) {
2163
+ brakitDebug(`ensureGitignore failed: ${getErrorMessage(err)}`);
2164
+ }
2079
2165
  }
2080
- var init_metrics = __esm({
2081
- "src/dashboard/api/metrics.ts"() {
2082
- "use strict";
2083
- init_shared2();
2084
- init_labels();
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)}`);
2085
2178
  }
2086
- });
2087
-
2088
- // src/dashboard/api/metrics-live.ts
2089
- function createLiveMetricsHandler(metricsStore) {
2090
- return (req, res) => {
2091
- if (!requireGet(req, res)) return;
2092
- sendJson(req, res, 200, { endpoints: metricsStore.getLiveEndpoints() });
2093
- };
2094
2179
  }
2095
- var init_metrics_live = __esm({
2096
- "src/dashboard/api/metrics-live.ts"() {
2180
+ var init_fs = __esm({
2181
+ "src/utils/fs.ts"() {
2097
2182
  "use strict";
2098
- init_shared2();
2183
+ init_config();
2184
+ init_log();
2185
+ init_type_guards();
2099
2186
  }
2100
2187
  });
2101
2188
 
2102
- // src/dashboard/api/activity.ts
2103
- function buildTimeline(services, requestId) {
2104
- const fetches = services.fetchStore.getByRequest(requestId);
2105
- const logs = services.logStore.getByRequest(requestId);
2106
- const errors = services.errorStore.getByRequest(requestId);
2107
- const queries = services.queryStore.getByRequest(requestId);
2108
- const timeline = [];
2109
- for (const fetch of fetches)
2110
- timeline.push({ type: TIMELINE_FETCH, timestamp: fetch.timestamp, data: fetch });
2111
- for (const log of logs)
2112
- timeline.push({ type: TIMELINE_LOG, timestamp: log.timestamp, data: log });
2113
- for (const error of errors)
2114
- timeline.push({ type: TIMELINE_ERROR, timestamp: error.timestamp, data: error });
2115
- for (const query of queries)
2116
- timeline.push({ type: TIMELINE_QUERY, timestamp: query.timestamp, data: query });
2117
- timeline.sort((a, b) => a.timestamp - b.timestamp);
2118
- return {
2119
- total: timeline.length,
2120
- timeline,
2121
- counts: {
2122
- fetches: fetches.length,
2123
- logs: logs.length,
2124
- errors: errors.length,
2125
- queries: queries.length
2126
- }
2127
- };
2128
- }
2129
- function createActivityHandler(services) {
2130
- return (req, res) => {
2131
- if (!requireGet(req, res)) return;
2132
- try {
2133
- const url = parseRequestUrl(req);
2134
- const requestId = url.searchParams.get("requestId");
2135
- const requestIds = url.searchParams.get("requestIds");
2136
- if (!requestId && !requestIds) {
2137
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId or requestIds parameter required" });
2138
- return;
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"() {
2200
+ "use strict";
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";
2139
2210
  }
2140
- if (requestId) {
2141
- const result = buildTimeline(services, requestId);
2142
- sendJson(req, res, HTTP_OK, { requestId, ...result });
2143
- 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
+ }
2144
2219
  }
2145
- const ids = (requestIds || "").split(",").filter(Boolean).slice(0, MAX_BATCH_IDS);
2146
- const activities = {};
2147
- for (const id of ids) {
2148
- 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
+ }
2149
2241
  }
2150
- sendJson(req, res, HTTP_OK, { requestIds: ids, activities });
2151
- } catch (err) {
2152
- brakitDebug(`activity handler error: ${err}`);
2153
- if (!res.headersSent) {
2154
- 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
+ }
2155
2249
  }
2156
- }
2157
- };
2158
- }
2159
- var MAX_BATCH_IDS;
2160
- var init_activity = __esm({
2161
- "src/dashboard/api/activity.ts"() {
2162
- "use strict";
2163
- init_shared2();
2164
- init_labels();
2165
- init_log();
2166
- MAX_BATCH_IDS = 50;
2167
- }
2168
- });
2169
-
2170
- // src/dashboard/api/index.ts
2171
- var init_api = __esm({
2172
- "src/dashboard/api/index.ts"() {
2173
- "use strict";
2174
- init_handlers();
2175
- init_ingest();
2176
- init_metrics();
2177
- init_metrics_live();
2178
- 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
+ };
2179
2259
  }
2180
2260
  });
2181
2261
 
2182
- // src/dashboard/api/issues.ts
2183
- function createIssuesHandler(issueStore) {
2184
- return (req, res) => {
2185
- if (!requireGet(req, res)) return;
2186
- const url = parseRequestUrl(req);
2187
- const stateParam = url.searchParams.get("state");
2188
- const categoryParam = url.searchParams.get("category");
2189
- let issues;
2190
- if (stateParam && isValidIssueState(stateParam)) {
2191
- issues = issueStore.getByState(stateParam);
2192
- } else if (categoryParam && isValidIssueCategory(categoryParam)) {
2193
- issues = issueStore.getByCategory(categoryParam);
2194
- } else {
2195
- issues = issueStore.getAll();
2196
- }
2197
- sendJson(req, res, HTTP_OK, { issues });
2198
- };
2199
- }
2200
- function createFindingsHandler(issueStore) {
2201
- return (req, res) => {
2202
- if (!requireGet(req, res)) return;
2203
- const url = parseRequestUrl(req);
2204
- const stateParam = url.searchParams.get("state");
2205
- let issues;
2206
- if (stateParam && isValidIssueState(stateParam)) {
2207
- issues = issueStore.getByState(stateParam);
2208
- } else {
2209
- issues = issueStore.getAll();
2210
- }
2211
- sendJson(req, res, HTTP_OK, {
2212
- total: issues.length,
2213
- findings: issues
2214
- });
2215
- };
2216
- }
2217
- function createIssuesReportHandler(issueStore, eventBus) {
2218
- return async (req, res) => {
2219
- if (req.method !== "POST") {
2220
- sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
2221
- return;
2222
- }
2223
- const body = await readJsonBody(req, res);
2224
- if (!body) return;
2225
- const { findingId, status, notes } = body;
2226
- if (!findingId || typeof findingId !== "string") {
2227
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "findingId is required" });
2228
- return;
2229
- }
2230
- if (!isValidAiFixStatus(status)) {
2231
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "status must be 'fixed' or 'wont_fix'" });
2232
- return;
2233
- }
2234
- if (!notes || typeof notes !== "string") {
2235
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "notes is required" });
2236
- return;
2237
- }
2238
- if (issueStore.reportFix(findingId, status, notes)) {
2239
- eventBus.emit("issues:changed", issueStore.getAll());
2240
- sendJson(req, res, HTTP_OK, { ok: true });
2241
- return;
2242
- }
2243
- sendJson(req, res, HTTP_NOT_FOUND, { error: "Finding not found" });
2244
- };
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);
2245
2268
  }
2246
- var init_issues = __esm({
2247
- "src/dashboard/api/issues.ts"() {
2269
+ var init_issue_id = __esm({
2270
+ "src/utils/issue-id.ts"() {
2248
2271
  "use strict";
2249
- init_shared2();
2250
- init_type_guards();
2251
- init_labels();
2272
+ init_config();
2252
2273
  }
2253
2274
  });
2254
2275
 
2255
- // src/dashboard/api/graph.ts
2256
- function createGraphHandler(services) {
2257
- return (req, res) => {
2258
- if (!requireGet(req, res)) return;
2259
- const url = parseRequestUrl(req);
2260
- const rawCluster = url.searchParams.get("cluster") ?? void 0;
2261
- const rawNode = url.searchParams.get("node") ?? void 0;
2262
- const rawLevel = url.searchParams.get("level") ?? void 0;
2263
- const rawGrouping = url.searchParams.get("grouping") ?? void 0;
2264
- const cluster = rawCluster && rawCluster.length <= MAX_PARAM_LENGTH ? rawCluster : void 0;
2265
- const node = rawNode && rawNode.length <= MAX_PARAM_LENGTH ? rawNode : void 0;
2266
- const level = rawLevel && VALID_LEVELS.has(rawLevel) ? rawLevel : void 0;
2267
- const grouping = rawGrouping && VALID_GROUPINGS.has(rawGrouping) ? rawGrouping : void 0;
2268
- const { graphBuilder, metricsStore } = services;
2269
- graphBuilder.enrichWithMetrics((endpointKey) => {
2270
- const metrics = metricsStore.getEndpoint(endpointKey);
2271
- if (!metrics || metrics.sessions.length === 0) return void 0;
2272
- const latest = metrics.sessions[metrics.sessions.length - 1];
2273
- return latest.p95DurationMs;
2274
- });
2275
- const data = graphBuilder.getApiResponse({ cluster, node, level, grouping });
2276
- sendJson(req, res, HTTP_OK, data);
2277
- };
2278
- }
2279
- var VALID_LEVELS, VALID_GROUPINGS, MAX_PARAM_LENGTH;
2280
- var init_graph = __esm({
2281
- "src/dashboard/api/graph.ts"() {
2282
- "use strict";
2283
- init_labels();
2284
- init_shared2();
2285
- VALID_LEVELS = /* @__PURE__ */ new Set(["endpoints", "clusters"]);
2286
- VALID_GROUPINGS = /* @__PURE__ */ new Set(["path", "auth-boundary", "data-domain"]);
2287
- MAX_PARAM_LENGTH = 200;
2288
- }
2289
- });
2290
-
2291
- // src/dashboard/sse.ts
2292
- function createSSEHandler(services) {
2293
- const clients = /* @__PURE__ */ new Set();
2294
- function broadcast(eventType, data) {
2295
- if (clients.size === 0) return;
2296
- const frame = eventType ? `event: ${eventType}
2297
- data: ${data}
2298
-
2299
- ` : `data: ${data}
2300
-
2301
- `;
2302
- for (const client of clients) {
2303
- if (client.res.destroyed) {
2304
- clients.delete(client);
2305
- continue;
2306
- }
2307
- try {
2308
- client.res.write(frame);
2309
- } catch {
2310
- clients.delete(client);
2311
- }
2312
- }
2313
- }
2314
- const bus = services.bus;
2315
- bus.on("request:completed", (r) => broadcast(null, JSON.stringify(r)));
2316
- bus.on("telemetry:fetch", (e) => broadcast(SSE_EVENT_FETCH, JSON.stringify(e)));
2317
- bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
2318
- bus.on("telemetry:error", (e) => broadcast(SSE_EVENT_ERROR, JSON.stringify(e)));
2319
- bus.on("telemetry:query", (e) => broadcast(SSE_EVENT_QUERY, JSON.stringify(e)));
2320
- bus.on("analysis:updated", ({ issues }) => {
2321
- broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
2322
- });
2323
- bus.on("issues:changed", (issues) => {
2324
- broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
2325
- });
2326
- return (req, res) => {
2327
- const headers2 = {
2328
- "content-type": "text/event-stream",
2329
- "cache-control": "no-cache",
2330
- connection: "keep-alive"
2331
- };
2332
- const corsOrigin = getCorsOrigin(req);
2333
- if (corsOrigin) {
2334
- headers2["access-control-allow-origin"] = corsOrigin;
2335
- }
2336
- res.writeHead(HTTP_OK, headers2);
2337
- res.write(":ok\n\n");
2338
- const heartbeat = setInterval(() => {
2339
- if (res.destroyed) {
2340
- clearInterval(heartbeat);
2341
- clients.delete(client);
2342
- return;
2343
- }
2344
- try {
2345
- res.write(":heartbeat\n\n");
2346
- } catch {
2347
- clearInterval(heartbeat);
2348
- clients.delete(client);
2349
- }
2350
- }, SSE_HEARTBEAT_INTERVAL_MS);
2351
- heartbeat.unref();
2352
- const client = { res, heartbeat };
2353
- clients.add(client);
2354
- req.on("close", () => {
2355
- clearInterval(heartbeat);
2356
- clients.delete(client);
2357
- });
2358
- };
2359
- }
2360
- var init_sse = __esm({
2361
- "src/dashboard/sse.ts"() {
2362
- "use strict";
2363
- init_constants();
2364
- init_labels();
2365
- init_shared2();
2366
- }
2367
- });
2368
-
2369
- // src/utils/fs.ts
2370
- import { access, readFile, writeFile } from "fs/promises";
2371
- import { existsSync, readFileSync, writeFileSync } from "fs";
2372
- import { createHash } from "crypto";
2373
- import { homedir } from "os";
2374
- import { resolve, join } from "path";
2375
- function getProjectDataDir(projectRoot) {
2376
- const absolute = resolve(projectRoot);
2377
- const hash = createHash("sha256").update(absolute).digest("hex").slice(0, PROJECT_HASH_LENGTH);
2378
- return join(homedir(), ".brakit", "projects", hash);
2379
- }
2380
- async function fileExists(path) {
2381
- try {
2382
- await access(path);
2383
- return true;
2384
- } catch {
2385
- return false;
2386
- }
2387
- }
2388
- function ensureGitignore(dir, entry) {
2389
- try {
2390
- const gitignorePath = resolve(dir, "../.gitignore");
2391
- if (existsSync(gitignorePath)) {
2392
- const content = readFileSync(gitignorePath, "utf-8");
2393
- if (content.split("\n").some((l) => l.trim() === entry)) return;
2394
- writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
2395
- } else {
2396
- writeFileSync(gitignorePath, entry + "\n");
2397
- }
2398
- } catch (err) {
2399
- brakitDebug(`ensureGitignore failed: ${getErrorMessage(err)}`);
2400
- }
2401
- }
2402
- async function ensureGitignoreAsync(dir, entry) {
2403
- try {
2404
- const gitignorePath = resolve(dir, "../.gitignore");
2405
- if (await fileExists(gitignorePath)) {
2406
- const content = await readFile(gitignorePath, "utf-8");
2407
- if (content.split("\n").some((l) => l.trim() === entry)) return;
2408
- await writeFile(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
2409
- } else {
2410
- await writeFile(gitignorePath, entry + "\n");
2411
- }
2412
- } catch (err) {
2413
- brakitDebug(`ensureGitignoreAsync failed: ${getErrorMessage(err)}`);
2414
- }
2415
- }
2416
- var init_fs = __esm({
2417
- "src/utils/fs.ts"() {
2418
- "use strict";
2419
- init_config();
2420
- init_log();
2421
- init_type_guards();
2422
- }
2423
- });
2424
-
2425
- // src/utils/atomic-writer.ts
2426
- import {
2427
- writeFileSync as writeFileSync2,
2428
- existsSync as existsSync2,
2429
- mkdirSync as mkdirSync2,
2430
- renameSync
2431
- } from "fs";
2432
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
2433
- var AtomicWriter;
2434
- var init_atomic_writer = __esm({
2435
- "src/utils/atomic-writer.ts"() {
2436
- "use strict";
2437
- init_fs();
2438
- init_log();
2439
- init_type_guards();
2440
- AtomicWriter = class {
2441
- constructor(opts) {
2442
- this.opts = opts;
2443
- this.writing = false;
2444
- this.pendingContent = null;
2445
- this.tmpPath = opts.filePath + ".tmp";
2446
- }
2447
- writeSync(content) {
2448
- try {
2449
- this.ensureDir();
2450
- writeFileSync2(this.tmpPath, content);
2451
- renameSync(this.tmpPath, this.opts.filePath);
2452
- } catch (err) {
2453
- brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2454
- }
2455
- }
2456
- async writeAsync(content) {
2457
- if (this.writing) {
2458
- this.pendingContent = content;
2459
- return;
2460
- }
2461
- this.writing = true;
2462
- try {
2463
- await this.ensureDirAsync();
2464
- await writeFile2(this.tmpPath, content);
2465
- await rename(this.tmpPath, this.opts.filePath);
2466
- } catch (err) {
2467
- brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2468
- } finally {
2469
- this.writing = false;
2470
- if (this.pendingContent !== null) {
2471
- const next = this.pendingContent;
2472
- this.pendingContent = null;
2473
- this.writeAsync(next).catch(() => {
2474
- });
2475
- }
2476
- }
2477
- }
2478
- ensureDir() {
2479
- if (!existsSync2(this.opts.dir)) {
2480
- mkdirSync2(this.opts.dir, { recursive: true });
2481
- if (this.opts.gitignoreEntry) {
2482
- ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
2483
- }
2484
- }
2485
- }
2486
- async ensureDirAsync() {
2487
- if (!await fileExists(this.opts.dir)) {
2488
- await mkdir(this.opts.dir, { recursive: true });
2489
- if (this.opts.gitignoreEntry) {
2490
- await ensureGitignoreAsync(this.opts.dir, this.opts.gitignoreEntry);
2491
- }
2492
- }
2493
- }
2494
- };
2495
- }
2496
- });
2497
-
2498
- // src/utils/issue-id.ts
2499
- import { createHash as createHash2 } from "crypto";
2500
- function computeIssueId(issue) {
2501
- const stableDesc = issue.desc.replace(/\d[\d,.]*\s*\w*/g, "#");
2502
- const key = `${issue.rule}:${issue.endpoint ?? "global"}:${stableDesc}`;
2503
- return createHash2("sha256").update(key).digest("hex").slice(0, ISSUE_ID_HASH_LENGTH);
2504
- }
2505
- var init_issue_id = __esm({
2506
- "src/utils/issue-id.ts"() {
2507
- "use strict";
2508
- init_config();
2509
- }
2510
- });
2511
-
2512
- // src/store/issue-store.ts
2513
- import { readFile as readFile2 } from "fs/promises";
2514
- import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
2515
- import { resolve as resolve2 } from "path";
2516
- var IssueStore;
2517
- var init_issue_store = __esm({
2518
- "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"() {
2519
2283
  "use strict";
2520
2284
  init_fs();
2521
2285
  init_config();
@@ -2658,7 +2422,7 @@ var init_issue_store = __esm({
2658
2422
  this.issues.clear();
2659
2423
  this.dirty = false;
2660
2424
  try {
2661
- if (existsSync3(this.issuesPath)) {
2425
+ if (existsSync4(this.issuesPath)) {
2662
2426
  unlinkSync(this.issuesPath);
2663
2427
  }
2664
2428
  } catch {
@@ -2680,17 +2444,23 @@ var init_issue_store = __esm({
2680
2444
  /** Sync load for tests only — not used in production paths. */
2681
2445
  loadSync() {
2682
2446
  try {
2683
- if (existsSync3(this.issuesPath)) {
2684
- const raw = readFileSync2(this.issuesPath, "utf-8");
2447
+ if (existsSync4(this.issuesPath)) {
2448
+ const raw = readFileSync3(this.issuesPath, "utf-8");
2685
2449
  this.hydrate(raw);
2686
2450
  }
2687
2451
  } catch (err) {
2688
2452
  brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
2689
2453
  }
2690
2454
  }
2691
- /** Parse and populate issues from a raw JSON string. */
2692
2455
  hydrate(raw) {
2693
- 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);
2694
2464
  if (!validated) return;
2695
2465
  for (const issue of validated.issues) {
2696
2466
  this.issues.set(issue.issueId, issue);
@@ -2703,8 +2473,12 @@ var init_issue_store = __esm({
2703
2473
  }
2704
2474
  flushSync() {
2705
2475
  if (!this.dirty) return;
2706
- this.writer.writeSync(this.serialize());
2707
- 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
+ }
2708
2482
  }
2709
2483
  serialize() {
2710
2484
  const data = {
@@ -2719,19 +2493,26 @@ var init_issue_store = __esm({
2719
2493
 
2720
2494
  // src/detect/project.ts
2721
2495
  import { readFile as readFile3, readdir } from "fs/promises";
2722
- import { existsSync as existsSync4 } from "fs";
2723
- import { join as join2, relative } from "path";
2496
+ import { existsSync as existsSync5 } from "fs";
2497
+ import { join as join3, relative } from "path";
2724
2498
  function detectFrameworkFromDeps(allDeps) {
2725
2499
  for (const f of FRAMEWORKS) {
2726
2500
  if (allDeps[f.dep]) return f.name;
2727
2501
  }
2728
2502
  return "unknown";
2729
2503
  }
2730
- function detectPackageManagerSync(rootDir) {
2731
- if (existsSync4(join2(rootDir, "bun.lockb")) || existsSync4(join2(rootDir, "bun.lock"))) return "bun";
2732
- if (existsSync4(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
2733
- if (existsSync4(join2(rootDir, "yarn.lock"))) return "yarn";
2734
- 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";
2735
2516
  return "unknown";
2736
2517
  }
2737
2518
  var FRAMEWORKS;
@@ -2739,6 +2520,7 @@ var init_project = __esm({
2739
2520
  "src/detect/project.ts"() {
2740
2521
  "use strict";
2741
2522
  init_fs();
2523
+ init_detection();
2742
2524
  FRAMEWORKS = [
2743
2525
  // Meta-frameworks first (they bundle Express/Vite internally)
2744
2526
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
@@ -3491,14 +3273,14 @@ var init_disposable = __esm({
3491
3273
  "use strict";
3492
3274
  SubscriptionBag = class {
3493
3275
  constructor() {
3494
- this.items = [];
3276
+ this.items = /* @__PURE__ */ new Set();
3495
3277
  }
3496
3278
  add(teardown) {
3497
- this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
3279
+ this.items.add(typeof teardown === "function" ? { dispose: teardown } : teardown);
3498
3280
  }
3499
3281
  dispose() {
3500
3282
  for (const d of this.items) d.dispose();
3501
- this.items.length = 0;
3283
+ this.items.clear();
3502
3284
  }
3503
3285
  };
3504
3286
  }
@@ -4113,275 +3895,936 @@ var init_pattern_rules = __esm({
4113
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."
4114
3896
  });
4115
3897
  }
4116
- 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();
4117
4395
  }
4118
4396
  };
4119
- crossEndpointRule = {
4120
- id: "cross-endpoint",
4121
- check(ctx) {
4122
- const insights = [];
4123
- const queryMap = /* @__PURE__ */ new Map();
4124
- const allEndpoints = /* @__PURE__ */ new Set();
4125
- for (const [reqId, reqQueries] of ctx.queriesByReq) {
4126
- const req = ctx.reqById.get(reqId);
4127
- if (!req) continue;
4128
- const endpoint = getEndpointKey(req.method, req.path);
4129
- allEndpoints.add(endpoint);
4130
- const seenInReq = /* @__PURE__ */ new Set();
4131
- for (const query of reqQueries) {
4132
- const shape = getQueryShape(query);
4133
- let entry = queryMap.get(shape);
4134
- if (!entry) {
4135
- entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: query };
4136
- queryMap.set(shape, entry);
4137
- }
4138
- entry.count++;
4139
- if (!seenInReq.has(shape)) {
4140
- seenInReq.add(shape);
4141
- 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;
4142
4488
  }
4489
+ routeSDKEvent(event, stores);
4143
4490
  }
4491
+ res.writeHead(HTTP_NO_CONTENT);
4492
+ res.end();
4493
+ return;
4144
4494
  }
4145
- if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
4146
- for (const [, queryMetric] of queryMap) {
4147
- if (queryMetric.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
4148
- if (queryMetric.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
4149
- const coveragePct = Math.round(queryMetric.endpoints.size / allEndpoints.size * 100);
4150
- if (coveragePct < CROSS_ENDPOINT_PCT) continue;
4151
- const info = getQueryInfo(queryMetric.first);
4152
- const label = info.op + (info.table ? ` ${info.table}` : "");
4153
- insights.push({
4154
- severity: "warning",
4155
- type: "cross-endpoint",
4156
- title: "Repeated Query Across Endpoints",
4157
- desc: `${label} runs on ${queryMetric.endpoints.size} of ${allEndpoints.size} endpoints (${coveragePct}%).`,
4158
- hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
4159
- detail: `Endpoints: ${[...queryMetric.endpoints].slice(0, 5).join(", ")}${queryMetric.endpoints.size > 5 ? ` +${queryMetric.endpoints.size - 5} more` : ""}. Total: ${queryMetric.count} executions.`
4160
- });
4495
+ if (isBrakitBatch(body)) {
4496
+ for (const event of body.events) {
4497
+ routeEvent(event);
4161
4498
  }
4499
+ res.writeHead(HTTP_NO_CONTENT);
4500
+ res.end();
4501
+ return;
4162
4502
  }
4163
- return insights;
4503
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid batch" });
4504
+ } catch {
4505
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid JSON" });
4164
4506
  }
4165
- };
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();
4166
4525
  }
4167
4526
  });
4168
4527
 
4169
- // src/analysis/insights/rules/security.ts
4170
- var securityRule;
4171
- var init_security = __esm({
4172
- "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"() {
4173
4544
  "use strict";
4174
- securityRule = {
4175
- id: "security",
4176
- check(ctx) {
4177
- if (!ctx.securityFindings) return [];
4178
- return ctx.securityFindings.map((finding) => ({
4179
- severity: finding.severity,
4180
- type: "security",
4181
- title: finding.title,
4182
- desc: finding.desc,
4183
- hint: finding.hint,
4184
- detail: finding.detail
4185
- }));
4186
- }
4187
- };
4545
+ init_shared2();
4546
+ init_labels();
4188
4547
  }
4189
4548
  });
4190
4549
 
4191
- // src/analysis/insights/rules/index.ts
4192
- var init_rules2 = __esm({
4193
- "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"() {
4194
4559
  "use strict";
4195
- init_query_rules();
4196
- init_response_rules();
4197
- init_reliability_rules();
4198
- init_pattern_rules();
4199
- init_security();
4560
+ init_shared2();
4200
4561
  }
4201
4562
  });
4202
4563
 
4203
- // src/analysis/insights/index.ts
4204
- function createDefaultInsightRunner() {
4205
- const runner = new InsightRunner();
4206
- runner.register(n1Rule);
4207
- runner.register(crossEndpointRule);
4208
- runner.register(redundantQueryRule);
4209
- runner.register(errorRule);
4210
- runner.register(errorHotspotRule);
4211
- runner.register(duplicateRule);
4212
- runner.register(slowRule);
4213
- runner.register(queryHeavyRule);
4214
- runner.register(selectStarRule);
4215
- runner.register(highRowsRule);
4216
- runner.register(responseOverfetchRule);
4217
- runner.register(largeResponseRule);
4218
- runner.register(regressionRule);
4219
- runner.register(securityRule);
4220
- 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
+ };
4221
4590
  }
4222
- function computeInsights(ctx) {
4223
- 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
+ };
4224
4620
  }
4225
- var init_insights = __esm({
4226
- "src/analysis/insights/index.ts"() {
4621
+ var MAX_BATCH_IDS;
4622
+ var init_activity = __esm({
4623
+ "src/dashboard/api/activity.ts"() {
4227
4624
  "use strict";
4228
- init_runner();
4229
- init_runner();
4230
- init_rules2();
4625
+ init_shared2();
4626
+ init_labels();
4627
+ init_log();
4628
+ MAX_BATCH_IDS = 50;
4231
4629
  }
4232
4630
  });
4233
4631
 
4234
- // src/analysis/insights.ts
4235
- var init_insights2 = __esm({
4236
- "src/analysis/insights.ts"() {
4632
+ // src/dashboard/api/index.ts
4633
+ var init_api = __esm({
4634
+ "src/dashboard/api/index.ts"() {
4237
4635
  "use strict";
4238
- init_insights();
4636
+ init_handlers();
4637
+ init_ingest();
4638
+ init_metrics();
4639
+ init_metrics_live();
4640
+ init_activity();
4239
4641
  }
4240
4642
  });
4241
4643
 
4242
- // src/analysis/issue-mappers.ts
4243
- function categorizeInsight(type) {
4244
- if (type === "security") return "security";
4245
- if (type === "error" || type === "error-hotspot") return "reliability";
4246
- 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
+ };
4247
4661
  }
4248
- function insightToIssue(insight) {
4249
- return {
4250
- category: categorizeInsight(insight.type),
4251
- rule: insight.type,
4252
- severity: insight.severity,
4253
- title: insight.title,
4254
- desc: insight.desc,
4255
- hint: insight.hint,
4256
- detail: insight.detail,
4257
- 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
+ });
4258
4677
  };
4259
4678
  }
4260
- function securityFindingToIssue(finding) {
4261
- return {
4262
- category: "security",
4263
- rule: finding.rule,
4264
- severity: finding.severity,
4265
- title: finding.title,
4266
- desc: finding.desc,
4267
- hint: finding.hint,
4268
- detail: finding.detail,
4269
- 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" });
4270
4706
  };
4271
4707
  }
4272
- var init_issue_mappers = __esm({
4273
- "src/analysis/issue-mappers.ts"() {
4708
+ var init_issues = __esm({
4709
+ "src/dashboard/api/issues.ts"() {
4274
4710
  "use strict";
4275
- init_endpoint();
4711
+ init_shared2();
4712
+ init_type_guards();
4713
+ init_labels();
4276
4714
  }
4277
4715
  });
4278
4716
 
4279
- // src/analysis/engine.ts
4280
- var AnalysisEngine;
4281
- var init_engine = __esm({
4282
- "src/analysis/engine.ts"() {
4283
- "use strict";
4284
- init_config();
4285
- init_disposable();
4286
- init_group();
4287
- init_rules();
4288
- init_insights2();
4289
- init_issue_mappers();
4290
- init_issue_id();
4291
- init_prepare();
4292
- AnalysisEngine = class {
4293
- constructor(services, debounceMs = ANALYSIS_DEBOUNCE_MS) {
4294
- this.services = services;
4295
- this.debounceMs = debounceMs;
4296
- this.cachedInsights = [];
4297
- this.cachedFindings = [];
4298
- this.debounceTimer = null;
4299
- this.subs = new SubscriptionBag();
4300
- this.scanner = createDefaultScanner();
4301
- }
4302
- start() {
4303
- const bus = this.services.bus;
4304
- this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
4305
- this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
4306
- this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
4307
- this.subs.add(bus.on("telemetry:log", () => this.scheduleRecompute()));
4308
- }
4309
- stop() {
4310
- this.subs.dispose();
4311
- if (this.debounceTimer) {
4312
- clearTimeout(this.debounceTimer);
4313
- this.debounceTimer = null;
4314
- }
4315
- }
4316
- getInsights() {
4317
- return this.cachedInsights;
4318
- }
4319
- getFindings() {
4320
- return this.cachedFindings;
4321
- }
4322
- scheduleRecompute() {
4323
- if (this.debounceTimer) return;
4324
- this.debounceTimer = setTimeout(() => {
4325
- this.debounceTimer = null;
4326
- this.recompute();
4327
- }, this.debounceMs);
4328
- }
4329
- recompute() {
4330
- const allRequests = this.services.requestStore.getAll();
4331
- const queries = this.services.queryStore.getAll();
4332
- const errors = this.services.errorStore.getAll();
4333
- const logs = this.services.logStore.getAll();
4334
- const fetches = this.services.fetchStore.getAll();
4335
- const requests = keepRecentPerEndpoint(allRequests);
4336
- const flows = groupRequestsIntoFlows(requests);
4337
- this.cachedFindings = this.scanner.scan({ requests, logs });
4338
- this.cachedInsights = computeInsights({
4339
- requests,
4340
- queries,
4341
- errors,
4342
- flows,
4343
- fetches,
4344
- previousMetrics: this.services.metricsStore.getAll(),
4345
- securityFindings: this.cachedFindings
4346
- });
4347
- const issueStore = this.services.issueStore;
4348
- const currentIssueIds = /* @__PURE__ */ new Set();
4349
- for (const finding of this.cachedFindings) {
4350
- const issue = securityFindingToIssue(finding);
4351
- issueStore.upsert(issue, "passive");
4352
- currentIssueIds.add(computeIssueId(issue));
4353
- }
4354
- for (const insight of this.cachedInsights) {
4355
- const issue = insightToIssue(insight);
4356
- issueStore.upsert(issue, "passive");
4357
- currentIssueIds.add(computeIssueId(issue));
4358
- }
4359
- const activeEndpoints = extractActiveEndpoints(allRequests);
4360
- issueStore.reconcile(currentIssueIds, activeEndpoints);
4361
- const update = {
4362
- insights: this.cachedInsights,
4363
- findings: this.cachedFindings,
4364
- issues: issueStore.getAll()
4365
- };
4366
- this.services.bus.emit("analysis:updated", update);
4367
- }
4368
- };
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"() {
4744
+ "use strict";
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;
4369
4750
  }
4370
4751
  });
4371
4752
 
4372
- // src/index.ts
4373
- var VERSION;
4374
- var init_src = __esm({
4375
- "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"() {
4376
4824
  "use strict";
4377
- init_issue_store();
4378
- init_project();
4379
- init_adapter_registry();
4380
- init_rules();
4381
- init_engine();
4382
- init_insights2();
4383
- init_insights();
4384
- VERSION = "0.10.1";
4825
+ init_constants();
4826
+ init_labels();
4827
+ init_shared2();
4385
4828
  }
4386
4829
  });
4387
4830
 
@@ -5323,13 +5766,24 @@ function getInsightsStyles() {
5323
5766
  .insights-chip-count{font-size:10px;font-family:var(--mono);background:rgba(0,0,0,.08);padding:1px 5px;border-radius:8px}
5324
5767
  .insights-chip.active .insights-chip-count{background:rgba(255,255,255,.25)}
5325
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
+
5326
5780
  /* Insights card list */
5327
5781
  .insights-list{padding:16px 28px}
5328
5782
 
5329
5783
  .insights-empty{display:flex;align-items:center;gap:10px;padding:24px;color:var(--green);font-size:14px;font-weight:500}
5330
5784
  .insights-empty-icon{font-size:18px}
5331
5785
 
5332
- .insights-card{display:flex;align-items:flex-start;gap:12px;padding:14px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .15s;margin-bottom:8px}
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}
5333
5787
  .insights-card:hover{border-color:var(--border-light);box-shadow:0 2px 8px rgba(0,0,0,.04)}
5334
5788
  .insights-card.expanded{border-color:var(--border-light);box-shadow:0 2px 8px rgba(0,0,0,.04)}
5335
5789
  .insights-card.resolved{opacity:.55}
@@ -5410,13 +5864,13 @@ var init_layout2 = __esm({
5410
5864
  });
5411
5865
 
5412
5866
  // src/dashboard/page.ts
5413
- import { readFileSync as readFileSync3 } from "fs";
5867
+ import { readFileSync as readFileSync4 } from "fs";
5414
5868
  import { resolve as resolve3, dirname } from "path";
5415
5869
  import { fileURLToPath } from "url";
5416
5870
  function getClientBundle() {
5417
5871
  if (clientBundle) return clientBundle;
5418
5872
  const bundlePath = resolve3(__dirname, "../dashboard-client.global.js");
5419
- clientBundle = readFileSync3(bundlePath, "utf-8");
5873
+ clientBundle = readFileSync4(bundlePath, "utf-8");
5420
5874
  return clientBundle;
5421
5875
  }
5422
5876
  function getDashboardHtml(config) {
@@ -5447,247 +5901,6 @@ var init_page = __esm({
5447
5901
  }
5448
5902
  });
5449
5903
 
5450
- // src/telemetry/config.ts
5451
- import { homedir as homedir2, platform } from "os";
5452
- import { join as join3 } from "path";
5453
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
5454
- import { randomUUID as randomUUID5 } from "crypto";
5455
- function readConfig() {
5456
- try {
5457
- if (!existsSync5(CONFIG_PATH)) return null;
5458
- return JSON.parse(readFileSync4(CONFIG_PATH, "utf-8"));
5459
- } catch {
5460
- return null;
5461
- }
5462
- }
5463
- function writeConfig(config) {
5464
- try {
5465
- if (!existsSync5(CONFIG_DIR))
5466
- mkdirSync3(CONFIG_DIR, { recursive: true, ...IS_WINDOWS ? {} : { mode: DIR_MODE_OWNER_ONLY } });
5467
- writeFileSync3(
5468
- CONFIG_PATH,
5469
- JSON.stringify(config, null, 2) + "\n",
5470
- IS_WINDOWS ? {} : { mode: FILE_MODE_OWNER_ONLY }
5471
- );
5472
- } catch (err) {
5473
- if (process.env.BRAKIT_DEBUG) {
5474
- process.stderr.write(`[brakit] config write failed: ${err?.message ?? err}
5475
- `);
5476
- }
5477
- }
5478
- }
5479
- function getOrCreateConfig() {
5480
- const existing = readConfig();
5481
- if (existing && typeof existing.telemetry === "boolean" && existing.anonymousId) {
5482
- return existing;
5483
- }
5484
- const config = { telemetry: true, anonymousId: randomUUID5() };
5485
- writeConfig(config);
5486
- return config;
5487
- }
5488
- function isTelemetryEnabled() {
5489
- if (cachedEnabled !== null) return cachedEnabled;
5490
- const env = process.env.BRAKIT_TELEMETRY;
5491
- if (env !== void 0) {
5492
- cachedEnabled = env !== "false" && env !== "0" && env !== "off";
5493
- return cachedEnabled;
5494
- }
5495
- cachedEnabled = readConfig()?.telemetry ?? true;
5496
- return cachedEnabled;
5497
- }
5498
- var IS_WINDOWS, CONFIG_DIR, CONFIG_PATH, cachedEnabled;
5499
- var init_config2 = __esm({
5500
- "src/telemetry/config.ts"() {
5501
- "use strict";
5502
- init_features();
5503
- IS_WINDOWS = platform() === "win32";
5504
- CONFIG_DIR = join3(homedir2(), ".brakit");
5505
- CONFIG_PATH = join3(CONFIG_DIR, "config.json");
5506
- cachedEnabled = null;
5507
- }
5508
- });
5509
-
5510
- // src/telemetry/index.ts
5511
- import { platform as platform2, release, arch } from "os";
5512
- import { spawn } from "child_process";
5513
- function commonProperties() {
5514
- return {
5515
- brakit_version: VERSION,
5516
- node_version: process.version,
5517
- os: `${platform2()}-${release()}`,
5518
- arch: arch(),
5519
- $lib: "brakit",
5520
- $process_person_profile: false,
5521
- $geoip_disable: true
5522
- };
5523
- }
5524
- function sendToPosthog(event, properties) {
5525
- if (!isTelemetryEnabled()) return;
5526
- const config = getOrCreateConfig();
5527
- const payload = {
5528
- api_key: POSTHOG_KEY,
5529
- event,
5530
- distinct_id: config.anonymousId,
5531
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5532
- properties: { ...commonProperties(), ...properties }
5533
- };
5534
- try {
5535
- const body = JSON.stringify(payload);
5536
- const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
5537
- const child = spawn(
5538
- process.execPath,
5539
- [
5540
- "-e",
5541
- `fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
5542
- ],
5543
- { detached: true, stdio: "ignore" }
5544
- );
5545
- child.unref();
5546
- } catch {
5547
- }
5548
- }
5549
- function trackEvent(event, properties) {
5550
- sendToPosthog(event, { sdk: "node", ...properties });
5551
- }
5552
- function initSession(framework, packageManager, isCustomCommand, adapters) {
5553
- getOrCreateConfig();
5554
- session.startTime = Date.now();
5555
- session.framework = framework;
5556
- session.packageManager = packageManager;
5557
- session.isCustomCommand = isCustomCommand;
5558
- session.adapters = adapters;
5559
- }
5560
- function recordRequestCount(count) {
5561
- session.requestCount = count;
5562
- }
5563
- function recordInsightTypes(types) {
5564
- for (const t of types) session.insightTypes.add(t);
5565
- }
5566
- function recordRulesTriggered(rules) {
5567
- for (const r of rules) session.rulesTriggered.add(r);
5568
- }
5569
- function recordTabViewed(tab) {
5570
- session.tabsViewed.add(tab);
5571
- }
5572
- function recordDashboardOpened() {
5573
- if (session.dashboardOpened) return;
5574
- session.dashboardOpened = true;
5575
- session.dashboardOpenedAt = Date.now();
5576
- trackEvent(TELEMETRY_EVENT_DASHBOARD_VIEWED, {
5577
- time_to_dashboard_ms: session.startTime > 0 ? Date.now() - session.startTime : null,
5578
- request_count_at_open: session.requestCount
5579
- });
5580
- }
5581
- function recordGraphFeature(feature, detail) {
5582
- trackEvent(TELEMETRY_EVENT_GRAPH_FEATURE, {
5583
- feature,
5584
- ...detail ? { detail } : {}
5585
- });
5586
- }
5587
- function recordSetupCompleted(info) {
5588
- session.frameworkCandidates = info.frameworkCandidates;
5589
- session.adaptersFailed = info.adaptersFailed;
5590
- session.setupDurationMs = info.setupDurationMs;
5591
- session.setupSucceeded = true;
5592
- }
5593
- function recordFirstRequest() {
5594
- if (!session.firstRequestAt) session.firstRequestAt = Date.now();
5595
- }
5596
- function recordExitReason(reason) {
5597
- if (session.exitReason === "unknown") session.exitReason = reason;
5598
- }
5599
- function speedBucket(ms) {
5600
- if (ms === 0) return "none";
5601
- const t = SPEED_BUCKET_THRESHOLDS;
5602
- if (ms < t[0]) return `<${t[0]}ms`;
5603
- for (let i = 1; i < t.length; i++) {
5604
- if (ms < t[i]) return `${t[i - 1]}-${t[i]}ms`;
5605
- }
5606
- return `>${t[t.length - 1]}ms`;
5607
- }
5608
- function trackSession(services) {
5609
- if (!isTelemetryEnabled()) return;
5610
- const isFirstSession = readConfig() === null;
5611
- const metricsStore = services.metricsStore;
5612
- const analysisEngine = services.analysisEngine;
5613
- const live = metricsStore.getLiveEndpoints();
5614
- const insights = analysisEngine.getInsights();
5615
- const findings = analysisEngine.getFindings();
5616
- let totalRequests = 0;
5617
- let totalDuration = 0;
5618
- let slowestP95 = 0;
5619
- for (const ep of live) {
5620
- totalRequests += ep.summary.totalRequests;
5621
- totalDuration += ep.summary.p95Ms * ep.summary.totalRequests;
5622
- if (ep.summary.p95Ms > slowestP95) slowestP95 = ep.summary.p95Ms;
5623
- }
5624
- const now = Date.now();
5625
- sendToPosthog(TELEMETRY_EVENT_SESSION, {
5626
- sdk: "node",
5627
- framework: session.framework,
5628
- package_manager: session.packageManager,
5629
- is_custom_command: session.isCustomCommand,
5630
- first_session: isFirstSession,
5631
- adapters_detected: session.adapters,
5632
- request_count: session.requestCount,
5633
- error_count: services.errorStore.getAll().length,
5634
- query_count: services.queryStore.getAll().length,
5635
- fetch_count: services.fetchStore.getAll().length,
5636
- insight_count: insights.length,
5637
- finding_count: findings.length,
5638
- insight_types: [...session.insightTypes],
5639
- rules_triggered: [...session.rulesTriggered],
5640
- endpoint_count: live.length,
5641
- avg_duration_ms: totalRequests > 0 ? Math.round(totalDuration / totalRequests) : 0,
5642
- slowest_endpoint_bucket: speedBucket(slowestP95),
5643
- tabs_viewed: [...session.tabsViewed],
5644
- dashboard_opened: session.dashboardOpened,
5645
- explain_used: session.explainUsed,
5646
- session_duration_s: Math.ceil((now - session.startTime) / 1e3),
5647
- session_duration_ms: now - session.startTime,
5648
- setup_succeeded: session.setupSucceeded,
5649
- setup_duration_ms: session.setupDurationMs,
5650
- framework_detection_candidates: session.frameworkCandidates,
5651
- adapters_failed: session.adaptersFailed,
5652
- time_to_first_request_ms: session.firstRequestAt ? session.firstRequestAt - session.startTime : null,
5653
- time_to_dashboard_ms: session.dashboardOpenedAt ? session.dashboardOpenedAt - session.startTime : null,
5654
- exit_reason: session.exitReason
5655
- });
5656
- getOrCreateConfig();
5657
- }
5658
- var POSTHOG_KEY, session;
5659
- var init_telemetry = __esm({
5660
- "src/telemetry/index.ts"() {
5661
- "use strict";
5662
- init_src();
5663
- init_config2();
5664
- init_labels();
5665
- init_config();
5666
- init_config2();
5667
- POSTHOG_KEY = "phc_E9TwydCGnSfPLIUhNxChpeg32TSowjk31KiPhnLPP0x";
5668
- session = {
5669
- startTime: 0,
5670
- framework: "",
5671
- packageManager: "",
5672
- isCustomCommand: false,
5673
- adapters: [],
5674
- requestCount: 0,
5675
- insightTypes: /* @__PURE__ */ new Set(),
5676
- rulesTriggered: /* @__PURE__ */ new Set(),
5677
- tabsViewed: /* @__PURE__ */ new Set(),
5678
- dashboardOpened: false,
5679
- explainUsed: false,
5680
- frameworkCandidates: [],
5681
- adaptersFailed: [],
5682
- setupDurationMs: 0,
5683
- setupSucceeded: false,
5684
- firstRequestAt: 0,
5685
- dashboardOpenedAt: 0,
5686
- exitReason: "unknown"
5687
- };
5688
- }
5689
- });
5690
-
5691
5904
  // src/dashboard/router.ts
5692
5905
  function isDashboardRequest(url) {
5693
5906
  return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
@@ -5722,7 +5935,7 @@ function createDashboardHandler(services) {
5722
5935
  const url = new URL(req.url ?? "/", "http://localhost");
5723
5936
  const tab = url.searchParams.get("tab");
5724
5937
  if (tab && tab.length <= MAX_TAB_NAME_LENGTH && VALID_TABS.has(tab)) {
5725
- recordTabViewed(tab);
5938
+ session.recordTabViewed(tab);
5726
5939
  }
5727
5940
  const event = url.searchParams.get("event");
5728
5941
  if (event && event.length <= MAX_TAB_NAME_LENGTH) {
@@ -5740,7 +5953,7 @@ function createDashboardHandler(services) {
5740
5953
  handler(req, res);
5741
5954
  return;
5742
5955
  }
5743
- if (isTelemetryEnabled()) recordDashboardOpened();
5956
+ if (isTelemetryEnabled()) session.recordDashboardOpened();
5744
5957
  res.writeHead(HTTP_OK, {
5745
5958
  "content-type": "text/html; charset=utf-8",
5746
5959
  "cache-control": "no-cache",
@@ -5760,7 +5973,7 @@ var init_router = __esm({
5760
5973
  init_graph();
5761
5974
  init_sse();
5762
5975
  init_page();
5763
- init_telemetry();
5976
+ init_telemetry2();
5764
5977
  }
5765
5978
  });
5766
5979
 
@@ -5899,7 +6112,7 @@ var init_telemetry_store = __esm({
5899
6112
  constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
5900
6113
  this.maxEntries = maxEntries;
5901
6114
  this.entries = [];
5902
- this.listeners = [];
6115
+ this.listeners = /* @__PURE__ */ new Set();
5903
6116
  }
5904
6117
  add(data) {
5905
6118
  const entry = { id: randomUUID6(), ...data };
@@ -5918,11 +6131,10 @@ var init_telemetry_store = __esm({
5918
6131
  this.entries.length = 0;
5919
6132
  }
5920
6133
  onEntry(fn) {
5921
- this.listeners.push(fn);
6134
+ this.listeners.add(fn);
5922
6135
  }
5923
6136
  offEntry(fn) {
5924
- const idx = this.listeners.indexOf(fn);
5925
- if (idx !== -1) this.listeners.splice(idx, 1);
6137
+ this.listeners.delete(fn);
5926
6138
  }
5927
6139
  };
5928
6140
  }
@@ -6905,55 +7117,34 @@ import pc from "picocolors";
6905
7117
  function print(line) {
6906
7118
  process.stdout.write(line + "\n");
6907
7119
  }
6908
- function severityIcon(severity) {
6909
- return SEVERITY_COLOR[severity](SEVERITY_ICON[severity]);
6910
- }
6911
- function colorTitle(severity, text) {
6912
- const color = SEVERITY_COLOR[severity];
6913
- return severity === "info" ? color(text) : color(pc.bold(text));
6914
- }
6915
- function truncate(s, max = TERMINAL_TRUNCATE_LENGTH) {
6916
- return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
6917
- }
6918
- function formatConsoleLine(issue, suffix) {
6919
- const icon = severityIcon(issue.severity);
6920
- const title = colorTitle(issue.severity, issue.title);
6921
- const desc = pc.dim(truncate(issue.desc) + (suffix ?? ""));
6922
- let line = ` ${icon} ${title} \u2014 ${desc}`;
6923
- if (issue.detail) {
6924
- line += `
6925
- ${pc.dim("\u2514 " + issue.detail)}`;
6926
- }
6927
- return line;
7120
+ function pluralize(n, word) {
7121
+ return n === 1 ? `${n} ${word}` : `${n} ${word}s`;
6928
7122
  }
6929
7123
  function startTerminalInsights(services, proxyPort) {
6930
7124
  const bus = services.bus;
6931
- const metricsStore = services.metricsStore;
6932
7125
  const printedKeys = /* @__PURE__ */ new Set();
6933
7126
  const resolvedKeys = /* @__PURE__ */ new Set();
6934
- 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")}`;
6935
7130
  return bus.on("analysis:updated", ({ issues }) => {
6936
- const newLines = [];
6937
- const resolvedLines = [];
6938
- const regressedLines = [];
7131
+ let newCount = 0;
7132
+ let resolvedCount = 0;
7133
+ let regressedCount = 0;
6939
7134
  for (const si of issues) {
6940
7135
  if (si.aiStatus === "wont_fix") continue;
6941
7136
  if (si.state === "resolved") {
6942
7137
  if (resolvedKeys.has(si.issueId)) continue;
6943
7138
  resolvedKeys.add(si.issueId);
6944
7139
  printedKeys.delete(si.issueId);
6945
- const title = pc.green(pc.bold(`\u2713 ${si.issue.title}`));
6946
- const desc = pc.dim(truncate(si.issue.desc));
6947
- resolvedLines.push(` ${title} \u2014 ${desc} ${pc.green("resolved")}`);
7140
+ resolvedCount++;
6948
7141
  continue;
6949
7142
  }
6950
7143
  if (si.state === "regressed") {
6951
7144
  if (!printedKeys.has(si.issueId)) {
6952
7145
  printedKeys.add(si.issueId);
6953
7146
  resolvedKeys.delete(si.issueId);
6954
- const title = pc.red(pc.bold(`\u26A0 ${si.issue.title}`));
6955
- const desc = pc.dim(truncate(si.issue.desc));
6956
- regressedLines.push(` ${title} \u2014 ${desc} ${pc.red("regressed")}`);
7147
+ regressedCount++;
6957
7148
  }
6958
7149
  continue;
6959
7150
  }
@@ -6961,52 +7152,27 @@ function startTerminalInsights(services, proxyPort) {
6961
7152
  if (si.issue.severity === "info") continue;
6962
7153
  if (printedKeys.has(si.issueId)) continue;
6963
7154
  printedKeys.add(si.issueId);
6964
- let suffix;
6965
- if (si.issue.rule === "slow") {
6966
- const endpoint = si.issue.endpoint;
6967
- if (endpoint) {
6968
- const ep = metricsStore.getEndpoint(endpoint);
6969
- if (ep && ep.sessions.length > 1) {
6970
- const prev = ep.sessions[ep.sessions.length - 2];
6971
- suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
6972
- }
6973
- }
6974
- }
6975
- newLines.push(formatConsoleLine(si.issue, suffix));
7155
+ newCount++;
6976
7156
  }
6977
- if (newLines.length > 0) {
6978
- print("");
6979
- for (const line of newLines) print(line);
7157
+ if (newCount > 0) {
6980
7158
  print("");
6981
- 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}`);
6982
7160
  }
6983
- if (regressedLines.length > 0) {
7161
+ if (regressedCount > 0) {
6984
7162
  print("");
6985
- for (const line of regressedLines) print(line);
6986
- print("");
6987
- 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}`);
6988
7164
  }
6989
- if (resolvedLines.length > 0) {
6990
- print("");
6991
- for (const line of resolvedLines) print(line);
7165
+ if (resolvedCount > 0) {
6992
7166
  print("");
6993
- 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)}`);
6994
7168
  }
6995
7169
  });
6996
7170
  }
6997
- var SEVERITY_COLOR;
6998
7171
  var init_terminal = __esm({
6999
7172
  "src/output/terminal.ts"() {
7000
7173
  "use strict";
7001
7174
  init_src();
7002
7175
  init_constants();
7003
- init_config();
7004
- init_labels();
7005
- SEVERITY_COLOR = {
7006
- critical: pc.red,
7007
- warning: pc.yellow,
7008
- info: pc.dim
7009
- };
7010
7176
  }
7011
7177
  });
7012
7178
 
@@ -7154,13 +7320,46 @@ var init_interceptor = __esm({
7154
7320
  }
7155
7321
  });
7156
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
+
7157
7356
  // src/runtime/setup.ts
7158
7357
  var setup_exports = {};
7159
7358
  __export(setup_exports, {
7160
7359
  setup: () => setup
7161
7360
  });
7162
7361
  import { readFile as readFile5, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
7163
- import { existsSync as existsSync7, unlinkSync as unlinkSync3 } from "fs";
7362
+ import { existsSync as existsSync7, unlinkSync as unlinkSync3, readFileSync as readFileSync6 } from "fs";
7164
7363
  import { resolve as resolve5 } from "path";
7165
7364
  function setup() {
7166
7365
  if (initPromise) return initPromise;
@@ -7192,21 +7391,25 @@ function installHooks(bus) {
7192
7391
  adapterRegistry.patchAll(telemetryEmit);
7193
7392
  const cwd = process.cwd();
7194
7393
  let framework = "unknown";
7195
- let frameworkCandidates = [];
7394
+ let detectedDependencies = [];
7395
+ let configFilesDetected = [];
7196
7396
  try {
7197
7397
  const pkg = JSON.parse(
7198
- __require("fs").readFileSync(resolve5(cwd, "package.json"), "utf-8")
7398
+ readFileSync6(resolve5(cwd, "package.json"), "utf-8")
7199
7399
  );
7200
7400
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7201
7401
  framework = detectFrameworkFromDeps(allDeps);
7202
- frameworkCandidates = KNOWN_DEPENDENCY_NAMES.filter((dep) => dep in allDeps);
7402
+ detectedDependencies = KNOWN_DEPENDENCY_NAMES.filter((dep) => dep in allDeps);
7403
+ configFilesDetected = detectConfigFiles(cwd);
7203
7404
  } catch {
7204
7405
  }
7205
7406
  return {
7206
7407
  framework,
7207
7408
  adapterNames: adapterRegistry.getActive().map((a) => a.name),
7208
7409
  adaptersFailed: [...adapterRegistry.getFailed()],
7209
- frameworkCandidates
7410
+ detectedDependencies,
7411
+ configFilesDetected,
7412
+ jsRuntime: detectJsRuntime()
7210
7413
  };
7211
7414
  }
7212
7415
  function startAnalysis(bus, stores, dataDir, services) {
@@ -7235,14 +7438,12 @@ function registerLifecycle(allServices, stores, services, cwd) {
7235
7438
  const sendTelemetry = () => {
7236
7439
  if (telemetrySent) return;
7237
7440
  telemetrySent = true;
7238
- recordRequestCount(stores.requestStore.getAll().length);
7239
- recordInsightTypes(
7240
- services.analysisEngine.getInsights().map((i) => i.type)
7241
- );
7242
- recordRulesTriggered(
7441
+ session.recordCounts(
7442
+ stores.requestStore.getAll().length,
7443
+ services.analysisEngine.getInsights().map((i) => i.type),
7243
7444
  services.analysisEngine.getFindings().map((f) => f.rule)
7244
7445
  );
7245
- trackSession(allServices);
7446
+ flushSession(allServices);
7246
7447
  };
7247
7448
  let teardownCalled = false;
7248
7449
  const runTeardown = () => {
@@ -7262,14 +7463,14 @@ function registerLifecycle(allServices, stores, services, cwd) {
7262
7463
  };
7263
7464
  health.setTeardown(runTeardown);
7264
7465
  process.on("SIGINT", () => {
7265
- recordExitReason(EXIT_REASON_SIGINT);
7466
+ session.recordExitReason(EXIT_REASON_SIGINT);
7266
7467
  });
7267
7468
  process.on("SIGTERM", () => {
7268
- recordExitReason(EXIT_REASON_SIGTERM);
7469
+ session.recordExitReason(EXIT_REASON_SIGTERM);
7269
7470
  });
7270
7471
  process.on("beforeExit", async () => {
7271
7472
  await drainPendingCaptures();
7272
- recordExitReason(EXIT_REASON_CLEAN);
7473
+ session.recordExitReason(EXIT_REASON_CLEAN);
7273
7474
  sendTelemetry();
7274
7475
  });
7275
7476
  process.on("exit", () => {
@@ -7286,17 +7487,19 @@ async function doSetup() {
7286
7487
  bus,
7287
7488
  ...stores
7288
7489
  };
7289
- const { framework, adapterNames, adaptersFailed, frameworkCandidates } = installHooks(bus);
7290
- initSession(framework, detectPackageManagerSync(cwd), false, adapterNames);
7490
+ const detection = installHooks(bus);
7491
+ session.init(detection.framework, detectPackageManager(cwd), false, detection.adapterNames);
7291
7492
  const setupDurationMs = Date.now() - setupStart;
7292
- recordSetupCompleted({ frameworkCandidates, adaptersFailed, setupDurationMs });
7493
+ session.recordSetup(detection, setupDurationMs);
7293
7494
  trackEvent(TELEMETRY_EVENT_SETUP_COMPLETED, {
7294
- framework,
7295
- framework_detection_candidates: frameworkCandidates,
7296
- adapters_detected: adapterNames,
7297
- adapters_failed: adaptersFailed,
7298
- hooks_installed: ["fetch", "console", "error"],
7299
- 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
7300
7503
  });
7301
7504
  const graphBuilder = new GraphBuilder(bus, stores.requestStore);
7302
7505
  graphBuilder.start();
@@ -7317,10 +7520,13 @@ async function doSetup() {
7317
7520
  onFirstRequest(port) {
7318
7521
  setBrakitPort(port);
7319
7522
  brakitDebug(`[setup] onFirstRequest fired, port=${port}`);
7320
- recordFirstRequest();
7523
+ const loadedPackages = extractLoadedPackages();
7524
+ session.recordFirstRequest(loadedPackages);
7321
7525
  trackEvent(TELEMETRY_EVENT_FIRST_REQUEST, {
7322
7526
  port,
7323
- 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
7324
7530
  });
7325
7531
  void (async () => {
7326
7532
  try {
@@ -7376,13 +7582,15 @@ var init_setup = __esm({
7376
7582
  init_terminal();
7377
7583
  init_src();
7378
7584
  init_constants();
7585
+ init_detection();
7379
7586
  init_health();
7380
7587
  init_interceptor();
7381
7588
  init_log();
7382
7589
  init_type_guards();
7383
7590
  init_fs();
7384
7591
  init_project();
7385
- init_telemetry();
7592
+ init_runtime();
7593
+ init_telemetry2();
7386
7594
  initPromise = null;
7387
7595
  }
7388
7596
  });