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.
- package/dist/api.d.ts +41 -14
- package/dist/api.js +105 -17
- package/dist/bin/brakit.js +307 -37
- package/dist/dashboard-client.global.js +204 -191
- package/dist/dashboard.html +216 -192
- package/dist/mcp/server.js +78 -1
- package/dist/runtime/index.js +1330 -1122
- package/package.json +1 -1
package/dist/runtime/index.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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/
|
|
1976
|
-
|
|
1977
|
-
|
|
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
|
|
1980
|
-
|
|
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
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2057
|
-
|
|
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
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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/
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
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
|
-
|
|
2078
|
-
|
|
2162
|
+
} catch (err) {
|
|
2163
|
+
brakitDebug(`ensureGitignore failed: ${getErrorMessage(err)}`);
|
|
2164
|
+
}
|
|
2079
2165
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
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
|
|
2096
|
-
"src/
|
|
2180
|
+
var init_fs = __esm({
|
|
2181
|
+
"src/utils/fs.ts"() {
|
|
2097
2182
|
"use strict";
|
|
2098
|
-
|
|
2183
|
+
init_config();
|
|
2184
|
+
init_log();
|
|
2185
|
+
init_type_guards();
|
|
2099
2186
|
}
|
|
2100
2187
|
});
|
|
2101
2188
|
|
|
2102
|
-
// src/
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
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
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
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
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
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
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
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
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
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/
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
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
|
|
2247
|
-
"src/
|
|
2269
|
+
var init_issue_id = __esm({
|
|
2270
|
+
"src/utils/issue-id.ts"() {
|
|
2248
2271
|
"use strict";
|
|
2249
|
-
|
|
2250
|
-
init_type_guards();
|
|
2251
|
-
init_labels();
|
|
2272
|
+
init_config();
|
|
2252
2273
|
}
|
|
2253
2274
|
});
|
|
2254
2275
|
|
|
2255
|
-
// src/
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
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 (
|
|
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 (
|
|
2684
|
-
const raw =
|
|
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
|
-
|
|
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
|
-
|
|
2707
|
-
|
|
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
|
|
2723
|
-
import { join as
|
|
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
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
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 (
|
|
4146
|
-
for (const
|
|
4147
|
-
|
|
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
|
-
|
|
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/
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
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
|
-
|
|
4175
|
-
|
|
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/
|
|
4192
|
-
|
|
4193
|
-
|
|
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
|
-
|
|
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/
|
|
4204
|
-
function
|
|
4205
|
-
const
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
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
|
|
4223
|
-
return
|
|
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
|
|
4226
|
-
|
|
4621
|
+
var MAX_BATCH_IDS;
|
|
4622
|
+
var init_activity = __esm({
|
|
4623
|
+
"src/dashboard/api/activity.ts"() {
|
|
4227
4624
|
"use strict";
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4625
|
+
init_shared2();
|
|
4626
|
+
init_labels();
|
|
4627
|
+
init_log();
|
|
4628
|
+
MAX_BATCH_IDS = 50;
|
|
4231
4629
|
}
|
|
4232
4630
|
});
|
|
4233
4631
|
|
|
4234
|
-
// src/
|
|
4235
|
-
var
|
|
4236
|
-
"src/
|
|
4632
|
+
// src/dashboard/api/index.ts
|
|
4633
|
+
var init_api = __esm({
|
|
4634
|
+
"src/dashboard/api/index.ts"() {
|
|
4237
4635
|
"use strict";
|
|
4238
|
-
|
|
4636
|
+
init_handlers();
|
|
4637
|
+
init_ingest();
|
|
4638
|
+
init_metrics();
|
|
4639
|
+
init_metrics_live();
|
|
4640
|
+
init_activity();
|
|
4239
4641
|
}
|
|
4240
4642
|
});
|
|
4241
4643
|
|
|
4242
|
-
// src/
|
|
4243
|
-
function
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
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
|
|
4249
|
-
return {
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
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
|
|
4261
|
-
return {
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
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
|
|
4273
|
-
"src/
|
|
4708
|
+
var init_issues = __esm({
|
|
4709
|
+
"src/dashboard/api/issues.ts"() {
|
|
4274
4710
|
"use strict";
|
|
4275
|
-
|
|
4711
|
+
init_shared2();
|
|
4712
|
+
init_type_guards();
|
|
4713
|
+
init_labels();
|
|
4276
4714
|
}
|
|
4277
4715
|
});
|
|
4278
4716
|
|
|
4279
|
-
// src/
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
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/
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
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
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
6134
|
+
this.listeners.add(fn);
|
|
5922
6135
|
}
|
|
5923
6136
|
offEntry(fn) {
|
|
5924
|
-
|
|
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
|
|
6909
|
-
return
|
|
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
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
6978
|
-
print("");
|
|
6979
|
-
for (const line of newLines) print(line);
|
|
7157
|
+
if (newCount > 0) {
|
|
6980
7158
|
print("");
|
|
6981
|
-
print(
|
|
7159
|
+
print(`${prefix} ${pc.yellow(pluralize(newCount, "new issue"))} ${pc.dim(UNICODE_EM_DASH)} ${actionHint}`);
|
|
6982
7160
|
}
|
|
6983
|
-
if (
|
|
7161
|
+
if (regressedCount > 0) {
|
|
6984
7162
|
print("");
|
|
6985
|
-
|
|
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 (
|
|
6990
|
-
print("");
|
|
6991
|
-
for (const line of resolvedLines) print(line);
|
|
7165
|
+
if (resolvedCount > 0) {
|
|
6992
7166
|
print("");
|
|
6993
|
-
print(
|
|
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
|
|
7394
|
+
let detectedDependencies = [];
|
|
7395
|
+
let configFilesDetected = [];
|
|
7196
7396
|
try {
|
|
7197
7397
|
const pkg = JSON.parse(
|
|
7198
|
-
|
|
7398
|
+
readFileSync6(resolve5(cwd, "package.json"), "utf-8")
|
|
7199
7399
|
);
|
|
7200
7400
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
7201
7401
|
framework = detectFrameworkFromDeps(allDeps);
|
|
7202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7239
|
-
|
|
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
|
-
|
|
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
|
|
7290
|
-
|
|
7490
|
+
const detection = installHooks(bus);
|
|
7491
|
+
session.init(detection.framework, detectPackageManager(cwd), false, detection.adapterNames);
|
|
7291
7492
|
const setupDurationMs = Date.now() - setupStart;
|
|
7292
|
-
|
|
7493
|
+
session.recordSetup(detection, setupDurationMs);
|
|
7293
7494
|
trackEvent(TELEMETRY_EVENT_SETUP_COMPLETED, {
|
|
7294
|
-
framework,
|
|
7295
|
-
framework_detection_candidates:
|
|
7296
|
-
adapters_detected: adapterNames,
|
|
7297
|
-
adapters_failed: adaptersFailed,
|
|
7298
|
-
hooks_installed: [
|
|
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
|
-
|
|
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
|
-
|
|
7592
|
+
init_runtime();
|
|
7593
|
+
init_telemetry2();
|
|
7386
7594
|
initPromise = null;
|
|
7387
7595
|
}
|
|
7388
7596
|
});
|