brakit 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +142 -0
- package/dist/api.js +212 -42
- package/dist/bin/brakit.js +112 -14
- package/dist/dashboard-client.global.js +587 -276
- package/dist/dashboard.html +691 -276
- package/dist/mcp/server.js +4 -2
- package/dist/runtime/index.js +1103 -74
- 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";
|
|
@@ -118,7 +119,7 @@ var init_config = __esm({
|
|
|
118
119
|
});
|
|
119
120
|
|
|
120
121
|
// 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;
|
|
122
|
+
var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, DASHBOARD_API_GRAPH, VALID_TABS_TUPLE, VALID_TABS, BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES, HTTP_OK, HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, HTTP_INTERNAL_ERROR, SECURITY_HEADERS, CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE, SEVERITY_ICON, SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES, SDK_EVENT_REQUEST, SDK_EVENT_DB_QUERY, SDK_EVENT_FETCH, SDK_EVENT_LOG, SDK_EVENT_ERROR, SDK_EVENT_AUTH_CHECK, POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS, TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY;
|
|
122
123
|
var init_labels = __esm({
|
|
123
124
|
"src/constants/labels.ts"() {
|
|
124
125
|
"use strict";
|
|
@@ -140,6 +141,7 @@ var init_labels = __esm({
|
|
|
140
141
|
DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
141
142
|
DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
142
143
|
DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
144
|
+
DASHBOARD_API_GRAPH = `${DASHBOARD_PREFIX}/api/graph`;
|
|
143
145
|
VALID_TABS_TUPLE = [
|
|
144
146
|
"overview",
|
|
145
147
|
"actions",
|
|
@@ -149,7 +151,8 @@ var init_labels = __esm({
|
|
|
149
151
|
"errors",
|
|
150
152
|
"logs",
|
|
151
153
|
"performance",
|
|
152
|
-
"security"
|
|
154
|
+
"security",
|
|
155
|
+
"graph"
|
|
153
156
|
];
|
|
154
157
|
VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
155
158
|
BRAKIT_REQUEST_ID_HEADER = "x-brakit-request-id";
|
|
@@ -173,7 +176,7 @@ var init_labels = __esm({
|
|
|
173
176
|
"x-content-type-options": "nosniff",
|
|
174
177
|
"x-frame-options": "DENY",
|
|
175
178
|
"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:"
|
|
179
|
+
"content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data: blob:"
|
|
177
180
|
};
|
|
178
181
|
CONTENT_ENCODING_GZIP = "gzip";
|
|
179
182
|
CONTENT_ENCODING_BR = "br";
|
|
@@ -486,7 +489,15 @@ var init_adapter_registry = __esm({
|
|
|
486
489
|
function normalizeSQL(sql) {
|
|
487
490
|
if (!sql) return { op: "OTHER", table: "" };
|
|
488
491
|
const trimmed = sql.trim();
|
|
489
|
-
|
|
492
|
+
let spaceIdx = -1;
|
|
493
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
494
|
+
const c = trimmed[i];
|
|
495
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
496
|
+
spaceIdx = i;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const keyword = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toUpperCase();
|
|
490
501
|
const op = VALID_OPS.has(keyword) ? keyword : "OTHER";
|
|
491
502
|
const table = trimmed.match(TABLE_RE)?.[1] ?? "";
|
|
492
503
|
return { op, table };
|
|
@@ -865,8 +876,51 @@ var init_adapters = __esm({
|
|
|
865
876
|
});
|
|
866
877
|
|
|
867
878
|
// src/utils/endpoint.ts
|
|
879
|
+
function isUUID(s) {
|
|
880
|
+
if (s.length !== UUID_LEN) return false;
|
|
881
|
+
for (let i = 0; i < s.length; i++) {
|
|
882
|
+
const c = s[i];
|
|
883
|
+
if (i === 8 || i === 13 || i === 18 || i === 23) {
|
|
884
|
+
if (c !== "-") return false;
|
|
885
|
+
} else {
|
|
886
|
+
if (!isHexChar(c)) return false;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
function isHexChar(c) {
|
|
892
|
+
const code = c.charCodeAt(0);
|
|
893
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
|
|
894
|
+
}
|
|
895
|
+
function isNumericId(s) {
|
|
896
|
+
if (s.length === 0) return false;
|
|
897
|
+
for (let i = 0; i < s.length; i++) {
|
|
898
|
+
const code = s.charCodeAt(i);
|
|
899
|
+
if (code < 48 || code > 57) return false;
|
|
900
|
+
}
|
|
901
|
+
return true;
|
|
902
|
+
}
|
|
903
|
+
function isHexHash(s) {
|
|
904
|
+
if (s.length < MIN_HEX_LEN) return false;
|
|
905
|
+
for (let i = 0; i < s.length; i++) {
|
|
906
|
+
if (!isHexChar(s[i])) return false;
|
|
907
|
+
}
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
function isAlphanumericToken(s) {
|
|
911
|
+
if (s.length < MIN_TOKEN_LEN) return false;
|
|
912
|
+
let hasLetter = false;
|
|
913
|
+
let hasDigit = false;
|
|
914
|
+
for (let i = 0; i < s.length; i++) {
|
|
915
|
+
const code = s.charCodeAt(i);
|
|
916
|
+
if (code >= 65 && code <= 90 || code >= 97 && code <= 122) hasLetter = true;
|
|
917
|
+
else if (code >= 48 && code <= 57) hasDigit = true;
|
|
918
|
+
else if (code !== 95 && code !== 45) return false;
|
|
919
|
+
}
|
|
920
|
+
return hasLetter && hasDigit;
|
|
921
|
+
}
|
|
868
922
|
function isDynamicSegment(segment) {
|
|
869
|
-
return
|
|
923
|
+
return isUUID(segment) || isNumericId(segment) || isHexHash(segment) || isAlphanumericToken(segment);
|
|
870
924
|
}
|
|
871
925
|
function normalizePath(path) {
|
|
872
926
|
const qIdx = path.indexOf("?");
|
|
@@ -877,22 +931,24 @@ function getEndpointKey(method, path) {
|
|
|
877
931
|
return `${method} ${normalizePath(path)}`;
|
|
878
932
|
}
|
|
879
933
|
function extractEndpointFromDesc(desc) {
|
|
880
|
-
|
|
934
|
+
const spaceIdx = desc.indexOf(" ");
|
|
935
|
+
if (spaceIdx <= 0) return null;
|
|
936
|
+
const secondSpace = desc.indexOf(" ", spaceIdx + 1);
|
|
937
|
+
if (secondSpace === -1) return desc;
|
|
938
|
+
return desc.slice(0, secondSpace);
|
|
881
939
|
}
|
|
882
940
|
function stripQueryString(path) {
|
|
883
941
|
const i = path.indexOf("?");
|
|
884
942
|
return i === -1 ? path : path.slice(0, i);
|
|
885
943
|
}
|
|
886
|
-
var
|
|
944
|
+
var UUID_LEN, MIN_HEX_LEN, MIN_TOKEN_LEN, DYNAMIC_SEGMENT_PLACEHOLDER;
|
|
887
945
|
var init_endpoint = __esm({
|
|
888
946
|
"src/utils/endpoint.ts"() {
|
|
889
947
|
"use strict";
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
ALPHA_TOKEN_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/;
|
|
948
|
+
UUID_LEN = 36;
|
|
949
|
+
MIN_HEX_LEN = 12;
|
|
950
|
+
MIN_TOKEN_LEN = 8;
|
|
894
951
|
DYNAMIC_SEGMENT_PLACEHOLDER = ":id";
|
|
895
|
-
ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
896
952
|
}
|
|
897
953
|
});
|
|
898
954
|
|
|
@@ -913,6 +969,10 @@ var init_http_status = __esm({
|
|
|
913
969
|
});
|
|
914
970
|
|
|
915
971
|
// src/analysis/categorize.ts
|
|
972
|
+
function isAuthPath(path) {
|
|
973
|
+
const lower = path.toLowerCase();
|
|
974
|
+
return lower.startsWith("/api/auth") || lower.startsWith("/clerk") || lower.startsWith("/api/clerk");
|
|
975
|
+
}
|
|
916
976
|
function detectCategory(req) {
|
|
917
977
|
const { method, url, statusCode, responseHeaders } = req;
|
|
918
978
|
if (req.isStatic) return "static";
|
|
@@ -921,7 +981,7 @@ function detectCategory(req) {
|
|
|
921
981
|
return "auth-handshake";
|
|
922
982
|
}
|
|
923
983
|
const effectivePath = getEffectivePath(req);
|
|
924
|
-
if (
|
|
984
|
+
if (isAuthPath(effectivePath)) {
|
|
925
985
|
return "auth-check";
|
|
926
986
|
}
|
|
927
987
|
if (method === "POST" && !effectivePath.startsWith("/api/")) {
|
|
@@ -945,6 +1005,13 @@ function detectCategory(req) {
|
|
|
945
1005
|
}
|
|
946
1006
|
return "unknown";
|
|
947
1007
|
}
|
|
1008
|
+
function hasAuthCredentials(req) {
|
|
1009
|
+
if (req.headers["authorization"]) return true;
|
|
1010
|
+
const cookie = (req.headers["cookie"] || "").toLowerCase();
|
|
1011
|
+
if (cookie && AUTH_COOKIE_NAMES.some((name) => cookie.includes(name))) return true;
|
|
1012
|
+
if (req.statusCode === 401) return true;
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
948
1015
|
function getEffectivePath(req) {
|
|
949
1016
|
const rewrite = req.responseHeaders["x-middleware-rewrite"];
|
|
950
1017
|
if (!rewrite) return req.path;
|
|
@@ -955,9 +1022,21 @@ function getEffectivePath(req) {
|
|
|
955
1022
|
return rewrite.startsWith("/") ? rewrite : req.path;
|
|
956
1023
|
}
|
|
957
1024
|
}
|
|
1025
|
+
var AUTH_COOKIE_NAMES;
|
|
958
1026
|
var init_categorize = __esm({
|
|
959
1027
|
"src/analysis/categorize.ts"() {
|
|
960
1028
|
"use strict";
|
|
1029
|
+
AUTH_COOKIE_NAMES = [
|
|
1030
|
+
"__session=",
|
|
1031
|
+
"__clerk",
|
|
1032
|
+
"__host-next-auth",
|
|
1033
|
+
"next-auth.session-token=",
|
|
1034
|
+
"auth_token=",
|
|
1035
|
+
"session_id=",
|
|
1036
|
+
"access_token=",
|
|
1037
|
+
"_session=",
|
|
1038
|
+
"appsession="
|
|
1039
|
+
];
|
|
961
1040
|
}
|
|
962
1041
|
});
|
|
963
1042
|
|
|
@@ -1016,8 +1095,11 @@ function generateHumanLabel(req, category) {
|
|
|
1016
1095
|
return failed ? `${req.method} ${req.path} failed` : `${req.method} ${req.path}`;
|
|
1017
1096
|
}
|
|
1018
1097
|
}
|
|
1098
|
+
function stripApiPrefix(s) {
|
|
1099
|
+
return s.startsWith("/api/") ? s.slice(5) : s;
|
|
1100
|
+
}
|
|
1019
1101
|
function prettifyEndpoint(name) {
|
|
1020
|
-
const cleaned = name.
|
|
1102
|
+
const cleaned = stripApiPrefix(name).split("/").join(" ").split("...").join("").trim();
|
|
1021
1103
|
if (!cleaned) return "data";
|
|
1022
1104
|
return cleaned.split(" ").map((word) => {
|
|
1023
1105
|
if (word.endsWith("ses") || word.endsWith("us") || word.endsWith("ss"))
|
|
@@ -1029,22 +1111,8 @@ function prettifyEndpoint(name) {
|
|
|
1029
1111
|
}
|
|
1030
1112
|
function deriveActionVerb(method, endpointName) {
|
|
1031
1113
|
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;
|
|
1114
|
+
for (const [keyword, verb] of VERB_MAP) {
|
|
1115
|
+
if (lower.includes(keyword)) return verb;
|
|
1048
1116
|
}
|
|
1049
1117
|
switch (method) {
|
|
1050
1118
|
case "POST":
|
|
@@ -1059,24 +1127,45 @@ function deriveActionVerb(method, endpointName) {
|
|
|
1059
1127
|
}
|
|
1060
1128
|
}
|
|
1061
1129
|
function getEndpointName(path) {
|
|
1062
|
-
const parts = path
|
|
1130
|
+
const parts = stripApiPrefix(path).split("/");
|
|
1063
1131
|
if (parts.length <= 2) return parts.join("/");
|
|
1064
1132
|
return parts.map((p) => p.length > ENDPOINT_TRUNCATE_LENGTH ? "..." : p).join("/");
|
|
1065
1133
|
}
|
|
1066
1134
|
function prettifyPageName(path) {
|
|
1067
|
-
|
|
1135
|
+
let clean = path;
|
|
1136
|
+
if (clean.startsWith("/")) clean = clean.slice(1);
|
|
1137
|
+
if (clean.endsWith("/")) clean = clean.slice(0, -1);
|
|
1068
1138
|
if (!clean) return "Home";
|
|
1069
|
-
return clean.split("/").map((s) => capitalize(s.
|
|
1139
|
+
return clean.split("/").map((s) => capitalize(s.split("-").join(" ").split("_").join(" "))).join(" ");
|
|
1070
1140
|
}
|
|
1071
1141
|
function capitalize(s) {
|
|
1072
1142
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1073
1143
|
}
|
|
1144
|
+
var VERB_MAP;
|
|
1074
1145
|
var init_label = __esm({
|
|
1075
1146
|
"src/analysis/label.ts"() {
|
|
1076
1147
|
"use strict";
|
|
1077
1148
|
init_constants();
|
|
1078
1149
|
init_categorize();
|
|
1079
1150
|
init_http_status();
|
|
1151
|
+
VERB_MAP = [
|
|
1152
|
+
["enhance", "Enhanced"],
|
|
1153
|
+
["generate", "Generated"],
|
|
1154
|
+
["create", "Created"],
|
|
1155
|
+
["update", "Updated"],
|
|
1156
|
+
["delete", "Deleted"],
|
|
1157
|
+
["remove", "Deleted"],
|
|
1158
|
+
["send", "Sent"],
|
|
1159
|
+
["upload", "Uploaded"],
|
|
1160
|
+
["save", "Saved"],
|
|
1161
|
+
["submit", "Submitted"],
|
|
1162
|
+
["login", "Logged in"],
|
|
1163
|
+
["signin", "Logged in"],
|
|
1164
|
+
["logout", "Logged out"],
|
|
1165
|
+
["signout", "Logged out"],
|
|
1166
|
+
["register", "Registered"],
|
|
1167
|
+
["signup", "Registered"]
|
|
1168
|
+
];
|
|
1080
1169
|
}
|
|
1081
1170
|
});
|
|
1082
1171
|
|
|
@@ -1522,33 +1611,85 @@ var init_handlers = __esm({
|
|
|
1522
1611
|
|
|
1523
1612
|
// src/utils/static-patterns.ts
|
|
1524
1613
|
function isStaticPath(urlPath) {
|
|
1525
|
-
|
|
1614
|
+
const dotIdx = urlPath.lastIndexOf(".");
|
|
1615
|
+
if (dotIdx !== -1) {
|
|
1616
|
+
const ext = urlPath.slice(dotIdx).toLowerCase();
|
|
1617
|
+
if (STATIC_EXTENSIONS.has(ext)) return true;
|
|
1618
|
+
}
|
|
1619
|
+
return STATIC_PREFIXES.some((p) => urlPath.startsWith(p));
|
|
1526
1620
|
}
|
|
1527
1621
|
function isHealthCheckPath(urlPath) {
|
|
1528
|
-
return
|
|
1622
|
+
return HEALTH_CHECK_PATHS.has(urlPath.toLowerCase());
|
|
1529
1623
|
}
|
|
1530
|
-
var
|
|
1624
|
+
var STATIC_EXTENSIONS, STATIC_PREFIXES, HEALTH_CHECK_PATHS;
|
|
1531
1625
|
var init_static_patterns = __esm({
|
|
1532
1626
|
"src/utils/static-patterns.ts"() {
|
|
1533
1627
|
"use strict";
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1628
|
+
STATIC_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1629
|
+
".js",
|
|
1630
|
+
".css",
|
|
1631
|
+
".map",
|
|
1632
|
+
".ico",
|
|
1633
|
+
".png",
|
|
1634
|
+
".jpg",
|
|
1635
|
+
".jpeg",
|
|
1636
|
+
".gif",
|
|
1637
|
+
".svg",
|
|
1638
|
+
".webp",
|
|
1639
|
+
".woff",
|
|
1640
|
+
".woff2",
|
|
1641
|
+
".ttf",
|
|
1642
|
+
".eot"
|
|
1643
|
+
]);
|
|
1644
|
+
STATIC_PREFIXES = [
|
|
1645
|
+
"/favicon",
|
|
1646
|
+
"/node_modules/",
|
|
1647
|
+
// Next.js
|
|
1648
|
+
"/_next/",
|
|
1649
|
+
"/__nextjs",
|
|
1650
|
+
// Vite (also used by Nuxt, Astro in dev)
|
|
1651
|
+
"/@vite/",
|
|
1652
|
+
"/__vite",
|
|
1653
|
+
"/@fs/",
|
|
1654
|
+
"/@id/",
|
|
1655
|
+
// Remix
|
|
1656
|
+
"/__remix",
|
|
1657
|
+
// Nuxt
|
|
1658
|
+
"/_nuxt/",
|
|
1659
|
+
"/__nuxt",
|
|
1660
|
+
// Astro
|
|
1661
|
+
"/@astro",
|
|
1662
|
+
"/_astro/",
|
|
1663
|
+
// Django
|
|
1664
|
+
"/static/",
|
|
1665
|
+
"/media/",
|
|
1666
|
+
"/__debug__/",
|
|
1667
|
+
// Flask / Werkzeug
|
|
1668
|
+
"/_debugtoolbar/",
|
|
1669
|
+
// FastAPI / Starlette
|
|
1670
|
+
"/openapi.json",
|
|
1671
|
+
"/docs",
|
|
1672
|
+
"/redoc",
|
|
1673
|
+
// Rails
|
|
1674
|
+
"/assets/",
|
|
1675
|
+
"/packs/",
|
|
1676
|
+
// Browser probes
|
|
1677
|
+
"/.well-known/"
|
|
1551
1678
|
];
|
|
1679
|
+
HEALTH_CHECK_PATHS = /* @__PURE__ */ new Set([
|
|
1680
|
+
"/health",
|
|
1681
|
+
"/healthz",
|
|
1682
|
+
"/healthcheck",
|
|
1683
|
+
"/ping",
|
|
1684
|
+
"/ready",
|
|
1685
|
+
"/readiness",
|
|
1686
|
+
"/liveness",
|
|
1687
|
+
"/status",
|
|
1688
|
+
"/__health",
|
|
1689
|
+
"/api/health",
|
|
1690
|
+
"/api/healthz",
|
|
1691
|
+
"/api/healthcheck"
|
|
1692
|
+
]);
|
|
1552
1693
|
}
|
|
1553
1694
|
});
|
|
1554
1695
|
|
|
@@ -1977,6 +2118,42 @@ var init_issues = __esm({
|
|
|
1977
2118
|
}
|
|
1978
2119
|
});
|
|
1979
2120
|
|
|
2121
|
+
// src/dashboard/api/graph.ts
|
|
2122
|
+
function createGraphHandler(services) {
|
|
2123
|
+
return (req, res) => {
|
|
2124
|
+
if (!requireGet(req, res)) return;
|
|
2125
|
+
const url = parseRequestUrl(req);
|
|
2126
|
+
const rawCluster = url.searchParams.get("cluster") ?? void 0;
|
|
2127
|
+
const rawNode = url.searchParams.get("node") ?? void 0;
|
|
2128
|
+
const rawLevel = url.searchParams.get("level") ?? void 0;
|
|
2129
|
+
const rawGrouping = url.searchParams.get("grouping") ?? void 0;
|
|
2130
|
+
const cluster = rawCluster && rawCluster.length <= MAX_PARAM_LENGTH ? rawCluster : void 0;
|
|
2131
|
+
const node = rawNode && rawNode.length <= MAX_PARAM_LENGTH ? rawNode : void 0;
|
|
2132
|
+
const level = rawLevel && VALID_LEVELS.has(rawLevel) ? rawLevel : void 0;
|
|
2133
|
+
const grouping = rawGrouping && VALID_GROUPINGS.has(rawGrouping) ? rawGrouping : void 0;
|
|
2134
|
+
const { graphBuilder, metricsStore } = services;
|
|
2135
|
+
graphBuilder.enrichWithMetrics((endpointKey) => {
|
|
2136
|
+
const metrics = metricsStore.getEndpoint(endpointKey);
|
|
2137
|
+
if (!metrics || metrics.sessions.length === 0) return void 0;
|
|
2138
|
+
const latest = metrics.sessions[metrics.sessions.length - 1];
|
|
2139
|
+
return latest.p95DurationMs;
|
|
2140
|
+
});
|
|
2141
|
+
const data = graphBuilder.getApiResponse({ cluster, node, level, grouping });
|
|
2142
|
+
sendJson(req, res, HTTP_OK, data);
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
var VALID_LEVELS, VALID_GROUPINGS, MAX_PARAM_LENGTH;
|
|
2146
|
+
var init_graph = __esm({
|
|
2147
|
+
"src/dashboard/api/graph.ts"() {
|
|
2148
|
+
"use strict";
|
|
2149
|
+
init_labels();
|
|
2150
|
+
init_shared2();
|
|
2151
|
+
VALID_LEVELS = /* @__PURE__ */ new Set(["endpoints", "clusters"]);
|
|
2152
|
+
VALID_GROUPINGS = /* @__PURE__ */ new Set(["path", "auth-boundary", "data-domain"]);
|
|
2153
|
+
MAX_PARAM_LENGTH = 200;
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
|
|
1980
2157
|
// src/dashboard/sse.ts
|
|
1981
2158
|
function createSSEHandler(services) {
|
|
1982
2159
|
const clients = /* @__PURE__ */ new Set();
|
|
@@ -2478,26 +2655,128 @@ var init_response = __esm({
|
|
|
2478
2655
|
});
|
|
2479
2656
|
|
|
2480
2657
|
// src/analysis/rules/patterns.ts
|
|
2481
|
-
var SECRET_KEYS, TOKEN_PARAMS, SAFE_PARAMS,
|
|
2658
|
+
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
2659
|
var init_patterns = __esm({
|
|
2483
2660
|
"src/analysis/rules/patterns.ts"() {
|
|
2484
2661
|
"use strict";
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2662
|
+
SECRET_KEY_SET = /* @__PURE__ */ new Set([
|
|
2663
|
+
"password",
|
|
2664
|
+
"passwd",
|
|
2665
|
+
"secret",
|
|
2666
|
+
"api_key",
|
|
2667
|
+
"apiKey",
|
|
2668
|
+
"api_secret",
|
|
2669
|
+
"apiSecret",
|
|
2670
|
+
"private_key",
|
|
2671
|
+
"privateKey",
|
|
2672
|
+
"client_secret",
|
|
2673
|
+
"clientSecret"
|
|
2674
|
+
]);
|
|
2675
|
+
SECRET_KEYS = { test: (s) => SECRET_KEY_SET.has(s) };
|
|
2676
|
+
TOKEN_PARAM_SET = /* @__PURE__ */ new Set([
|
|
2677
|
+
"token",
|
|
2678
|
+
"api_key",
|
|
2679
|
+
"apiKey",
|
|
2680
|
+
"secret",
|
|
2681
|
+
"password",
|
|
2682
|
+
"access_token",
|
|
2683
|
+
"session_id",
|
|
2684
|
+
"sessionId"
|
|
2685
|
+
]);
|
|
2686
|
+
TOKEN_PARAMS = { test: (s) => TOKEN_PARAM_SET.has(s) };
|
|
2687
|
+
SAFE_PARAM_SET = /* @__PURE__ */ new Set([
|
|
2688
|
+
"_rsc",
|
|
2689
|
+
"__clerk_handshake",
|
|
2690
|
+
"__clerk_db_jwt",
|
|
2691
|
+
"callback",
|
|
2692
|
+
"code",
|
|
2693
|
+
"state",
|
|
2694
|
+
"nonce",
|
|
2695
|
+
"redirect_uri",
|
|
2696
|
+
"utm_",
|
|
2697
|
+
"fbclid",
|
|
2698
|
+
"gclid"
|
|
2699
|
+
]);
|
|
2700
|
+
SAFE_PARAMS = { test: (s) => SAFE_PARAM_SET.has(s) };
|
|
2701
|
+
INTERNAL_ID_KEY_SET = /* @__PURE__ */ new Set([
|
|
2702
|
+
"id",
|
|
2703
|
+
"_id",
|
|
2704
|
+
"userId",
|
|
2705
|
+
"user_id",
|
|
2706
|
+
"createdBy",
|
|
2707
|
+
"updatedBy",
|
|
2708
|
+
"organizationId",
|
|
2709
|
+
"org_id",
|
|
2710
|
+
"tenantId",
|
|
2711
|
+
"tenant_id"
|
|
2712
|
+
]);
|
|
2713
|
+
INTERNAL_ID_KEYS = { test: (s) => INTERNAL_ID_KEY_SET.has(s) };
|
|
2714
|
+
INTERNAL_ID_SUFFIX = {
|
|
2715
|
+
test: (s) => s.endsWith("Id") || s.endsWith("_id")
|
|
2716
|
+
};
|
|
2717
|
+
SENSITIVE_FIELD_SET = /* @__PURE__ */ new Set([
|
|
2718
|
+
"phone",
|
|
2719
|
+
"phonenumber",
|
|
2720
|
+
"phone_number",
|
|
2721
|
+
"ssn",
|
|
2722
|
+
"socialsecuritynumber",
|
|
2723
|
+
"social_security_number",
|
|
2724
|
+
"dateofbirth",
|
|
2725
|
+
"date_of_birth",
|
|
2726
|
+
"dob",
|
|
2727
|
+
"address",
|
|
2728
|
+
"streetaddress",
|
|
2729
|
+
"street_address",
|
|
2730
|
+
"creditcard",
|
|
2731
|
+
"credit_card",
|
|
2732
|
+
"cardnumber",
|
|
2733
|
+
"card_number",
|
|
2734
|
+
"bankaccount",
|
|
2735
|
+
"bank_account",
|
|
2736
|
+
"passport",
|
|
2737
|
+
"passportnumber",
|
|
2738
|
+
"passport_number",
|
|
2739
|
+
"nationalid",
|
|
2740
|
+
"national_id"
|
|
2741
|
+
]);
|
|
2742
|
+
SENSITIVE_FIELD_NAMES = {
|
|
2743
|
+
test: (s) => SENSITIVE_FIELD_SET.has(s.toLowerCase())
|
|
2744
|
+
};
|
|
2745
|
+
SELF_SERVICE_SEGMENTS = /* @__PURE__ */ new Set(["me", "account", "profile", "settings", "self"]);
|
|
2746
|
+
SELF_SERVICE_PATH = {
|
|
2747
|
+
test: (path) => {
|
|
2748
|
+
const segments = path.toLowerCase().split(/[/?#]/);
|
|
2749
|
+
return segments.some((seg) => SELF_SERVICE_SEGMENTS.has(seg));
|
|
2750
|
+
}
|
|
2751
|
+
};
|
|
2752
|
+
MASKED_LITERALS = ["[REDACTED]", "[FILTERED]", "CHANGE_ME"];
|
|
2753
|
+
MASKED_RE = {
|
|
2754
|
+
test: (s) => {
|
|
2755
|
+
const upper = s.toUpperCase();
|
|
2756
|
+
if (MASKED_LITERALS.some((m) => upper.includes(m))) return true;
|
|
2757
|
+
if (s.length > 0 && s.split("").every((c) => c === "*")) return true;
|
|
2758
|
+
if (s.length >= 3 && s.split("").every((c) => c === "x" || c === "X")) return true;
|
|
2759
|
+
return false;
|
|
2760
|
+
}
|
|
2761
|
+
};
|
|
2762
|
+
DB_PROTOCOLS = ["postgres://", "mysql://", "mongodb://", "redis://"];
|
|
2763
|
+
DB_CONN_RE = {
|
|
2764
|
+
test: (s) => DB_PROTOCOLS.some((p) => s.includes(p))
|
|
2765
|
+
};
|
|
2766
|
+
SELECT_STAR_RE = {
|
|
2767
|
+
test: (s) => {
|
|
2768
|
+
const t = s.trimStart().toUpperCase();
|
|
2769
|
+
return t.startsWith("SELECT *") || t.startsWith("SELECT *");
|
|
2770
|
+
}
|
|
2771
|
+
};
|
|
2772
|
+
SELECT_DOT_STAR_RE = {
|
|
2773
|
+
test: (s) => s.toUpperCase().includes(".* FROM")
|
|
2774
|
+
};
|
|
2488
2775
|
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
2776
|
SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
|
|
2491
2777
|
SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
|
|
2492
2778
|
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
2779
|
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
2780
|
RULE_HINTS = {
|
|
2502
2781
|
"exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
|
|
2503
2782
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
@@ -3956,7 +4235,7 @@ var init_src = __esm({
|
|
|
3956
4235
|
init_engine();
|
|
3957
4236
|
init_insights2();
|
|
3958
4237
|
init_insights();
|
|
3959
|
-
VERSION = "0.
|
|
4238
|
+
VERSION = "0.10.0";
|
|
3960
4239
|
}
|
|
3961
4240
|
});
|
|
3962
4241
|
|
|
@@ -4041,6 +4320,7 @@ function getLayoutStyles() {
|
|
|
4041
4320
|
.sidebar-item:hover .item-icon{opacity:.8}
|
|
4042
4321
|
.sidebar-item .item-label{flex:1}
|
|
4043
4322
|
.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}
|
|
4323
|
+
.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
4324
|
.sidebar-item.disabled{opacity:.35;cursor:default;pointer-events:none}
|
|
4045
4325
|
.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
4326
|
.sidebar-footer{padding:16px 24px;border-top:1px solid var(--border-subtle);font-size:12px;color:var(--text-muted);font-family:var(--mono)}
|
|
@@ -4066,6 +4346,7 @@ function getLayoutStyles() {
|
|
|
4066
4346
|
.main-content{flex:1;overflow-y:auto}
|
|
4067
4347
|
bk-dashboard{display:contents}
|
|
4068
4348
|
bk-overview-view,bk-flows-view,bk-requests-view,bk-fetches-view,bk-queries-view,bk-errors-view,bk-logs-view,bk-security-view,bk-performance-view,bk-timeline-panel,bk-empty-state{display:block}
|
|
4349
|
+
bk-graph-view{display:block}
|
|
4069
4350
|
bk-method-badge,bk-status-pill,bk-duration-label,bk-copy-button{display:inline-flex;flex-shrink:0}
|
|
4070
4351
|
bk-stat-card{display:inline-flex}
|
|
4071
4352
|
bk-toast{display:block;position:fixed;top:0;left:0;right:0;z-index:100;pointer-events:none}
|
|
@@ -4442,7 +4723,7 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
|
|
|
4442
4723
|
.perf-trend-faster{color:var(--green)}
|
|
4443
4724
|
`;
|
|
4444
4725
|
}
|
|
4445
|
-
var
|
|
4726
|
+
var init_graph2 = __esm({
|
|
4446
4727
|
"src/dashboard/styles/graph.ts"() {
|
|
4447
4728
|
"use strict";
|
|
4448
4729
|
}
|
|
@@ -4779,9 +5060,121 @@ var init_timeline = __esm({
|
|
|
4779
5060
|
}
|
|
4780
5061
|
});
|
|
4781
5062
|
|
|
5063
|
+
// src/dashboard/styles/graph-view.ts
|
|
5064
|
+
function getGraphViewStyles() {
|
|
5065
|
+
return `
|
|
5066
|
+
.graph-wrapper{display:flex;flex-direction:column;height:calc(100vh - 120px);outline:none}
|
|
5067
|
+
|
|
5068
|
+
/* Toolbar \u2014 centered search, layers left, flow picker right */
|
|
5069
|
+
.graph-toolbar{display:flex;align-items:center;gap:10px;padding:8px 16px;border-bottom:1px solid var(--border)}
|
|
5070
|
+
|
|
5071
|
+
/* Layer toggles */
|
|
5072
|
+
.graph-layer-toggles{display:flex;gap:4px;flex-shrink:0}
|
|
5073
|
+
.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}
|
|
5074
|
+
.graph-layer-btn:hover{border-color:var(--text-muted);color:var(--text)}
|
|
5075
|
+
.graph-layer-btn.active{background:var(--bg-card);font-weight:600}
|
|
5076
|
+
|
|
5077
|
+
/* Search \u2014 takes remaining space, centered */
|
|
5078
|
+
.graph-search{flex:1;position:relative;display:flex;align-items:center;max-width:360px;margin:0 auto}
|
|
5079
|
+
.graph-search-icon{position:absolute;left:10px;color:var(--text-muted);font-size:13px;pointer-events:none;opacity:0.5}
|
|
5080
|
+
.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}
|
|
5081
|
+
.graph-search-input:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.1)}
|
|
5082
|
+
.graph-search-input::placeholder{color:var(--text-muted);opacity:0.5}
|
|
5083
|
+
.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}
|
|
5084
|
+
.graph-search-clear:hover{color:var(--text);background:var(--bg-card)}
|
|
5085
|
+
|
|
5086
|
+
/* Flow picker */
|
|
5087
|
+
.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}
|
|
5088
|
+
|
|
5089
|
+
/* Auth legend \u2014 inline in toolbar */
|
|
5090
|
+
.graph-auth-legend{display:flex;gap:8px;align-items:center;font-size:10px;color:var(--text-muted);flex-shrink:0}
|
|
5091
|
+
.graph-auth-legend-item{display:flex;align-items:center;gap:3px;white-space:nowrap}
|
|
5092
|
+
|
|
5093
|
+
/* Canvas */
|
|
5094
|
+
.graph-body{display:flex;flex:1;min-height:0}
|
|
5095
|
+
.graph-canvas{flex:1;overflow:hidden;padding:0;position:relative;min-height:0}
|
|
5096
|
+
.graph-svg{display:block}
|
|
5097
|
+
.graph-col-header{fill:#c4c4cc;font-size:9px;font-weight:600;font-family:'Inter',system-ui,sans-serif;letter-spacing:1.5px}
|
|
5098
|
+
|
|
5099
|
+
/* Floating controls \u2014 bottom-center pill */
|
|
5100
|
+
.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}
|
|
5101
|
+
.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}
|
|
5102
|
+
.graph-float-btn:hover{background:var(--bg);color:var(--text)}
|
|
5103
|
+
.graph-float-btn-accent{font-size:11px;font-weight:600;color:#6366f1}
|
|
5104
|
+
.graph-float-btn-accent:hover{background:rgba(99,102,241,.08);color:#4f46e5}
|
|
5105
|
+
.graph-float-zoom{font-size:10px;color:var(--text-muted);font-family:var(--mono);min-width:36px;text-align:center;user-select:none}
|
|
5106
|
+
.graph-float-sep{width:1px;height:16px;background:var(--border);margin:0 2px;flex-shrink:0}
|
|
5107
|
+
|
|
5108
|
+
/* Empty & loading states */
|
|
5109
|
+
.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}
|
|
5110
|
+
.graph-empty-icon{font-size:40px;opacity:0.25;margin-bottom:12px}
|
|
5111
|
+
.graph-empty-title{font-size:15px;font-weight:600;color:var(--text);margin-bottom:6px}
|
|
5112
|
+
.graph-empty-desc{font-size:12px;max-width:320px;line-height:1.5}
|
|
5113
|
+
.graph-loading{display:flex;align-items:center;justify-content:center;min-height:400px;color:var(--text-muted);font-size:13px}
|
|
5114
|
+
|
|
5115
|
+
/* Detail panel */
|
|
5116
|
+
.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)}
|
|
5117
|
+
.graph-detail-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px}
|
|
5118
|
+
.graph-detail-badge{font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px}
|
|
5119
|
+
.graph-detail-name{font-size:14px;font-weight:700;color:var(--text);word-break:break-all;font-family:var(--mono)}
|
|
5120
|
+
.graph-detail-close{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:16px;padding:0 4px;line-height:1}
|
|
5121
|
+
.graph-detail-close:hover{color:var(--text)}
|
|
5122
|
+
|
|
5123
|
+
.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}
|
|
5124
|
+
.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}
|
|
5125
|
+
|
|
5126
|
+
/* Detail tabs */
|
|
5127
|
+
.graph-detail-tabs{display:flex;gap:2px;margin-bottom:12px;border-bottom:1px solid var(--border);padding-bottom:0}
|
|
5128
|
+
.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}
|
|
5129
|
+
.graph-detail-tab:hover{color:var(--text)}
|
|
5130
|
+
.graph-detail-tab.active{color:#6366f1;border-bottom-color:#6366f1}
|
|
5131
|
+
|
|
5132
|
+
/* Detail stats */
|
|
5133
|
+
.graph-detail-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}
|
|
5134
|
+
.graph-detail-stat{background:var(--bg);border-radius:var(--radius-sm);padding:10px 12px}
|
|
5135
|
+
.graph-detail-val{font-size:20px;font-weight:700;font-family:var(--mono);color:var(--text);line-height:1.2}
|
|
5136
|
+
.graph-detail-lbl{font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.6px;margin-top:2px}
|
|
5137
|
+
|
|
5138
|
+
/* Detail sections */
|
|
5139
|
+
.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)}
|
|
5140
|
+
.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)}
|
|
5141
|
+
.graph-detail-edge-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
|
5142
|
+
.graph-detail-edge-type{font-size:10px;font-weight:600;text-transform:uppercase;min-width:42px}
|
|
5143
|
+
.graph-detail-dim{color:var(--text-muted);font-size:10px;margin-left:auto;white-space:nowrap}
|
|
5144
|
+
.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)}
|
|
5145
|
+
|
|
5146
|
+
/* Security findings in detail */
|
|
5147
|
+
.graph-detail-finding{padding:8px 10px;background:var(--bg);border-radius:var(--radius-sm);margin-bottom:6px;border:1px solid var(--border)}
|
|
5148
|
+
.graph-detail-finding-title{font-size:12px;font-weight:600;color:var(--text);margin-top:4px}
|
|
5149
|
+
.graph-detail-finding-meta{font-size:10px;color:var(--text-muted);margin-top:2px;font-family:var(--mono)}
|
|
5150
|
+
.graph-detail-severity{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px;padding:1px 6px;border-radius:3px}
|
|
5151
|
+
.graph-detail-severity-critical{background:#fef2f2;color:#dc2626;border:1px solid #fecaca}
|
|
5152
|
+
.graph-detail-severity-warning{background:#fffbeb;color:#d97706;border:1px solid #fde68a}
|
|
5153
|
+
.graph-detail-severity-info{background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe}
|
|
5154
|
+
|
|
5155
|
+
/* Issues in detail */
|
|
5156
|
+
.graph-detail-issue-summary{margin-bottom:12px}
|
|
5157
|
+
.graph-detail-hint{font-size:11px;color:var(--text-muted);line-height:1.5;margin:0}
|
|
5158
|
+
.graph-detail-empty{font-size:12px;color:var(--text-muted);padding:16px;text-align:center}
|
|
5159
|
+
|
|
5160
|
+
/* Pulse animation for critical security badges */
|
|
5161
|
+
@keyframes graph-pulse{0%,100%{opacity:1}50%{opacity:0.5}}
|
|
5162
|
+
.graph-pulse{animation:graph-pulse 2s ease-in-out infinite}
|
|
5163
|
+
|
|
5164
|
+
/* Flow edge animation */
|
|
5165
|
+
@keyframes graph-flow-dash{to{stroke-dashoffset:-24}}
|
|
5166
|
+
.graph-flow-edge{animation:graph-flow-dash 1s linear infinite}
|
|
5167
|
+
`;
|
|
5168
|
+
}
|
|
5169
|
+
var init_graph_view = __esm({
|
|
5170
|
+
"src/dashboard/styles/graph-view.ts"() {
|
|
5171
|
+
"use strict";
|
|
5172
|
+
}
|
|
5173
|
+
});
|
|
5174
|
+
|
|
4782
5175
|
// src/dashboard/styles.ts
|
|
4783
5176
|
function getStyles() {
|
|
4784
|
-
return getBaseStyles() + getLayoutStyles() + getFlowStyles() + getRequestStyles() + getPerformanceStyles() + getOverviewStyles() + getSecurityStyles() + getTimelineStyles();
|
|
5177
|
+
return getBaseStyles() + getLayoutStyles() + getFlowStyles() + getRequestStyles() + getPerformanceStyles() + getOverviewStyles() + getSecurityStyles() + getTimelineStyles() + getGraphViewStyles();
|
|
4785
5178
|
}
|
|
4786
5179
|
var init_styles = __esm({
|
|
4787
5180
|
"src/dashboard/styles.ts"() {
|
|
@@ -4790,10 +5183,11 @@ var init_styles = __esm({
|
|
|
4790
5183
|
init_layout();
|
|
4791
5184
|
init_flows();
|
|
4792
5185
|
init_requests();
|
|
4793
|
-
|
|
5186
|
+
init_graph2();
|
|
4794
5187
|
init_overview();
|
|
4795
5188
|
init_security2();
|
|
4796
5189
|
init_timeline();
|
|
5190
|
+
init_graph_view();
|
|
4797
5191
|
}
|
|
4798
5192
|
});
|
|
4799
5193
|
|
|
@@ -4976,6 +5370,12 @@ function recordDashboardOpened() {
|
|
|
4976
5370
|
request_count_at_open: session.requestCount
|
|
4977
5371
|
});
|
|
4978
5372
|
}
|
|
5373
|
+
function recordGraphFeature(feature, detail) {
|
|
5374
|
+
trackEvent(TELEMETRY_EVENT_GRAPH_FEATURE, {
|
|
5375
|
+
feature,
|
|
5376
|
+
...detail ? { detail } : {}
|
|
5377
|
+
});
|
|
5378
|
+
}
|
|
4979
5379
|
function recordSetupCompleted(info) {
|
|
4980
5380
|
session.frameworkCandidates = info.frameworkCandidates;
|
|
4981
5381
|
session.adaptersFailed = info.adaptersFailed;
|
|
@@ -5108,11 +5508,19 @@ function createDashboardHandler(services) {
|
|
|
5108
5508
|
issueStore,
|
|
5109
5509
|
services.bus
|
|
5110
5510
|
);
|
|
5511
|
+
routes[DASHBOARD_API_GRAPH] = createGraphHandler(services);
|
|
5111
5512
|
routes[DASHBOARD_API_TAB] = (req, res) => {
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
const tab =
|
|
5115
|
-
if (VALID_TABS.has(tab)
|
|
5513
|
+
if (isTelemetryEnabled()) {
|
|
5514
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
5515
|
+
const tab = url.searchParams.get("tab");
|
|
5516
|
+
if (tab && tab.length <= MAX_TAB_NAME_LENGTH && VALID_TABS.has(tab)) {
|
|
5517
|
+
recordTabViewed(tab);
|
|
5518
|
+
}
|
|
5519
|
+
const event = url.searchParams.get("event");
|
|
5520
|
+
if (event && event.length <= MAX_TAB_NAME_LENGTH) {
|
|
5521
|
+
const detail = url.searchParams.get("detail") ?? void 0;
|
|
5522
|
+
recordGraphFeature(event, detail?.slice(0, MAX_TAB_NAME_LENGTH));
|
|
5523
|
+
}
|
|
5116
5524
|
}
|
|
5117
5525
|
res.writeHead(HTTP_NO_CONTENT);
|
|
5118
5526
|
res.end();
|
|
@@ -5141,6 +5549,7 @@ var init_router = __esm({
|
|
|
5141
5549
|
init_labels();
|
|
5142
5550
|
init_api();
|
|
5143
5551
|
init_issues();
|
|
5552
|
+
init_graph();
|
|
5144
5553
|
init_sse();
|
|
5145
5554
|
init_page();
|
|
5146
5555
|
init_telemetry();
|
|
@@ -5667,6 +6076,622 @@ var init_store = __esm({
|
|
|
5667
6076
|
}
|
|
5668
6077
|
});
|
|
5669
6078
|
|
|
6079
|
+
// src/graph/constants.ts
|
|
6080
|
+
var MAX_PATTERNS_PER_EDGE, CLUSTER_SPLIT_THRESHOLD, COMMON_PATH_PREFIXES, PENDING_BUFFER_MAX, PENDING_EVICTION_TARGET, PENDING_TTL_MS;
|
|
6081
|
+
var init_constants2 = __esm({
|
|
6082
|
+
"src/graph/constants.ts"() {
|
|
6083
|
+
"use strict";
|
|
6084
|
+
MAX_PATTERNS_PER_EDGE = 10;
|
|
6085
|
+
CLUSTER_SPLIT_THRESHOLD = 15;
|
|
6086
|
+
COMMON_PATH_PREFIXES = /* @__PURE__ */ new Set(["api", "v1", "v2", "v3", "v4"]);
|
|
6087
|
+
PENDING_BUFFER_MAX = 500;
|
|
6088
|
+
PENDING_EVICTION_TARGET = 200;
|
|
6089
|
+
PENDING_TTL_MS = 6e4;
|
|
6090
|
+
}
|
|
6091
|
+
});
|
|
6092
|
+
|
|
6093
|
+
// src/graph/graph-builder.ts
|
|
6094
|
+
function shouldSkipRequest(req) {
|
|
6095
|
+
if (req.isStatic || req.isHealthCheck) return true;
|
|
6096
|
+
if (isDashboardRequest(req.path)) return true;
|
|
6097
|
+
return false;
|
|
6098
|
+
}
|
|
6099
|
+
function extractHostname(url) {
|
|
6100
|
+
try {
|
|
6101
|
+
const parsed = new URL(url);
|
|
6102
|
+
const host = parsed.hostname;
|
|
6103
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1")
|
|
6104
|
+
return null;
|
|
6105
|
+
return host;
|
|
6106
|
+
} catch {
|
|
6107
|
+
return null;
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
function makeEdgeId(source, target) {
|
|
6111
|
+
return `${source} -> ${target}`;
|
|
6112
|
+
}
|
|
6113
|
+
function upsertNode(graph, id, type, label, now) {
|
|
6114
|
+
let node = graph.nodes.get(id);
|
|
6115
|
+
if (!node) {
|
|
6116
|
+
node = {
|
|
6117
|
+
id,
|
|
6118
|
+
type,
|
|
6119
|
+
label,
|
|
6120
|
+
stats: {
|
|
6121
|
+
requestCount: 0,
|
|
6122
|
+
avgLatencyMs: 0,
|
|
6123
|
+
errorRate: 0,
|
|
6124
|
+
avgQueryCount: 0,
|
|
6125
|
+
lastSeenAt: now,
|
|
6126
|
+
firstSeenAt: now
|
|
6127
|
+
}
|
|
6128
|
+
};
|
|
6129
|
+
graph.nodes.set(id, node);
|
|
6130
|
+
}
|
|
6131
|
+
node.stats.lastSeenAt = now;
|
|
6132
|
+
return node;
|
|
6133
|
+
}
|
|
6134
|
+
function upsertEdge(graph, source, target, type, now) {
|
|
6135
|
+
const id = makeEdgeId(source, target);
|
|
6136
|
+
let edge = graph.edges.get(id);
|
|
6137
|
+
if (!edge) {
|
|
6138
|
+
edge = {
|
|
6139
|
+
id,
|
|
6140
|
+
source,
|
|
6141
|
+
target,
|
|
6142
|
+
type,
|
|
6143
|
+
stats: {
|
|
6144
|
+
frequency: 0,
|
|
6145
|
+
avgLatencyMs: 0,
|
|
6146
|
+
lastSeenAt: now,
|
|
6147
|
+
firstSeenAt: now
|
|
6148
|
+
}
|
|
6149
|
+
};
|
|
6150
|
+
graph.edges.set(id, edge);
|
|
6151
|
+
}
|
|
6152
|
+
edge.stats.lastSeenAt = now;
|
|
6153
|
+
return edge;
|
|
6154
|
+
}
|
|
6155
|
+
function updateRollingAvg(current, newValue, count) {
|
|
6156
|
+
return Math.round(current + (newValue - current) / count);
|
|
6157
|
+
}
|
|
6158
|
+
var GraphBuilder;
|
|
6159
|
+
var init_graph_builder = __esm({
|
|
6160
|
+
"src/graph/graph-builder.ts"() {
|
|
6161
|
+
"use strict";
|
|
6162
|
+
init_endpoint();
|
|
6163
|
+
init_normalize();
|
|
6164
|
+
init_router();
|
|
6165
|
+
init_categorize();
|
|
6166
|
+
init_constants2();
|
|
6167
|
+
GraphBuilder = class {
|
|
6168
|
+
constructor(bus, requestStore) {
|
|
6169
|
+
this.bus = bus;
|
|
6170
|
+
this.requestStore = requestStore;
|
|
6171
|
+
this.graph = {
|
|
6172
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
6173
|
+
edges: /* @__PURE__ */ new Map(),
|
|
6174
|
+
metadata: { totalObservations: 0, lastUpdatedAt: Date.now() }
|
|
6175
|
+
};
|
|
6176
|
+
/**
|
|
6177
|
+
* Buffered telemetry events waiting for their parent request to complete.
|
|
6178
|
+
* Key = parentRequestId, value = pending queries/fetches.
|
|
6179
|
+
*/
|
|
6180
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
6181
|
+
/** Accumulated request categories per endpoint node. */
|
|
6182
|
+
this.nodeCategories = /* @__PURE__ */ new Map();
|
|
6183
|
+
/** Latest analysis snapshot — refreshed on every analysis:updated event. */
|
|
6184
|
+
this.latestAnalysis = null;
|
|
6185
|
+
this.cleanupUnsubs = [];
|
|
6186
|
+
}
|
|
6187
|
+
start() {
|
|
6188
|
+
this.cleanupUnsubs.push(
|
|
6189
|
+
this.bus.on("request:completed", (req) => this.handleRequest(req)),
|
|
6190
|
+
this.bus.on("telemetry:query", (q) => this.handleQuery(q)),
|
|
6191
|
+
this.bus.on("telemetry:fetch", (f) => this.handleFetch(f)),
|
|
6192
|
+
this.bus.on(
|
|
6193
|
+
"analysis:updated",
|
|
6194
|
+
(update) => this.handleAnalysisUpdate(update)
|
|
6195
|
+
)
|
|
6196
|
+
);
|
|
6197
|
+
}
|
|
6198
|
+
stop() {
|
|
6199
|
+
for (const unsub of this.cleanupUnsubs) unsub();
|
|
6200
|
+
this.cleanupUnsubs.length = 0;
|
|
6201
|
+
}
|
|
6202
|
+
getGraph() {
|
|
6203
|
+
return this.graph;
|
|
6204
|
+
}
|
|
6205
|
+
/**
|
|
6206
|
+
* Enrich endpoint nodes with p95 from an external metrics source.
|
|
6207
|
+
* Called by the API handler which has access to MetricsStore.
|
|
6208
|
+
*/
|
|
6209
|
+
enrichWithMetrics(getP95) {
|
|
6210
|
+
for (const node of this.graph.nodes.values()) {
|
|
6211
|
+
if (node.type !== "endpoint") continue;
|
|
6212
|
+
const key = node.label;
|
|
6213
|
+
const p95 = getP95(key);
|
|
6214
|
+
if (p95 !== void 0) {
|
|
6215
|
+
if (!node.annotations) node.annotations = {};
|
|
6216
|
+
node.annotations.p95Ms = Math.round(p95);
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
getApiResponse(options) {
|
|
6221
|
+
const grouping = options?.grouping ?? "path";
|
|
6222
|
+
const clusters = this.computeClusters(grouping);
|
|
6223
|
+
if (options?.level === "endpoints") {
|
|
6224
|
+
return this.getEndpointView();
|
|
6225
|
+
}
|
|
6226
|
+
if (options?.node) {
|
|
6227
|
+
return this.getNodeNeighborhood(options.node, clusters);
|
|
6228
|
+
}
|
|
6229
|
+
if (options?.cluster) {
|
|
6230
|
+
return this.getClusterExpanded(options.cluster, clusters);
|
|
6231
|
+
}
|
|
6232
|
+
return this.getClusterView(clusters);
|
|
6233
|
+
}
|
|
6234
|
+
clear() {
|
|
6235
|
+
this.graph.nodes.clear();
|
|
6236
|
+
this.graph.edges.clear();
|
|
6237
|
+
this.graph.metadata.totalObservations = 0;
|
|
6238
|
+
this.pending.clear();
|
|
6239
|
+
this.nodeCategories.clear();
|
|
6240
|
+
this.latestAnalysis = null;
|
|
6241
|
+
}
|
|
6242
|
+
handleAnalysisUpdate(update) {
|
|
6243
|
+
this.latestAnalysis = update;
|
|
6244
|
+
this.enrichNodesFromAnalysis();
|
|
6245
|
+
}
|
|
6246
|
+
enrichNodesFromAnalysis() {
|
|
6247
|
+
if (!this.latestAnalysis) return;
|
|
6248
|
+
const { findings, insights, issues } = this.latestAnalysis;
|
|
6249
|
+
for (const node of this.graph.nodes.values()) {
|
|
6250
|
+
if (node.type !== "endpoint") continue;
|
|
6251
|
+
if (node.annotations) {
|
|
6252
|
+
node.annotations.securityFindings = void 0;
|
|
6253
|
+
node.annotations.insights = void 0;
|
|
6254
|
+
node.annotations.openIssueCount = void 0;
|
|
6255
|
+
}
|
|
6256
|
+
}
|
|
6257
|
+
for (const f of findings) {
|
|
6258
|
+
if (!f.endpoint) continue;
|
|
6259
|
+
const nodeId = `endpoint:${f.endpoint}`;
|
|
6260
|
+
const node = this.graph.nodes.get(nodeId);
|
|
6261
|
+
if (!node) continue;
|
|
6262
|
+
if (!node.annotations) node.annotations = {};
|
|
6263
|
+
if (!node.annotations.securityFindings)
|
|
6264
|
+
node.annotations.securityFindings = [];
|
|
6265
|
+
const existing = node.annotations.securityFindings.find(
|
|
6266
|
+
(s) => s.rule === f.rule
|
|
6267
|
+
);
|
|
6268
|
+
if (existing) {
|
|
6269
|
+
existing.count = f.count;
|
|
6270
|
+
} else {
|
|
6271
|
+
node.annotations.securityFindings.push({
|
|
6272
|
+
rule: f.rule,
|
|
6273
|
+
severity: f.severity,
|
|
6274
|
+
title: f.title,
|
|
6275
|
+
count: f.count
|
|
6276
|
+
});
|
|
6277
|
+
}
|
|
6278
|
+
}
|
|
6279
|
+
const endpointNodesByLabel = /* @__PURE__ */ new Map();
|
|
6280
|
+
for (const node of this.graph.nodes.values()) {
|
|
6281
|
+
if (node.type === "endpoint") endpointNodesByLabel.set(node.label, node);
|
|
6282
|
+
}
|
|
6283
|
+
for (const insight of insights) {
|
|
6284
|
+
if (!insight.nav) continue;
|
|
6285
|
+
for (const [label, node] of endpointNodesByLabel) {
|
|
6286
|
+
if (insight.title.includes(label) || insight.desc.includes(label)) {
|
|
6287
|
+
if (!node.annotations) node.annotations = {};
|
|
6288
|
+
if (!node.annotations.insights) node.annotations.insights = [];
|
|
6289
|
+
if (!node.annotations.insights.some(
|
|
6290
|
+
(i) => i.type === insight.type && i.title === insight.title
|
|
6291
|
+
)) {
|
|
6292
|
+
node.annotations.insights.push({
|
|
6293
|
+
type: insight.type,
|
|
6294
|
+
severity: insight.severity,
|
|
6295
|
+
title: insight.title
|
|
6296
|
+
});
|
|
6297
|
+
}
|
|
6298
|
+
}
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6301
|
+
for (const si of issues) {
|
|
6302
|
+
if (!si.issue.endpoint) continue;
|
|
6303
|
+
const nodeId = `endpoint:${si.issue.endpoint}`;
|
|
6304
|
+
const node = this.graph.nodes.get(nodeId);
|
|
6305
|
+
if (!node) continue;
|
|
6306
|
+
if (si.state === "open" || si.state === "regressed") {
|
|
6307
|
+
if (!node.annotations) node.annotations = {};
|
|
6308
|
+
node.annotations.openIssueCount = (node.annotations.openIssueCount ?? 0) + 1;
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
for (const insight of insights) {
|
|
6312
|
+
if (insight.type !== "n1" && insight.type !== "redundant-query") continue;
|
|
6313
|
+
for (const edge of this.graph.edges.values()) {
|
|
6314
|
+
if (edge.type !== "reads" && edge.type !== "writes") continue;
|
|
6315
|
+
const sourceNode = this.graph.nodes.get(edge.source);
|
|
6316
|
+
if (sourceNode?.annotations?.insights?.some(
|
|
6317
|
+
(i) => i.type === insight.type
|
|
6318
|
+
)) {
|
|
6319
|
+
if (!edge.annotations) edge.annotations = {};
|
|
6320
|
+
edge.annotations.hasIssue = true;
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
}
|
|
6324
|
+
}
|
|
6325
|
+
handleRequest(req) {
|
|
6326
|
+
if (shouldSkipRequest(req)) return;
|
|
6327
|
+
const now = Date.now();
|
|
6328
|
+
const endpointKey = getEndpointKey(req.method, req.path);
|
|
6329
|
+
const nodeId = `endpoint:${endpointKey}`;
|
|
6330
|
+
const node = upsertNode(this.graph, nodeId, "endpoint", endpointKey, now);
|
|
6331
|
+
node.stats.requestCount++;
|
|
6332
|
+
const category = detectCategory(req);
|
|
6333
|
+
let cats = this.nodeCategories.get(nodeId);
|
|
6334
|
+
if (!cats) {
|
|
6335
|
+
cats = /* @__PURE__ */ new Set();
|
|
6336
|
+
this.nodeCategories.set(nodeId, cats);
|
|
6337
|
+
}
|
|
6338
|
+
cats.add(category);
|
|
6339
|
+
if (!node.annotations) node.annotations = {};
|
|
6340
|
+
node.annotations.categories = [...cats];
|
|
6341
|
+
node.annotations.isMiddleware = cats.has("middleware");
|
|
6342
|
+
if (!node.annotations.hasAuth) {
|
|
6343
|
+
node.annotations.hasAuth = cats.has("auth-check") || cats.has("auth-handshake") || hasAuthCredentials(req);
|
|
6344
|
+
}
|
|
6345
|
+
node.stats.avgLatencyMs = updateRollingAvg(
|
|
6346
|
+
node.stats.avgLatencyMs,
|
|
6347
|
+
req.durationMs,
|
|
6348
|
+
node.stats.requestCount
|
|
6349
|
+
);
|
|
6350
|
+
const isError = req.statusCode >= 400 ? 1 : 0;
|
|
6351
|
+
node.stats.errorRate = node.stats.errorRate + (isError - node.stats.errorRate) / node.stats.requestCount;
|
|
6352
|
+
this.graph.metadata.totalObservations++;
|
|
6353
|
+
this.graph.metadata.lastUpdatedAt = now;
|
|
6354
|
+
const buffered = this.pending.get(req.id);
|
|
6355
|
+
if (buffered) {
|
|
6356
|
+
let queryCount = 0;
|
|
6357
|
+
for (const query of buffered.queries) {
|
|
6358
|
+
this.processQuery(query, nodeId, now);
|
|
6359
|
+
queryCount++;
|
|
6360
|
+
}
|
|
6361
|
+
for (const fetch of buffered.fetches) {
|
|
6362
|
+
this.processFetch(fetch, nodeId, now);
|
|
6363
|
+
}
|
|
6364
|
+
this.pending.delete(req.id);
|
|
6365
|
+
if (queryCount > 0) {
|
|
6366
|
+
node.stats.avgQueryCount = updateRollingAvg(
|
|
6367
|
+
node.stats.avgQueryCount,
|
|
6368
|
+
queryCount,
|
|
6369
|
+
node.stats.requestCount
|
|
6370
|
+
);
|
|
6371
|
+
}
|
|
6372
|
+
}
|
|
6373
|
+
const now2 = Date.now();
|
|
6374
|
+
for (const [key, entry] of this.pending) {
|
|
6375
|
+
if (now2 - entry.createdAt > PENDING_TTL_MS) this.pending.delete(key);
|
|
6376
|
+
}
|
|
6377
|
+
if (this.pending.size > PENDING_BUFFER_MAX) {
|
|
6378
|
+
const keys = [...this.pending.keys()];
|
|
6379
|
+
for (let i = 0; i < keys.length - PENDING_EVICTION_TARGET; i++) {
|
|
6380
|
+
this.pending.delete(keys[i]);
|
|
6381
|
+
}
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
handleQuery(query) {
|
|
6385
|
+
if (!query.parentRequestId || !query.table) return;
|
|
6386
|
+
let pending2 = this.pending.get(query.parentRequestId);
|
|
6387
|
+
if (!pending2) {
|
|
6388
|
+
pending2 = { queries: [], fetches: [], createdAt: Date.now() };
|
|
6389
|
+
this.pending.set(query.parentRequestId, pending2);
|
|
6390
|
+
}
|
|
6391
|
+
pending2.queries.push(query);
|
|
6392
|
+
}
|
|
6393
|
+
handleFetch(fetch) {
|
|
6394
|
+
if (!fetch.parentRequestId) return;
|
|
6395
|
+
const hostname = extractHostname(fetch.url);
|
|
6396
|
+
if (!hostname) return;
|
|
6397
|
+
let pending2 = this.pending.get(fetch.parentRequestId);
|
|
6398
|
+
if (!pending2) {
|
|
6399
|
+
pending2 = { queries: [], fetches: [], createdAt: Date.now() };
|
|
6400
|
+
this.pending.set(fetch.parentRequestId, pending2);
|
|
6401
|
+
}
|
|
6402
|
+
pending2.fetches.push(fetch);
|
|
6403
|
+
}
|
|
6404
|
+
// ── Process buffered events (called after request completes) ──
|
|
6405
|
+
processQuery(query, endpointNodeId, now) {
|
|
6406
|
+
if (!query.table) return;
|
|
6407
|
+
const tableNodeId = `table:${query.table}`;
|
|
6408
|
+
upsertNode(this.graph, tableNodeId, "table", query.table, now);
|
|
6409
|
+
const edgeType = query.normalizedOp === "SELECT" ? "reads" : "writes";
|
|
6410
|
+
const edge = upsertEdge(
|
|
6411
|
+
this.graph,
|
|
6412
|
+
endpointNodeId,
|
|
6413
|
+
tableNodeId,
|
|
6414
|
+
edgeType,
|
|
6415
|
+
now
|
|
6416
|
+
);
|
|
6417
|
+
edge.stats.frequency++;
|
|
6418
|
+
edge.stats.avgLatencyMs = updateRollingAvg(
|
|
6419
|
+
edge.stats.avgLatencyMs,
|
|
6420
|
+
query.durationMs,
|
|
6421
|
+
edge.stats.frequency
|
|
6422
|
+
);
|
|
6423
|
+
if (query.sql) {
|
|
6424
|
+
const normalized = normalizeQueryParams(query.sql);
|
|
6425
|
+
if (normalized) {
|
|
6426
|
+
if (!edge.patterns) edge.patterns = [];
|
|
6427
|
+
if (!edge.patterns.includes(normalized) && edge.patterns.length < MAX_PATTERNS_PER_EDGE) {
|
|
6428
|
+
edge.patterns.push(normalized);
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
6433
|
+
processFetch(fetch, endpointNodeId, now) {
|
|
6434
|
+
const hostname = extractHostname(fetch.url);
|
|
6435
|
+
if (!hostname) return;
|
|
6436
|
+
const externalNodeId = `external:${hostname}`;
|
|
6437
|
+
upsertNode(this.graph, externalNodeId, "external", hostname, now);
|
|
6438
|
+
const edge = upsertEdge(
|
|
6439
|
+
this.graph,
|
|
6440
|
+
endpointNodeId,
|
|
6441
|
+
externalNodeId,
|
|
6442
|
+
"fetches",
|
|
6443
|
+
now
|
|
6444
|
+
);
|
|
6445
|
+
edge.stats.frequency++;
|
|
6446
|
+
edge.stats.avgLatencyMs = updateRollingAvg(
|
|
6447
|
+
edge.stats.avgLatencyMs,
|
|
6448
|
+
fetch.durationMs,
|
|
6449
|
+
edge.stats.frequency
|
|
6450
|
+
);
|
|
6451
|
+
}
|
|
6452
|
+
// ── Clustering ──
|
|
6453
|
+
computeClusters(strategy = "path") {
|
|
6454
|
+
const endpointNodes = [...this.graph.nodes.values()].filter(
|
|
6455
|
+
(n) => n.type === "endpoint"
|
|
6456
|
+
);
|
|
6457
|
+
if (strategy === "auth-boundary") {
|
|
6458
|
+
return this.clusterByAuthBoundary(endpointNodes);
|
|
6459
|
+
}
|
|
6460
|
+
if (strategy === "data-domain") {
|
|
6461
|
+
return this.clusterByDataDomain(endpointNodes);
|
|
6462
|
+
}
|
|
6463
|
+
return this.clusterByPath(endpointNodes);
|
|
6464
|
+
}
|
|
6465
|
+
clusterByPath(endpointNodes) {
|
|
6466
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6467
|
+
for (const node of endpointNodes) {
|
|
6468
|
+
const parts = node.label.split(" ");
|
|
6469
|
+
const path = parts[1] || parts[0];
|
|
6470
|
+
const segments = path.split("/").filter(Boolean);
|
|
6471
|
+
let i = 0;
|
|
6472
|
+
while (i < segments.length && COMMON_PATH_PREFIXES.has(segments[i])) {
|
|
6473
|
+
i++;
|
|
6474
|
+
}
|
|
6475
|
+
const groupKey = segments[i] || segments[0] || "root";
|
|
6476
|
+
if (!groups.has(groupKey)) groups.set(groupKey, []);
|
|
6477
|
+
groups.get(groupKey).push(node.id);
|
|
6478
|
+
}
|
|
6479
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
6480
|
+
for (const [key, children] of groups) {
|
|
6481
|
+
if (children.length > CLUSTER_SPLIT_THRESHOLD) {
|
|
6482
|
+
const subGroups = /* @__PURE__ */ new Map();
|
|
6483
|
+
for (const childId of children) {
|
|
6484
|
+
const node = this.graph.nodes.get(childId);
|
|
6485
|
+
const parts = node.label.split(" ");
|
|
6486
|
+
const path = parts[1] || parts[0];
|
|
6487
|
+
const segments = path.split("/").filter(Boolean);
|
|
6488
|
+
let idx = segments.indexOf(key);
|
|
6489
|
+
if (idx === -1) idx = 0;
|
|
6490
|
+
const subKey = `${key}/${segments[idx + 1] || "root"}`;
|
|
6491
|
+
if (!subGroups.has(subKey)) subGroups.set(subKey, []);
|
|
6492
|
+
subGroups.get(subKey).push(childId);
|
|
6493
|
+
}
|
|
6494
|
+
for (const [subKey, subChildren] of subGroups) {
|
|
6495
|
+
clusters.set(subKey, this.buildCluster(subKey, subChildren));
|
|
6496
|
+
}
|
|
6497
|
+
} else {
|
|
6498
|
+
clusters.set(key, this.buildCluster(key, children));
|
|
6499
|
+
}
|
|
6500
|
+
}
|
|
6501
|
+
return clusters;
|
|
6502
|
+
}
|
|
6503
|
+
clusterByAuthBoundary(endpointNodes) {
|
|
6504
|
+
const authed = [];
|
|
6505
|
+
const unauthed = [];
|
|
6506
|
+
for (const node of endpointNodes) {
|
|
6507
|
+
if (node.annotations?.hasAuth) {
|
|
6508
|
+
authed.push(node.id);
|
|
6509
|
+
} else {
|
|
6510
|
+
unauthed.push(node.id);
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
6513
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
6514
|
+
if (authed.length > 0)
|
|
6515
|
+
clusters.set("authenticated", this.buildCluster("authenticated", authed));
|
|
6516
|
+
if (unauthed.length > 0)
|
|
6517
|
+
clusters.set(
|
|
6518
|
+
"unauthenticated",
|
|
6519
|
+
this.buildCluster("unauthenticated", unauthed)
|
|
6520
|
+
);
|
|
6521
|
+
return clusters;
|
|
6522
|
+
}
|
|
6523
|
+
clusterByDataDomain(endpointNodes) {
|
|
6524
|
+
const endpointTables = /* @__PURE__ */ new Map();
|
|
6525
|
+
for (const edge of this.graph.edges.values()) {
|
|
6526
|
+
if (edge.type !== "reads" && edge.type !== "writes") continue;
|
|
6527
|
+
const tableLabel = this.graph.nodes.get(edge.target)?.label;
|
|
6528
|
+
if (!tableLabel) continue;
|
|
6529
|
+
let tables = endpointTables.get(edge.source);
|
|
6530
|
+
if (!tables) {
|
|
6531
|
+
tables = /* @__PURE__ */ new Set();
|
|
6532
|
+
endpointTables.set(edge.source, tables);
|
|
6533
|
+
}
|
|
6534
|
+
tables.add(tableLabel);
|
|
6535
|
+
}
|
|
6536
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6537
|
+
for (const node of endpointNodes) {
|
|
6538
|
+
const tables = endpointTables.get(node.id);
|
|
6539
|
+
const groupKey = tables && tables.size > 0 ? [...tables].sort().join("+") : "no-db";
|
|
6540
|
+
if (!groups.has(groupKey)) groups.set(groupKey, []);
|
|
6541
|
+
groups.get(groupKey).push(node.id);
|
|
6542
|
+
}
|
|
6543
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
6544
|
+
for (const [key, children] of groups) {
|
|
6545
|
+
clusters.set(key, this.buildCluster(key, children));
|
|
6546
|
+
}
|
|
6547
|
+
return clusters;
|
|
6548
|
+
}
|
|
6549
|
+
buildCluster(key, children) {
|
|
6550
|
+
let totalLatency = 0;
|
|
6551
|
+
let totalRequests = 0;
|
|
6552
|
+
let totalErrors = 0;
|
|
6553
|
+
let totalQueries = 0;
|
|
6554
|
+
let firstSeen = Infinity;
|
|
6555
|
+
let lastSeen = 0;
|
|
6556
|
+
for (const childId of children) {
|
|
6557
|
+
const node = this.graph.nodes.get(childId);
|
|
6558
|
+
if (!node) continue;
|
|
6559
|
+
totalRequests += node.stats.requestCount;
|
|
6560
|
+
totalLatency += node.stats.avgLatencyMs * node.stats.requestCount;
|
|
6561
|
+
totalErrors += node.stats.errorRate * node.stats.requestCount;
|
|
6562
|
+
totalQueries += node.stats.avgQueryCount * node.stats.requestCount;
|
|
6563
|
+
firstSeen = Math.min(firstSeen, node.stats.firstSeenAt);
|
|
6564
|
+
lastSeen = Math.max(lastSeen, node.stats.lastSeenAt);
|
|
6565
|
+
}
|
|
6566
|
+
return {
|
|
6567
|
+
id: `cluster:${key}`,
|
|
6568
|
+
label: key,
|
|
6569
|
+
children,
|
|
6570
|
+
stats: {
|
|
6571
|
+
requestCount: totalRequests,
|
|
6572
|
+
avgLatencyMs: totalRequests > 0 ? Math.round(totalLatency / totalRequests) : 0,
|
|
6573
|
+
errorRate: totalRequests > 0 ? totalErrors / totalRequests : 0,
|
|
6574
|
+
avgQueryCount: totalRequests > 0 ? Math.round(totalQueries / totalRequests * 10) / 10 : 0,
|
|
6575
|
+
lastSeenAt: lastSeen || Date.now(),
|
|
6576
|
+
firstSeenAt: firstSeen === Infinity ? Date.now() : firstSeen
|
|
6577
|
+
}
|
|
6578
|
+
};
|
|
6579
|
+
}
|
|
6580
|
+
// ── API response builders ──
|
|
6581
|
+
getEndpointView() {
|
|
6582
|
+
const allNodes = [...this.graph.nodes.values()];
|
|
6583
|
+
const allEdges = [...this.graph.edges.values()];
|
|
6584
|
+
return {
|
|
6585
|
+
nodes: allNodes,
|
|
6586
|
+
edges: allEdges,
|
|
6587
|
+
clusters: [],
|
|
6588
|
+
metadata: this.graph.metadata
|
|
6589
|
+
};
|
|
6590
|
+
}
|
|
6591
|
+
getClusterView(clusters) {
|
|
6592
|
+
const clusterArr = [...clusters.values()];
|
|
6593
|
+
const otherNodes = [...this.graph.nodes.values()].filter(
|
|
6594
|
+
(n) => n.type !== "endpoint"
|
|
6595
|
+
);
|
|
6596
|
+
const endpointToCluster = /* @__PURE__ */ new Map();
|
|
6597
|
+
for (const c of clusterArr) {
|
|
6598
|
+
for (const childId of c.children) {
|
|
6599
|
+
endpointToCluster.set(childId, c.id);
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
const edgeAgg = /* @__PURE__ */ new Map();
|
|
6603
|
+
for (const edge of this.graph.edges.values()) {
|
|
6604
|
+
const sourceCluster = endpointToCluster.get(edge.source) ?? edge.source;
|
|
6605
|
+
const target = edge.target;
|
|
6606
|
+
const aggId = makeEdgeId(sourceCluster, target);
|
|
6607
|
+
let agg = edgeAgg.get(aggId);
|
|
6608
|
+
if (!agg) {
|
|
6609
|
+
agg = {
|
|
6610
|
+
id: aggId,
|
|
6611
|
+
source: sourceCluster,
|
|
6612
|
+
target,
|
|
6613
|
+
type: edge.type,
|
|
6614
|
+
stats: { ...edge.stats },
|
|
6615
|
+
patterns: edge.patterns ? [...edge.patterns] : void 0
|
|
6616
|
+
};
|
|
6617
|
+
edgeAgg.set(aggId, agg);
|
|
6618
|
+
} else {
|
|
6619
|
+
agg.stats.frequency += edge.stats.frequency;
|
|
6620
|
+
agg.stats.avgLatencyMs = Math.round(
|
|
6621
|
+
(agg.stats.avgLatencyMs + edge.stats.avgLatencyMs) / 2
|
|
6622
|
+
);
|
|
6623
|
+
agg.stats.lastSeenAt = Math.max(
|
|
6624
|
+
agg.stats.lastSeenAt,
|
|
6625
|
+
edge.stats.lastSeenAt
|
|
6626
|
+
);
|
|
6627
|
+
agg.stats.firstSeenAt = Math.min(
|
|
6628
|
+
agg.stats.firstSeenAt,
|
|
6629
|
+
edge.stats.firstSeenAt
|
|
6630
|
+
);
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
return {
|
|
6634
|
+
nodes: otherNodes,
|
|
6635
|
+
edges: [...edgeAgg.values()],
|
|
6636
|
+
clusters: clusterArr,
|
|
6637
|
+
metadata: this.graph.metadata
|
|
6638
|
+
};
|
|
6639
|
+
}
|
|
6640
|
+
getClusterExpanded(clusterId, clusters) {
|
|
6641
|
+
const clusterKey = clusterId.startsWith("cluster:") ? clusterId.slice(8) : clusterId;
|
|
6642
|
+
const cluster = clusters.get(clusterKey);
|
|
6643
|
+
if (!cluster) return this.getClusterView(clusters);
|
|
6644
|
+
const childNodes = [];
|
|
6645
|
+
const connectedNodeIds = /* @__PURE__ */ new Set();
|
|
6646
|
+
const relevantEdges = [];
|
|
6647
|
+
for (const childId of cluster.children) {
|
|
6648
|
+
const node = this.graph.nodes.get(childId);
|
|
6649
|
+
if (node) childNodes.push(node);
|
|
6650
|
+
for (const edge of this.graph.edges.values()) {
|
|
6651
|
+
if (edge.source === childId || edge.target === childId) {
|
|
6652
|
+
relevantEdges.push(edge);
|
|
6653
|
+
if (edge.source !== childId) connectedNodeIds.add(edge.source);
|
|
6654
|
+
if (edge.target !== childId) connectedNodeIds.add(edge.target);
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6658
|
+
for (const nodeId of connectedNodeIds) {
|
|
6659
|
+
if (!cluster.children.includes(nodeId)) {
|
|
6660
|
+
const node = this.graph.nodes.get(nodeId);
|
|
6661
|
+
if (node) childNodes.push(node);
|
|
6662
|
+
}
|
|
6663
|
+
}
|
|
6664
|
+
return {
|
|
6665
|
+
nodes: childNodes,
|
|
6666
|
+
edges: relevantEdges,
|
|
6667
|
+
clusters: [cluster],
|
|
6668
|
+
metadata: this.graph.metadata
|
|
6669
|
+
};
|
|
6670
|
+
}
|
|
6671
|
+
getNodeNeighborhood(nodeId, clusters) {
|
|
6672
|
+
const centerNode = this.graph.nodes.get(nodeId);
|
|
6673
|
+
if (!centerNode) return this.getClusterView(clusters);
|
|
6674
|
+
const nodes = [centerNode];
|
|
6675
|
+
const edges = [];
|
|
6676
|
+
for (const edge of this.graph.edges.values()) {
|
|
6677
|
+
if (edge.source === nodeId || edge.target === nodeId) {
|
|
6678
|
+
edges.push(edge);
|
|
6679
|
+
const otherId = edge.source === nodeId ? edge.target : edge.source;
|
|
6680
|
+
const otherNode = this.graph.nodes.get(otherId);
|
|
6681
|
+
if (otherNode) nodes.push(otherNode);
|
|
6682
|
+
}
|
|
6683
|
+
}
|
|
6684
|
+
return {
|
|
6685
|
+
nodes,
|
|
6686
|
+
edges,
|
|
6687
|
+
clusters: [],
|
|
6688
|
+
metadata: this.graph.metadata
|
|
6689
|
+
};
|
|
6690
|
+
}
|
|
6691
|
+
};
|
|
6692
|
+
}
|
|
6693
|
+
});
|
|
6694
|
+
|
|
5670
6695
|
// src/output/terminal.ts
|
|
5671
6696
|
import pc from "picocolors";
|
|
5672
6697
|
function print(line) {
|
|
@@ -6182,6 +7207,9 @@ async function doSetup() {
|
|
|
6182
7207
|
hooks_installed: ["fetch", "console", "error"],
|
|
6183
7208
|
setup_duration_ms: setupDurationMs
|
|
6184
7209
|
});
|
|
7210
|
+
const graphBuilder = new GraphBuilder(bus, stores.requestStore);
|
|
7211
|
+
graphBuilder.start();
|
|
7212
|
+
services.graphBuilder = graphBuilder;
|
|
6185
7213
|
const dataDir = getProjectDataDir(cwd);
|
|
6186
7214
|
const analysisServices = startAnalysis(bus, stores, dataDir, services);
|
|
6187
7215
|
const config = {
|
|
@@ -6252,6 +7280,7 @@ var init_setup = __esm({
|
|
|
6252
7280
|
init_store();
|
|
6253
7281
|
init_issue_store();
|
|
6254
7282
|
init_engine();
|
|
7283
|
+
init_graph_builder();
|
|
6255
7284
|
init_terminal();
|
|
6256
7285
|
init_src();
|
|
6257
7286
|
init_constants();
|