brakit 0.9.2 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +148 -5
- package/dist/api.js +232 -54
- package/dist/bin/brakit.js +228 -35
- package/dist/dashboard-client.global.js +742 -385
- package/dist/dashboard.html +933 -432
- package/dist/mcp/server.js +99 -21
- package/dist/runtime/index.js +1551 -430
- 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, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS, MAX_JSON_BODY_BYTES, ANALYSIS_DEBOUNCE_MS, ISSUE_ID_HASH_LENGTH, ISSUES_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER, PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_API_LIMIT, MAX_OBJECT_SCAN_DEPTH, MAX_UNIQUE_ENDPOINTS, MAX_ACCUMULATOR_ENTRIES, ISSUE_PRUNE_TTL_MS, FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, CLEAN_HITS_FOR_RESOLUTION, STALE_ISSUE_TTL_MS, STRICT_MODE_MAX_GAP_MS, BASELINE_MIN_SESSIONS, BASELINE_MIN_REQUESTS_PER_SESSION, BASELINE_PENDING_POINTS_MIN, METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS, SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS, VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES, TELEMETRY_EVENT_SETUP_COMPLETED, TELEMETRY_EVENT_FIRST_REQUEST, TELEMETRY_EVENT_DASHBOARD_VIEWED, TELEMETRY_EVENT_SESSION, EXIT_REASON_CLEAN, EXIT_REASON_SIGINT, EXIT_REASON_SIGTERM, DETAIL_PREVIEW_LENGTH, KNOWN_DEPENDENCY_NAMES;
|
|
18
|
+
var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS, MAX_JSON_BODY_BYTES, ANALYSIS_DEBOUNCE_MS, ISSUE_ID_HASH_LENGTH, ISSUES_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER, PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_API_LIMIT, MAX_OBJECT_SCAN_DEPTH, MAX_UNIQUE_ENDPOINTS, MAX_ACCUMULATOR_ENTRIES, ISSUE_PRUNE_TTL_MS, FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, CLEAN_HITS_FOR_RESOLUTION, STALE_ISSUE_TTL_MS, STRICT_MODE_MAX_GAP_MS, BASELINE_MIN_SESSIONS, BASELINE_MIN_REQUESTS_PER_SESSION, BASELINE_PENDING_POINTS_MIN, METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS, SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS, VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES, TELEMETRY_EVENT_SETUP_COMPLETED, TELEMETRY_EVENT_FIRST_REQUEST, TELEMETRY_EVENT_DASHBOARD_VIEWED, TELEMETRY_EVENT_SESSION, TELEMETRY_EVENT_GRAPH_FEATURE, EXIT_REASON_CLEAN, EXIT_REASON_SIGINT, EXIT_REASON_SIGTERM, DETAIL_PREVIEW_LENGTH, KNOWN_DEPENDENCY_NAMES;
|
|
19
19
|
var init_config = __esm({
|
|
20
20
|
"src/constants/config.ts"() {
|
|
21
21
|
"use strict";
|
|
@@ -94,6 +94,7 @@ var init_config = __esm({
|
|
|
94
94
|
TELEMETRY_EVENT_FIRST_REQUEST = "first_request";
|
|
95
95
|
TELEMETRY_EVENT_DASHBOARD_VIEWED = "dashboard_viewed";
|
|
96
96
|
TELEMETRY_EVENT_SESSION = "session";
|
|
97
|
+
TELEMETRY_EVENT_GRAPH_FEATURE = "graph_feature";
|
|
97
98
|
EXIT_REASON_CLEAN = "clean";
|
|
98
99
|
EXIT_REASON_SIGINT = "sigint";
|
|
99
100
|
EXIT_REASON_SIGTERM = "sigterm";
|
|
@@ -104,11 +105,14 @@ var init_config = __esm({
|
|
|
104
105
|
"nuxt",
|
|
105
106
|
"vite",
|
|
106
107
|
"astro",
|
|
108
|
+
"@nestjs/core",
|
|
109
|
+
"@adonisjs/core",
|
|
110
|
+
"sails",
|
|
107
111
|
"express",
|
|
108
112
|
"fastify",
|
|
109
113
|
"hono",
|
|
110
114
|
"koa",
|
|
111
|
-
"
|
|
115
|
+
"@hapi/hapi",
|
|
112
116
|
"prisma",
|
|
113
117
|
"drizzle-orm",
|
|
114
118
|
"typeorm",
|
|
@@ -118,7 +122,7 @@ var init_config = __esm({
|
|
|
118
122
|
});
|
|
119
123
|
|
|
120
124
|
// src/constants/labels.ts
|
|
121
|
-
var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, VALID_TABS_TUPLE, VALID_TABS, BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES, HTTP_OK, HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, HTTP_INTERNAL_ERROR, SECURITY_HEADERS, CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE, SEVERITY_ICON, SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES, SDK_EVENT_REQUEST, SDK_EVENT_DB_QUERY, SDK_EVENT_FETCH, SDK_EVENT_LOG, SDK_EVENT_ERROR, SDK_EVENT_AUTH_CHECK, POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS, TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY;
|
|
125
|
+
var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, DASHBOARD_API_GRAPH, VALID_TABS_TUPLE, VALID_TABS, BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES, HTTP_OK, HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, HTTP_INTERNAL_ERROR, SECURITY_HEADERS, CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE, SEVERITY_ICON, SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES, SDK_EVENT_REQUEST, SDK_EVENT_DB_QUERY, SDK_EVENT_FETCH, SDK_EVENT_LOG, SDK_EVENT_ERROR, SDK_EVENT_AUTH_CHECK, POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS, TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY;
|
|
122
126
|
var init_labels = __esm({
|
|
123
127
|
"src/constants/labels.ts"() {
|
|
124
128
|
"use strict";
|
|
@@ -140,16 +144,14 @@ var init_labels = __esm({
|
|
|
140
144
|
DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
141
145
|
DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
142
146
|
DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
147
|
+
DASHBOARD_API_GRAPH = `${DASHBOARD_PREFIX}/api/graph`;
|
|
143
148
|
VALID_TABS_TUPLE = [
|
|
144
149
|
"overview",
|
|
145
150
|
"actions",
|
|
146
|
-
"
|
|
147
|
-
"fetches",
|
|
148
|
-
"queries",
|
|
149
|
-
"errors",
|
|
150
|
-
"logs",
|
|
151
|
+
"insights",
|
|
151
152
|
"performance",
|
|
152
|
-
"
|
|
153
|
+
"graph",
|
|
154
|
+
"explorer"
|
|
153
155
|
];
|
|
154
156
|
VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
155
157
|
BRAKIT_REQUEST_ID_HEADER = "x-brakit-request-id";
|
|
@@ -173,7 +175,7 @@ var init_labels = __esm({
|
|
|
173
175
|
"x-content-type-options": "nosniff",
|
|
174
176
|
"x-frame-options": "DENY",
|
|
175
177
|
"referrer-policy": "no-referrer",
|
|
176
|
-
"content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
|
|
178
|
+
"content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data: blob:"
|
|
177
179
|
};
|
|
178
180
|
CONTENT_ENCODING_GZIP = "gzip";
|
|
179
181
|
CONTENT_ENCODING_BR = "br";
|
|
@@ -336,6 +338,214 @@ var init_fetch = __esm({
|
|
|
336
338
|
}
|
|
337
339
|
});
|
|
338
340
|
|
|
341
|
+
// src/utils/log.ts
|
|
342
|
+
function brakitWarn(message) {
|
|
343
|
+
process.stderr.write(`${PREFIX} ${message}
|
|
344
|
+
`);
|
|
345
|
+
}
|
|
346
|
+
function brakitDebug(message) {
|
|
347
|
+
if (process.env.DEBUG_BRAKIT) {
|
|
348
|
+
process.stderr.write(`${PREFIX}:debug ${message}
|
|
349
|
+
`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
var PREFIX;
|
|
353
|
+
var init_log = __esm({
|
|
354
|
+
"src/utils/log.ts"() {
|
|
355
|
+
"use strict";
|
|
356
|
+
PREFIX = "[brakit]";
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// src/utils/type-guards.ts
|
|
361
|
+
function isString(val) {
|
|
362
|
+
return typeof val === "string";
|
|
363
|
+
}
|
|
364
|
+
function isNumber(val) {
|
|
365
|
+
return typeof val === "number" && !isNaN(val);
|
|
366
|
+
}
|
|
367
|
+
function isBoolean(val) {
|
|
368
|
+
return typeof val === "boolean";
|
|
369
|
+
}
|
|
370
|
+
function isThenable(value) {
|
|
371
|
+
return value != null && typeof value.then === "function";
|
|
372
|
+
}
|
|
373
|
+
function getErrorMessage(err) {
|
|
374
|
+
if (err instanceof Error) return err.message;
|
|
375
|
+
if (typeof err === "string") return err;
|
|
376
|
+
return String(err);
|
|
377
|
+
}
|
|
378
|
+
function isValidIssueState(val) {
|
|
379
|
+
return typeof val === "string" && VALID_ISSUE_STATES.has(val);
|
|
380
|
+
}
|
|
381
|
+
function isValidIssueCategory(val) {
|
|
382
|
+
return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
|
|
383
|
+
}
|
|
384
|
+
function isValidAiFixStatus(val) {
|
|
385
|
+
return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
|
|
386
|
+
}
|
|
387
|
+
function validateIssuesData(parsed) {
|
|
388
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
389
|
+
const obj = parsed;
|
|
390
|
+
if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
|
|
391
|
+
return parsed;
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
function validateMetricsData(parsed) {
|
|
396
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
397
|
+
const obj = parsed;
|
|
398
|
+
if (obj.version === 1 && Array.isArray(obj.endpoints)) {
|
|
399
|
+
return parsed;
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
var init_type_guards = __esm({
|
|
404
|
+
"src/utils/type-guards.ts"() {
|
|
405
|
+
"use strict";
|
|
406
|
+
init_config();
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// src/runtime/capture.ts
|
|
411
|
+
import { gunzip, brotliDecompress, inflate } from "zlib";
|
|
412
|
+
function outgoingToIncoming(headers2) {
|
|
413
|
+
const result = {};
|
|
414
|
+
for (const [key, value] of Object.entries(headers2)) {
|
|
415
|
+
if (value === void 0) continue;
|
|
416
|
+
if (Array.isArray(value)) {
|
|
417
|
+
result[key] = value.map(String);
|
|
418
|
+
} else {
|
|
419
|
+
result[key] = String(value);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
function getDecompressor(encoding) {
|
|
425
|
+
if (encoding === CONTENT_ENCODING_GZIP) return gunzip;
|
|
426
|
+
if (encoding === CONTENT_ENCODING_BR) return brotliDecompress;
|
|
427
|
+
if (encoding === CONTENT_ENCODING_DEFLATE) return inflate;
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
function decompressAsync(body, encoding) {
|
|
431
|
+
const decompressor = getDecompressor(encoding);
|
|
432
|
+
if (!decompressor) return Promise.resolve(body);
|
|
433
|
+
return new Promise((resolve6) => {
|
|
434
|
+
decompressor(body, (err, result) => {
|
|
435
|
+
resolve6(err ? body : result);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function toBuffer(chunk) {
|
|
440
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
441
|
+
if (chunk instanceof Uint8Array) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
442
|
+
if (typeof chunk === "string") return Buffer.from(chunk);
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
function drainPendingCaptures() {
|
|
446
|
+
if (pendingCaptures === 0) return Promise.resolve();
|
|
447
|
+
return new Promise((resolve6) => {
|
|
448
|
+
drainResolvers.push(resolve6);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function onCaptureSettled() {
|
|
452
|
+
pendingCaptures--;
|
|
453
|
+
if (pendingCaptures === 0 && drainResolvers.length > 0) {
|
|
454
|
+
const resolvers = drainResolvers;
|
|
455
|
+
drainResolvers = [];
|
|
456
|
+
for (const resolve6 of resolvers) resolve6();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function captureInProcess(req, res, requestId, requestStore, isChild = false) {
|
|
460
|
+
const startTime = performance.now();
|
|
461
|
+
const method = req.method ?? "GET";
|
|
462
|
+
const resChunks = [];
|
|
463
|
+
let resSize = 0;
|
|
464
|
+
const originalWrite = res.write;
|
|
465
|
+
const originalEnd = res.end;
|
|
466
|
+
let truncated = false;
|
|
467
|
+
res.write = function(...args) {
|
|
468
|
+
try {
|
|
469
|
+
const chunk = args[0];
|
|
470
|
+
if (chunk != null && typeof chunk !== "function") {
|
|
471
|
+
if (resSize < DEFAULT_MAX_BODY_CAPTURE) {
|
|
472
|
+
const buf = toBuffer(chunk);
|
|
473
|
+
if (buf) {
|
|
474
|
+
resChunks.push(buf);
|
|
475
|
+
resSize += buf.length;
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
truncated = true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
} catch (e) {
|
|
482
|
+
brakitDebug(`capture write: ${getErrorMessage(e)}`);
|
|
483
|
+
}
|
|
484
|
+
return originalWrite.apply(this, args);
|
|
485
|
+
};
|
|
486
|
+
res.end = function(...args) {
|
|
487
|
+
try {
|
|
488
|
+
const chunk = typeof args[0] !== "function" ? args[0] : void 0;
|
|
489
|
+
if (chunk != null && resSize < DEFAULT_MAX_BODY_CAPTURE) {
|
|
490
|
+
const buf = toBuffer(chunk);
|
|
491
|
+
if (buf) {
|
|
492
|
+
resChunks.push(buf);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} catch (e) {
|
|
496
|
+
brakitDebug(`capture end: ${getErrorMessage(e)}`);
|
|
497
|
+
}
|
|
498
|
+
const result = originalEnd.apply(this, args);
|
|
499
|
+
const endTime = performance.now();
|
|
500
|
+
const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
|
|
501
|
+
const statusCode = res.statusCode;
|
|
502
|
+
const responseHeaders = outgoingToIncoming(res.getHeaders());
|
|
503
|
+
const responseContentType = String(res.getHeader("content-type") ?? "");
|
|
504
|
+
const capturedChunks = resChunks.slice();
|
|
505
|
+
if (!isChild) {
|
|
506
|
+
pendingCaptures++;
|
|
507
|
+
void (async () => {
|
|
508
|
+
try {
|
|
509
|
+
let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
|
|
510
|
+
if (body && encoding && !truncated) {
|
|
511
|
+
body = await decompressAsync(body, encoding);
|
|
512
|
+
}
|
|
513
|
+
requestStore.capture({
|
|
514
|
+
requestId,
|
|
515
|
+
method,
|
|
516
|
+
url: req.url ?? "/",
|
|
517
|
+
requestHeaders: req.headers,
|
|
518
|
+
requestBody: null,
|
|
519
|
+
statusCode,
|
|
520
|
+
responseHeaders,
|
|
521
|
+
responseBody: body,
|
|
522
|
+
responseContentType,
|
|
523
|
+
startTime,
|
|
524
|
+
endTime,
|
|
525
|
+
config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
|
|
526
|
+
});
|
|
527
|
+
} catch (e) {
|
|
528
|
+
brakitDebug(`capture store: ${getErrorMessage(e)}`);
|
|
529
|
+
} finally {
|
|
530
|
+
onCaptureSettled();
|
|
531
|
+
}
|
|
532
|
+
})();
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
var pendingCaptures, drainResolvers;
|
|
538
|
+
var init_capture = __esm({
|
|
539
|
+
"src/runtime/capture.ts"() {
|
|
540
|
+
"use strict";
|
|
541
|
+
init_constants();
|
|
542
|
+
init_log();
|
|
543
|
+
init_type_guards();
|
|
544
|
+
pendingCaptures = 0;
|
|
545
|
+
drainResolvers = [];
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
339
549
|
// src/instrument/hooks/console.ts
|
|
340
550
|
import { format } from "util";
|
|
341
551
|
function setupConsoleHook(emit) {
|
|
@@ -486,7 +696,15 @@ var init_adapter_registry = __esm({
|
|
|
486
696
|
function normalizeSQL(sql) {
|
|
487
697
|
if (!sql) return { op: "OTHER", table: "" };
|
|
488
698
|
const trimmed = sql.trim();
|
|
489
|
-
|
|
699
|
+
let spaceIdx = -1;
|
|
700
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
701
|
+
const c = trimmed[i];
|
|
702
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
703
|
+
spaceIdx = i;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const keyword = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toUpperCase();
|
|
490
708
|
const op = VALID_OPS.has(keyword) ? keyword : "OTHER";
|
|
491
709
|
const table = trimmed.match(TABLE_RE)?.[1] ?? "";
|
|
492
710
|
return { op, table };
|
|
@@ -528,75 +746,6 @@ var init_normalize = __esm({
|
|
|
528
746
|
}
|
|
529
747
|
});
|
|
530
748
|
|
|
531
|
-
// src/utils/type-guards.ts
|
|
532
|
-
function isString(val) {
|
|
533
|
-
return typeof val === "string";
|
|
534
|
-
}
|
|
535
|
-
function isNumber(val) {
|
|
536
|
-
return typeof val === "number" && !isNaN(val);
|
|
537
|
-
}
|
|
538
|
-
function isBoolean(val) {
|
|
539
|
-
return typeof val === "boolean";
|
|
540
|
-
}
|
|
541
|
-
function isThenable(value) {
|
|
542
|
-
return value != null && typeof value.then === "function";
|
|
543
|
-
}
|
|
544
|
-
function getErrorMessage(err) {
|
|
545
|
-
if (err instanceof Error) return err.message;
|
|
546
|
-
if (typeof err === "string") return err;
|
|
547
|
-
return String(err);
|
|
548
|
-
}
|
|
549
|
-
function isValidIssueState(val) {
|
|
550
|
-
return typeof val === "string" && VALID_ISSUE_STATES.has(val);
|
|
551
|
-
}
|
|
552
|
-
function isValidIssueCategory(val) {
|
|
553
|
-
return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
|
|
554
|
-
}
|
|
555
|
-
function isValidAiFixStatus(val) {
|
|
556
|
-
return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
|
|
557
|
-
}
|
|
558
|
-
function validateIssuesData(parsed) {
|
|
559
|
-
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
560
|
-
const obj = parsed;
|
|
561
|
-
if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
|
|
562
|
-
return parsed;
|
|
563
|
-
}
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
function validateMetricsData(parsed) {
|
|
567
|
-
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
568
|
-
const obj = parsed;
|
|
569
|
-
if (obj.version === 1 && Array.isArray(obj.endpoints)) {
|
|
570
|
-
return parsed;
|
|
571
|
-
}
|
|
572
|
-
return null;
|
|
573
|
-
}
|
|
574
|
-
var init_type_guards = __esm({
|
|
575
|
-
"src/utils/type-guards.ts"() {
|
|
576
|
-
"use strict";
|
|
577
|
-
init_config();
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
// src/utils/log.ts
|
|
582
|
-
function brakitWarn(message) {
|
|
583
|
-
process.stderr.write(`${PREFIX} ${message}
|
|
584
|
-
`);
|
|
585
|
-
}
|
|
586
|
-
function brakitDebug(message) {
|
|
587
|
-
if (process.env.DEBUG_BRAKIT) {
|
|
588
|
-
process.stderr.write(`${PREFIX}:debug ${message}
|
|
589
|
-
`);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
var PREFIX;
|
|
593
|
-
var init_log = __esm({
|
|
594
|
-
"src/utils/log.ts"() {
|
|
595
|
-
"use strict";
|
|
596
|
-
PREFIX = "[brakit]";
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
|
|
600
749
|
// src/instrument/adapters/shared.ts
|
|
601
750
|
import { createRequire } from "module";
|
|
602
751
|
function tryRequire(id) {
|
|
@@ -862,11 +1011,54 @@ var init_adapters = __esm({
|
|
|
862
1011
|
init_mysql2();
|
|
863
1012
|
init_prisma();
|
|
864
1013
|
}
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
// src/utils/endpoint.ts
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// src/utils/endpoint.ts
|
|
1017
|
+
function isUUID(s) {
|
|
1018
|
+
if (s.length !== UUID_LEN) return false;
|
|
1019
|
+
for (let i = 0; i < s.length; i++) {
|
|
1020
|
+
const c = s[i];
|
|
1021
|
+
if (i === 8 || i === 13 || i === 18 || i === 23) {
|
|
1022
|
+
if (c !== "-") return false;
|
|
1023
|
+
} else {
|
|
1024
|
+
if (!isHexChar(c)) return false;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
function isHexChar(c) {
|
|
1030
|
+
const code = c.charCodeAt(0);
|
|
1031
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
|
|
1032
|
+
}
|
|
1033
|
+
function isNumericId(s) {
|
|
1034
|
+
if (s.length === 0) return false;
|
|
1035
|
+
for (let i = 0; i < s.length; i++) {
|
|
1036
|
+
const code = s.charCodeAt(i);
|
|
1037
|
+
if (code < 48 || code > 57) return false;
|
|
1038
|
+
}
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
function isHexHash(s) {
|
|
1042
|
+
if (s.length < MIN_HEX_LEN) return false;
|
|
1043
|
+
for (let i = 0; i < s.length; i++) {
|
|
1044
|
+
if (!isHexChar(s[i])) return false;
|
|
1045
|
+
}
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
function isAlphanumericToken(s) {
|
|
1049
|
+
if (s.length < MIN_TOKEN_LEN) return false;
|
|
1050
|
+
let hasLetter = false;
|
|
1051
|
+
let hasDigit = false;
|
|
1052
|
+
for (let i = 0; i < s.length; i++) {
|
|
1053
|
+
const code = s.charCodeAt(i);
|
|
1054
|
+
if (code >= 65 && code <= 90 || code >= 97 && code <= 122) hasLetter = true;
|
|
1055
|
+
else if (code >= 48 && code <= 57) hasDigit = true;
|
|
1056
|
+
else if (code !== 95 && code !== 45) return false;
|
|
1057
|
+
}
|
|
1058
|
+
return hasLetter && hasDigit;
|
|
1059
|
+
}
|
|
868
1060
|
function isDynamicSegment(segment) {
|
|
869
|
-
return
|
|
1061
|
+
return isUUID(segment) || isNumericId(segment) || isHexHash(segment) || isAlphanumericToken(segment);
|
|
870
1062
|
}
|
|
871
1063
|
function normalizePath(path) {
|
|
872
1064
|
const qIdx = path.indexOf("?");
|
|
@@ -877,22 +1069,24 @@ function getEndpointKey(method, path) {
|
|
|
877
1069
|
return `${method} ${normalizePath(path)}`;
|
|
878
1070
|
}
|
|
879
1071
|
function extractEndpointFromDesc(desc) {
|
|
880
|
-
|
|
1072
|
+
const spaceIdx = desc.indexOf(" ");
|
|
1073
|
+
if (spaceIdx <= 0) return null;
|
|
1074
|
+
const secondSpace = desc.indexOf(" ", spaceIdx + 1);
|
|
1075
|
+
if (secondSpace === -1) return desc;
|
|
1076
|
+
return desc.slice(0, secondSpace);
|
|
881
1077
|
}
|
|
882
1078
|
function stripQueryString(path) {
|
|
883
1079
|
const i = path.indexOf("?");
|
|
884
1080
|
return i === -1 ? path : path.slice(0, i);
|
|
885
1081
|
}
|
|
886
|
-
var
|
|
1082
|
+
var UUID_LEN, MIN_HEX_LEN, MIN_TOKEN_LEN, DYNAMIC_SEGMENT_PLACEHOLDER;
|
|
887
1083
|
var init_endpoint = __esm({
|
|
888
1084
|
"src/utils/endpoint.ts"() {
|
|
889
1085
|
"use strict";
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
ALPHA_TOKEN_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/;
|
|
1086
|
+
UUID_LEN = 36;
|
|
1087
|
+
MIN_HEX_LEN = 12;
|
|
1088
|
+
MIN_TOKEN_LEN = 8;
|
|
894
1089
|
DYNAMIC_SEGMENT_PLACEHOLDER = ":id";
|
|
895
|
-
ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
896
1090
|
}
|
|
897
1091
|
});
|
|
898
1092
|
|
|
@@ -913,6 +1107,10 @@ var init_http_status = __esm({
|
|
|
913
1107
|
});
|
|
914
1108
|
|
|
915
1109
|
// src/analysis/categorize.ts
|
|
1110
|
+
function isAuthPath(path) {
|
|
1111
|
+
const lower = path.toLowerCase();
|
|
1112
|
+
return lower.startsWith("/api/auth") || lower.startsWith("/clerk") || lower.startsWith("/api/clerk");
|
|
1113
|
+
}
|
|
916
1114
|
function detectCategory(req) {
|
|
917
1115
|
const { method, url, statusCode, responseHeaders } = req;
|
|
918
1116
|
if (req.isStatic) return "static";
|
|
@@ -921,7 +1119,7 @@ function detectCategory(req) {
|
|
|
921
1119
|
return "auth-handshake";
|
|
922
1120
|
}
|
|
923
1121
|
const effectivePath = getEffectivePath(req);
|
|
924
|
-
if (
|
|
1122
|
+
if (isAuthPath(effectivePath)) {
|
|
925
1123
|
return "auth-check";
|
|
926
1124
|
}
|
|
927
1125
|
if (method === "POST" && !effectivePath.startsWith("/api/")) {
|
|
@@ -945,6 +1143,13 @@ function detectCategory(req) {
|
|
|
945
1143
|
}
|
|
946
1144
|
return "unknown";
|
|
947
1145
|
}
|
|
1146
|
+
function hasAuthCredentials(req) {
|
|
1147
|
+
if (req.headers["authorization"]) return true;
|
|
1148
|
+
const cookie = (req.headers["cookie"] || "").toLowerCase();
|
|
1149
|
+
if (cookie && AUTH_COOKIE_NAMES.some((name) => cookie.includes(name))) return true;
|
|
1150
|
+
if (req.statusCode === 401) return true;
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
948
1153
|
function getEffectivePath(req) {
|
|
949
1154
|
const rewrite = req.responseHeaders["x-middleware-rewrite"];
|
|
950
1155
|
if (!rewrite) return req.path;
|
|
@@ -955,9 +1160,21 @@ function getEffectivePath(req) {
|
|
|
955
1160
|
return rewrite.startsWith("/") ? rewrite : req.path;
|
|
956
1161
|
}
|
|
957
1162
|
}
|
|
1163
|
+
var AUTH_COOKIE_NAMES;
|
|
958
1164
|
var init_categorize = __esm({
|
|
959
1165
|
"src/analysis/categorize.ts"() {
|
|
960
1166
|
"use strict";
|
|
1167
|
+
AUTH_COOKIE_NAMES = [
|
|
1168
|
+
"__session=",
|
|
1169
|
+
"__clerk",
|
|
1170
|
+
"__host-next-auth",
|
|
1171
|
+
"next-auth.session-token=",
|
|
1172
|
+
"auth_token=",
|
|
1173
|
+
"session_id=",
|
|
1174
|
+
"access_token=",
|
|
1175
|
+
"_session=",
|
|
1176
|
+
"appsession="
|
|
1177
|
+
];
|
|
961
1178
|
}
|
|
962
1179
|
});
|
|
963
1180
|
|
|
@@ -1016,8 +1233,11 @@ function generateHumanLabel(req, category) {
|
|
|
1016
1233
|
return failed ? `${req.method} ${req.path} failed` : `${req.method} ${req.path}`;
|
|
1017
1234
|
}
|
|
1018
1235
|
}
|
|
1236
|
+
function stripApiPrefix(s) {
|
|
1237
|
+
return s.startsWith("/api/") ? s.slice(5) : s;
|
|
1238
|
+
}
|
|
1019
1239
|
function prettifyEndpoint(name) {
|
|
1020
|
-
const cleaned = name.
|
|
1240
|
+
const cleaned = stripApiPrefix(name).split("/").join(" ").split("...").join("").trim();
|
|
1021
1241
|
if (!cleaned) return "data";
|
|
1022
1242
|
return cleaned.split(" ").map((word) => {
|
|
1023
1243
|
if (word.endsWith("ses") || word.endsWith("us") || word.endsWith("ss"))
|
|
@@ -1029,22 +1249,8 @@ function prettifyEndpoint(name) {
|
|
|
1029
1249
|
}
|
|
1030
1250
|
function deriveActionVerb(method, endpointName) {
|
|
1031
1251
|
const lower = endpointName.toLowerCase();
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
[/generate/, "Generated"],
|
|
1035
|
-
[/create/, "Created"],
|
|
1036
|
-
[/update/, "Updated"],
|
|
1037
|
-
[/delete|remove/, "Deleted"],
|
|
1038
|
-
[/send/, "Sent"],
|
|
1039
|
-
[/upload/, "Uploaded"],
|
|
1040
|
-
[/save/, "Saved"],
|
|
1041
|
-
[/submit/, "Submitted"],
|
|
1042
|
-
[/login|signin/, "Logged in"],
|
|
1043
|
-
[/logout|signout/, "Logged out"],
|
|
1044
|
-
[/register|signup/, "Registered"]
|
|
1045
|
-
];
|
|
1046
|
-
for (const [pattern, verb] of VERB_PATTERNS) {
|
|
1047
|
-
if (pattern.test(lower)) return verb;
|
|
1252
|
+
for (const [keyword, verb] of VERB_MAP) {
|
|
1253
|
+
if (lower.includes(keyword)) return verb;
|
|
1048
1254
|
}
|
|
1049
1255
|
switch (method) {
|
|
1050
1256
|
case "POST":
|
|
@@ -1059,24 +1265,45 @@ function deriveActionVerb(method, endpointName) {
|
|
|
1059
1265
|
}
|
|
1060
1266
|
}
|
|
1061
1267
|
function getEndpointName(path) {
|
|
1062
|
-
const parts = path
|
|
1268
|
+
const parts = stripApiPrefix(path).split("/");
|
|
1063
1269
|
if (parts.length <= 2) return parts.join("/");
|
|
1064
1270
|
return parts.map((p) => p.length > ENDPOINT_TRUNCATE_LENGTH ? "..." : p).join("/");
|
|
1065
1271
|
}
|
|
1066
1272
|
function prettifyPageName(path) {
|
|
1067
|
-
|
|
1273
|
+
let clean = path;
|
|
1274
|
+
if (clean.startsWith("/")) clean = clean.slice(1);
|
|
1275
|
+
if (clean.endsWith("/")) clean = clean.slice(0, -1);
|
|
1068
1276
|
if (!clean) return "Home";
|
|
1069
|
-
return clean.split("/").map((s) => capitalize(s.
|
|
1277
|
+
return clean.split("/").map((s) => capitalize(s.split("-").join(" ").split("_").join(" "))).join(" ");
|
|
1070
1278
|
}
|
|
1071
1279
|
function capitalize(s) {
|
|
1072
1280
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1073
1281
|
}
|
|
1282
|
+
var VERB_MAP;
|
|
1074
1283
|
var init_label = __esm({
|
|
1075
1284
|
"src/analysis/label.ts"() {
|
|
1076
1285
|
"use strict";
|
|
1077
1286
|
init_constants();
|
|
1078
1287
|
init_categorize();
|
|
1079
1288
|
init_http_status();
|
|
1289
|
+
VERB_MAP = [
|
|
1290
|
+
["enhance", "Enhanced"],
|
|
1291
|
+
["generate", "Generated"],
|
|
1292
|
+
["create", "Created"],
|
|
1293
|
+
["update", "Updated"],
|
|
1294
|
+
["delete", "Deleted"],
|
|
1295
|
+
["remove", "Deleted"],
|
|
1296
|
+
["send", "Sent"],
|
|
1297
|
+
["upload", "Uploaded"],
|
|
1298
|
+
["save", "Saved"],
|
|
1299
|
+
["submit", "Submitted"],
|
|
1300
|
+
["login", "Logged in"],
|
|
1301
|
+
["signin", "Logged in"],
|
|
1302
|
+
["logout", "Logged out"],
|
|
1303
|
+
["signout", "Logged out"],
|
|
1304
|
+
["register", "Registered"],
|
|
1305
|
+
["signup", "Registered"]
|
|
1306
|
+
];
|
|
1080
1307
|
}
|
|
1081
1308
|
});
|
|
1082
1309
|
|
|
@@ -1522,33 +1749,85 @@ var init_handlers = __esm({
|
|
|
1522
1749
|
|
|
1523
1750
|
// src/utils/static-patterns.ts
|
|
1524
1751
|
function isStaticPath(urlPath) {
|
|
1525
|
-
|
|
1752
|
+
const dotIdx = urlPath.lastIndexOf(".");
|
|
1753
|
+
if (dotIdx !== -1) {
|
|
1754
|
+
const ext = urlPath.slice(dotIdx).toLowerCase();
|
|
1755
|
+
if (STATIC_EXTENSIONS.has(ext)) return true;
|
|
1756
|
+
}
|
|
1757
|
+
return STATIC_PREFIXES.some((p) => urlPath.startsWith(p));
|
|
1526
1758
|
}
|
|
1527
1759
|
function isHealthCheckPath(urlPath) {
|
|
1528
|
-
return
|
|
1760
|
+
return HEALTH_CHECK_PATHS.has(urlPath.toLowerCase());
|
|
1529
1761
|
}
|
|
1530
|
-
var
|
|
1762
|
+
var STATIC_EXTENSIONS, STATIC_PREFIXES, HEALTH_CHECK_PATHS;
|
|
1531
1763
|
var init_static_patterns = __esm({
|
|
1532
1764
|
"src/utils/static-patterns.ts"() {
|
|
1533
1765
|
"use strict";
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1766
|
+
STATIC_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1767
|
+
".js",
|
|
1768
|
+
".css",
|
|
1769
|
+
".map",
|
|
1770
|
+
".ico",
|
|
1771
|
+
".png",
|
|
1772
|
+
".jpg",
|
|
1773
|
+
".jpeg",
|
|
1774
|
+
".gif",
|
|
1775
|
+
".svg",
|
|
1776
|
+
".webp",
|
|
1777
|
+
".woff",
|
|
1778
|
+
".woff2",
|
|
1779
|
+
".ttf",
|
|
1780
|
+
".eot"
|
|
1781
|
+
]);
|
|
1782
|
+
STATIC_PREFIXES = [
|
|
1783
|
+
"/favicon",
|
|
1784
|
+
"/node_modules/",
|
|
1785
|
+
// Next.js
|
|
1786
|
+
"/_next/",
|
|
1787
|
+
"/__nextjs",
|
|
1788
|
+
// Vite (also used by Nuxt, Astro in dev)
|
|
1789
|
+
"/@vite/",
|
|
1790
|
+
"/__vite",
|
|
1791
|
+
"/@fs/",
|
|
1792
|
+
"/@id/",
|
|
1793
|
+
// Remix
|
|
1794
|
+
"/__remix",
|
|
1795
|
+
// Nuxt
|
|
1796
|
+
"/_nuxt/",
|
|
1797
|
+
"/__nuxt",
|
|
1798
|
+
// Astro
|
|
1799
|
+
"/@astro",
|
|
1800
|
+
"/_astro/",
|
|
1801
|
+
// Django
|
|
1802
|
+
"/static/",
|
|
1803
|
+
"/media/",
|
|
1804
|
+
"/__debug__/",
|
|
1805
|
+
// Flask / Werkzeug
|
|
1806
|
+
"/_debugtoolbar/",
|
|
1807
|
+
// FastAPI / Starlette
|
|
1808
|
+
"/openapi.json",
|
|
1809
|
+
"/docs",
|
|
1810
|
+
"/redoc",
|
|
1811
|
+
// Rails
|
|
1812
|
+
"/assets/",
|
|
1813
|
+
"/packs/",
|
|
1814
|
+
// Browser probes
|
|
1815
|
+
"/.well-known/"
|
|
1551
1816
|
];
|
|
1817
|
+
HEALTH_CHECK_PATHS = /* @__PURE__ */ new Set([
|
|
1818
|
+
"/health",
|
|
1819
|
+
"/healthz",
|
|
1820
|
+
"/healthcheck",
|
|
1821
|
+
"/ping",
|
|
1822
|
+
"/ready",
|
|
1823
|
+
"/readiness",
|
|
1824
|
+
"/liveness",
|
|
1825
|
+
"/status",
|
|
1826
|
+
"/__health",
|
|
1827
|
+
"/api/health",
|
|
1828
|
+
"/api/healthz",
|
|
1829
|
+
"/api/healthcheck"
|
|
1830
|
+
]);
|
|
1552
1831
|
}
|
|
1553
1832
|
});
|
|
1554
1833
|
|
|
@@ -1704,29 +1983,25 @@ function createIngestHandler(services) {
|
|
|
1704
1983
|
const routeEvent = (event) => {
|
|
1705
1984
|
switch (event.type) {
|
|
1706
1985
|
case TIMELINE_FETCH:
|
|
1707
|
-
|
|
1986
|
+
bus.emit("telemetry:fetch", event.data);
|
|
1708
1987
|
break;
|
|
1709
1988
|
case TIMELINE_LOG:
|
|
1710
|
-
|
|
1989
|
+
bus.emit("telemetry:log", event.data);
|
|
1711
1990
|
break;
|
|
1712
1991
|
case TIMELINE_ERROR:
|
|
1713
|
-
|
|
1992
|
+
bus.emit("telemetry:error", event.data);
|
|
1714
1993
|
break;
|
|
1715
1994
|
case TIMELINE_QUERY:
|
|
1716
|
-
|
|
1995
|
+
bus.emit("telemetry:query", event.data);
|
|
1717
1996
|
break;
|
|
1718
1997
|
}
|
|
1719
1998
|
};
|
|
1720
|
-
const
|
|
1721
|
-
const fetchStore = services.fetchStore;
|
|
1722
|
-
const logStore = services.logStore;
|
|
1723
|
-
const errorStore = services.errorStore;
|
|
1724
|
-
const requestStore = services.requestStore;
|
|
1999
|
+
const { bus, requestStore } = services;
|
|
1725
2000
|
const stores = {
|
|
1726
|
-
addQuery: (data) =>
|
|
1727
|
-
addFetch: (data) =>
|
|
1728
|
-
addLog: (data) =>
|
|
1729
|
-
addError: (data) =>
|
|
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),
|
|
1730
2005
|
addRequest: (data) => requestStore.add(data)
|
|
1731
2006
|
};
|
|
1732
2007
|
return (req, res) => {
|
|
@@ -1977,6 +2252,42 @@ var init_issues = __esm({
|
|
|
1977
2252
|
}
|
|
1978
2253
|
});
|
|
1979
2254
|
|
|
2255
|
+
// src/dashboard/api/graph.ts
|
|
2256
|
+
function createGraphHandler(services) {
|
|
2257
|
+
return (req, res) => {
|
|
2258
|
+
if (!requireGet(req, res)) return;
|
|
2259
|
+
const url = parseRequestUrl(req);
|
|
2260
|
+
const rawCluster = url.searchParams.get("cluster") ?? void 0;
|
|
2261
|
+
const rawNode = url.searchParams.get("node") ?? void 0;
|
|
2262
|
+
const rawLevel = url.searchParams.get("level") ?? void 0;
|
|
2263
|
+
const rawGrouping = url.searchParams.get("grouping") ?? void 0;
|
|
2264
|
+
const cluster = rawCluster && rawCluster.length <= MAX_PARAM_LENGTH ? rawCluster : void 0;
|
|
2265
|
+
const node = rawNode && rawNode.length <= MAX_PARAM_LENGTH ? rawNode : void 0;
|
|
2266
|
+
const level = rawLevel && VALID_LEVELS.has(rawLevel) ? rawLevel : void 0;
|
|
2267
|
+
const grouping = rawGrouping && VALID_GROUPINGS.has(rawGrouping) ? rawGrouping : void 0;
|
|
2268
|
+
const { graphBuilder, metricsStore } = services;
|
|
2269
|
+
graphBuilder.enrichWithMetrics((endpointKey) => {
|
|
2270
|
+
const metrics = metricsStore.getEndpoint(endpointKey);
|
|
2271
|
+
if (!metrics || metrics.sessions.length === 0) return void 0;
|
|
2272
|
+
const latest = metrics.sessions[metrics.sessions.length - 1];
|
|
2273
|
+
return latest.p95DurationMs;
|
|
2274
|
+
});
|
|
2275
|
+
const data = graphBuilder.getApiResponse({ cluster, node, level, grouping });
|
|
2276
|
+
sendJson(req, res, HTTP_OK, data);
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
var VALID_LEVELS, VALID_GROUPINGS, MAX_PARAM_LENGTH;
|
|
2280
|
+
var init_graph = __esm({
|
|
2281
|
+
"src/dashboard/api/graph.ts"() {
|
|
2282
|
+
"use strict";
|
|
2283
|
+
init_labels();
|
|
2284
|
+
init_shared2();
|
|
2285
|
+
VALID_LEVELS = /* @__PURE__ */ new Set(["endpoints", "clusters"]);
|
|
2286
|
+
VALID_GROUPINGS = /* @__PURE__ */ new Set(["path", "auth-boundary", "data-domain"]);
|
|
2287
|
+
MAX_PARAM_LENGTH = 200;
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
|
|
1980
2291
|
// src/dashboard/sse.ts
|
|
1981
2292
|
function createSSEHandler(services) {
|
|
1982
2293
|
const clients = /* @__PURE__ */ new Set();
|
|
@@ -2249,7 +2560,7 @@ var init_issue_store = __esm({
|
|
|
2249
2560
|
existing.occurrences++;
|
|
2250
2561
|
existing.issue = issue;
|
|
2251
2562
|
existing.cleanHitsSinceLastSeen = 0;
|
|
2252
|
-
if (existing.state === "resolved" || existing.state === "stale") {
|
|
2563
|
+
if (existing.aiStatus !== "wont_fix" && (existing.state === "resolved" || existing.state === "stale")) {
|
|
2253
2564
|
existing.state = "regressed";
|
|
2254
2565
|
existing.resolvedAt = null;
|
|
2255
2566
|
}
|
|
@@ -2275,10 +2586,11 @@ var init_issue_store = __esm({
|
|
|
2275
2586
|
return stateful;
|
|
2276
2587
|
}
|
|
2277
2588
|
/**
|
|
2278
|
-
*
|
|
2279
|
-
*
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2589
|
+
* Evidence-based reconciliation: for each active issue whose endpoint had
|
|
2590
|
+
* traffic but the issue was NOT re-detected, increment cleanHitsSinceLastSeen.
|
|
2591
|
+
* After CLEAN_HITS_FOR_RESOLUTION consecutive clean cycles, auto-resolve.
|
|
2592
|
+
* Issues on endpoints with no recent traffic are marked stale after STALE_ISSUE_TTL_MS.
|
|
2593
|
+
* Resolved and stale issues are pruned after their respective TTLs expire.
|
|
2282
2594
|
*/
|
|
2283
2595
|
reconcile(currentIssueIds, activeEndpoints) {
|
|
2284
2596
|
const now = Date.now();
|
|
@@ -2428,11 +2740,22 @@ var init_project = __esm({
|
|
|
2428
2740
|
"use strict";
|
|
2429
2741
|
init_fs();
|
|
2430
2742
|
FRAMEWORKS = [
|
|
2743
|
+
// Meta-frameworks first (they bundle Express/Vite internally)
|
|
2431
2744
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
2432
2745
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
2433
2746
|
{ name: "nuxt", dep: "nuxt", devCmd: "nuxt dev", bin: "nuxt", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
2434
|
-
{ name: "
|
|
2435
|
-
{ name: "
|
|
2747
|
+
{ name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] },
|
|
2748
|
+
{ name: "nestjs", dep: "@nestjs/core", devCmd: "nest start", bin: "nest", defaultPort: 3e3, devArgs: ["--watch"] },
|
|
2749
|
+
{ name: "adonis", dep: "@adonisjs/core", devCmd: "node ace serve", bin: "ace", defaultPort: 3333, devArgs: ["serve", "--watch"] },
|
|
2750
|
+
{ name: "sails", dep: "sails", devCmd: "sails lift", bin: "sails", defaultPort: 1337, devArgs: ["lift"] },
|
|
2751
|
+
// Server frameworks
|
|
2752
|
+
{ name: "hono", dep: "hono", devCmd: "node", bin: "node", defaultPort: 3e3 },
|
|
2753
|
+
{ name: "fastify", dep: "fastify", devCmd: "node", bin: "node", defaultPort: 3e3 },
|
|
2754
|
+
{ name: "koa", dep: "koa", devCmd: "node", bin: "node", defaultPort: 3e3 },
|
|
2755
|
+
{ name: "hapi", dep: "@hapi/hapi", devCmd: "node", bin: "node", defaultPort: 3e3 },
|
|
2756
|
+
{ name: "express", dep: "express", devCmd: "node", bin: "node", defaultPort: 3e3 },
|
|
2757
|
+
// Bundlers (last — likely used alongside a framework above)
|
|
2758
|
+
{ name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] }
|
|
2436
2759
|
];
|
|
2437
2760
|
}
|
|
2438
2761
|
});
|
|
@@ -2478,26 +2801,128 @@ var init_response = __esm({
|
|
|
2478
2801
|
});
|
|
2479
2802
|
|
|
2480
2803
|
// src/analysis/rules/patterns.ts
|
|
2481
|
-
var SECRET_KEYS, TOKEN_PARAMS, SAFE_PARAMS,
|
|
2804
|
+
var SECRET_KEY_SET, SECRET_KEYS, TOKEN_PARAM_SET, TOKEN_PARAMS, SAFE_PARAM_SET, SAFE_PARAMS, INTERNAL_ID_KEY_SET, INTERNAL_ID_KEYS, INTERNAL_ID_SUFFIX, SENSITIVE_FIELD_SET, SENSITIVE_FIELD_NAMES, SELF_SERVICE_SEGMENTS, SELF_SERVICE_PATH, MASKED_LITERALS, MASKED_RE, DB_PROTOCOLS, DB_CONN_RE, SELECT_STAR_RE, SELECT_DOT_STAR_RE, STACK_TRACE_RE, SQL_FRAGMENT_RE, SECRET_VAL_RE, LOG_SECRET_RE, EMAIL_RE, RULE_HINTS;
|
|
2482
2805
|
var init_patterns = __esm({
|
|
2483
2806
|
"src/analysis/rules/patterns.ts"() {
|
|
2484
2807
|
"use strict";
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2808
|
+
SECRET_KEY_SET = /* @__PURE__ */ new Set([
|
|
2809
|
+
"password",
|
|
2810
|
+
"passwd",
|
|
2811
|
+
"secret",
|
|
2812
|
+
"api_key",
|
|
2813
|
+
"apiKey",
|
|
2814
|
+
"api_secret",
|
|
2815
|
+
"apiSecret",
|
|
2816
|
+
"private_key",
|
|
2817
|
+
"privateKey",
|
|
2818
|
+
"client_secret",
|
|
2819
|
+
"clientSecret"
|
|
2820
|
+
]);
|
|
2821
|
+
SECRET_KEYS = { test: (s) => SECRET_KEY_SET.has(s) };
|
|
2822
|
+
TOKEN_PARAM_SET = /* @__PURE__ */ new Set([
|
|
2823
|
+
"token",
|
|
2824
|
+
"api_key",
|
|
2825
|
+
"apiKey",
|
|
2826
|
+
"secret",
|
|
2827
|
+
"password",
|
|
2828
|
+
"access_token",
|
|
2829
|
+
"session_id",
|
|
2830
|
+
"sessionId"
|
|
2831
|
+
]);
|
|
2832
|
+
TOKEN_PARAMS = { test: (s) => TOKEN_PARAM_SET.has(s) };
|
|
2833
|
+
SAFE_PARAM_SET = /* @__PURE__ */ new Set([
|
|
2834
|
+
"_rsc",
|
|
2835
|
+
"__clerk_handshake",
|
|
2836
|
+
"__clerk_db_jwt",
|
|
2837
|
+
"callback",
|
|
2838
|
+
"code",
|
|
2839
|
+
"state",
|
|
2840
|
+
"nonce",
|
|
2841
|
+
"redirect_uri",
|
|
2842
|
+
"utm_",
|
|
2843
|
+
"fbclid",
|
|
2844
|
+
"gclid"
|
|
2845
|
+
]);
|
|
2846
|
+
SAFE_PARAMS = { test: (s) => SAFE_PARAM_SET.has(s) };
|
|
2847
|
+
INTERNAL_ID_KEY_SET = /* @__PURE__ */ new Set([
|
|
2848
|
+
"id",
|
|
2849
|
+
"_id",
|
|
2850
|
+
"userId",
|
|
2851
|
+
"user_id",
|
|
2852
|
+
"createdBy",
|
|
2853
|
+
"updatedBy",
|
|
2854
|
+
"organizationId",
|
|
2855
|
+
"org_id",
|
|
2856
|
+
"tenantId",
|
|
2857
|
+
"tenant_id"
|
|
2858
|
+
]);
|
|
2859
|
+
INTERNAL_ID_KEYS = { test: (s) => INTERNAL_ID_KEY_SET.has(s) };
|
|
2860
|
+
INTERNAL_ID_SUFFIX = {
|
|
2861
|
+
test: (s) => s.endsWith("Id") || s.endsWith("_id")
|
|
2862
|
+
};
|
|
2863
|
+
SENSITIVE_FIELD_SET = /* @__PURE__ */ new Set([
|
|
2864
|
+
"phone",
|
|
2865
|
+
"phonenumber",
|
|
2866
|
+
"phone_number",
|
|
2867
|
+
"ssn",
|
|
2868
|
+
"socialsecuritynumber",
|
|
2869
|
+
"social_security_number",
|
|
2870
|
+
"dateofbirth",
|
|
2871
|
+
"date_of_birth",
|
|
2872
|
+
"dob",
|
|
2873
|
+
"address",
|
|
2874
|
+
"streetaddress",
|
|
2875
|
+
"street_address",
|
|
2876
|
+
"creditcard",
|
|
2877
|
+
"credit_card",
|
|
2878
|
+
"cardnumber",
|
|
2879
|
+
"card_number",
|
|
2880
|
+
"bankaccount",
|
|
2881
|
+
"bank_account",
|
|
2882
|
+
"passport",
|
|
2883
|
+
"passportnumber",
|
|
2884
|
+
"passport_number",
|
|
2885
|
+
"nationalid",
|
|
2886
|
+
"national_id"
|
|
2887
|
+
]);
|
|
2888
|
+
SENSITIVE_FIELD_NAMES = {
|
|
2889
|
+
test: (s) => SENSITIVE_FIELD_SET.has(s.toLowerCase())
|
|
2890
|
+
};
|
|
2891
|
+
SELF_SERVICE_SEGMENTS = /* @__PURE__ */ new Set(["me", "account", "profile", "settings", "self"]);
|
|
2892
|
+
SELF_SERVICE_PATH = {
|
|
2893
|
+
test: (path) => {
|
|
2894
|
+
const segments = path.toLowerCase().split(/[/?#]/);
|
|
2895
|
+
return segments.some((seg) => SELF_SERVICE_SEGMENTS.has(seg));
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
MASKED_LITERALS = ["[REDACTED]", "[FILTERED]", "CHANGE_ME"];
|
|
2899
|
+
MASKED_RE = {
|
|
2900
|
+
test: (s) => {
|
|
2901
|
+
const upper = s.toUpperCase();
|
|
2902
|
+
if (MASKED_LITERALS.some((m) => upper.includes(m))) return true;
|
|
2903
|
+
if (s.length > 0 && s.split("").every((c) => c === "*")) return true;
|
|
2904
|
+
if (s.length >= 3 && s.split("").every((c) => c === "x" || c === "X")) return true;
|
|
2905
|
+
return false;
|
|
2906
|
+
}
|
|
2907
|
+
};
|
|
2908
|
+
DB_PROTOCOLS = ["postgres://", "mysql://", "mongodb://", "redis://"];
|
|
2909
|
+
DB_CONN_RE = {
|
|
2910
|
+
test: (s) => DB_PROTOCOLS.some((p) => s.includes(p))
|
|
2911
|
+
};
|
|
2912
|
+
SELECT_STAR_RE = {
|
|
2913
|
+
test: (s) => {
|
|
2914
|
+
const t = s.trimStart().toUpperCase();
|
|
2915
|
+
return t.startsWith("SELECT *") || t.startsWith("SELECT *");
|
|
2916
|
+
}
|
|
2917
|
+
};
|
|
2918
|
+
SELECT_DOT_STAR_RE = {
|
|
2919
|
+
test: (s) => s.toUpperCase().includes(".* FROM")
|
|
2920
|
+
};
|
|
2488
2921
|
STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections|Traceback \(most recent call last\)|File ".+", line \d+/;
|
|
2489
|
-
DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
|
|
2490
2922
|
SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
|
|
2491
2923
|
SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
|
|
2492
2924
|
LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
|
|
2493
|
-
MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
|
|
2494
2925
|
EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
2495
|
-
INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
2496
|
-
INTERNAL_ID_SUFFIX = /Id$|_id$/;
|
|
2497
|
-
SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
|
|
2498
|
-
SENSITIVE_FIELD_NAMES = /^(phone|phoneNumber|phone_number|ssn|socialSecurityNumber|social_security_number|dateOfBirth|date_of_birth|dob|address|streetAddress|street_address|creditCard|credit_card|cardNumber|card_number|bankAccount|bank_account|passport|passportNumber|passport_number|nationalId|national_id)$/i;
|
|
2499
|
-
SELECT_STAR_RE = /^SELECT\s+\*/i;
|
|
2500
|
-
SELECT_DOT_STAR_RE = /\.\*\s+FROM/i;
|
|
2501
2926
|
RULE_HINTS = {
|
|
2502
2927
|
"exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
|
|
2503
2928
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
@@ -3956,7 +4381,7 @@ var init_src = __esm({
|
|
|
3956
4381
|
init_engine();
|
|
3957
4382
|
init_insights2();
|
|
3958
4383
|
init_insights();
|
|
3959
|
-
VERSION = "0.
|
|
4384
|
+
VERSION = "0.10.1";
|
|
3960
4385
|
}
|
|
3961
4386
|
});
|
|
3962
4387
|
|
|
@@ -3976,7 +4401,10 @@ function getBaseStyles() {
|
|
|
3976
4401
|
--red:#dc2626;
|
|
3977
4402
|
--cyan:#0891b2;
|
|
3978
4403
|
--green-bg:rgba(22,163,74,0.08);--green-bg-subtle:rgba(22,163,74,0.05);--green-border:rgba(22,163,74,0.2);--green-border-subtle:rgba(22,163,74,0.15);
|
|
3979
|
-
--amber-bg:rgba(217,119,6,0.
|
|
4404
|
+
--amber-bg:rgba(217,119,6,0.08);--amber-border:rgba(217,119,6,0.15);
|
|
4405
|
+
--red-bg:rgba(220,38,38,0.08);--red-border:rgba(220,38,38,0.2);
|
|
4406
|
+
--blue-bg:rgba(37,99,235,0.08);--cyan-bg:rgba(8,145,178,0.07);
|
|
4407
|
+
--accent-bg:rgba(99,102,241,0.08);
|
|
3980
4408
|
--sidebar-width:232px;--header-height:52px;
|
|
3981
4409
|
--radius:8px;--radius-sm:6px;
|
|
3982
4410
|
--shadow-sm:0 1px 3px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.03);
|
|
@@ -4033,6 +4461,7 @@ function getLayoutStyles() {
|
|
|
4033
4461
|
.sidebar-logo .logo-version{font-weight:400;font-size:11px;color:var(--text-muted);margin-left:8px;letter-spacing:0}
|
|
4034
4462
|
.sidebar-nav{padding:12px;flex:1}
|
|
4035
4463
|
.sidebar-section{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);padding:16px 12px 8px}
|
|
4464
|
+
.sidebar-divider{height:1px;background:var(--border-subtle);margin:8px 12px}
|
|
4036
4465
|
.sidebar-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:var(--radius);color:var(--text-dim);font-size:14px;font-weight:500;cursor:pointer;transition:all .15s;border:none;background:transparent;width:100%;text-align:left;font-family:var(--sans)}
|
|
4037
4466
|
.sidebar-item:hover{background:var(--bg-hover);color:var(--text)}
|
|
4038
4467
|
.sidebar-item.active{background:var(--bg-active);color:var(--accent)}
|
|
@@ -4041,6 +4470,7 @@ function getLayoutStyles() {
|
|
|
4041
4470
|
.sidebar-item:hover .item-icon{opacity:.8}
|
|
4042
4471
|
.sidebar-item .item-label{flex:1}
|
|
4043
4472
|
.sidebar-item .item-count{font-size:12px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:2px 8px;border-radius:10px;min-width:24px;text-align:center}
|
|
4473
|
+
.sidebar-beta{font-size:9px;color:#6366f1;background:#eef2ff;border:1px solid #e0e7ff;border-radius:4px;padding:0 5px;margin-left:auto;line-height:16px}
|
|
4044
4474
|
.sidebar-item.disabled{opacity:.35;cursor:default;pointer-events:none}
|
|
4045
4475
|
.sidebar-item .coming-soon{font-size:10px;color:var(--text-muted);background:var(--bg-muted);padding:2px 8px;border-radius:10px;font-weight:600;letter-spacing:.3px}
|
|
4046
4476
|
.sidebar-footer{padding:16px 24px;border-top:1px solid var(--border-subtle);font-size:12px;color:var(--text-muted);font-family:var(--mono)}
|
|
@@ -4065,7 +4495,8 @@ function getLayoutStyles() {
|
|
|
4065
4495
|
/* Content */
|
|
4066
4496
|
.main-content{flex:1;overflow-y:auto}
|
|
4067
4497
|
bk-dashboard{display:contents}
|
|
4068
|
-
bk-overview-view,bk-flows-view,bk-requests-view,bk-fetches-view,bk-queries-view,bk-errors-view,bk-logs-view,bk-security-view,bk-performance-view,bk-timeline-panel,bk-empty-state{display:block}
|
|
4498
|
+
bk-overview-view,bk-flows-view,bk-requests-view,bk-fetches-view,bk-queries-view,bk-errors-view,bk-logs-view,bk-security-view,bk-performance-view,bk-explorer-view,bk-insights-view,bk-timeline-panel,bk-empty-state{display:block}
|
|
4499
|
+
bk-graph-view{display:block}
|
|
4069
4500
|
bk-method-badge,bk-status-pill,bk-duration-label,bk-copy-button{display:inline-flex;flex-shrink:0}
|
|
4070
4501
|
bk-stat-card{display:inline-flex}
|
|
4071
4502
|
bk-toast{display:block;position:fixed;top:0;left:0;right:0;z-index:100;pointer-events:none}
|
|
@@ -4442,7 +4873,7 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
|
|
|
4442
4873
|
.perf-trend-faster{color:var(--green)}
|
|
4443
4874
|
`;
|
|
4444
4875
|
}
|
|
4445
|
-
var
|
|
4876
|
+
var init_graph2 = __esm({
|
|
4446
4877
|
"src/dashboard/styles/graph.ts"() {
|
|
4447
4878
|
"use strict";
|
|
4448
4879
|
}
|
|
@@ -4451,53 +4882,25 @@ var init_graph = __esm({
|
|
|
4451
4882
|
// src/dashboard/styles/overview.ts
|
|
4452
4883
|
function getOverviewStyles() {
|
|
4453
4884
|
return `
|
|
4454
|
-
|
|
4455
|
-
.ov-container{padding:24px 28px}
|
|
4456
|
-
|
|
4457
|
-
/* Summary banner */
|
|
4458
|
-
.ov-summary{display:flex;gap:10px;margin-bottom:24px;flex-wrap:wrap}
|
|
4459
|
-
.ov-stat{display:flex;flex-direction:column;gap:4px;flex:1;min-width:100px;padding:16px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-sm);transition:box-shadow var(--transition, .15s ease)}
|
|
4460
|
-
.ov-stat:hover{box-shadow:var(--shadow-md)}
|
|
4461
|
-
.ov-stat-value{font-size:22px;font-weight:700;font-family:var(--mono);color:var(--text);line-height:1.2}
|
|
4462
|
-
.ov-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
|
|
4463
|
-
|
|
4464
|
-
/* Section header */
|
|
4465
|
-
.ov-section-title{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
|
4466
|
-
.ov-issue-count{font-size:11px;font-family:var(--mono);color:var(--text-dim);background:var(--bg-muted);border:1px solid var(--border);padding:1px 8px;border-radius:10px}
|
|
4467
|
-
|
|
4468
|
-
/* Insight cards */
|
|
4469
|
-
.ov-cards{display:flex;flex-direction:column;gap:8px}
|
|
4470
|
-
.ov-card{display:flex;align-items:flex-start;gap:14px;padding:16px 20px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;transition:all var(--transition, .15s ease);box-shadow:var(--shadow-sm)}
|
|
4471
|
-
.ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md);transform:translateY(-1px)}
|
|
4472
|
-
.ov-card-icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:10px;border-radius:50%;margin-top:2px}
|
|
4473
|
-
.ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
|
|
4474
|
-
.ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
|
|
4475
|
-
.ov-card-icon.info{background:rgba(37,99,235,.08);color:var(--blue)}
|
|
4476
|
-
.ov-card-icon.resolved{background:var(--green-bg);color:var(--green)}
|
|
4477
|
-
.ov-card-body{flex:1;min-width:0}
|
|
4478
|
-
.ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
|
|
4479
|
-
.ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
|
|
4480
|
-
.ov-card-detail{font-size:11px;font-family:var(--mono);color:var(--text-muted);margin-top:6px;padding:8px 10px;background:var(--bg-muted);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);line-height:1.5}
|
|
4481
|
-
.ov-card-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
|
|
4482
|
-
.ov-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;margin-top:2px;font-family:var(--mono);transition:transform .15s}
|
|
4483
|
-
|
|
4484
|
-
/* Expanded card */
|
|
4485
|
-
.ov-card.expanded{border-color:var(--border-light);box-shadow:var(--shadow-md)}
|
|
4486
|
-
.ov-card-expand{display:none;margin-top:10px;padding-top:10px;border-top:1px solid var(--border)}
|
|
4487
|
-
.ov-card-hint{font-size:12px;color:var(--text-dim);line-height:1.5;margin-bottom:10px}
|
|
4488
|
-
.ov-card-link{font-size:12px;font-weight:600;color:var(--blue);cursor:pointer;display:inline-block;padding:4px 0}
|
|
4489
|
-
.ov-card-link:hover{text-decoration:underline}
|
|
4490
|
-
.ov-detail-label{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
|
|
4491
|
-
.ov-detail-item{font-size:12px;color:var(--text);font-family:var(--mono);padding:2px 0}
|
|
4492
|
-
|
|
4493
|
-
/* All-clear banner */
|
|
4494
|
-
.ov-clear{display:flex;align-items:center;gap:12px;padding:16px 20px;background:var(--green-bg-subtle);border:1px solid var(--green-border);border-radius:var(--radius);color:var(--green);font-size:13px;font-weight:500}
|
|
4495
|
-
.ov-clear-icon{font-size:16px}
|
|
4885
|
+
.ov-container{padding:28px}
|
|
4496
4886
|
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
.ov-card-
|
|
4500
|
-
.ov-card-
|
|
4887
|
+
.ov-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
|
|
4888
|
+
|
|
4889
|
+
.ov-card-nav{display:flex;align-items:center;gap:16px;padding:20px 24px;background:var(--bg-card);border:1px solid var(--border);border-radius:12px;cursor:pointer;transition:all .18s ease;box-shadow:0 1px 3px rgba(0,0,0,.04)}
|
|
4890
|
+
.ov-card-nav:hover{border-color:var(--border-light);box-shadow:0 4px 16px rgba(0,0,0,.06);transform:translateY(-2px)}
|
|
4891
|
+
.ov-card-nav:active{transform:translateY(0)}
|
|
4892
|
+
|
|
4893
|
+
.ov-card-empty{opacity:.55}
|
|
4894
|
+
.ov-card-empty:hover{opacity:.8}
|
|
4895
|
+
|
|
4896
|
+
.ov-card-icon-lg{font-size:22px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;flex-shrink:0;background:var(--bg-muted);border-radius:10px;color:var(--text-muted)}
|
|
4897
|
+
|
|
4898
|
+
.ov-card-content{flex:1;min-width:0}
|
|
4899
|
+
.ov-card-headline{font-size:16px;font-weight:700;color:var(--text);font-family:var(--mono);line-height:1.3}
|
|
4900
|
+
.ov-card-context{font-size:12px;color:var(--text-muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
4901
|
+
|
|
4902
|
+
.ov-card-arrow{font-size:16px;color:var(--text-muted);opacity:0;transition:opacity .15s,transform .15s;flex-shrink:0}
|
|
4903
|
+
.ov-card-nav:hover .ov-card-arrow{opacity:1;transform:translateX(2px)}
|
|
4501
4904
|
`;
|
|
4502
4905
|
}
|
|
4503
4906
|
var init_overview = __esm({
|
|
@@ -4779,9 +5182,205 @@ var init_timeline = __esm({
|
|
|
4779
5182
|
}
|
|
4780
5183
|
});
|
|
4781
5184
|
|
|
5185
|
+
// src/dashboard/styles/graph-view.ts
|
|
5186
|
+
function getGraphViewStyles() {
|
|
5187
|
+
return `
|
|
5188
|
+
.graph-wrapper{display:flex;flex-direction:column;height:calc(100vh - 120px);outline:none}
|
|
5189
|
+
|
|
5190
|
+
/* Toolbar \u2014 centered search, layers left, flow picker right */
|
|
5191
|
+
.graph-toolbar{display:flex;align-items:center;gap:10px;padding:8px 16px;border-bottom:1px solid var(--border)}
|
|
5192
|
+
|
|
5193
|
+
/* Layer toggles */
|
|
5194
|
+
.graph-layer-toggles{display:flex;gap:4px;flex-shrink:0}
|
|
5195
|
+
.graph-layer-btn{display:flex;align-items:center;gap:3px;font-size:10px;font-weight:500;padding:3px 8px;border:1px solid var(--border);border-radius:12px;background:var(--bg);color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap}
|
|
5196
|
+
.graph-layer-btn:hover{border-color:var(--text-muted);color:var(--text)}
|
|
5197
|
+
.graph-layer-btn.active{background:var(--bg-card);font-weight:600}
|
|
5198
|
+
|
|
5199
|
+
/* Search \u2014 takes remaining space, centered */
|
|
5200
|
+
.graph-search{flex:1;position:relative;display:flex;align-items:center;max-width:360px;margin:0 auto}
|
|
5201
|
+
.graph-search-icon{position:absolute;left:10px;color:var(--text-muted);font-size:13px;pointer-events:none;opacity:0.5}
|
|
5202
|
+
.graph-search-input{width:100%;font-size:11px;padding:6px 28px 6px 28px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);font-family:var(--mono);outline:none;transition:border-color .15s,box-shadow .15s}
|
|
5203
|
+
.graph-search-input:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.1)}
|
|
5204
|
+
.graph-search-input::placeholder{color:var(--text-muted);opacity:0.5}
|
|
5205
|
+
.graph-search-clear{position:absolute;right:8px;background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:11px;padding:0 4px;line-height:1;border-radius:3px}
|
|
5206
|
+
.graph-search-clear:hover{color:var(--text);background:var(--bg-card)}
|
|
5207
|
+
|
|
5208
|
+
/* Flow picker */
|
|
5209
|
+
.graph-flow-picker{font-size:10px;padding:4px 8px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);cursor:pointer;font-family:var(--mono);max-width:200px;flex-shrink:0}
|
|
5210
|
+
|
|
5211
|
+
/* Auth legend \u2014 inline in toolbar */
|
|
5212
|
+
.graph-auth-legend{display:flex;gap:8px;align-items:center;font-size:10px;color:var(--text-muted);flex-shrink:0}
|
|
5213
|
+
.graph-auth-legend-item{display:flex;align-items:center;gap:3px;white-space:nowrap}
|
|
5214
|
+
|
|
5215
|
+
/* Canvas */
|
|
5216
|
+
.graph-body{display:flex;flex:1;min-height:0}
|
|
5217
|
+
.graph-canvas{flex:1;overflow:hidden;padding:0;position:relative;min-height:0}
|
|
5218
|
+
.graph-svg{display:block}
|
|
5219
|
+
.graph-col-header{fill:#c4c4cc;font-size:9px;font-weight:600;font-family:'Inter',system-ui,sans-serif;letter-spacing:1.5px}
|
|
5220
|
+
|
|
5221
|
+
/* Floating controls \u2014 bottom-center pill */
|
|
5222
|
+
.graph-float{position:absolute;top:12px;right:12px;display:flex;align-items:center;gap:2px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:4px 6px;box-shadow:0 2px 12px rgba(0,0,0,.08);z-index:10}
|
|
5223
|
+
.graph-float-btn{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:13px;padding:4px 8px;line-height:1;border-radius:6px;transition:all .12s;white-space:nowrap}
|
|
5224
|
+
.graph-float-btn:hover{background:var(--bg);color:var(--text)}
|
|
5225
|
+
.graph-float-btn-accent{font-size:11px;font-weight:600;color:#6366f1}
|
|
5226
|
+
.graph-float-btn-accent:hover{background:rgba(99,102,241,.08);color:#4f46e5}
|
|
5227
|
+
.graph-float-zoom{font-size:10px;color:var(--text-muted);font-family:var(--mono);min-width:36px;text-align:center;user-select:none}
|
|
5228
|
+
.graph-float-sep{width:1px;height:16px;background:var(--border);margin:0 2px;flex-shrink:0}
|
|
5229
|
+
|
|
5230
|
+
/* Empty & loading states */
|
|
5231
|
+
.graph-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:400px;color:var(--text-muted);text-align:center;padding:40px}
|
|
5232
|
+
.graph-empty-icon{font-size:40px;opacity:0.25;margin-bottom:12px}
|
|
5233
|
+
.graph-empty-title{font-size:15px;font-weight:600;color:var(--text);margin-bottom:6px}
|
|
5234
|
+
.graph-empty-desc{font-size:12px;max-width:320px;line-height:1.5}
|
|
5235
|
+
.graph-loading{display:flex;align-items:center;justify-content:center;min-height:400px;color:var(--text-muted);font-size:13px}
|
|
5236
|
+
|
|
5237
|
+
/* Detail panel */
|
|
5238
|
+
.graph-detail{width:320px;border-left:1px solid var(--border);overflow-y:auto;padding:16px;background:var(--bg-card);flex-shrink:0;max-height:calc(100vh - 160px)}
|
|
5239
|
+
.graph-detail-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px}
|
|
5240
|
+
.graph-detail-badge{font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px}
|
|
5241
|
+
.graph-detail-name{font-size:14px;font-weight:700;color:var(--text);word-break:break-all;font-family:var(--mono)}
|
|
5242
|
+
.graph-detail-close{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:16px;padding:0 4px;line-height:1}
|
|
5243
|
+
.graph-detail-close:hover{color:var(--text)}
|
|
5244
|
+
|
|
5245
|
+
.graph-detail-auth-badge{display:inline-block;font-size:10px;font-weight:500;color:#059669;background:#ecfdf5;border:1px solid #a7f3d0;border-radius:4px;padding:1px 6px;margin-top:4px}
|
|
5246
|
+
.graph-detail-mw-badge{display:inline-block;font-size:10px;font-weight:500;color:#6b7280;background:#f3f4f6;border:1px solid #d1d5db;border-radius:4px;padding:1px 6px;margin-top:4px;margin-left:4px}
|
|
5247
|
+
|
|
5248
|
+
/* Detail tabs */
|
|
5249
|
+
.graph-detail-tabs{display:flex;gap:2px;margin-bottom:12px;border-bottom:1px solid var(--border);padding-bottom:0}
|
|
5250
|
+
.graph-detail-tab{background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:11px;font-weight:500;padding:6px 10px;transition:all .12s}
|
|
5251
|
+
.graph-detail-tab:hover{color:var(--text)}
|
|
5252
|
+
.graph-detail-tab.active{color:#6366f1;border-bottom-color:#6366f1}
|
|
5253
|
+
|
|
5254
|
+
/* Detail stats */
|
|
5255
|
+
.graph-detail-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}
|
|
5256
|
+
.graph-detail-stat{background:var(--bg);border-radius:var(--radius-sm);padding:10px 12px}
|
|
5257
|
+
.graph-detail-val{font-size:20px;font-weight:700;font-family:var(--mono);color:var(--text);line-height:1.2}
|
|
5258
|
+
.graph-detail-lbl{font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.6px;margin-top:2px}
|
|
5259
|
+
|
|
5260
|
+
/* Detail sections */
|
|
5261
|
+
.graph-detail-sec{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.8px;margin:12px 0 8px;padding-top:10px;border-top:1px solid var(--border)}
|
|
5262
|
+
.graph-detail-conn{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text);padding:6px 8px;background:var(--bg);border-radius:var(--radius-sm);margin-bottom:4px;font-family:var(--mono)}
|
|
5263
|
+
.graph-detail-edge-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
|
5264
|
+
.graph-detail-edge-type{font-size:10px;font-weight:600;text-transform:uppercase;min-width:42px}
|
|
5265
|
+
.graph-detail-dim{color:var(--text-muted);font-size:10px;margin-left:auto;white-space:nowrap}
|
|
5266
|
+
.graph-detail-sql{font-size:10px;color:var(--text-muted);padding:8px 10px;background:var(--bg);border-radius:var(--radius-sm);font-family:var(--mono);word-break:break-all;line-height:1.5;margin:0 0 4px;white-space:pre-wrap;border:1px solid var(--border)}
|
|
5267
|
+
|
|
5268
|
+
/* Security findings in detail */
|
|
5269
|
+
.graph-detail-finding{padding:8px 10px;background:var(--bg);border-radius:var(--radius-sm);margin-bottom:6px;border:1px solid var(--border)}
|
|
5270
|
+
.graph-detail-finding-title{font-size:12px;font-weight:600;color:var(--text);margin-top:4px}
|
|
5271
|
+
.graph-detail-finding-meta{font-size:10px;color:var(--text-muted);margin-top:2px;font-family:var(--mono)}
|
|
5272
|
+
.graph-detail-severity{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px;padding:1px 6px;border-radius:3px}
|
|
5273
|
+
.graph-detail-severity-critical{background:#fef2f2;color:#dc2626;border:1px solid #fecaca}
|
|
5274
|
+
.graph-detail-severity-warning{background:#fffbeb;color:#d97706;border:1px solid #fde68a}
|
|
5275
|
+
.graph-detail-severity-info{background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe}
|
|
5276
|
+
|
|
5277
|
+
/* Issues in detail */
|
|
5278
|
+
.graph-detail-issue-summary{margin-bottom:12px}
|
|
5279
|
+
.graph-detail-hint{font-size:11px;color:var(--text-muted);line-height:1.5;margin:0}
|
|
5280
|
+
.graph-detail-empty{font-size:12px;color:var(--text-muted);padding:16px;text-align:center}
|
|
5281
|
+
|
|
5282
|
+
/* Pulse animation for critical security badges */
|
|
5283
|
+
@keyframes graph-pulse{0%,100%{opacity:1}50%{opacity:0.5}}
|
|
5284
|
+
.graph-pulse{animation:graph-pulse 2s ease-in-out infinite}
|
|
5285
|
+
|
|
5286
|
+
/* Flow edge animation */
|
|
5287
|
+
@keyframes graph-flow-dash{to{stroke-dashoffset:-24}}
|
|
5288
|
+
.graph-flow-edge{animation:graph-flow-dash 1s linear infinite}
|
|
5289
|
+
`;
|
|
5290
|
+
}
|
|
5291
|
+
var init_graph_view = __esm({
|
|
5292
|
+
"src/dashboard/styles/graph-view.ts"() {
|
|
5293
|
+
"use strict";
|
|
5294
|
+
}
|
|
5295
|
+
});
|
|
5296
|
+
|
|
5297
|
+
// src/dashboard/styles/explorer.ts
|
|
5298
|
+
function getExplorerStyles() {
|
|
5299
|
+
return `
|
|
5300
|
+
/* Explorer sub-tabs */
|
|
5301
|
+
.explorer-tabs{display:flex;gap:0;border-bottom:1px solid var(--border);padding:0 28px;background:var(--bg);position:sticky;top:0;z-index:2}
|
|
5302
|
+
.explorer-tab{padding:10px 16px;font-size:13px;font-weight:500;color:var(--text-muted);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:6px;font-family:var(--sans);white-space:nowrap}
|
|
5303
|
+
.explorer-tab:hover{color:var(--text)}
|
|
5304
|
+
.explorer-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
|
5305
|
+
.explorer-tab-count{font-size:11px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:1px 6px;border-radius:8px}
|
|
5306
|
+
.explorer-tab.active .explorer-tab-count{color:var(--accent);background:var(--accent-bg)}
|
|
5307
|
+
`;
|
|
5308
|
+
}
|
|
5309
|
+
var init_explorer = __esm({
|
|
5310
|
+
"src/dashboard/styles/explorer.ts"() {
|
|
5311
|
+
"use strict";
|
|
5312
|
+
}
|
|
5313
|
+
});
|
|
5314
|
+
|
|
5315
|
+
// src/dashboard/styles/insights.ts
|
|
5316
|
+
function getInsightsStyles() {
|
|
5317
|
+
return `
|
|
5318
|
+
/* Insights filter chips */
|
|
5319
|
+
.insights-filters{display:flex;gap:6px;padding:16px 28px;border-bottom:1px solid var(--border);background:var(--bg);position:sticky;top:0;z-index:2}
|
|
5320
|
+
.insights-chip{font-size:12px;font-weight:500;padding:5px 14px;border:1px solid var(--border);border-radius:20px;background:var(--bg);color:var(--text-muted);cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:5px;font-family:var(--sans)}
|
|
5321
|
+
.insights-chip:hover{border-color:var(--text-muted);color:var(--text)}
|
|
5322
|
+
.insights-chip.active{background:var(--accent);color:white;border-color:var(--accent)}
|
|
5323
|
+
.insights-chip-count{font-size:10px;font-family:var(--mono);background:rgba(0,0,0,.08);padding:1px 5px;border-radius:8px}
|
|
5324
|
+
.insights-chip.active .insights-chip-count{background:rgba(255,255,255,.25)}
|
|
5325
|
+
|
|
5326
|
+
/* Insights card list */
|
|
5327
|
+
.insights-list{padding:16px 28px}
|
|
5328
|
+
|
|
5329
|
+
.insights-empty{display:flex;align-items:center;gap:10px;padding:24px;color:var(--green);font-size:14px;font-weight:500}
|
|
5330
|
+
.insights-empty-icon{font-size:18px}
|
|
5331
|
+
|
|
5332
|
+
.insights-card{display:flex;align-items:flex-start;gap:12px;padding:14px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .15s;margin-bottom:8px}
|
|
5333
|
+
.insights-card:hover{border-color:var(--border-light);box-shadow:0 2px 8px rgba(0,0,0,.04)}
|
|
5334
|
+
.insights-card.expanded{border-color:var(--border-light);box-shadow:0 2px 8px rgba(0,0,0,.04)}
|
|
5335
|
+
.insights-card.resolved{opacity:.55}
|
|
5336
|
+
.insights-card.resolved:hover{opacity:.8}
|
|
5337
|
+
|
|
5338
|
+
.insights-card-left{flex-shrink:0;padding-top:2px}
|
|
5339
|
+
.insights-sev{width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:10px;border-radius:50%}
|
|
5340
|
+
.insights-sev.critical{background:var(--red-bg);color:var(--red)}
|
|
5341
|
+
.insights-sev.warning{background:var(--amber-bg);color:var(--amber)}
|
|
5342
|
+
.insights-sev.info{background:var(--blue-bg);color:var(--blue)}
|
|
5343
|
+
|
|
5344
|
+
.insights-card-body{flex:1;min-width:0}
|
|
5345
|
+
.insights-card-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:3px}
|
|
5346
|
+
.insights-card-title{font-size:13px;font-weight:600;color:var(--text)}
|
|
5347
|
+
.insights-card-title.resolved{text-decoration:line-through;color:var(--text-muted)}
|
|
5348
|
+
.insights-card-cat{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);background:var(--bg-muted);padding:1px 6px;border-radius:4px}
|
|
5349
|
+
.insights-card-count{font-size:11px;font-family:var(--mono);color:var(--text-muted)}
|
|
5350
|
+
.insights-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
|
|
5351
|
+
.insights-card-detail{font-size:11px;font-family:var(--mono);color:var(--text-muted);margin-top:6px;padding:6px 10px;background:var(--bg-muted);border:1px solid var(--border-subtle);border-radius:6px;line-height:1.5}
|
|
5352
|
+
.insights-card-progress{font-size:11px;color:var(--text-muted);margin-top:4px;font-family:var(--mono)}
|
|
5353
|
+
.insights-card-hint{font-size:12px;color:var(--text-dim);line-height:1.6;margin-top:8px;padding-top:8px;border-top:1px solid var(--border)}
|
|
5354
|
+
|
|
5355
|
+
.insights-badge-regressed{font-size:9px;font-weight:700;color:var(--red);background:var(--red-bg);padding:1px 6px;border-radius:4px}
|
|
5356
|
+
.insights-badge-verifying{font-size:9px;font-weight:700;color:var(--amber);background:var(--amber-bg);padding:1px 6px;border-radius:4px}
|
|
5357
|
+
.insights-badge-resolved{font-size:9px;font-weight:700;color:var(--green);background:var(--green-bg);padding:1px 6px;border-radius:4px}
|
|
5358
|
+
|
|
5359
|
+
.insights-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;padding-top:2px;font-family:var(--mono);transition:transform .15s}
|
|
5360
|
+
|
|
5361
|
+
.insights-section{display:flex;align-items:center;gap:8px;padding:14px 0 8px;margin-top:4px;font-size:12px;font-weight:700;color:var(--text);text-transform:uppercase;letter-spacing:.5px;border-top:1px solid var(--border);user-select:none}
|
|
5362
|
+
.insights-section:first-child{border-top:none;margin-top:0}
|
|
5363
|
+
.insights-section-icon{font-size:11px;width:16px;text-align:center}
|
|
5364
|
+
.insights-section-count{font-size:11px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:1px 7px;border-radius:8px;font-weight:500}
|
|
5365
|
+
.insights-section-regressed{color:var(--red)}
|
|
5366
|
+
.insights-section-regressed .insights-section-count{color:var(--red);background:var(--red-bg)}
|
|
5367
|
+
.insights-section-verifying{color:var(--amber)}
|
|
5368
|
+
.insights-section-verifying .insights-section-count{color:var(--amber);background:var(--amber-bg)}
|
|
5369
|
+
.insights-section-resolved{color:var(--green)}
|
|
5370
|
+
.insights-section-resolved .insights-section-count{color:var(--green);background:var(--green-bg)}
|
|
5371
|
+
.insights-section-dismissed{color:var(--text-muted);cursor:pointer}
|
|
5372
|
+
.insights-section-dismissed:hover{color:var(--text)}
|
|
5373
|
+
`;
|
|
5374
|
+
}
|
|
5375
|
+
var init_insights3 = __esm({
|
|
5376
|
+
"src/dashboard/styles/insights.ts"() {
|
|
5377
|
+
"use strict";
|
|
5378
|
+
}
|
|
5379
|
+
});
|
|
5380
|
+
|
|
4782
5381
|
// src/dashboard/styles.ts
|
|
4783
5382
|
function getStyles() {
|
|
4784
|
-
return getBaseStyles() + getLayoutStyles() + getFlowStyles() + getRequestStyles() + getPerformanceStyles() + getOverviewStyles() + getSecurityStyles() + getTimelineStyles();
|
|
5383
|
+
return getBaseStyles() + getLayoutStyles() + getFlowStyles() + getRequestStyles() + getPerformanceStyles() + getOverviewStyles() + getSecurityStyles() + getTimelineStyles() + getGraphViewStyles() + getExplorerStyles() + getInsightsStyles();
|
|
4785
5384
|
}
|
|
4786
5385
|
var init_styles = __esm({
|
|
4787
5386
|
"src/dashboard/styles.ts"() {
|
|
@@ -4790,10 +5389,13 @@ var init_styles = __esm({
|
|
|
4790
5389
|
init_layout();
|
|
4791
5390
|
init_flows();
|
|
4792
5391
|
init_requests();
|
|
4793
|
-
|
|
5392
|
+
init_graph2();
|
|
4794
5393
|
init_overview();
|
|
4795
5394
|
init_security2();
|
|
4796
5395
|
init_timeline();
|
|
5396
|
+
init_graph_view();
|
|
5397
|
+
init_explorer();
|
|
5398
|
+
init_insights3();
|
|
4797
5399
|
}
|
|
4798
5400
|
});
|
|
4799
5401
|
|
|
@@ -4976,6 +5578,12 @@ function recordDashboardOpened() {
|
|
|
4976
5578
|
request_count_at_open: session.requestCount
|
|
4977
5579
|
});
|
|
4978
5580
|
}
|
|
5581
|
+
function recordGraphFeature(feature, detail) {
|
|
5582
|
+
trackEvent(TELEMETRY_EVENT_GRAPH_FEATURE, {
|
|
5583
|
+
feature,
|
|
5584
|
+
...detail ? { detail } : {}
|
|
5585
|
+
});
|
|
5586
|
+
}
|
|
4979
5587
|
function recordSetupCompleted(info) {
|
|
4980
5588
|
session.frameworkCandidates = info.frameworkCandidates;
|
|
4981
5589
|
session.adaptersFailed = info.adaptersFailed;
|
|
@@ -5035,8 +5643,8 @@ function trackSession(services) {
|
|
|
5035
5643
|
tabs_viewed: [...session.tabsViewed],
|
|
5036
5644
|
dashboard_opened: session.dashboardOpened,
|
|
5037
5645
|
explain_used: session.explainUsed,
|
|
5038
|
-
session_duration_s: Math.
|
|
5039
|
-
|
|
5646
|
+
session_duration_s: Math.ceil((now - session.startTime) / 1e3),
|
|
5647
|
+
session_duration_ms: now - session.startTime,
|
|
5040
5648
|
setup_succeeded: session.setupSucceeded,
|
|
5041
5649
|
setup_duration_ms: session.setupDurationMs,
|
|
5042
5650
|
framework_detection_candidates: session.frameworkCandidates,
|
|
@@ -5108,11 +5716,19 @@ function createDashboardHandler(services) {
|
|
|
5108
5716
|
issueStore,
|
|
5109
5717
|
services.bus
|
|
5110
5718
|
);
|
|
5719
|
+
routes[DASHBOARD_API_GRAPH] = createGraphHandler(services);
|
|
5111
5720
|
routes[DASHBOARD_API_TAB] = (req, res) => {
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
const tab =
|
|
5115
|
-
if (VALID_TABS.has(tab)
|
|
5721
|
+
if (isTelemetryEnabled()) {
|
|
5722
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
5723
|
+
const tab = url.searchParams.get("tab");
|
|
5724
|
+
if (tab && tab.length <= MAX_TAB_NAME_LENGTH && VALID_TABS.has(tab)) {
|
|
5725
|
+
recordTabViewed(tab);
|
|
5726
|
+
}
|
|
5727
|
+
const event = url.searchParams.get("event");
|
|
5728
|
+
if (event && event.length <= MAX_TAB_NAME_LENGTH) {
|
|
5729
|
+
const detail = url.searchParams.get("detail") ?? void 0;
|
|
5730
|
+
recordGraphFeature(event, detail?.slice(0, MAX_TAB_NAME_LENGTH));
|
|
5731
|
+
}
|
|
5116
5732
|
}
|
|
5117
5733
|
res.writeHead(HTTP_NO_CONTENT);
|
|
5118
5734
|
res.end();
|
|
@@ -5141,6 +5757,7 @@ var init_router = __esm({
|
|
|
5141
5757
|
init_labels();
|
|
5142
5758
|
init_api();
|
|
5143
5759
|
init_issues();
|
|
5760
|
+
init_graph();
|
|
5144
5761
|
init_sse();
|
|
5145
5762
|
init_page();
|
|
5146
5763
|
init_telemetry();
|
|
@@ -5557,116 +6174,732 @@ var init_metrics_store = __esm({
|
|
|
5557
6174
|
if (epMetrics.sessions.length > METRICS_MAX_SESSIONS) {
|
|
5558
6175
|
epMetrics.sessions = epMetrics.sessions.slice(-METRICS_MAX_SESSIONS);
|
|
5559
6176
|
}
|
|
5560
|
-
acc.durations.length = 0;
|
|
5561
|
-
acc.queryCounts.length = 0;
|
|
5562
|
-
acc.errorCount = 0;
|
|
6177
|
+
acc.durations.length = 0;
|
|
6178
|
+
acc.queryCounts.length = 0;
|
|
6179
|
+
acc.errorCount = 0;
|
|
6180
|
+
}
|
|
6181
|
+
for (const [endpoint, points] of this.pendingPoints) {
|
|
6182
|
+
if (points.length === 0) continue;
|
|
6183
|
+
const epMetrics = this.getOrCreateEndpoint(endpoint);
|
|
6184
|
+
const existing = epMetrics.dataPoints ?? [];
|
|
6185
|
+
epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
|
|
6186
|
+
}
|
|
6187
|
+
this.pendingPoints.clear();
|
|
6188
|
+
this.baselineCache.clear();
|
|
6189
|
+
if (!this.dirty) return;
|
|
6190
|
+
if (sync) {
|
|
6191
|
+
this.persistence.saveSync(this.data);
|
|
6192
|
+
} else {
|
|
6193
|
+
this.persistence.save(this.data);
|
|
6194
|
+
}
|
|
6195
|
+
this.dirty = false;
|
|
6196
|
+
}
|
|
6197
|
+
getOrCreateEndpoint(endpoint) {
|
|
6198
|
+
let ep = this.endpointIndex.get(endpoint);
|
|
6199
|
+
if (!ep) {
|
|
6200
|
+
ep = { endpoint, sessions: [] };
|
|
6201
|
+
this.data.endpoints.push(ep);
|
|
6202
|
+
this.endpointIndex.set(endpoint, ep);
|
|
6203
|
+
}
|
|
6204
|
+
return ep;
|
|
6205
|
+
}
|
|
6206
|
+
};
|
|
6207
|
+
}
|
|
6208
|
+
});
|
|
6209
|
+
|
|
6210
|
+
// src/store/metrics/persistence.ts
|
|
6211
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
6212
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6, unlinkSync as unlinkSync2 } from "fs";
|
|
6213
|
+
import { resolve as resolve4 } from "path";
|
|
6214
|
+
var DEFAULT_METRICS, FileMetricsPersistence;
|
|
6215
|
+
var init_persistence = __esm({
|
|
6216
|
+
"src/store/metrics/persistence.ts"() {
|
|
6217
|
+
"use strict";
|
|
6218
|
+
init_constants();
|
|
6219
|
+
init_atomic_writer();
|
|
6220
|
+
init_fs();
|
|
6221
|
+
init_log();
|
|
6222
|
+
init_type_guards();
|
|
6223
|
+
DEFAULT_METRICS = { version: 1, endpoints: [] };
|
|
6224
|
+
FileMetricsPersistence = class {
|
|
6225
|
+
constructor(dataDir) {
|
|
6226
|
+
this.metricsPath = resolve4(dataDir, METRICS_FILE);
|
|
6227
|
+
this.writer = new AtomicWriter({
|
|
6228
|
+
dir: dataDir,
|
|
6229
|
+
filePath: this.metricsPath,
|
|
6230
|
+
label: "metrics"
|
|
6231
|
+
});
|
|
6232
|
+
}
|
|
6233
|
+
load() {
|
|
6234
|
+
try {
|
|
6235
|
+
if (existsSync6(this.metricsPath)) {
|
|
6236
|
+
return this.parseMetrics(readFileSync5(this.metricsPath, "utf-8"));
|
|
6237
|
+
}
|
|
6238
|
+
} catch (err) {
|
|
6239
|
+
brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
|
|
6240
|
+
}
|
|
6241
|
+
return { ...DEFAULT_METRICS };
|
|
6242
|
+
}
|
|
6243
|
+
async loadAsync() {
|
|
6244
|
+
try {
|
|
6245
|
+
if (await fileExists(this.metricsPath)) {
|
|
6246
|
+
return this.parseMetrics(await readFile4(this.metricsPath, "utf-8"));
|
|
6247
|
+
}
|
|
6248
|
+
} catch (err) {
|
|
6249
|
+
brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
|
|
6250
|
+
}
|
|
6251
|
+
return { ...DEFAULT_METRICS };
|
|
6252
|
+
}
|
|
6253
|
+
/** Parse and validate metrics JSON, returning default empty data on invalid input. */
|
|
6254
|
+
parseMetrics(raw) {
|
|
6255
|
+
return validateMetricsData(JSON.parse(raw)) ?? { ...DEFAULT_METRICS };
|
|
6256
|
+
}
|
|
6257
|
+
save(data) {
|
|
6258
|
+
this.writer.writeAsync(JSON.stringify(data));
|
|
6259
|
+
}
|
|
6260
|
+
saveSync(data) {
|
|
6261
|
+
this.writer.writeSync(JSON.stringify(data));
|
|
6262
|
+
}
|
|
6263
|
+
remove() {
|
|
6264
|
+
try {
|
|
6265
|
+
if (existsSync6(this.metricsPath)) {
|
|
6266
|
+
unlinkSync2(this.metricsPath);
|
|
6267
|
+
}
|
|
6268
|
+
} catch (err) {
|
|
6269
|
+
brakitDebug(`failed to remove metrics file: ${getErrorMessage(err)}`);
|
|
6270
|
+
}
|
|
6271
|
+
}
|
|
6272
|
+
};
|
|
6273
|
+
}
|
|
6274
|
+
});
|
|
6275
|
+
|
|
6276
|
+
// src/store/index.ts
|
|
6277
|
+
var init_store = __esm({
|
|
6278
|
+
"src/store/index.ts"() {
|
|
6279
|
+
"use strict";
|
|
6280
|
+
init_request_store();
|
|
6281
|
+
init_telemetry_store();
|
|
6282
|
+
init_metrics_store();
|
|
6283
|
+
init_persistence();
|
|
6284
|
+
}
|
|
6285
|
+
});
|
|
6286
|
+
|
|
6287
|
+
// src/graph/constants.ts
|
|
6288
|
+
var MAX_PATTERNS_PER_EDGE, CLUSTER_SPLIT_THRESHOLD, COMMON_PATH_PREFIXES, PENDING_BUFFER_MAX, PENDING_EVICTION_TARGET, PENDING_TTL_MS;
|
|
6289
|
+
var init_constants2 = __esm({
|
|
6290
|
+
"src/graph/constants.ts"() {
|
|
6291
|
+
"use strict";
|
|
6292
|
+
MAX_PATTERNS_PER_EDGE = 10;
|
|
6293
|
+
CLUSTER_SPLIT_THRESHOLD = 15;
|
|
6294
|
+
COMMON_PATH_PREFIXES = /* @__PURE__ */ new Set(["api", "v1", "v2", "v3", "v4"]);
|
|
6295
|
+
PENDING_BUFFER_MAX = 500;
|
|
6296
|
+
PENDING_EVICTION_TARGET = 200;
|
|
6297
|
+
PENDING_TTL_MS = 6e4;
|
|
6298
|
+
}
|
|
6299
|
+
});
|
|
6300
|
+
|
|
6301
|
+
// src/graph/graph-builder.ts
|
|
6302
|
+
function shouldSkipRequest(req) {
|
|
6303
|
+
if (req.isStatic || req.isHealthCheck) return true;
|
|
6304
|
+
if (isDashboardRequest(req.path)) return true;
|
|
6305
|
+
return false;
|
|
6306
|
+
}
|
|
6307
|
+
function extractHostname(url) {
|
|
6308
|
+
try {
|
|
6309
|
+
const parsed = new URL(url);
|
|
6310
|
+
const host = parsed.hostname;
|
|
6311
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1")
|
|
6312
|
+
return null;
|
|
6313
|
+
return host;
|
|
6314
|
+
} catch {
|
|
6315
|
+
return null;
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
function makeEdgeId(source, target) {
|
|
6319
|
+
return `${source} -> ${target}`;
|
|
6320
|
+
}
|
|
6321
|
+
function upsertNode(graph, id, type, label, now) {
|
|
6322
|
+
let node = graph.nodes.get(id);
|
|
6323
|
+
if (!node) {
|
|
6324
|
+
node = {
|
|
6325
|
+
id,
|
|
6326
|
+
type,
|
|
6327
|
+
label,
|
|
6328
|
+
stats: {
|
|
6329
|
+
requestCount: 0,
|
|
6330
|
+
avgLatencyMs: 0,
|
|
6331
|
+
errorRate: 0,
|
|
6332
|
+
avgQueryCount: 0,
|
|
6333
|
+
lastSeenAt: now,
|
|
6334
|
+
firstSeenAt: now
|
|
6335
|
+
}
|
|
6336
|
+
};
|
|
6337
|
+
graph.nodes.set(id, node);
|
|
6338
|
+
}
|
|
6339
|
+
node.stats.lastSeenAt = now;
|
|
6340
|
+
return node;
|
|
6341
|
+
}
|
|
6342
|
+
function upsertEdge(graph, source, target, type, now) {
|
|
6343
|
+
const id = makeEdgeId(source, target);
|
|
6344
|
+
let edge = graph.edges.get(id);
|
|
6345
|
+
if (!edge) {
|
|
6346
|
+
edge = {
|
|
6347
|
+
id,
|
|
6348
|
+
source,
|
|
6349
|
+
target,
|
|
6350
|
+
type,
|
|
6351
|
+
stats: {
|
|
6352
|
+
frequency: 0,
|
|
6353
|
+
avgLatencyMs: 0,
|
|
6354
|
+
lastSeenAt: now,
|
|
6355
|
+
firstSeenAt: now
|
|
6356
|
+
}
|
|
6357
|
+
};
|
|
6358
|
+
graph.edges.set(id, edge);
|
|
6359
|
+
}
|
|
6360
|
+
edge.stats.lastSeenAt = now;
|
|
6361
|
+
return edge;
|
|
6362
|
+
}
|
|
6363
|
+
function updateRollingAvg(current, newValue, count) {
|
|
6364
|
+
return Math.round(current + (newValue - current) / count);
|
|
6365
|
+
}
|
|
6366
|
+
var GraphBuilder;
|
|
6367
|
+
var init_graph_builder = __esm({
|
|
6368
|
+
"src/graph/graph-builder.ts"() {
|
|
6369
|
+
"use strict";
|
|
6370
|
+
init_endpoint();
|
|
6371
|
+
init_normalize();
|
|
6372
|
+
init_router();
|
|
6373
|
+
init_categorize();
|
|
6374
|
+
init_constants2();
|
|
6375
|
+
GraphBuilder = class {
|
|
6376
|
+
constructor(bus, requestStore) {
|
|
6377
|
+
this.bus = bus;
|
|
6378
|
+
this.requestStore = requestStore;
|
|
6379
|
+
this.graph = {
|
|
6380
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
6381
|
+
edges: /* @__PURE__ */ new Map(),
|
|
6382
|
+
metadata: { totalObservations: 0, lastUpdatedAt: Date.now() }
|
|
6383
|
+
};
|
|
6384
|
+
/**
|
|
6385
|
+
* Buffered telemetry events waiting for their parent request to complete.
|
|
6386
|
+
* Key = parentRequestId, value = pending queries/fetches.
|
|
6387
|
+
*/
|
|
6388
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
6389
|
+
/** Accumulated request categories per endpoint node. */
|
|
6390
|
+
this.nodeCategories = /* @__PURE__ */ new Map();
|
|
6391
|
+
/** Latest analysis snapshot — refreshed on every analysis:updated event. */
|
|
6392
|
+
this.latestAnalysis = null;
|
|
6393
|
+
this.cleanupUnsubs = [];
|
|
6394
|
+
}
|
|
6395
|
+
start() {
|
|
6396
|
+
this.cleanupUnsubs.push(
|
|
6397
|
+
this.bus.on("request:completed", (req) => this.handleRequest(req)),
|
|
6398
|
+
this.bus.on("telemetry:query", (q) => this.handleQuery(q)),
|
|
6399
|
+
this.bus.on("telemetry:fetch", (f) => this.handleFetch(f)),
|
|
6400
|
+
this.bus.on(
|
|
6401
|
+
"analysis:updated",
|
|
6402
|
+
(update) => this.handleAnalysisUpdate(update)
|
|
6403
|
+
)
|
|
6404
|
+
);
|
|
6405
|
+
}
|
|
6406
|
+
stop() {
|
|
6407
|
+
for (const unsub of this.cleanupUnsubs) unsub();
|
|
6408
|
+
this.cleanupUnsubs.length = 0;
|
|
6409
|
+
}
|
|
6410
|
+
getGraph() {
|
|
6411
|
+
return this.graph;
|
|
6412
|
+
}
|
|
6413
|
+
/**
|
|
6414
|
+
* Enrich endpoint nodes with p95 from an external metrics source.
|
|
6415
|
+
* Called by the API handler which has access to MetricsStore.
|
|
6416
|
+
*/
|
|
6417
|
+
enrichWithMetrics(getP95) {
|
|
6418
|
+
for (const node of this.graph.nodes.values()) {
|
|
6419
|
+
if (node.type !== "endpoint") continue;
|
|
6420
|
+
const key = node.label;
|
|
6421
|
+
const p95 = getP95(key);
|
|
6422
|
+
if (p95 !== void 0) {
|
|
6423
|
+
if (!node.annotations) node.annotations = {};
|
|
6424
|
+
node.annotations.p95Ms = Math.round(p95);
|
|
6425
|
+
}
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
getApiResponse(options) {
|
|
6429
|
+
const grouping = options?.grouping ?? "path";
|
|
6430
|
+
const clusters = this.computeClusters(grouping);
|
|
6431
|
+
if (options?.level === "endpoints") {
|
|
6432
|
+
return this.getEndpointView();
|
|
6433
|
+
}
|
|
6434
|
+
if (options?.node) {
|
|
6435
|
+
return this.getNodeNeighborhood(options.node, clusters);
|
|
6436
|
+
}
|
|
6437
|
+
if (options?.cluster) {
|
|
6438
|
+
return this.getClusterExpanded(options.cluster, clusters);
|
|
6439
|
+
}
|
|
6440
|
+
return this.getClusterView(clusters);
|
|
6441
|
+
}
|
|
6442
|
+
clear() {
|
|
6443
|
+
this.graph.nodes.clear();
|
|
6444
|
+
this.graph.edges.clear();
|
|
6445
|
+
this.graph.metadata.totalObservations = 0;
|
|
6446
|
+
this.pending.clear();
|
|
6447
|
+
this.nodeCategories.clear();
|
|
6448
|
+
this.latestAnalysis = null;
|
|
6449
|
+
}
|
|
6450
|
+
handleAnalysisUpdate(update) {
|
|
6451
|
+
this.latestAnalysis = update;
|
|
6452
|
+
this.enrichNodesFromAnalysis();
|
|
6453
|
+
}
|
|
6454
|
+
enrichNodesFromAnalysis() {
|
|
6455
|
+
if (!this.latestAnalysis) return;
|
|
6456
|
+
const { findings, insights, issues } = this.latestAnalysis;
|
|
6457
|
+
for (const node of this.graph.nodes.values()) {
|
|
6458
|
+
if (node.type !== "endpoint") continue;
|
|
6459
|
+
if (node.annotations) {
|
|
6460
|
+
node.annotations.securityFindings = void 0;
|
|
6461
|
+
node.annotations.insights = void 0;
|
|
6462
|
+
node.annotations.openIssueCount = void 0;
|
|
6463
|
+
}
|
|
6464
|
+
}
|
|
6465
|
+
for (const f of findings) {
|
|
6466
|
+
if (!f.endpoint) continue;
|
|
6467
|
+
const nodeId = `endpoint:${f.endpoint}`;
|
|
6468
|
+
const node = this.graph.nodes.get(nodeId);
|
|
6469
|
+
if (!node) continue;
|
|
6470
|
+
if (!node.annotations) node.annotations = {};
|
|
6471
|
+
if (!node.annotations.securityFindings)
|
|
6472
|
+
node.annotations.securityFindings = [];
|
|
6473
|
+
const existing = node.annotations.securityFindings.find(
|
|
6474
|
+
(s) => s.rule === f.rule
|
|
6475
|
+
);
|
|
6476
|
+
if (existing) {
|
|
6477
|
+
existing.count = f.count;
|
|
6478
|
+
} else {
|
|
6479
|
+
node.annotations.securityFindings.push({
|
|
6480
|
+
rule: f.rule,
|
|
6481
|
+
severity: f.severity,
|
|
6482
|
+
title: f.title,
|
|
6483
|
+
count: f.count
|
|
6484
|
+
});
|
|
6485
|
+
}
|
|
6486
|
+
}
|
|
6487
|
+
const endpointNodesByLabel = /* @__PURE__ */ new Map();
|
|
6488
|
+
for (const node of this.graph.nodes.values()) {
|
|
6489
|
+
if (node.type === "endpoint") endpointNodesByLabel.set(node.label, node);
|
|
6490
|
+
}
|
|
6491
|
+
for (const insight of insights) {
|
|
6492
|
+
if (!insight.nav) continue;
|
|
6493
|
+
for (const [label, node] of endpointNodesByLabel) {
|
|
6494
|
+
if (insight.title.includes(label) || insight.desc.includes(label)) {
|
|
6495
|
+
if (!node.annotations) node.annotations = {};
|
|
6496
|
+
if (!node.annotations.insights) node.annotations.insights = [];
|
|
6497
|
+
if (!node.annotations.insights.some(
|
|
6498
|
+
(i) => i.type === insight.type && i.title === insight.title
|
|
6499
|
+
)) {
|
|
6500
|
+
node.annotations.insights.push({
|
|
6501
|
+
type: insight.type,
|
|
6502
|
+
severity: insight.severity,
|
|
6503
|
+
title: insight.title
|
|
6504
|
+
});
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
for (const si of issues) {
|
|
6510
|
+
if (!si.issue.endpoint) continue;
|
|
6511
|
+
const nodeId = `endpoint:${si.issue.endpoint}`;
|
|
6512
|
+
const node = this.graph.nodes.get(nodeId);
|
|
6513
|
+
if (!node) continue;
|
|
6514
|
+
if (si.state === "open" || si.state === "regressed") {
|
|
6515
|
+
if (!node.annotations) node.annotations = {};
|
|
6516
|
+
node.annotations.openIssueCount = (node.annotations.openIssueCount ?? 0) + 1;
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
for (const insight of insights) {
|
|
6520
|
+
if (insight.type !== "n1" && insight.type !== "redundant-query") continue;
|
|
6521
|
+
for (const edge of this.graph.edges.values()) {
|
|
6522
|
+
if (edge.type !== "reads" && edge.type !== "writes") continue;
|
|
6523
|
+
const sourceNode = this.graph.nodes.get(edge.source);
|
|
6524
|
+
if (sourceNode?.annotations?.insights?.some(
|
|
6525
|
+
(i) => i.type === insight.type
|
|
6526
|
+
)) {
|
|
6527
|
+
if (!edge.annotations) edge.annotations = {};
|
|
6528
|
+
edge.annotations.hasIssue = true;
|
|
6529
|
+
}
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
6533
|
+
handleRequest(req) {
|
|
6534
|
+
if (shouldSkipRequest(req)) return;
|
|
6535
|
+
const now = Date.now();
|
|
6536
|
+
const endpointKey = getEndpointKey(req.method, req.path);
|
|
6537
|
+
const nodeId = `endpoint:${endpointKey}`;
|
|
6538
|
+
const node = upsertNode(this.graph, nodeId, "endpoint", endpointKey, now);
|
|
6539
|
+
node.stats.requestCount++;
|
|
6540
|
+
const category = detectCategory(req);
|
|
6541
|
+
let cats = this.nodeCategories.get(nodeId);
|
|
6542
|
+
if (!cats) {
|
|
6543
|
+
cats = /* @__PURE__ */ new Set();
|
|
6544
|
+
this.nodeCategories.set(nodeId, cats);
|
|
6545
|
+
}
|
|
6546
|
+
cats.add(category);
|
|
6547
|
+
if (!node.annotations) node.annotations = {};
|
|
6548
|
+
node.annotations.categories = [...cats];
|
|
6549
|
+
node.annotations.isMiddleware = cats.has("middleware");
|
|
6550
|
+
if (!node.annotations.hasAuth) {
|
|
6551
|
+
node.annotations.hasAuth = cats.has("auth-check") || cats.has("auth-handshake") || hasAuthCredentials(req);
|
|
6552
|
+
}
|
|
6553
|
+
node.stats.avgLatencyMs = updateRollingAvg(
|
|
6554
|
+
node.stats.avgLatencyMs,
|
|
6555
|
+
req.durationMs,
|
|
6556
|
+
node.stats.requestCount
|
|
6557
|
+
);
|
|
6558
|
+
const isError = req.statusCode >= 400 ? 1 : 0;
|
|
6559
|
+
node.stats.errorRate = node.stats.errorRate + (isError - node.stats.errorRate) / node.stats.requestCount;
|
|
6560
|
+
this.graph.metadata.totalObservations++;
|
|
6561
|
+
this.graph.metadata.lastUpdatedAt = now;
|
|
6562
|
+
const buffered = this.pending.get(req.id);
|
|
6563
|
+
if (buffered) {
|
|
6564
|
+
let queryCount = 0;
|
|
6565
|
+
for (const query of buffered.queries) {
|
|
6566
|
+
this.processQuery(query, nodeId, now);
|
|
6567
|
+
queryCount++;
|
|
6568
|
+
}
|
|
6569
|
+
for (const fetch of buffered.fetches) {
|
|
6570
|
+
this.processFetch(fetch, nodeId, now);
|
|
6571
|
+
}
|
|
6572
|
+
this.pending.delete(req.id);
|
|
6573
|
+
if (queryCount > 0) {
|
|
6574
|
+
node.stats.avgQueryCount = updateRollingAvg(
|
|
6575
|
+
node.stats.avgQueryCount,
|
|
6576
|
+
queryCount,
|
|
6577
|
+
node.stats.requestCount
|
|
6578
|
+
);
|
|
6579
|
+
}
|
|
5563
6580
|
}
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
const existing = epMetrics.dataPoints ?? [];
|
|
5568
|
-
epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
|
|
6581
|
+
const now2 = Date.now();
|
|
6582
|
+
for (const [key, entry] of this.pending) {
|
|
6583
|
+
if (now2 - entry.createdAt > PENDING_TTL_MS) this.pending.delete(key);
|
|
5569
6584
|
}
|
|
5570
|
-
this.
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
} else {
|
|
5576
|
-
this.persistence.save(this.data);
|
|
6585
|
+
if (this.pending.size > PENDING_BUFFER_MAX) {
|
|
6586
|
+
const keys = [...this.pending.keys()];
|
|
6587
|
+
for (let i = 0; i < keys.length - PENDING_EVICTION_TARGET; i++) {
|
|
6588
|
+
this.pending.delete(keys[i]);
|
|
6589
|
+
}
|
|
5577
6590
|
}
|
|
5578
|
-
this.dirty = false;
|
|
5579
6591
|
}
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
this.
|
|
6592
|
+
handleQuery(query) {
|
|
6593
|
+
if (!query.parentRequestId || !query.table) return;
|
|
6594
|
+
let pending2 = this.pending.get(query.parentRequestId);
|
|
6595
|
+
if (!pending2) {
|
|
6596
|
+
pending2 = { queries: [], fetches: [], createdAt: Date.now() };
|
|
6597
|
+
this.pending.set(query.parentRequestId, pending2);
|
|
5586
6598
|
}
|
|
5587
|
-
|
|
5588
|
-
}
|
|
5589
|
-
};
|
|
5590
|
-
}
|
|
5591
|
-
});
|
|
5592
|
-
|
|
5593
|
-
// src/store/metrics/persistence.ts
|
|
5594
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
5595
|
-
import { readFileSync as readFileSync5, existsSync as existsSync6, unlinkSync as unlinkSync2 } from "fs";
|
|
5596
|
-
import { resolve as resolve4 } from "path";
|
|
5597
|
-
var DEFAULT_METRICS, FileMetricsPersistence;
|
|
5598
|
-
var init_persistence = __esm({
|
|
5599
|
-
"src/store/metrics/persistence.ts"() {
|
|
5600
|
-
"use strict";
|
|
5601
|
-
init_constants();
|
|
5602
|
-
init_atomic_writer();
|
|
5603
|
-
init_fs();
|
|
5604
|
-
init_log();
|
|
5605
|
-
init_type_guards();
|
|
5606
|
-
DEFAULT_METRICS = { version: 1, endpoints: [] };
|
|
5607
|
-
FileMetricsPersistence = class {
|
|
5608
|
-
constructor(dataDir) {
|
|
5609
|
-
this.metricsPath = resolve4(dataDir, METRICS_FILE);
|
|
5610
|
-
this.writer = new AtomicWriter({
|
|
5611
|
-
dir: dataDir,
|
|
5612
|
-
filePath: this.metricsPath,
|
|
5613
|
-
label: "metrics"
|
|
5614
|
-
});
|
|
6599
|
+
pending2.queries.push(query);
|
|
5615
6600
|
}
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
6601
|
+
handleFetch(fetch) {
|
|
6602
|
+
if (!fetch.parentRequestId) return;
|
|
6603
|
+
const hostname = extractHostname(fetch.url);
|
|
6604
|
+
if (!hostname) return;
|
|
6605
|
+
let pending2 = this.pending.get(fetch.parentRequestId);
|
|
6606
|
+
if (!pending2) {
|
|
6607
|
+
pending2 = { queries: [], fetches: [], createdAt: Date.now() };
|
|
6608
|
+
this.pending.set(fetch.parentRequestId, pending2);
|
|
6609
|
+
}
|
|
6610
|
+
pending2.fetches.push(fetch);
|
|
6611
|
+
}
|
|
6612
|
+
// ── Process buffered events (called after request completes) ──
|
|
6613
|
+
processQuery(query, endpointNodeId, now) {
|
|
6614
|
+
if (!query.table) return;
|
|
6615
|
+
const tableNodeId = `table:${query.table}`;
|
|
6616
|
+
upsertNode(this.graph, tableNodeId, "table", query.table, now);
|
|
6617
|
+
const edgeType = query.normalizedOp === "SELECT" ? "reads" : "writes";
|
|
6618
|
+
const edge = upsertEdge(
|
|
6619
|
+
this.graph,
|
|
6620
|
+
endpointNodeId,
|
|
6621
|
+
tableNodeId,
|
|
6622
|
+
edgeType,
|
|
6623
|
+
now
|
|
6624
|
+
);
|
|
6625
|
+
edge.stats.frequency++;
|
|
6626
|
+
edge.stats.avgLatencyMs = updateRollingAvg(
|
|
6627
|
+
edge.stats.avgLatencyMs,
|
|
6628
|
+
query.durationMs,
|
|
6629
|
+
edge.stats.frequency
|
|
6630
|
+
);
|
|
6631
|
+
if (query.sql) {
|
|
6632
|
+
const normalized = normalizeQueryParams(query.sql);
|
|
6633
|
+
if (normalized) {
|
|
6634
|
+
if (!edge.patterns) edge.patterns = [];
|
|
6635
|
+
if (!edge.patterns.includes(normalized) && edge.patterns.length < MAX_PATTERNS_PER_EDGE) {
|
|
6636
|
+
edge.patterns.push(normalized);
|
|
6637
|
+
}
|
|
5620
6638
|
}
|
|
5621
|
-
} catch (err) {
|
|
5622
|
-
brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
|
|
5623
6639
|
}
|
|
5624
|
-
return { ...DEFAULT_METRICS };
|
|
5625
6640
|
}
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
6641
|
+
processFetch(fetch, endpointNodeId, now) {
|
|
6642
|
+
const hostname = extractHostname(fetch.url);
|
|
6643
|
+
if (!hostname) return;
|
|
6644
|
+
const externalNodeId = `external:${hostname}`;
|
|
6645
|
+
upsertNode(this.graph, externalNodeId, "external", hostname, now);
|
|
6646
|
+
const edge = upsertEdge(
|
|
6647
|
+
this.graph,
|
|
6648
|
+
endpointNodeId,
|
|
6649
|
+
externalNodeId,
|
|
6650
|
+
"fetches",
|
|
6651
|
+
now
|
|
6652
|
+
);
|
|
6653
|
+
edge.stats.frequency++;
|
|
6654
|
+
edge.stats.avgLatencyMs = updateRollingAvg(
|
|
6655
|
+
edge.stats.avgLatencyMs,
|
|
6656
|
+
fetch.durationMs,
|
|
6657
|
+
edge.stats.frequency
|
|
6658
|
+
);
|
|
6659
|
+
}
|
|
6660
|
+
// ── Clustering ──
|
|
6661
|
+
computeClusters(strategy = "path") {
|
|
6662
|
+
const endpointNodes = [...this.graph.nodes.values()].filter(
|
|
6663
|
+
(n) => n.type === "endpoint"
|
|
6664
|
+
);
|
|
6665
|
+
if (strategy === "auth-boundary") {
|
|
6666
|
+
return this.clusterByAuthBoundary(endpointNodes);
|
|
6667
|
+
}
|
|
6668
|
+
if (strategy === "data-domain") {
|
|
6669
|
+
return this.clusterByDataDomain(endpointNodes);
|
|
6670
|
+
}
|
|
6671
|
+
return this.clusterByPath(endpointNodes);
|
|
6672
|
+
}
|
|
6673
|
+
clusterByPath(endpointNodes) {
|
|
6674
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6675
|
+
for (const node of endpointNodes) {
|
|
6676
|
+
const parts = node.label.split(" ");
|
|
6677
|
+
const path = parts[1] || parts[0];
|
|
6678
|
+
const segments = path.split("/").filter(Boolean);
|
|
6679
|
+
let i = 0;
|
|
6680
|
+
while (i < segments.length && COMMON_PATH_PREFIXES.has(segments[i])) {
|
|
6681
|
+
i++;
|
|
5630
6682
|
}
|
|
5631
|
-
|
|
5632
|
-
|
|
6683
|
+
const groupKey = segments[i] || segments[0] || "root";
|
|
6684
|
+
if (!groups.has(groupKey)) groups.set(groupKey, []);
|
|
6685
|
+
groups.get(groupKey).push(node.id);
|
|
5633
6686
|
}
|
|
5634
|
-
|
|
6687
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
6688
|
+
for (const [key, children] of groups) {
|
|
6689
|
+
if (children.length > CLUSTER_SPLIT_THRESHOLD) {
|
|
6690
|
+
const subGroups = /* @__PURE__ */ new Map();
|
|
6691
|
+
for (const childId of children) {
|
|
6692
|
+
const node = this.graph.nodes.get(childId);
|
|
6693
|
+
const parts = node.label.split(" ");
|
|
6694
|
+
const path = parts[1] || parts[0];
|
|
6695
|
+
const segments = path.split("/").filter(Boolean);
|
|
6696
|
+
let idx = segments.indexOf(key);
|
|
6697
|
+
if (idx === -1) idx = 0;
|
|
6698
|
+
const subKey = `${key}/${segments[idx + 1] || "root"}`;
|
|
6699
|
+
if (!subGroups.has(subKey)) subGroups.set(subKey, []);
|
|
6700
|
+
subGroups.get(subKey).push(childId);
|
|
6701
|
+
}
|
|
6702
|
+
for (const [subKey, subChildren] of subGroups) {
|
|
6703
|
+
clusters.set(subKey, this.buildCluster(subKey, subChildren));
|
|
6704
|
+
}
|
|
6705
|
+
} else {
|
|
6706
|
+
clusters.set(key, this.buildCluster(key, children));
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
return clusters;
|
|
6710
|
+
}
|
|
6711
|
+
clusterByAuthBoundary(endpointNodes) {
|
|
6712
|
+
const authed = [];
|
|
6713
|
+
const unauthed = [];
|
|
6714
|
+
for (const node of endpointNodes) {
|
|
6715
|
+
if (node.annotations?.hasAuth) {
|
|
6716
|
+
authed.push(node.id);
|
|
6717
|
+
} else {
|
|
6718
|
+
unauthed.push(node.id);
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
6722
|
+
if (authed.length > 0)
|
|
6723
|
+
clusters.set("authenticated", this.buildCluster("authenticated", authed));
|
|
6724
|
+
if (unauthed.length > 0)
|
|
6725
|
+
clusters.set(
|
|
6726
|
+
"unauthenticated",
|
|
6727
|
+
this.buildCluster("unauthenticated", unauthed)
|
|
6728
|
+
);
|
|
6729
|
+
return clusters;
|
|
6730
|
+
}
|
|
6731
|
+
clusterByDataDomain(endpointNodes) {
|
|
6732
|
+
const endpointTables = /* @__PURE__ */ new Map();
|
|
6733
|
+
for (const edge of this.graph.edges.values()) {
|
|
6734
|
+
if (edge.type !== "reads" && edge.type !== "writes") continue;
|
|
6735
|
+
const tableLabel = this.graph.nodes.get(edge.target)?.label;
|
|
6736
|
+
if (!tableLabel) continue;
|
|
6737
|
+
let tables = endpointTables.get(edge.source);
|
|
6738
|
+
if (!tables) {
|
|
6739
|
+
tables = /* @__PURE__ */ new Set();
|
|
6740
|
+
endpointTables.set(edge.source, tables);
|
|
6741
|
+
}
|
|
6742
|
+
tables.add(tableLabel);
|
|
6743
|
+
}
|
|
6744
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6745
|
+
for (const node of endpointNodes) {
|
|
6746
|
+
const tables = endpointTables.get(node.id);
|
|
6747
|
+
const groupKey = tables && tables.size > 0 ? [...tables].sort().join("+") : "no-db";
|
|
6748
|
+
if (!groups.has(groupKey)) groups.set(groupKey, []);
|
|
6749
|
+
groups.get(groupKey).push(node.id);
|
|
6750
|
+
}
|
|
6751
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
6752
|
+
for (const [key, children] of groups) {
|
|
6753
|
+
clusters.set(key, this.buildCluster(key, children));
|
|
6754
|
+
}
|
|
6755
|
+
return clusters;
|
|
6756
|
+
}
|
|
6757
|
+
buildCluster(key, children) {
|
|
6758
|
+
let totalLatency = 0;
|
|
6759
|
+
let totalRequests = 0;
|
|
6760
|
+
let totalErrors = 0;
|
|
6761
|
+
let totalQueries = 0;
|
|
6762
|
+
let firstSeen = Infinity;
|
|
6763
|
+
let lastSeen = 0;
|
|
6764
|
+
for (const childId of children) {
|
|
6765
|
+
const node = this.graph.nodes.get(childId);
|
|
6766
|
+
if (!node) continue;
|
|
6767
|
+
totalRequests += node.stats.requestCount;
|
|
6768
|
+
totalLatency += node.stats.avgLatencyMs * node.stats.requestCount;
|
|
6769
|
+
totalErrors += node.stats.errorRate * node.stats.requestCount;
|
|
6770
|
+
totalQueries += node.stats.avgQueryCount * node.stats.requestCount;
|
|
6771
|
+
firstSeen = Math.min(firstSeen, node.stats.firstSeenAt);
|
|
6772
|
+
lastSeen = Math.max(lastSeen, node.stats.lastSeenAt);
|
|
6773
|
+
}
|
|
6774
|
+
return {
|
|
6775
|
+
id: `cluster:${key}`,
|
|
6776
|
+
label: key,
|
|
6777
|
+
children,
|
|
6778
|
+
stats: {
|
|
6779
|
+
requestCount: totalRequests,
|
|
6780
|
+
avgLatencyMs: totalRequests > 0 ? Math.round(totalLatency / totalRequests) : 0,
|
|
6781
|
+
errorRate: totalRequests > 0 ? totalErrors / totalRequests : 0,
|
|
6782
|
+
avgQueryCount: totalRequests > 0 ? Math.round(totalQueries / totalRequests * 10) / 10 : 0,
|
|
6783
|
+
lastSeenAt: lastSeen || Date.now(),
|
|
6784
|
+
firstSeenAt: firstSeen === Infinity ? Date.now() : firstSeen
|
|
6785
|
+
}
|
|
6786
|
+
};
|
|
5635
6787
|
}
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
6788
|
+
// ── API response builders ──
|
|
6789
|
+
getEndpointView() {
|
|
6790
|
+
const allNodes = [...this.graph.nodes.values()];
|
|
6791
|
+
const allEdges = [...this.graph.edges.values()];
|
|
6792
|
+
return {
|
|
6793
|
+
nodes: allNodes,
|
|
6794
|
+
edges: allEdges,
|
|
6795
|
+
clusters: [],
|
|
6796
|
+
metadata: this.graph.metadata
|
|
6797
|
+
};
|
|
5639
6798
|
}
|
|
5640
|
-
|
|
5641
|
-
|
|
6799
|
+
getClusterView(clusters) {
|
|
6800
|
+
const clusterArr = [...clusters.values()];
|
|
6801
|
+
const otherNodes = [...this.graph.nodes.values()].filter(
|
|
6802
|
+
(n) => n.type !== "endpoint"
|
|
6803
|
+
);
|
|
6804
|
+
const endpointToCluster = /* @__PURE__ */ new Map();
|
|
6805
|
+
for (const c of clusterArr) {
|
|
6806
|
+
for (const childId of c.children) {
|
|
6807
|
+
endpointToCluster.set(childId, c.id);
|
|
6808
|
+
}
|
|
6809
|
+
}
|
|
6810
|
+
const edgeAgg = /* @__PURE__ */ new Map();
|
|
6811
|
+
for (const edge of this.graph.edges.values()) {
|
|
6812
|
+
const sourceCluster = endpointToCluster.get(edge.source) ?? edge.source;
|
|
6813
|
+
const target = edge.target;
|
|
6814
|
+
const aggId = makeEdgeId(sourceCluster, target);
|
|
6815
|
+
let agg = edgeAgg.get(aggId);
|
|
6816
|
+
if (!agg) {
|
|
6817
|
+
agg = {
|
|
6818
|
+
id: aggId,
|
|
6819
|
+
source: sourceCluster,
|
|
6820
|
+
target,
|
|
6821
|
+
type: edge.type,
|
|
6822
|
+
stats: { ...edge.stats },
|
|
6823
|
+
patterns: edge.patterns ? [...edge.patterns] : void 0
|
|
6824
|
+
};
|
|
6825
|
+
edgeAgg.set(aggId, agg);
|
|
6826
|
+
} else {
|
|
6827
|
+
agg.stats.frequency += edge.stats.frequency;
|
|
6828
|
+
agg.stats.avgLatencyMs = Math.round(
|
|
6829
|
+
(agg.stats.avgLatencyMs + edge.stats.avgLatencyMs) / 2
|
|
6830
|
+
);
|
|
6831
|
+
agg.stats.lastSeenAt = Math.max(
|
|
6832
|
+
agg.stats.lastSeenAt,
|
|
6833
|
+
edge.stats.lastSeenAt
|
|
6834
|
+
);
|
|
6835
|
+
agg.stats.firstSeenAt = Math.min(
|
|
6836
|
+
agg.stats.firstSeenAt,
|
|
6837
|
+
edge.stats.firstSeenAt
|
|
6838
|
+
);
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6841
|
+
return {
|
|
6842
|
+
nodes: otherNodes,
|
|
6843
|
+
edges: [...edgeAgg.values()],
|
|
6844
|
+
clusters: clusterArr,
|
|
6845
|
+
metadata: this.graph.metadata
|
|
6846
|
+
};
|
|
5642
6847
|
}
|
|
5643
|
-
|
|
5644
|
-
|
|
6848
|
+
getClusterExpanded(clusterId, clusters) {
|
|
6849
|
+
const clusterKey = clusterId.startsWith("cluster:") ? clusterId.slice(8) : clusterId;
|
|
6850
|
+
const cluster = clusters.get(clusterKey);
|
|
6851
|
+
if (!cluster) return this.getClusterView(clusters);
|
|
6852
|
+
const childNodes = [];
|
|
6853
|
+
const connectedNodeIds = /* @__PURE__ */ new Set();
|
|
6854
|
+
const relevantEdges = [];
|
|
6855
|
+
for (const childId of cluster.children) {
|
|
6856
|
+
const node = this.graph.nodes.get(childId);
|
|
6857
|
+
if (node) childNodes.push(node);
|
|
6858
|
+
for (const edge of this.graph.edges.values()) {
|
|
6859
|
+
if (edge.source === childId || edge.target === childId) {
|
|
6860
|
+
relevantEdges.push(edge);
|
|
6861
|
+
if (edge.source !== childId) connectedNodeIds.add(edge.source);
|
|
6862
|
+
if (edge.target !== childId) connectedNodeIds.add(edge.target);
|
|
6863
|
+
}
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6866
|
+
for (const nodeId of connectedNodeIds) {
|
|
6867
|
+
if (!cluster.children.includes(nodeId)) {
|
|
6868
|
+
const node = this.graph.nodes.get(nodeId);
|
|
6869
|
+
if (node) childNodes.push(node);
|
|
6870
|
+
}
|
|
6871
|
+
}
|
|
6872
|
+
return {
|
|
6873
|
+
nodes: childNodes,
|
|
6874
|
+
edges: relevantEdges,
|
|
6875
|
+
clusters: [cluster],
|
|
6876
|
+
metadata: this.graph.metadata
|
|
6877
|
+
};
|
|
5645
6878
|
}
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
6879
|
+
getNodeNeighborhood(nodeId, clusters) {
|
|
6880
|
+
const centerNode = this.graph.nodes.get(nodeId);
|
|
6881
|
+
if (!centerNode) return this.getClusterView(clusters);
|
|
6882
|
+
const nodes = [centerNode];
|
|
6883
|
+
const edges = [];
|
|
6884
|
+
for (const edge of this.graph.edges.values()) {
|
|
6885
|
+
if (edge.source === nodeId || edge.target === nodeId) {
|
|
6886
|
+
edges.push(edge);
|
|
6887
|
+
const otherId = edge.source === nodeId ? edge.target : edge.source;
|
|
6888
|
+
const otherNode = this.graph.nodes.get(otherId);
|
|
6889
|
+
if (otherNode) nodes.push(otherNode);
|
|
5650
6890
|
}
|
|
5651
|
-
} catch (err) {
|
|
5652
|
-
brakitDebug(`failed to remove metrics file: ${getErrorMessage(err)}`);
|
|
5653
6891
|
}
|
|
6892
|
+
return {
|
|
6893
|
+
nodes,
|
|
6894
|
+
edges,
|
|
6895
|
+
clusters: [],
|
|
6896
|
+
metadata: this.graph.metadata
|
|
6897
|
+
};
|
|
5654
6898
|
}
|
|
5655
6899
|
};
|
|
5656
6900
|
}
|
|
5657
6901
|
});
|
|
5658
6902
|
|
|
5659
|
-
// src/store/index.ts
|
|
5660
|
-
var init_store = __esm({
|
|
5661
|
-
"src/store/index.ts"() {
|
|
5662
|
-
"use strict";
|
|
5663
|
-
init_request_store();
|
|
5664
|
-
init_telemetry_store();
|
|
5665
|
-
init_metrics_store();
|
|
5666
|
-
init_persistence();
|
|
5667
|
-
}
|
|
5668
|
-
});
|
|
5669
|
-
|
|
5670
6903
|
// src/output/terminal.ts
|
|
5671
6904
|
import pc from "picocolors";
|
|
5672
6905
|
function print(line) {
|
|
@@ -5704,6 +6937,7 @@ function startTerminalInsights(services, proxyPort) {
|
|
|
5704
6937
|
const resolvedLines = [];
|
|
5705
6938
|
const regressedLines = [];
|
|
5706
6939
|
for (const si of issues) {
|
|
6940
|
+
if (si.aiStatus === "wont_fix") continue;
|
|
5707
6941
|
if (si.state === "resolved") {
|
|
5708
6942
|
if (resolvedKeys.has(si.issueId)) continue;
|
|
5709
6943
|
resolvedKeys.add(si.issueId);
|
|
@@ -5848,125 +7082,6 @@ var init_guard = __esm({
|
|
|
5848
7082
|
}
|
|
5849
7083
|
});
|
|
5850
7084
|
|
|
5851
|
-
// src/runtime/capture.ts
|
|
5852
|
-
import { gunzip, brotliDecompress, inflate } from "zlib";
|
|
5853
|
-
function outgoingToIncoming(headers2) {
|
|
5854
|
-
const result = {};
|
|
5855
|
-
for (const [key, value] of Object.entries(headers2)) {
|
|
5856
|
-
if (value === void 0) continue;
|
|
5857
|
-
if (Array.isArray(value)) {
|
|
5858
|
-
result[key] = value.map(String);
|
|
5859
|
-
} else {
|
|
5860
|
-
result[key] = String(value);
|
|
5861
|
-
}
|
|
5862
|
-
}
|
|
5863
|
-
return result;
|
|
5864
|
-
}
|
|
5865
|
-
function getDecompressor(encoding) {
|
|
5866
|
-
if (encoding === CONTENT_ENCODING_GZIP) return gunzip;
|
|
5867
|
-
if (encoding === CONTENT_ENCODING_BR) return brotliDecompress;
|
|
5868
|
-
if (encoding === CONTENT_ENCODING_DEFLATE) return inflate;
|
|
5869
|
-
return null;
|
|
5870
|
-
}
|
|
5871
|
-
function decompressAsync(body, encoding) {
|
|
5872
|
-
const decompressor = getDecompressor(encoding);
|
|
5873
|
-
if (!decompressor) return Promise.resolve(body);
|
|
5874
|
-
return new Promise((resolve6) => {
|
|
5875
|
-
decompressor(body, (err, result) => {
|
|
5876
|
-
resolve6(err ? body : result);
|
|
5877
|
-
});
|
|
5878
|
-
});
|
|
5879
|
-
}
|
|
5880
|
-
function toBuffer(chunk) {
|
|
5881
|
-
if (Buffer.isBuffer(chunk)) return chunk;
|
|
5882
|
-
if (chunk instanceof Uint8Array) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
5883
|
-
if (typeof chunk === "string") return Buffer.from(chunk);
|
|
5884
|
-
return null;
|
|
5885
|
-
}
|
|
5886
|
-
function captureInProcess(req, res, requestId, requestStore, isChild = false) {
|
|
5887
|
-
const startTime = performance.now();
|
|
5888
|
-
const method = req.method ?? "GET";
|
|
5889
|
-
const resChunks = [];
|
|
5890
|
-
let resSize = 0;
|
|
5891
|
-
const originalWrite = res.write;
|
|
5892
|
-
const originalEnd = res.end;
|
|
5893
|
-
let truncated = false;
|
|
5894
|
-
res.write = function(...args) {
|
|
5895
|
-
try {
|
|
5896
|
-
const chunk = args[0];
|
|
5897
|
-
if (chunk != null && typeof chunk !== "function") {
|
|
5898
|
-
if (resSize < DEFAULT_MAX_BODY_CAPTURE) {
|
|
5899
|
-
const buf = toBuffer(chunk);
|
|
5900
|
-
if (buf) {
|
|
5901
|
-
resChunks.push(buf);
|
|
5902
|
-
resSize += buf.length;
|
|
5903
|
-
}
|
|
5904
|
-
} else {
|
|
5905
|
-
truncated = true;
|
|
5906
|
-
}
|
|
5907
|
-
}
|
|
5908
|
-
} catch (e) {
|
|
5909
|
-
brakitDebug(`capture write: ${getErrorMessage(e)}`);
|
|
5910
|
-
}
|
|
5911
|
-
return originalWrite.apply(this, args);
|
|
5912
|
-
};
|
|
5913
|
-
res.end = function(...args) {
|
|
5914
|
-
try {
|
|
5915
|
-
const chunk = typeof args[0] !== "function" ? args[0] : void 0;
|
|
5916
|
-
if (chunk != null && resSize < DEFAULT_MAX_BODY_CAPTURE) {
|
|
5917
|
-
const buf = toBuffer(chunk);
|
|
5918
|
-
if (buf) {
|
|
5919
|
-
resChunks.push(buf);
|
|
5920
|
-
}
|
|
5921
|
-
}
|
|
5922
|
-
} catch (e) {
|
|
5923
|
-
brakitDebug(`capture end: ${getErrorMessage(e)}`);
|
|
5924
|
-
}
|
|
5925
|
-
const result = originalEnd.apply(this, args);
|
|
5926
|
-
const endTime = performance.now();
|
|
5927
|
-
const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
|
|
5928
|
-
const statusCode = res.statusCode;
|
|
5929
|
-
const responseHeaders = outgoingToIncoming(res.getHeaders());
|
|
5930
|
-
const responseContentType = String(res.getHeader("content-type") ?? "");
|
|
5931
|
-
const capturedChunks = resChunks.slice();
|
|
5932
|
-
if (!isChild) {
|
|
5933
|
-
void (async () => {
|
|
5934
|
-
try {
|
|
5935
|
-
let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
|
|
5936
|
-
if (body && encoding && !truncated) {
|
|
5937
|
-
body = await decompressAsync(body, encoding);
|
|
5938
|
-
}
|
|
5939
|
-
requestStore.capture({
|
|
5940
|
-
requestId,
|
|
5941
|
-
method,
|
|
5942
|
-
url: req.url ?? "/",
|
|
5943
|
-
requestHeaders: req.headers,
|
|
5944
|
-
requestBody: null,
|
|
5945
|
-
statusCode,
|
|
5946
|
-
responseHeaders,
|
|
5947
|
-
responseBody: body,
|
|
5948
|
-
responseContentType,
|
|
5949
|
-
startTime,
|
|
5950
|
-
endTime,
|
|
5951
|
-
config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
|
|
5952
|
-
});
|
|
5953
|
-
} catch (e) {
|
|
5954
|
-
brakitDebug(`capture store: ${getErrorMessage(e)}`);
|
|
5955
|
-
}
|
|
5956
|
-
})();
|
|
5957
|
-
}
|
|
5958
|
-
return result;
|
|
5959
|
-
};
|
|
5960
|
-
}
|
|
5961
|
-
var init_capture = __esm({
|
|
5962
|
-
"src/runtime/capture.ts"() {
|
|
5963
|
-
"use strict";
|
|
5964
|
-
init_constants();
|
|
5965
|
-
init_log();
|
|
5966
|
-
init_type_guards();
|
|
5967
|
-
}
|
|
5968
|
-
});
|
|
5969
|
-
|
|
5970
7085
|
// src/runtime/interceptor.ts
|
|
5971
7086
|
import http from "http";
|
|
5972
7087
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
@@ -6152,7 +7267,8 @@ function registerLifecycle(allServices, stores, services, cwd) {
|
|
|
6152
7267
|
process.on("SIGTERM", () => {
|
|
6153
7268
|
recordExitReason(EXIT_REASON_SIGTERM);
|
|
6154
7269
|
});
|
|
6155
|
-
process.on("beforeExit", () => {
|
|
7270
|
+
process.on("beforeExit", async () => {
|
|
7271
|
+
await drainPendingCaptures();
|
|
6156
7272
|
recordExitReason(EXIT_REASON_CLEAN);
|
|
6157
7273
|
sendTelemetry();
|
|
6158
7274
|
});
|
|
@@ -6182,6 +7298,9 @@ async function doSetup() {
|
|
|
6182
7298
|
hooks_installed: ["fetch", "console", "error"],
|
|
6183
7299
|
setup_duration_ms: setupDurationMs
|
|
6184
7300
|
});
|
|
7301
|
+
const graphBuilder = new GraphBuilder(bus, stores.requestStore);
|
|
7302
|
+
graphBuilder.start();
|
|
7303
|
+
services.graphBuilder = graphBuilder;
|
|
6185
7304
|
const dataDir = getProjectDataDir(cwd);
|
|
6186
7305
|
const analysisServices = startAnalysis(bus, stores, dataDir, services);
|
|
6187
7306
|
const config = {
|
|
@@ -6242,6 +7361,7 @@ var init_setup = __esm({
|
|
|
6242
7361
|
"src/runtime/setup.ts"() {
|
|
6243
7362
|
"use strict";
|
|
6244
7363
|
init_fetch();
|
|
7364
|
+
init_capture();
|
|
6245
7365
|
init_console();
|
|
6246
7366
|
init_errors();
|
|
6247
7367
|
init_adapters();
|
|
@@ -6252,6 +7372,7 @@ var init_setup = __esm({
|
|
|
6252
7372
|
init_store();
|
|
6253
7373
|
init_issue_store();
|
|
6254
7374
|
init_engine();
|
|
7375
|
+
init_graph_builder();
|
|
6255
7376
|
init_terminal();
|
|
6256
7377
|
init_src();
|
|
6257
7378
|
init_constants();
|