brakit 0.8.7 → 0.9.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/README.md +15 -3
- package/dist/api.d.ts +83 -46
- package/dist/api.js +776 -767
- package/dist/bin/brakit.js +305 -432
- package/dist/dashboard-client.global.js +465 -267
- package/dist/dashboard.html +584 -310
- package/dist/mcp/server.js +7 -15
- package/dist/runtime/index.js +1566 -1700
- package/package.json +1 -1
package/dist/runtime/index.js
CHANGED
|
@@ -14,48 +14,10 @@ var __export = (target, all) => {
|
|
|
14
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
// src/constants/
|
|
18
|
-
var
|
|
19
|
-
var
|
|
20
|
-
"src/constants/
|
|
21
|
-
"use strict";
|
|
22
|
-
DASHBOARD_PREFIX = "/__brakit";
|
|
23
|
-
DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
|
|
24
|
-
DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
|
|
25
|
-
DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
|
|
26
|
-
DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
|
|
27
|
-
DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
|
|
28
|
-
DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
|
|
29
|
-
DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
|
|
30
|
-
DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
|
|
31
|
-
DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
|
|
32
|
-
DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
|
|
33
|
-
DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
|
|
34
|
-
DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
|
|
35
|
-
DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
|
|
36
|
-
DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
|
|
37
|
-
DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
38
|
-
DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
39
|
-
DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
40
|
-
VALID_TABS_TUPLE = [
|
|
41
|
-
"overview",
|
|
42
|
-
"actions",
|
|
43
|
-
"requests",
|
|
44
|
-
"fetches",
|
|
45
|
-
"queries",
|
|
46
|
-
"errors",
|
|
47
|
-
"logs",
|
|
48
|
-
"performance",
|
|
49
|
-
"security"
|
|
50
|
-
];
|
|
51
|
-
VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// src/constants/limits.ts
|
|
56
|
-
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;
|
|
57
|
-
var init_limits = __esm({
|
|
58
|
-
"src/constants/limits.ts"() {
|
|
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;
|
|
19
|
+
var init_config = __esm({
|
|
20
|
+
"src/constants/config.ts"() {
|
|
59
21
|
"use strict";
|
|
60
22
|
MAX_REQUEST_ENTRIES = 1e3;
|
|
61
23
|
DEFAULT_MAX_BODY_CAPTURE = 10240;
|
|
@@ -82,21 +44,12 @@ var init_limits = __esm({
|
|
|
82
44
|
MAX_UNIQUE_ENDPOINTS = 500;
|
|
83
45
|
MAX_ACCUMULATOR_ENTRIES = 1e3;
|
|
84
46
|
ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// src/constants/thresholds.ts
|
|
89
|
-
var FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, 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;
|
|
90
|
-
var init_thresholds = __esm({
|
|
91
|
-
"src/constants/thresholds.ts"() {
|
|
92
|
-
"use strict";
|
|
93
47
|
FLOW_GAP_MS = 5e3;
|
|
94
48
|
SLOW_REQUEST_THRESHOLD_MS = 2e3;
|
|
95
49
|
MIN_POLLING_SEQUENCE = 3;
|
|
96
50
|
ENDPOINT_TRUNCATE_LENGTH = 12;
|
|
97
51
|
N1_QUERY_THRESHOLD = 5;
|
|
98
52
|
ERROR_RATE_THRESHOLD_PCT = 20;
|
|
99
|
-
SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
|
|
100
53
|
MIN_REQUESTS_FOR_INSIGHT = 2;
|
|
101
54
|
HIGH_QUERY_COUNT_PER_REQ = 5;
|
|
102
55
|
CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
|
|
@@ -119,25 +72,10 @@ var init_thresholds = __esm({
|
|
|
119
72
|
INSIGHT_WINDOW_PER_ENDPOINT = 20;
|
|
120
73
|
CLEAN_HITS_FOR_RESOLUTION = 5;
|
|
121
74
|
STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
var SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS;
|
|
127
|
-
var init_transport = __esm({
|
|
128
|
-
"src/constants/transport.ts"() {
|
|
129
|
-
"use strict";
|
|
130
|
-
SSE_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
131
|
-
NOISE_HOSTS = ["registry.npmjs.org", "telemetry.nextjs.org", "vitejs.dev"];
|
|
132
|
-
NOISE_PATH_PATTERNS = [".hot-update.", "__webpack", "__vite"];
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// src/constants/metrics.ts
|
|
137
|
-
var METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS;
|
|
138
|
-
var init_metrics = __esm({
|
|
139
|
-
"src/constants/metrics.ts"() {
|
|
140
|
-
"use strict";
|
|
75
|
+
STRICT_MODE_MAX_GAP_MS = 2e3;
|
|
76
|
+
BASELINE_MIN_SESSIONS = 2;
|
|
77
|
+
BASELINE_MIN_REQUESTS_PER_SESSION = 3;
|
|
78
|
+
BASELINE_PENDING_POINTS_MIN = 3;
|
|
141
79
|
METRICS_DIR = ".brakit";
|
|
142
80
|
METRICS_FILE = "metrics.json";
|
|
143
81
|
PORT_FILE = ".brakit/port";
|
|
@@ -146,14 +84,50 @@ var init_metrics = __esm({
|
|
|
146
84
|
METRICS_MAX_SESSIONS = 50;
|
|
147
85
|
METRICS_MAX_DATA_POINTS = 200;
|
|
148
86
|
ISSUES_FLUSH_INTERVAL_MS = 1e4;
|
|
87
|
+
SSE_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
88
|
+
NOISE_HOSTS = ["registry.npmjs.org", "telemetry.nextjs.org", "vitejs.dev"];
|
|
89
|
+
NOISE_PATH_PATTERNS = [".hot-update.", "__webpack", "__vite"];
|
|
90
|
+
VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
|
|
91
|
+
VALID_ISSUE_CATEGORIES = /* @__PURE__ */ new Set(["security", "performance", "reliability"]);
|
|
92
|
+
VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
|
|
149
93
|
}
|
|
150
94
|
});
|
|
151
95
|
|
|
152
|
-
// src/constants/
|
|
153
|
-
var BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES;
|
|
154
|
-
var
|
|
155
|
-
"src/constants/
|
|
96
|
+
// src/constants/labels.ts
|
|
97
|
+
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;
|
|
98
|
+
var init_labels = __esm({
|
|
99
|
+
"src/constants/labels.ts"() {
|
|
156
100
|
"use strict";
|
|
101
|
+
DASHBOARD_PREFIX = "/__brakit";
|
|
102
|
+
DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
|
|
103
|
+
DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
|
|
104
|
+
DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
|
|
105
|
+
DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
|
|
106
|
+
DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
|
|
107
|
+
DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
|
|
108
|
+
DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
|
|
109
|
+
DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
|
|
110
|
+
DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
|
|
111
|
+
DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
|
|
112
|
+
DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
|
|
113
|
+
DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
|
|
114
|
+
DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
|
|
115
|
+
DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
|
|
116
|
+
DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
117
|
+
DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
118
|
+
DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
119
|
+
VALID_TABS_TUPLE = [
|
|
120
|
+
"overview",
|
|
121
|
+
"actions",
|
|
122
|
+
"requests",
|
|
123
|
+
"fetches",
|
|
124
|
+
"queries",
|
|
125
|
+
"errors",
|
|
126
|
+
"logs",
|
|
127
|
+
"performance",
|
|
128
|
+
"security"
|
|
129
|
+
];
|
|
130
|
+
VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
157
131
|
BRAKIT_REQUEST_ID_HEADER = "x-brakit-request-id";
|
|
158
132
|
BRAKIT_FETCH_ID_HEADER = "x-brakit-fetch-id";
|
|
159
133
|
SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
@@ -164,13 +138,53 @@ var init_headers = __esm({
|
|
|
164
138
|
"x-api-key",
|
|
165
139
|
"x-auth-token"
|
|
166
140
|
]);
|
|
141
|
+
HTTP_OK = 200;
|
|
142
|
+
HTTP_NO_CONTENT = 204;
|
|
143
|
+
HTTP_BAD_REQUEST = 400;
|
|
144
|
+
HTTP_NOT_FOUND = 404;
|
|
145
|
+
HTTP_METHOD_NOT_ALLOWED = 405;
|
|
146
|
+
HTTP_PAYLOAD_TOO_LARGE = 413;
|
|
147
|
+
HTTP_INTERNAL_ERROR = 500;
|
|
148
|
+
SECURITY_HEADERS = {
|
|
149
|
+
"x-content-type-options": "nosniff",
|
|
150
|
+
"x-frame-options": "DENY",
|
|
151
|
+
"referrer-policy": "no-referrer",
|
|
152
|
+
"content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
|
|
153
|
+
};
|
|
154
|
+
CONTENT_ENCODING_GZIP = "gzip";
|
|
155
|
+
CONTENT_ENCODING_BR = "br";
|
|
156
|
+
CONTENT_ENCODING_DEFLATE = "deflate";
|
|
157
|
+
SEVERITY_ICON = {
|
|
158
|
+
critical: "\u2717",
|
|
159
|
+
warning: "\u26A0",
|
|
160
|
+
info: "\u2139"
|
|
161
|
+
};
|
|
162
|
+
SSE_EVENT_FETCH = "fetch";
|
|
163
|
+
SSE_EVENT_LOG = "log";
|
|
164
|
+
SSE_EVENT_ERROR = "error_event";
|
|
165
|
+
SSE_EVENT_QUERY = "query";
|
|
166
|
+
SSE_EVENT_ISSUES = "issues";
|
|
167
|
+
SDK_EVENT_REQUEST = "request";
|
|
168
|
+
SDK_EVENT_DB_QUERY = "db.query";
|
|
169
|
+
SDK_EVENT_FETCH = "fetch";
|
|
170
|
+
SDK_EVENT_LOG = "log";
|
|
171
|
+
SDK_EVENT_ERROR = "error";
|
|
172
|
+
SDK_EVENT_AUTH_CHECK = "auth.check";
|
|
173
|
+
POSTHOG_HOST = "https://us.i.posthog.com";
|
|
174
|
+
POSTHOG_CAPTURE_PATH = "/i/v0/e/";
|
|
175
|
+
POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
|
|
176
|
+
SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
|
|
177
|
+
TIMELINE_FETCH = "fetch";
|
|
178
|
+
TIMELINE_LOG = "log";
|
|
179
|
+
TIMELINE_ERROR = "error";
|
|
180
|
+
TIMELINE_QUERY = "query";
|
|
167
181
|
}
|
|
168
182
|
});
|
|
169
183
|
|
|
170
|
-
// src/constants/
|
|
184
|
+
// src/constants/features.ts
|
|
171
185
|
var CLOUD_SIGNALS, MAX_HEALTH_ERRORS, RECOVERY_WINDOW_MS, LOCALHOST_IPS, LOCALHOST_HOSTNAMES, URL_PARSE_BASE, DIR_MODE_OWNER_ONLY, FILE_MODE_OWNER_ONLY;
|
|
172
|
-
var
|
|
173
|
-
"src/constants/
|
|
186
|
+
var init_features = __esm({
|
|
187
|
+
"src/constants/features.ts"() {
|
|
174
188
|
"use strict";
|
|
175
189
|
CLOUD_SIGNALS = [
|
|
176
190
|
"VERCEL",
|
|
@@ -203,112 +217,13 @@ var init_network = __esm({
|
|
|
203
217
|
}
|
|
204
218
|
});
|
|
205
219
|
|
|
206
|
-
// src/constants/mcp.ts
|
|
207
|
-
var init_mcp = __esm({
|
|
208
|
-
"src/constants/mcp.ts"() {
|
|
209
|
-
"use strict";
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// src/constants/encoding.ts
|
|
214
|
-
var CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE;
|
|
215
|
-
var init_encoding = __esm({
|
|
216
|
-
"src/constants/encoding.ts"() {
|
|
217
|
-
"use strict";
|
|
218
|
-
CONTENT_ENCODING_GZIP = "gzip";
|
|
219
|
-
CONTENT_ENCODING_BR = "br";
|
|
220
|
-
CONTENT_ENCODING_DEFLATE = "deflate";
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// src/constants/severity.ts
|
|
225
|
-
var SEVERITY_ICON;
|
|
226
|
-
var init_severity = __esm({
|
|
227
|
-
"src/constants/severity.ts"() {
|
|
228
|
-
"use strict";
|
|
229
|
-
SEVERITY_ICON = {
|
|
230
|
-
critical: "\u2717",
|
|
231
|
-
warning: "\u26A0",
|
|
232
|
-
info: "\u2139"
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// src/constants/telemetry.ts
|
|
238
|
-
var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS;
|
|
239
|
-
var init_telemetry = __esm({
|
|
240
|
-
"src/constants/telemetry.ts"() {
|
|
241
|
-
"use strict";
|
|
242
|
-
POSTHOG_HOST = "https://us.i.posthog.com";
|
|
243
|
-
POSTHOG_CAPTURE_PATH = "/i/v0/e/";
|
|
244
|
-
POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
|
|
245
|
-
SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// src/constants/lifecycle.ts
|
|
250
|
-
var VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES;
|
|
251
|
-
var init_lifecycle = __esm({
|
|
252
|
-
"src/constants/lifecycle.ts"() {
|
|
253
|
-
"use strict";
|
|
254
|
-
VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
|
|
255
|
-
VALID_ISSUE_CATEGORIES = /* @__PURE__ */ new Set(["security", "performance", "reliability"]);
|
|
256
|
-
VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// src/constants/cli.ts
|
|
261
|
-
var init_cli = __esm({
|
|
262
|
-
"src/constants/cli.ts"() {
|
|
263
|
-
"use strict";
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
// src/constants/timeline.ts
|
|
268
|
-
var TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY;
|
|
269
|
-
var init_timeline = __esm({
|
|
270
|
-
"src/constants/timeline.ts"() {
|
|
271
|
-
"use strict";
|
|
272
|
-
TIMELINE_FETCH = "fetch";
|
|
273
|
-
TIMELINE_LOG = "log";
|
|
274
|
-
TIMELINE_ERROR = "error";
|
|
275
|
-
TIMELINE_QUERY = "query";
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// src/constants/sdk-events.ts
|
|
280
|
-
var SDK_EVENT_REQUEST, SDK_EVENT_DB_QUERY, SDK_EVENT_FETCH, SDK_EVENT_LOG, SDK_EVENT_ERROR, SDK_EVENT_AUTH_CHECK;
|
|
281
|
-
var init_sdk_events = __esm({
|
|
282
|
-
"src/constants/sdk-events.ts"() {
|
|
283
|
-
"use strict";
|
|
284
|
-
SDK_EVENT_REQUEST = "request";
|
|
285
|
-
SDK_EVENT_DB_QUERY = "db.query";
|
|
286
|
-
SDK_EVENT_FETCH = "fetch";
|
|
287
|
-
SDK_EVENT_LOG = "log";
|
|
288
|
-
SDK_EVENT_ERROR = "error";
|
|
289
|
-
SDK_EVENT_AUTH_CHECK = "auth.check";
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
|
|
293
220
|
// src/constants/index.ts
|
|
294
221
|
var init_constants = __esm({
|
|
295
222
|
"src/constants/index.ts"() {
|
|
296
223
|
"use strict";
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
init_transport();
|
|
301
|
-
init_metrics();
|
|
302
|
-
init_headers();
|
|
303
|
-
init_network();
|
|
304
|
-
init_mcp();
|
|
305
|
-
init_encoding();
|
|
306
|
-
init_severity();
|
|
307
|
-
init_telemetry();
|
|
308
|
-
init_lifecycle();
|
|
309
|
-
init_cli();
|
|
310
|
-
init_timeline();
|
|
311
|
-
init_sdk_events();
|
|
224
|
+
init_config();
|
|
225
|
+
init_labels();
|
|
226
|
+
init_features();
|
|
312
227
|
}
|
|
313
228
|
});
|
|
314
229
|
|
|
@@ -377,7 +292,7 @@ function setupFetchHook(emit) {
|
|
|
377
292
|
statusCode: msg.response.statusCode ?? 0,
|
|
378
293
|
durationMs: Math.round(performance.now() - info.startTime),
|
|
379
294
|
parentRequestId: info.parentRequestId,
|
|
380
|
-
timestamp:
|
|
295
|
+
timestamp: performance.now()
|
|
381
296
|
}
|
|
382
297
|
});
|
|
383
298
|
});
|
|
@@ -407,7 +322,7 @@ function setupConsoleHook(emit) {
|
|
|
407
322
|
const ctx = getRequestContext();
|
|
408
323
|
if (!ctx) return;
|
|
409
324
|
const message = format(...args);
|
|
410
|
-
const timestamp =
|
|
325
|
+
const timestamp = performance.now();
|
|
411
326
|
const parentRequestId = ctx.requestId;
|
|
412
327
|
if (level === "error") {
|
|
413
328
|
const errorArg = args.find((a) => a instanceof Error);
|
|
@@ -474,7 +389,7 @@ function createCaptureError(emit) {
|
|
|
474
389
|
message: error.message,
|
|
475
390
|
stack: error.stack ?? "",
|
|
476
391
|
parentRequestId: ctx?.requestId ?? null,
|
|
477
|
-
timestamp:
|
|
392
|
+
timestamp: performance.now()
|
|
478
393
|
}
|
|
479
394
|
});
|
|
480
395
|
};
|
|
@@ -538,65 +453,28 @@ var init_adapter_registry = __esm({
|
|
|
538
453
|
}
|
|
539
454
|
});
|
|
540
455
|
|
|
541
|
-
// src/instrument/adapters/shared.ts
|
|
542
|
-
import { createRequire } from "module";
|
|
543
|
-
function tryRequire(id) {
|
|
544
|
-
try {
|
|
545
|
-
return appRequire(id);
|
|
546
|
-
} catch {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
function captureRequestId() {
|
|
551
|
-
return getRequestContext()?.requestId ?? null;
|
|
552
|
-
}
|
|
553
|
-
var appRequire;
|
|
554
|
-
var init_shared = __esm({
|
|
555
|
-
"src/instrument/adapters/shared.ts"() {
|
|
556
|
-
"use strict";
|
|
557
|
-
init_context();
|
|
558
|
-
appRequire = createRequire(process.cwd() + "/index.js");
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
|
|
562
456
|
// src/instrument/adapters/normalize.ts
|
|
563
457
|
function normalizeSQL(sql) {
|
|
564
458
|
if (!sql) return { op: "OTHER", table: "" };
|
|
565
459
|
const trimmed = sql.trim();
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
const tableMatch = trimmed.match(/(?:FROM|INTO|UPDATE)\s+"?\w+"?\."?(\w+)"?/i);
|
|
572
|
-
const table = tableMatch?.[1] ?? "";
|
|
573
|
-
switch (op) {
|
|
574
|
-
case "SELECT":
|
|
575
|
-
return { op: "SELECT", table };
|
|
576
|
-
case "INSERT":
|
|
577
|
-
return { op: "INSERT", table };
|
|
578
|
-
case "UPDATE":
|
|
579
|
-
return { op: "UPDATE", table };
|
|
580
|
-
case "DELETE":
|
|
581
|
-
return { op: "DELETE", table };
|
|
582
|
-
default:
|
|
583
|
-
return { op: "OTHER", table };
|
|
584
|
-
}
|
|
460
|
+
const keyword = trimmed.split(/\s+/, 1)[0].toUpperCase();
|
|
461
|
+
const op = VALID_OPS.has(keyword) ? keyword : "OTHER";
|
|
462
|
+
const table = trimmed.match(TABLE_RE)?.[1] ?? "";
|
|
463
|
+
return { op, table };
|
|
585
464
|
}
|
|
586
465
|
function normalizePrismaOp(operation) {
|
|
587
466
|
return PRISMA_OP_MAP[operation] ?? "OTHER";
|
|
588
467
|
}
|
|
589
468
|
function normalizeQueryParams(sql) {
|
|
590
469
|
if (!sql) return null;
|
|
591
|
-
|
|
592
|
-
n = n.replace(/\b\d+(\.\d+)?\b/g, "?");
|
|
593
|
-
n = n.replace(/\$\d+/g, "?");
|
|
594
|
-
return n;
|
|
470
|
+
return sql.replace(SQL_PARAM_MARKER, "?").replace(SQL_STRING_LITERAL, "?").replace(SQL_NUMBER_LITERAL, "?");
|
|
595
471
|
}
|
|
596
|
-
var PRISMA_OP_MAP;
|
|
472
|
+
var VALID_OPS, TABLE_RE, PRISMA_OP_MAP, SQL_PARAM_MARKER, SQL_STRING_LITERAL, SQL_NUMBER_LITERAL;
|
|
597
473
|
var init_normalize = __esm({
|
|
598
474
|
"src/instrument/adapters/normalize.ts"() {
|
|
599
475
|
"use strict";
|
|
476
|
+
VALID_OPS = /* @__PURE__ */ new Set(["SELECT", "INSERT", "UPDATE", "DELETE"]);
|
|
477
|
+
TABLE_RE = /(?:FROM|INTO|UPDATE)\s+(?:"?\w+"?\.)?"?(\w+)"?/i;
|
|
600
478
|
PRISMA_OP_MAP = {
|
|
601
479
|
findUnique: "SELECT",
|
|
602
480
|
findUniqueOrThrow: "SELECT",
|
|
@@ -615,79 +493,199 @@ var init_normalize = __esm({
|
|
|
615
493
|
delete: "DELETE",
|
|
616
494
|
deleteMany: "DELETE"
|
|
617
495
|
};
|
|
496
|
+
SQL_PARAM_MARKER = /\$\d+/g;
|
|
497
|
+
SQL_STRING_LITERAL = /'[^']*'/g;
|
|
498
|
+
SQL_NUMBER_LITERAL = /\b\d+(\.\d+)?\b/g;
|
|
618
499
|
}
|
|
619
500
|
});
|
|
620
501
|
|
|
621
|
-
// src/
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
502
|
+
// src/utils/type-guards.ts
|
|
503
|
+
function isString(val) {
|
|
504
|
+
return typeof val === "string";
|
|
505
|
+
}
|
|
506
|
+
function isNumber(val) {
|
|
507
|
+
return typeof val === "number" && !isNaN(val);
|
|
508
|
+
}
|
|
509
|
+
function isBoolean(val) {
|
|
510
|
+
return typeof val === "boolean";
|
|
511
|
+
}
|
|
512
|
+
function isThenable(value) {
|
|
513
|
+
return value != null && typeof value.then === "function";
|
|
514
|
+
}
|
|
515
|
+
function getErrorMessage(err) {
|
|
516
|
+
if (err instanceof Error) return err.message;
|
|
517
|
+
if (typeof err === "string") return err;
|
|
518
|
+
return String(err);
|
|
519
|
+
}
|
|
520
|
+
function isValidIssueState(val) {
|
|
521
|
+
return typeof val === "string" && VALID_ISSUE_STATES.has(val);
|
|
522
|
+
}
|
|
523
|
+
function isValidIssueCategory(val) {
|
|
524
|
+
return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
|
|
525
|
+
}
|
|
526
|
+
function isValidAiFixStatus(val) {
|
|
527
|
+
return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
|
|
528
|
+
}
|
|
529
|
+
function validateIssuesData(parsed) {
|
|
530
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
531
|
+
const obj = parsed;
|
|
532
|
+
if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
|
|
533
|
+
return parsed;
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
function validateMetricsData(parsed) {
|
|
538
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
539
|
+
const obj = parsed;
|
|
540
|
+
if (obj.version === 1 && Array.isArray(obj.endpoints)) {
|
|
541
|
+
return parsed;
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
var init_type_guards = __esm({
|
|
546
|
+
"src/utils/type-guards.ts"() {
|
|
625
547
|
"use strict";
|
|
626
|
-
|
|
548
|
+
init_config();
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// src/utils/log.ts
|
|
553
|
+
function brakitWarn(message) {
|
|
554
|
+
process.stderr.write(`${PREFIX} ${message}
|
|
555
|
+
`);
|
|
556
|
+
}
|
|
557
|
+
function brakitDebug(message) {
|
|
558
|
+
if (process.env.DEBUG_BRAKIT) {
|
|
559
|
+
process.stderr.write(`${PREFIX}:debug ${message}
|
|
560
|
+
`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
var PREFIX;
|
|
564
|
+
var init_log = __esm({
|
|
565
|
+
"src/utils/log.ts"() {
|
|
566
|
+
"use strict";
|
|
567
|
+
PREFIX = "[brakit]";
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// src/instrument/adapters/shared.ts
|
|
572
|
+
import { createRequire } from "module";
|
|
573
|
+
function tryRequire(id) {
|
|
574
|
+
try {
|
|
575
|
+
return appRequire(id);
|
|
576
|
+
} catch {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function getActiveRequestId() {
|
|
581
|
+
return getRequestContext()?.requestId ?? null;
|
|
582
|
+
}
|
|
583
|
+
function getPrototype(lib, className) {
|
|
584
|
+
const defaultExport = lib.default;
|
|
585
|
+
const cls = defaultExport?.[className] ?? lib[className];
|
|
586
|
+
if (!cls || typeof cls !== "function") return null;
|
|
587
|
+
return cls.prototype ?? null;
|
|
588
|
+
}
|
|
589
|
+
function buildQueryEvent(config, sql, op, table, start, requestId, rowCount) {
|
|
590
|
+
return {
|
|
591
|
+
type: "query",
|
|
592
|
+
data: {
|
|
593
|
+
driver: config.driver,
|
|
594
|
+
source: config.driver,
|
|
595
|
+
sql,
|
|
596
|
+
normalizedOp: op,
|
|
597
|
+
table,
|
|
598
|
+
durationMs: Math.round(performance.now() - start),
|
|
599
|
+
rowCount: rowCount ?? void 0,
|
|
600
|
+
parentRequestId: requestId,
|
|
601
|
+
timestamp: performance.now()
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function wrapQueryMethod(original, emit, config) {
|
|
606
|
+
return function(...args) {
|
|
607
|
+
const sql = config.extractSql(args);
|
|
608
|
+
const start = performance.now();
|
|
609
|
+
const requestId = getActiveRequestId();
|
|
610
|
+
const { op, table } = normalizeSQL(sql ?? "");
|
|
611
|
+
const emitQuery = (result2) => {
|
|
612
|
+
const rowCount = config.extractRowCount?.(result2);
|
|
613
|
+
emit(buildQueryEvent(config, sql, op, table, start, requestId, rowCount));
|
|
614
|
+
};
|
|
615
|
+
const lastIdx = args.length - 1;
|
|
616
|
+
if (lastIdx >= 0 && typeof args[lastIdx] === "function") {
|
|
617
|
+
const originalCallback = args[lastIdx];
|
|
618
|
+
args[lastIdx] = function(...callbackArgs) {
|
|
619
|
+
emitQuery(callbackArgs[1]);
|
|
620
|
+
return originalCallback.apply(this, callbackArgs);
|
|
621
|
+
};
|
|
622
|
+
return original.apply(this, args);
|
|
623
|
+
}
|
|
624
|
+
const result = original.apply(this, args);
|
|
625
|
+
if (isThenable(result)) {
|
|
626
|
+
return result.then((res) => {
|
|
627
|
+
try {
|
|
628
|
+
emitQuery(res);
|
|
629
|
+
} catch (e) {
|
|
630
|
+
brakitDebug(`query telemetry: ${getErrorMessage(e)}`);
|
|
631
|
+
}
|
|
632
|
+
return res;
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (config.supportsEventEmitter && result && typeof result.on === "function") {
|
|
636
|
+
result.on(
|
|
637
|
+
"end",
|
|
638
|
+
(res) => emitQuery(res)
|
|
639
|
+
);
|
|
640
|
+
return result;
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
var appRequire;
|
|
646
|
+
var init_shared = __esm({
|
|
647
|
+
"src/instrument/adapters/shared.ts"() {
|
|
648
|
+
"use strict";
|
|
649
|
+
init_context();
|
|
627
650
|
init_normalize();
|
|
651
|
+
init_type_guards();
|
|
652
|
+
init_log();
|
|
653
|
+
appRequire = createRequire(process.cwd() + "/index.js");
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// src/instrument/adapters/pg.ts
|
|
658
|
+
var origQuery, proto, pgConfig, pgAdapter;
|
|
659
|
+
var init_pg = __esm({
|
|
660
|
+
"src/instrument/adapters/pg.ts"() {
|
|
661
|
+
"use strict";
|
|
662
|
+
init_shared();
|
|
628
663
|
origQuery = null;
|
|
629
664
|
proto = null;
|
|
665
|
+
pgConfig = {
|
|
666
|
+
driver: "pg",
|
|
667
|
+
extractSql: (args) => {
|
|
668
|
+
const q = args[0];
|
|
669
|
+
if (typeof q === "string") return q;
|
|
670
|
+
if (typeof q === "object" && q !== null && "text" in q) return q.text;
|
|
671
|
+
return void 0;
|
|
672
|
+
},
|
|
673
|
+
extractRowCount: (result) => result?.rowCount,
|
|
674
|
+
supportsEventEmitter: true
|
|
675
|
+
};
|
|
630
676
|
pgAdapter = {
|
|
631
677
|
name: "pg",
|
|
632
678
|
detect() {
|
|
633
679
|
return tryRequire("pg") !== null;
|
|
634
680
|
},
|
|
681
|
+
/** Monkeypatches pg's Client prototype to intercept database queries and emit telemetry events. */
|
|
635
682
|
patch(emit) {
|
|
636
683
|
const pg = tryRequire("pg");
|
|
637
684
|
if (!pg) return;
|
|
638
|
-
|
|
639
|
-
if (!Client || typeof Client !== "function") return;
|
|
640
|
-
proto = Client.prototype ?? null;
|
|
685
|
+
proto = getPrototype(pg, "Client");
|
|
641
686
|
if (!proto?.query) return;
|
|
642
687
|
origQuery = proto.query;
|
|
643
|
-
|
|
644
|
-
proto.query = function(...args) {
|
|
645
|
-
const first = args[0];
|
|
646
|
-
const sql = typeof first === "string" ? first : typeof first === "object" && first !== null && "text" in first ? first.text : void 0;
|
|
647
|
-
const start = performance.now();
|
|
648
|
-
const requestId = captureRequestId();
|
|
649
|
-
const { op, table } = normalizeSQL(sql ?? "");
|
|
650
|
-
const emitQuery = (rowCount) => {
|
|
651
|
-
emit({
|
|
652
|
-
type: "query",
|
|
653
|
-
data: {
|
|
654
|
-
driver: "pg",
|
|
655
|
-
source: "pg",
|
|
656
|
-
sql,
|
|
657
|
-
normalizedOp: op,
|
|
658
|
-
table,
|
|
659
|
-
durationMs: Math.round(performance.now() - start),
|
|
660
|
-
rowCount: rowCount ?? void 0,
|
|
661
|
-
parentRequestId: requestId,
|
|
662
|
-
timestamp: Date.now()
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
};
|
|
666
|
-
const lastIdx = args.length - 1;
|
|
667
|
-
if (lastIdx >= 0 && typeof args[lastIdx] === "function") {
|
|
668
|
-
const origCb = args[lastIdx];
|
|
669
|
-
args[lastIdx] = function(err, res) {
|
|
670
|
-
emitQuery(res?.rowCount ?? void 0);
|
|
671
|
-
return origCb.call(this, err, res);
|
|
672
|
-
};
|
|
673
|
-
return saved.apply(this, args);
|
|
674
|
-
}
|
|
675
|
-
const result = saved.apply(this, args);
|
|
676
|
-
if (result && typeof result.then === "function") {
|
|
677
|
-
return result.then((res) => {
|
|
678
|
-
try {
|
|
679
|
-
emitQuery(res?.rowCount ?? void 0);
|
|
680
|
-
} catch {
|
|
681
|
-
}
|
|
682
|
-
return res;
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
if (result && typeof result.on === "function") {
|
|
686
|
-
result.on("end", (res) => emitQuery(res?.rowCount ?? void 0));
|
|
687
|
-
return result;
|
|
688
|
-
}
|
|
689
|
-
return result;
|
|
690
|
-
};
|
|
688
|
+
proto.query = wrapQueryMethod(origQuery, emit, pgConfig);
|
|
691
689
|
},
|
|
692
690
|
unpatch() {
|
|
693
691
|
if (proto && origQuery) {
|
|
@@ -701,70 +699,34 @@ var init_pg = __esm({
|
|
|
701
699
|
});
|
|
702
700
|
|
|
703
701
|
// src/instrument/adapters/mysql2.ts
|
|
704
|
-
var originals2, proto2, mysql2Adapter;
|
|
702
|
+
var PATCHED_METHODS, originals2, proto2, mysql2Config, mysql2Adapter;
|
|
705
703
|
var init_mysql2 = __esm({
|
|
706
704
|
"src/instrument/adapters/mysql2.ts"() {
|
|
707
705
|
"use strict";
|
|
708
706
|
init_shared();
|
|
709
|
-
|
|
707
|
+
PATCHED_METHODS = ["query", "execute"];
|
|
710
708
|
originals2 = /* @__PURE__ */ new Map();
|
|
711
709
|
proto2 = null;
|
|
710
|
+
mysql2Config = {
|
|
711
|
+
driver: "mysql2",
|
|
712
|
+
extractSql: (args) => typeof args[0] === "string" ? args[0] : void 0
|
|
713
|
+
};
|
|
712
714
|
mysql2Adapter = {
|
|
713
715
|
name: "mysql2",
|
|
714
716
|
detect() {
|
|
715
717
|
return tryRequire("mysql2") !== null;
|
|
716
718
|
},
|
|
719
|
+
/** Monkeypatches mysql2's Connection prototype to intercept database queries and emit telemetry events. */
|
|
717
720
|
patch(emit) {
|
|
718
721
|
const mysql2 = tryRequire("mysql2");
|
|
719
722
|
if (!mysql2) return;
|
|
720
|
-
proto2 = mysql2
|
|
723
|
+
proto2 = getPrototype(mysql2, "Connection");
|
|
721
724
|
if (!proto2) return;
|
|
722
|
-
for (const method of
|
|
725
|
+
for (const method of PATCHED_METHODS) {
|
|
723
726
|
const orig = proto2[method];
|
|
724
727
|
if (typeof orig !== "function") continue;
|
|
725
728
|
originals2.set(method, orig);
|
|
726
|
-
proto2[method] =
|
|
727
|
-
const first = args[0];
|
|
728
|
-
const sql = typeof first === "string" ? first : void 0;
|
|
729
|
-
const start = performance.now();
|
|
730
|
-
const requestId = captureRequestId();
|
|
731
|
-
const { op, table } = normalizeSQL(sql ?? "");
|
|
732
|
-
const emitQuery = () => {
|
|
733
|
-
emit({
|
|
734
|
-
type: "query",
|
|
735
|
-
data: {
|
|
736
|
-
driver: "mysql2",
|
|
737
|
-
source: "mysql2",
|
|
738
|
-
sql,
|
|
739
|
-
normalizedOp: op,
|
|
740
|
-
table,
|
|
741
|
-
durationMs: Math.round(performance.now() - start),
|
|
742
|
-
parentRequestId: requestId,
|
|
743
|
-
timestamp: Date.now()
|
|
744
|
-
}
|
|
745
|
-
});
|
|
746
|
-
};
|
|
747
|
-
const lastIdx = args.length - 1;
|
|
748
|
-
if (lastIdx >= 0 && typeof args[lastIdx] === "function") {
|
|
749
|
-
const origCb = args[lastIdx];
|
|
750
|
-
args[lastIdx] = function() {
|
|
751
|
-
emitQuery();
|
|
752
|
-
return origCb.apply(this, arguments);
|
|
753
|
-
};
|
|
754
|
-
return orig.apply(this, args);
|
|
755
|
-
}
|
|
756
|
-
const result = orig.apply(this, args);
|
|
757
|
-
if (result && typeof result.then === "function") {
|
|
758
|
-
return result.then((res) => {
|
|
759
|
-
try {
|
|
760
|
-
emitQuery();
|
|
761
|
-
} catch {
|
|
762
|
-
}
|
|
763
|
-
return res;
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
return result;
|
|
767
|
-
};
|
|
729
|
+
proto2[method] = wrapQueryMethod(orig, emit, mysql2Config);
|
|
768
730
|
}
|
|
769
731
|
},
|
|
770
732
|
unpatch() {
|
|
@@ -797,9 +759,8 @@ var init_prisma = __esm({
|
|
|
797
759
|
patch(emit) {
|
|
798
760
|
const prismaModule = tryRequire("@prisma/client");
|
|
799
761
|
if (!prismaModule) return;
|
|
800
|
-
|
|
801
|
-
if (!
|
|
802
|
-
prismaProto = PrismaClient.prototype;
|
|
762
|
+
prismaProto = getPrototype(prismaModule, "PrismaClient");
|
|
763
|
+
if (!prismaProto) return;
|
|
803
764
|
origConnect = prismaProto.$connect;
|
|
804
765
|
if (typeof origConnect !== "function") return;
|
|
805
766
|
const saved = origConnect;
|
|
@@ -815,7 +776,7 @@ var init_prisma = __esm({
|
|
|
815
776
|
args: opArgs,
|
|
816
777
|
query
|
|
817
778
|
}) {
|
|
818
|
-
const requestId =
|
|
779
|
+
const requestId = getActiveRequestId();
|
|
819
780
|
const start = performance.now();
|
|
820
781
|
const result = await query(opArgs);
|
|
821
782
|
emit({
|
|
@@ -829,7 +790,7 @@ var init_prisma = __esm({
|
|
|
829
790
|
table: model,
|
|
830
791
|
durationMs: Math.round(performance.now() - start),
|
|
831
792
|
parentRequestId: requestId,
|
|
832
|
-
timestamp:
|
|
793
|
+
timestamp: performance.now()
|
|
833
794
|
}
|
|
834
795
|
});
|
|
835
796
|
return result;
|
|
@@ -874,24 +835,35 @@ var init_adapters = __esm({
|
|
|
874
835
|
}
|
|
875
836
|
});
|
|
876
837
|
|
|
877
|
-
// src/
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
838
|
+
// src/utils/endpoint.ts
|
|
839
|
+
function isDynamicSegment(segment) {
|
|
840
|
+
return UUID_RE.test(segment) || NUMERIC_ID_RE.test(segment) || HEX_HASH_RE.test(segment) || ALPHA_TOKEN_RE.test(segment);
|
|
841
|
+
}
|
|
842
|
+
function normalizePath(path) {
|
|
843
|
+
const qIdx = path.indexOf("?");
|
|
844
|
+
const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
|
|
845
|
+
return pathname.split("/").map((seg) => seg && isDynamicSegment(seg) ? DYNAMIC_SEGMENT_PLACEHOLDER : seg).join("/");
|
|
846
|
+
}
|
|
847
|
+
function getEndpointKey(method, path) {
|
|
848
|
+
return `${method} ${normalizePath(path)}`;
|
|
849
|
+
}
|
|
850
|
+
function extractEndpointFromDesc(desc) {
|
|
851
|
+
return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
|
|
852
|
+
}
|
|
853
|
+
function stripQueryString(path) {
|
|
854
|
+
const i = path.indexOf("?");
|
|
855
|
+
return i === -1 ? path : path.slice(0, i);
|
|
856
|
+
}
|
|
857
|
+
var UUID_RE, NUMERIC_ID_RE, HEX_HASH_RE, ALPHA_TOKEN_RE, DYNAMIC_SEGMENT_PLACEHOLDER, ENDPOINT_PREFIX_RE;
|
|
858
|
+
var init_endpoint = __esm({
|
|
859
|
+
"src/utils/endpoint.ts"() {
|
|
881
860
|
"use strict";
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
HTTP_INTERNAL_ERROR = 500;
|
|
889
|
-
SECURITY_HEADERS = {
|
|
890
|
-
"x-content-type-options": "nosniff",
|
|
891
|
-
"x-frame-options": "DENY",
|
|
892
|
-
"referrer-policy": "no-referrer",
|
|
893
|
-
"content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
|
|
894
|
-
};
|
|
861
|
+
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
862
|
+
NUMERIC_ID_RE = /^\d+$/;
|
|
863
|
+
HEX_HASH_RE = /^[0-9a-f]{12,}$/i;
|
|
864
|
+
ALPHA_TOKEN_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/;
|
|
865
|
+
DYNAMIC_SEGMENT_PLACEHOLDER = ":id";
|
|
866
|
+
ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
895
867
|
}
|
|
896
868
|
});
|
|
897
869
|
|
|
@@ -915,6 +887,7 @@ var init_http_status = __esm({
|
|
|
915
887
|
function detectCategory(req) {
|
|
916
888
|
const { method, url, statusCode, responseHeaders } = req;
|
|
917
889
|
if (req.isStatic) return "static";
|
|
890
|
+
if (req.isHealthCheck) return "health-check";
|
|
918
891
|
if (statusCode === 307 && (url.includes("__clerk_handshake") || url.includes("__clerk_db_jwt"))) {
|
|
919
892
|
return "auth-handshake";
|
|
920
893
|
}
|
|
@@ -1079,20 +1052,41 @@ var init_label = __esm({
|
|
|
1079
1052
|
});
|
|
1080
1053
|
|
|
1081
1054
|
// src/analysis/transforms.ts
|
|
1082
|
-
function
|
|
1055
|
+
function isDuplicateCandidate(req) {
|
|
1056
|
+
return DUPLICATE_CATEGORIES.has(req.category);
|
|
1057
|
+
}
|
|
1058
|
+
function buildRequestKey(req) {
|
|
1059
|
+
return `${req.method} ${stripQueryString(getEffectivePath(req))}`;
|
|
1060
|
+
}
|
|
1061
|
+
function isStrictModePattern(requests, counts) {
|
|
1062
|
+
if (counts.size === 0 || ![...counts.values()].every((c) => c === 2)) {
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
const firstByKey = /* @__PURE__ */ new Map();
|
|
1066
|
+
for (const req of requests) {
|
|
1067
|
+
if (!isDuplicateCandidate(req)) continue;
|
|
1068
|
+
const key = buildRequestKey(req);
|
|
1069
|
+
const first = firstByKey.get(key);
|
|
1070
|
+
if (!first) {
|
|
1071
|
+
firstByKey.set(key, req);
|
|
1072
|
+
} else if (Math.abs(req.startedAt - first.startedAt) > STRICT_MODE_MAX_GAP_MS) {
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
function flagDuplicateRequests(requests) {
|
|
1083
1079
|
const counts = /* @__PURE__ */ new Map();
|
|
1084
1080
|
for (const req of requests) {
|
|
1085
|
-
if (req
|
|
1086
|
-
|
|
1087
|
-
const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
|
|
1081
|
+
if (!isDuplicateCandidate(req)) continue;
|
|
1082
|
+
const key = buildRequestKey(req);
|
|
1088
1083
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1089
1084
|
}
|
|
1090
|
-
const isStrictMode =
|
|
1085
|
+
const isStrictMode = isStrictModePattern(requests, counts);
|
|
1091
1086
|
const seen = /* @__PURE__ */ new Set();
|
|
1092
1087
|
for (const req of requests) {
|
|
1093
|
-
if (req
|
|
1094
|
-
|
|
1095
|
-
const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
|
|
1088
|
+
if (!isDuplicateCandidate(req)) continue;
|
|
1089
|
+
const key = buildRequestKey(req);
|
|
1096
1090
|
if (seen.has(key)) {
|
|
1097
1091
|
if (isStrictMode) {
|
|
1098
1092
|
req.isStrictModeDupe = true;
|
|
@@ -1104,20 +1098,20 @@ function markDuplicates(requests) {
|
|
|
1104
1098
|
}
|
|
1105
1099
|
}
|
|
1106
1100
|
}
|
|
1107
|
-
function
|
|
1101
|
+
function mergePollingSequences(requests) {
|
|
1108
1102
|
const result = [];
|
|
1109
1103
|
let i = 0;
|
|
1110
1104
|
while (i < requests.length) {
|
|
1111
1105
|
const current = requests[i];
|
|
1112
|
-
const currentEffective = getEffectivePath(current)
|
|
1106
|
+
const currentEffective = stripQueryString(getEffectivePath(current));
|
|
1113
1107
|
if (current.method === "GET" && current.category === "data-fetch") {
|
|
1114
|
-
let
|
|
1115
|
-
while (
|
|
1116
|
-
|
|
1108
|
+
let nextIndex = i + 1;
|
|
1109
|
+
while (nextIndex < requests.length && requests[nextIndex].method === "GET" && stripQueryString(getEffectivePath(requests[nextIndex])) === currentEffective) {
|
|
1110
|
+
nextIndex++;
|
|
1117
1111
|
}
|
|
1118
|
-
const count =
|
|
1112
|
+
const count = nextIndex - i;
|
|
1119
1113
|
if (count >= MIN_POLLING_SEQUENCE) {
|
|
1120
|
-
const last = requests[
|
|
1114
|
+
const last = requests[nextIndex - 1];
|
|
1121
1115
|
const pollingDuration = last.startedAt + last.durationMs - current.startedAt;
|
|
1122
1116
|
const endpointName = prettifyEndpoint(currentEffective);
|
|
1123
1117
|
result.push({
|
|
@@ -1128,7 +1122,7 @@ function collapsePolling(requests) {
|
|
|
1128
1122
|
pollingDurationMs: pollingDuration,
|
|
1129
1123
|
isDuplicate: false
|
|
1130
1124
|
});
|
|
1131
|
-
i =
|
|
1125
|
+
i = nextIndex;
|
|
1132
1126
|
continue;
|
|
1133
1127
|
}
|
|
1134
1128
|
}
|
|
@@ -1141,18 +1135,18 @@ function formatDurationLabel(ms) {
|
|
|
1141
1135
|
if (ms < 1e3) return `${ms}ms`;
|
|
1142
1136
|
return `${(ms / 1e3).toFixed(1)}s`;
|
|
1143
1137
|
}
|
|
1144
|
-
function
|
|
1138
|
+
function collectRequestWarnings(requests) {
|
|
1145
1139
|
const warnings = [];
|
|
1146
1140
|
const duplicateCount = requests.filter((r) => r.isDuplicate).length;
|
|
1147
1141
|
if (duplicateCount > 0) {
|
|
1148
1142
|
const unique = new Set(
|
|
1149
|
-
requests.filter((r) => r.isDuplicate).map((r) =>
|
|
1143
|
+
requests.filter((r) => r.isDuplicate).map((r) => buildRequestKey(r))
|
|
1150
1144
|
);
|
|
1151
1145
|
const endpoints = unique.size;
|
|
1152
1146
|
const sameData = requests.filter((r) => r.isDuplicate).every((r) => {
|
|
1153
|
-
const key =
|
|
1147
|
+
const key = buildRequestKey(r);
|
|
1154
1148
|
const first = requests.find(
|
|
1155
|
-
(o) => !o.isDuplicate &&
|
|
1149
|
+
(o) => !o.isDuplicate && buildRequestKey(o) === key
|
|
1156
1150
|
);
|
|
1157
1151
|
return first && first.responseBody === r.responseBody;
|
|
1158
1152
|
});
|
|
@@ -1173,18 +1167,30 @@ function detectWarnings(requests) {
|
|
|
1173
1167
|
}
|
|
1174
1168
|
return warnings;
|
|
1175
1169
|
}
|
|
1170
|
+
var DUPLICATE_CATEGORIES;
|
|
1176
1171
|
var init_transforms = __esm({
|
|
1177
1172
|
"src/analysis/transforms.ts"() {
|
|
1178
1173
|
"use strict";
|
|
1179
1174
|
init_constants();
|
|
1175
|
+
init_config();
|
|
1180
1176
|
init_categorize();
|
|
1181
1177
|
init_label();
|
|
1182
1178
|
init_http_status();
|
|
1179
|
+
init_endpoint();
|
|
1180
|
+
DUPLICATE_CATEGORIES = /* @__PURE__ */ new Set(["data-fetch", "auth-check"]);
|
|
1183
1181
|
}
|
|
1184
1182
|
});
|
|
1185
1183
|
|
|
1186
1184
|
// src/analysis/group.ts
|
|
1187
1185
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1186
|
+
function shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, startedAt) {
|
|
1187
|
+
if (currentRequests.length === 0) return false;
|
|
1188
|
+
const sourcePage = labeled.sourcePage;
|
|
1189
|
+
const isNewPage = sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
|
|
1190
|
+
const isTimeGap = startedAt - lastEndTime > FLOW_GAP_MS;
|
|
1191
|
+
const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
|
|
1192
|
+
return isNewPage || isTimeGap || isPageLoad;
|
|
1193
|
+
}
|
|
1188
1194
|
function groupRequestsIntoFlows(requests) {
|
|
1189
1195
|
if (requests.length === 0) return [];
|
|
1190
1196
|
const flows = [];
|
|
@@ -1195,17 +1201,12 @@ function groupRequestsIntoFlows(requests) {
|
|
|
1195
1201
|
if (req.path.startsWith(DASHBOARD_PREFIX)) continue;
|
|
1196
1202
|
const labeled = labelRequest(req);
|
|
1197
1203
|
if (labeled.category === "static") continue;
|
|
1198
|
-
|
|
1199
|
-
const gap = currentRequests.length > 0 ? req.startedAt - lastEndTime : 0;
|
|
1200
|
-
const isNewPage = currentRequests.length > 0 && sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
|
|
1201
|
-
const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
|
|
1202
|
-
const isTimeGap = currentRequests.length > 0 && gap > FLOW_GAP_MS;
|
|
1203
|
-
if (currentRequests.length > 0 && (isNewPage || isTimeGap || isPageLoad)) {
|
|
1204
|
+
if (shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, req.startedAt)) {
|
|
1204
1205
|
flows.push(buildFlow(currentRequests));
|
|
1205
1206
|
currentRequests = [];
|
|
1206
1207
|
}
|
|
1207
1208
|
currentRequests.push(labeled);
|
|
1208
|
-
currentSourcePage = sourcePage ?? currentSourcePage;
|
|
1209
|
+
currentSourcePage = labeled.sourcePage ?? currentSourcePage;
|
|
1209
1210
|
lastEndTime = Math.max(lastEndTime, req.startedAt + req.durationMs);
|
|
1210
1211
|
}
|
|
1211
1212
|
if (currentRequests.length > 0) {
|
|
@@ -1214,8 +1215,8 @@ function groupRequestsIntoFlows(requests) {
|
|
|
1214
1215
|
return flows;
|
|
1215
1216
|
}
|
|
1216
1217
|
function buildFlow(rawRequests) {
|
|
1217
|
-
|
|
1218
|
-
const requests =
|
|
1218
|
+
flagDuplicateRequests(rawRequests);
|
|
1219
|
+
const requests = mergePollingSequences(rawRequests);
|
|
1219
1220
|
const first = requests[0];
|
|
1220
1221
|
const startTime = first.startedAt;
|
|
1221
1222
|
const endTime = Math.max(
|
|
@@ -1234,7 +1235,7 @@ function buildFlow(rawRequests) {
|
|
|
1234
1235
|
startTime,
|
|
1235
1236
|
totalDurationMs: Math.round(endTime - startTime),
|
|
1236
1237
|
hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
|
|
1237
|
-
warnings:
|
|
1238
|
+
warnings: collectRequestWarnings(rawRequests),
|
|
1238
1239
|
sourcePage,
|
|
1239
1240
|
redundancyPct
|
|
1240
1241
|
};
|
|
@@ -1246,20 +1247,20 @@ function getDominantSourcePage(requests) {
|
|
|
1246
1247
|
counts.set(req.sourcePage, (counts.get(req.sourcePage) ?? 0) + 1);
|
|
1247
1248
|
}
|
|
1248
1249
|
}
|
|
1249
|
-
let
|
|
1250
|
-
let
|
|
1250
|
+
let mostCommonPage = "";
|
|
1251
|
+
let highestCount = 0;
|
|
1251
1252
|
for (const [page, count] of counts) {
|
|
1252
|
-
if (count >
|
|
1253
|
-
|
|
1254
|
-
|
|
1253
|
+
if (count > highestCount) {
|
|
1254
|
+
mostCommonPage = page;
|
|
1255
|
+
highestCount = count;
|
|
1255
1256
|
}
|
|
1256
1257
|
}
|
|
1257
|
-
return
|
|
1258
|
+
return mostCommonPage || (requests[0]?.path ? stripQueryString(requests[0].path) : "") || "/";
|
|
1258
1259
|
}
|
|
1259
1260
|
function deriveFlowLabel(requests, sourcePage) {
|
|
1260
1261
|
const trigger = requests.find((r) => r.category === "api-call") ?? requests.find((r) => r.category === "server-action") ?? requests.find((r) => r.category === "page-load") ?? requests.find((r) => r.category === "navigation") ?? requests.find((r) => r.category === "data-fetch") ?? requests[0];
|
|
1261
1262
|
if (trigger.category === "page-load" || trigger.category === "navigation") {
|
|
1262
|
-
const pageName = prettifyPageName(trigger.path
|
|
1263
|
+
const pageName = prettifyPageName(stripQueryString(trigger.path));
|
|
1263
1264
|
return `${pageName} Page`;
|
|
1264
1265
|
}
|
|
1265
1266
|
if (trigger.category === "api-call") {
|
|
@@ -1288,6 +1289,7 @@ var init_group = __esm({
|
|
|
1288
1289
|
"use strict";
|
|
1289
1290
|
init_constants();
|
|
1290
1291
|
init_http_status();
|
|
1292
|
+
init_endpoint();
|
|
1291
1293
|
init_label();
|
|
1292
1294
|
init_categorize();
|
|
1293
1295
|
init_transforms();
|
|
@@ -1385,12 +1387,28 @@ var init_shared2 = __esm({
|
|
|
1385
1387
|
"src/dashboard/api/shared.ts"() {
|
|
1386
1388
|
"use strict";
|
|
1387
1389
|
init_constants();
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
+
init_config();
|
|
1391
|
+
init_labels();
|
|
1390
1392
|
}
|
|
1391
1393
|
});
|
|
1392
1394
|
|
|
1393
1395
|
// src/dashboard/api/handlers.ts
|
|
1396
|
+
function filterByStatusRange(requests, statusStr) {
|
|
1397
|
+
if (statusStr.endsWith("xx")) {
|
|
1398
|
+
const prefix = parseInt(statusStr[0], 10);
|
|
1399
|
+
return requests.filter(
|
|
1400
|
+
(r) => Math.floor(r.statusCode / 100) === prefix
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
const code = parseInt(statusStr, 10);
|
|
1404
|
+
return requests.filter((r) => r.statusCode === code);
|
|
1405
|
+
}
|
|
1406
|
+
function filterBySearch(requests, searchQuery) {
|
|
1407
|
+
const lower = searchQuery.toLowerCase();
|
|
1408
|
+
return requests.filter(
|
|
1409
|
+
(r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1394
1412
|
function sanitizeRequest(r) {
|
|
1395
1413
|
return {
|
|
1396
1414
|
...r,
|
|
@@ -1398,7 +1416,7 @@ function sanitizeRequest(r) {
|
|
|
1398
1416
|
responseHeaders: maskSensitiveHeaders(r.responseHeaders)
|
|
1399
1417
|
};
|
|
1400
1418
|
}
|
|
1401
|
-
function createRequestsHandler(
|
|
1419
|
+
function createRequestsHandler(services) {
|
|
1402
1420
|
return (req, res) => {
|
|
1403
1421
|
if (!requireGet(req, res)) return;
|
|
1404
1422
|
const url = parseRequestUrl(req);
|
|
@@ -1408,26 +1426,15 @@ function createRequestsHandler(registry) {
|
|
|
1408
1426
|
const rawLimit = parseInt(url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT), 10);
|
|
1409
1427
|
const limit = Math.min(Math.max(rawLimit || DEFAULT_API_LIMIT, 1), MAX_API_LIMIT);
|
|
1410
1428
|
const offset = Math.max(parseInt(url.searchParams.get("offset") ?? "0", 10) || 0, 0);
|
|
1411
|
-
let results = [...
|
|
1429
|
+
let results = [...services.requestStore.getAll()].reverse();
|
|
1412
1430
|
if (method) {
|
|
1413
1431
|
results = results.filter((r) => r.method === method.toUpperCase());
|
|
1414
1432
|
}
|
|
1415
1433
|
if (status) {
|
|
1416
|
-
|
|
1417
|
-
const prefix = parseInt(status[0], 10);
|
|
1418
|
-
results = results.filter(
|
|
1419
|
-
(r) => Math.floor(r.statusCode / 100) === prefix
|
|
1420
|
-
);
|
|
1421
|
-
} else {
|
|
1422
|
-
const code = parseInt(status, 10);
|
|
1423
|
-
results = results.filter((r) => r.statusCode === code);
|
|
1424
|
-
}
|
|
1434
|
+
results = filterByStatusRange(results, status);
|
|
1425
1435
|
}
|
|
1426
1436
|
if (search) {
|
|
1427
|
-
|
|
1428
|
-
results = results.filter(
|
|
1429
|
-
(r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
|
|
1430
|
-
);
|
|
1437
|
+
results = filterBySearch(results, search);
|
|
1431
1438
|
}
|
|
1432
1439
|
const total = results.length;
|
|
1433
1440
|
results = results.slice(offset, offset + limit);
|
|
@@ -1435,96 +1442,84 @@ function createRequestsHandler(registry) {
|
|
|
1435
1442
|
sendJson(req, res, HTTP_OK, { total, requests: sanitized });
|
|
1436
1443
|
};
|
|
1437
1444
|
}
|
|
1438
|
-
function createFlowsHandler(
|
|
1445
|
+
function createFlowsHandler(services) {
|
|
1439
1446
|
return (req, res) => {
|
|
1440
1447
|
if (!requireGet(req, res)) return;
|
|
1441
|
-
const flows = groupRequestsIntoFlows(
|
|
1448
|
+
const flows = groupRequestsIntoFlows(services.requestStore.getAll()).reverse().map((flow) => ({
|
|
1442
1449
|
...flow,
|
|
1443
1450
|
requests: flow.requests.map(sanitizeRequest)
|
|
1444
1451
|
}));
|
|
1445
1452
|
sendJson(req, res, HTTP_OK, { total: flows.length, flows });
|
|
1446
1453
|
};
|
|
1447
1454
|
}
|
|
1448
|
-
function createClearHandler(
|
|
1455
|
+
function createClearHandler(services) {
|
|
1449
1456
|
return (req, res) => {
|
|
1450
1457
|
if (req.method !== "POST") {
|
|
1451
1458
|
sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
|
|
1452
1459
|
return;
|
|
1453
1460
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1461
|
+
services.requestStore.clear();
|
|
1462
|
+
services.fetchStore.clear();
|
|
1463
|
+
services.logStore.clear();
|
|
1464
|
+
services.errorStore.clear();
|
|
1465
|
+
services.queryStore.clear();
|
|
1466
|
+
services.metricsStore.reset();
|
|
1467
|
+
services.issueStore.clear();
|
|
1468
|
+
services.bus.emit("store:cleared", void 0);
|
|
1462
1469
|
sendJson(req, res, HTTP_OK, { cleared: true });
|
|
1463
1470
|
};
|
|
1464
1471
|
}
|
|
1465
|
-
function createFetchesHandler(
|
|
1466
|
-
return (req, res) => handleTelemetryGet(req, res,
|
|
1472
|
+
function createFetchesHandler(services) {
|
|
1473
|
+
return (req, res) => handleTelemetryGet(req, res, services.fetchStore);
|
|
1467
1474
|
}
|
|
1468
|
-
function createLogsHandler(
|
|
1469
|
-
return (req, res) => handleTelemetryGet(req, res,
|
|
1475
|
+
function createLogsHandler(services) {
|
|
1476
|
+
return (req, res) => handleTelemetryGet(req, res, services.logStore);
|
|
1470
1477
|
}
|
|
1471
|
-
function createErrorsHandler(
|
|
1472
|
-
return (req, res) => handleTelemetryGet(req, res,
|
|
1478
|
+
function createErrorsHandler(services) {
|
|
1479
|
+
return (req, res) => handleTelemetryGet(req, res, services.errorStore);
|
|
1473
1480
|
}
|
|
1474
|
-
function createQueriesHandler(
|
|
1475
|
-
return (req, res) => handleTelemetryGet(req, res,
|
|
1481
|
+
function createQueriesHandler(services) {
|
|
1482
|
+
return (req, res) => handleTelemetryGet(req, res, services.queryStore);
|
|
1476
1483
|
}
|
|
1477
1484
|
var init_handlers = __esm({
|
|
1478
1485
|
"src/dashboard/api/handlers.ts"() {
|
|
1479
1486
|
"use strict";
|
|
1480
1487
|
init_group();
|
|
1481
1488
|
init_constants();
|
|
1482
|
-
|
|
1489
|
+
init_labels();
|
|
1483
1490
|
init_shared2();
|
|
1484
1491
|
}
|
|
1485
1492
|
});
|
|
1486
1493
|
|
|
1487
|
-
// src/utils/
|
|
1488
|
-
function
|
|
1489
|
-
return
|
|
1490
|
-
}
|
|
1491
|
-
function isNumber(val) {
|
|
1492
|
-
return typeof val === "number" && !isNaN(val);
|
|
1493
|
-
}
|
|
1494
|
-
function isBoolean(val) {
|
|
1495
|
-
return typeof val === "boolean";
|
|
1496
|
-
}
|
|
1497
|
-
function getErrorMessage(err) {
|
|
1498
|
-
if (err instanceof Error) return err.message;
|
|
1499
|
-
if (typeof err === "string") return err;
|
|
1500
|
-
return String(err);
|
|
1501
|
-
}
|
|
1502
|
-
function isValidIssueState(val) {
|
|
1503
|
-
return typeof val === "string" && VALID_ISSUE_STATES.has(val);
|
|
1504
|
-
}
|
|
1505
|
-
function isValidIssueCategory(val) {
|
|
1506
|
-
return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
|
|
1507
|
-
}
|
|
1508
|
-
function isValidAiFixStatus(val) {
|
|
1509
|
-
return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
|
|
1510
|
-
}
|
|
1511
|
-
function validateIssuesData(parsed) {
|
|
1512
|
-
if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === ISSUES_DATA_VERSION && Array.isArray(parsed.issues)) {
|
|
1513
|
-
return parsed;
|
|
1514
|
-
}
|
|
1515
|
-
return null;
|
|
1494
|
+
// src/utils/static-patterns.ts
|
|
1495
|
+
function isStaticPath(urlPath) {
|
|
1496
|
+
return STATIC_PATTERNS.some((p) => p.test(urlPath));
|
|
1516
1497
|
}
|
|
1517
|
-
function
|
|
1518
|
-
|
|
1519
|
-
return parsed;
|
|
1520
|
-
}
|
|
1521
|
-
return null;
|
|
1498
|
+
function isHealthCheckPath(urlPath) {
|
|
1499
|
+
return HEALTH_CHECK_PATTERNS.some((p) => p.test(urlPath));
|
|
1522
1500
|
}
|
|
1523
|
-
var
|
|
1524
|
-
|
|
1501
|
+
var STATIC_PATTERNS, HEALTH_CHECK_PATTERNS;
|
|
1502
|
+
var init_static_patterns = __esm({
|
|
1503
|
+
"src/utils/static-patterns.ts"() {
|
|
1525
1504
|
"use strict";
|
|
1526
|
-
|
|
1527
|
-
|
|
1505
|
+
STATIC_PATTERNS = [
|
|
1506
|
+
/\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
|
|
1507
|
+
/^\/favicon/,
|
|
1508
|
+
/^\/node_modules\//,
|
|
1509
|
+
// Framework-specific static/internal paths
|
|
1510
|
+
/^\/_next\//,
|
|
1511
|
+
/^\/__nextjs/,
|
|
1512
|
+
/^\/@vite\//,
|
|
1513
|
+
/^\/__vite/
|
|
1514
|
+
];
|
|
1515
|
+
HEALTH_CHECK_PATTERNS = [
|
|
1516
|
+
/^\/health(z|check)?$/i,
|
|
1517
|
+
/^\/ping$/i,
|
|
1518
|
+
/^\/(ready|readiness|liveness)$/i,
|
|
1519
|
+
/^\/status$/i,
|
|
1520
|
+
/^\/__health$/i,
|
|
1521
|
+
/^\/api\/health(z|check)?$/i
|
|
1522
|
+
];
|
|
1528
1523
|
}
|
|
1529
1524
|
});
|
|
1530
1525
|
|
|
@@ -1545,8 +1540,8 @@ function numOrUndef(val) {
|
|
|
1545
1540
|
function headers(val) {
|
|
1546
1541
|
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1547
1542
|
const result = {};
|
|
1548
|
-
for (const [
|
|
1549
|
-
if (typeof
|
|
1543
|
+
for (const [key, value] of Object.entries(val)) {
|
|
1544
|
+
if (typeof value === "string") result[key] = value;
|
|
1550
1545
|
}
|
|
1551
1546
|
return result;
|
|
1552
1547
|
}
|
|
@@ -1610,7 +1605,7 @@ function parseRequestEvent(data, ts) {
|
|
|
1610
1605
|
id: str(data.id, randomUUID4()),
|
|
1611
1606
|
method: str(data.method, "GET"),
|
|
1612
1607
|
url,
|
|
1613
|
-
path: url
|
|
1608
|
+
path: stripQueryString(url),
|
|
1614
1609
|
headers: headers(data.headers),
|
|
1615
1610
|
requestBody: isString(data.requestBody) ? data.requestBody : null,
|
|
1616
1611
|
statusCode: num(data.statusCode, 200),
|
|
@@ -1619,7 +1614,8 @@ function parseRequestEvent(data, ts) {
|
|
|
1619
1614
|
startedAt: ts,
|
|
1620
1615
|
durationMs: num(data.durationMs, 0),
|
|
1621
1616
|
responseSize: num(data.responseSize, 0),
|
|
1622
|
-
isStatic: isBoolean(data.isStatic) ? data.isStatic : false
|
|
1617
|
+
isStatic: isBoolean(data.isStatic) ? data.isStatic : false,
|
|
1618
|
+
isHealthCheck: isBoolean(data.isHealthCheck) ? data.isHealthCheck : isHealthCheckPath(stripQueryString(url))
|
|
1623
1619
|
};
|
|
1624
1620
|
}
|
|
1625
1621
|
function routeSDKEvent(event, stores) {
|
|
@@ -1653,7 +1649,9 @@ var init_sdk_event_parser = __esm({
|
|
|
1653
1649
|
"src/dashboard/api/sdk-event-parser.ts"() {
|
|
1654
1650
|
"use strict";
|
|
1655
1651
|
init_type_guards();
|
|
1656
|
-
|
|
1652
|
+
init_labels();
|
|
1653
|
+
init_static_patterns();
|
|
1654
|
+
init_endpoint();
|
|
1657
1655
|
LOG_LEVEL_MAP = {
|
|
1658
1656
|
debug: "debug",
|
|
1659
1657
|
info: "info",
|
|
@@ -1673,28 +1671,28 @@ function isBrakitBatch(msg) {
|
|
|
1673
1671
|
function isSDKPayload(msg) {
|
|
1674
1672
|
return typeof msg === "object" && msg !== null && "_brakit" in msg && "version" in msg && typeof msg.version === "number";
|
|
1675
1673
|
}
|
|
1676
|
-
function createIngestHandler(
|
|
1674
|
+
function createIngestHandler(services) {
|
|
1677
1675
|
const routeEvent = (event) => {
|
|
1678
1676
|
switch (event.type) {
|
|
1679
1677
|
case TIMELINE_FETCH:
|
|
1680
|
-
|
|
1678
|
+
services.fetchStore.add(event.data);
|
|
1681
1679
|
break;
|
|
1682
1680
|
case TIMELINE_LOG:
|
|
1683
|
-
|
|
1681
|
+
services.logStore.add(event.data);
|
|
1684
1682
|
break;
|
|
1685
1683
|
case TIMELINE_ERROR:
|
|
1686
|
-
|
|
1684
|
+
services.errorStore.add(event.data);
|
|
1687
1685
|
break;
|
|
1688
1686
|
case TIMELINE_QUERY:
|
|
1689
|
-
|
|
1687
|
+
services.queryStore.add(event.data);
|
|
1690
1688
|
break;
|
|
1691
1689
|
}
|
|
1692
1690
|
};
|
|
1693
|
-
const queryStore =
|
|
1694
|
-
const fetchStore =
|
|
1695
|
-
const logStore =
|
|
1696
|
-
const errorStore =
|
|
1697
|
-
const requestStore =
|
|
1691
|
+
const queryStore = services.queryStore;
|
|
1692
|
+
const fetchStore = services.fetchStore;
|
|
1693
|
+
const logStore = services.logStore;
|
|
1694
|
+
const errorStore = services.errorStore;
|
|
1695
|
+
const requestStore = services.requestStore;
|
|
1698
1696
|
const stores = {
|
|
1699
1697
|
addQuery: (data) => queryStore.add(data),
|
|
1700
1698
|
addFetch: (data) => fetchStore.add(data),
|
|
@@ -1754,9 +1752,8 @@ function createIngestHandler(registry) {
|
|
|
1754
1752
|
var init_ingest = __esm({
|
|
1755
1753
|
"src/dashboard/api/ingest.ts"() {
|
|
1756
1754
|
"use strict";
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
init_timeline();
|
|
1755
|
+
init_config();
|
|
1756
|
+
init_labels();
|
|
1760
1757
|
init_shared2();
|
|
1761
1758
|
init_sdk_event_parser();
|
|
1762
1759
|
}
|
|
@@ -1776,11 +1773,11 @@ function createMetricsHandler(metricsStore) {
|
|
|
1776
1773
|
sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
|
|
1777
1774
|
};
|
|
1778
1775
|
}
|
|
1779
|
-
var
|
|
1776
|
+
var init_metrics = __esm({
|
|
1780
1777
|
"src/dashboard/api/metrics.ts"() {
|
|
1781
1778
|
"use strict";
|
|
1782
1779
|
init_shared2();
|
|
1783
|
-
|
|
1780
|
+
init_labels();
|
|
1784
1781
|
}
|
|
1785
1782
|
});
|
|
1786
1783
|
|
|
@@ -1798,61 +1795,55 @@ var init_metrics_live = __esm({
|
|
|
1798
1795
|
}
|
|
1799
1796
|
});
|
|
1800
1797
|
|
|
1801
|
-
// src/utils/log.ts
|
|
1802
|
-
function brakitWarn(message) {
|
|
1803
|
-
process.stderr.write(`${PREFIX} ${message}
|
|
1804
|
-
`);
|
|
1805
|
-
}
|
|
1806
|
-
function brakitDebug(message) {
|
|
1807
|
-
if (process.env.DEBUG_BRAKIT) {
|
|
1808
|
-
process.stderr.write(`${PREFIX}:debug ${message}
|
|
1809
|
-
`);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
var PREFIX;
|
|
1813
|
-
var init_log = __esm({
|
|
1814
|
-
"src/utils/log.ts"() {
|
|
1815
|
-
"use strict";
|
|
1816
|
-
PREFIX = "[brakit]";
|
|
1817
|
-
}
|
|
1818
|
-
});
|
|
1819
|
-
|
|
1820
1798
|
// src/dashboard/api/activity.ts
|
|
1821
|
-
function
|
|
1799
|
+
function buildTimeline(services, requestId) {
|
|
1800
|
+
const fetches = services.fetchStore.getByRequest(requestId);
|
|
1801
|
+
const logs = services.logStore.getByRequest(requestId);
|
|
1802
|
+
const errors = services.errorStore.getByRequest(requestId);
|
|
1803
|
+
const queries = services.queryStore.getByRequest(requestId);
|
|
1804
|
+
const timeline = [];
|
|
1805
|
+
for (const fetch of fetches)
|
|
1806
|
+
timeline.push({ type: TIMELINE_FETCH, timestamp: fetch.timestamp, data: fetch });
|
|
1807
|
+
for (const log of logs)
|
|
1808
|
+
timeline.push({ type: TIMELINE_LOG, timestamp: log.timestamp, data: log });
|
|
1809
|
+
for (const error of errors)
|
|
1810
|
+
timeline.push({ type: TIMELINE_ERROR, timestamp: error.timestamp, data: error });
|
|
1811
|
+
for (const query of queries)
|
|
1812
|
+
timeline.push({ type: TIMELINE_QUERY, timestamp: query.timestamp, data: query });
|
|
1813
|
+
timeline.sort((a, b) => a.timestamp - b.timestamp);
|
|
1814
|
+
return {
|
|
1815
|
+
total: timeline.length,
|
|
1816
|
+
timeline,
|
|
1817
|
+
counts: {
|
|
1818
|
+
fetches: fetches.length,
|
|
1819
|
+
logs: logs.length,
|
|
1820
|
+
errors: errors.length,
|
|
1821
|
+
queries: queries.length
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
function createActivityHandler(services) {
|
|
1822
1826
|
return (req, res) => {
|
|
1823
1827
|
if (!requireGet(req, res)) return;
|
|
1824
1828
|
try {
|
|
1825
1829
|
const url = parseRequestUrl(req);
|
|
1826
1830
|
const requestId = url.searchParams.get("requestId");
|
|
1827
|
-
|
|
1828
|
-
|
|
1831
|
+
const requestIds = url.searchParams.get("requestIds");
|
|
1832
|
+
if (!requestId && !requestIds) {
|
|
1833
|
+
sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId or requestIds parameter required" });
|
|
1829
1834
|
return;
|
|
1830
1835
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
for (const
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
for (const q of queries)
|
|
1843
|
-
timeline.push({ type: TIMELINE_QUERY, timestamp: q.timestamp, data: q });
|
|
1844
|
-
timeline.sort((a, b) => a.timestamp - b.timestamp);
|
|
1845
|
-
sendJson(req, res, HTTP_OK, {
|
|
1846
|
-
requestId,
|
|
1847
|
-
total: timeline.length,
|
|
1848
|
-
timeline,
|
|
1849
|
-
counts: {
|
|
1850
|
-
fetches: fetches.length,
|
|
1851
|
-
logs: logs.length,
|
|
1852
|
-
errors: errors.length,
|
|
1853
|
-
queries: queries.length
|
|
1854
|
-
}
|
|
1855
|
-
});
|
|
1836
|
+
if (requestId) {
|
|
1837
|
+
const result = buildTimeline(services, requestId);
|
|
1838
|
+
sendJson(req, res, HTTP_OK, { requestId, ...result });
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const ids = (requestIds || "").split(",").filter(Boolean).slice(0, MAX_BATCH_IDS);
|
|
1842
|
+
const activities = {};
|
|
1843
|
+
for (const id of ids) {
|
|
1844
|
+
activities[id] = buildTimeline(services, id);
|
|
1845
|
+
}
|
|
1846
|
+
sendJson(req, res, HTTP_OK, { requestIds: ids, activities });
|
|
1856
1847
|
} catch (err) {
|
|
1857
1848
|
brakitDebug(`activity handler error: ${err}`);
|
|
1858
1849
|
if (!res.headersSent) {
|
|
@@ -1861,13 +1852,14 @@ function createActivityHandler(registry) {
|
|
|
1861
1852
|
}
|
|
1862
1853
|
};
|
|
1863
1854
|
}
|
|
1855
|
+
var MAX_BATCH_IDS;
|
|
1864
1856
|
var init_activity = __esm({
|
|
1865
1857
|
"src/dashboard/api/activity.ts"() {
|
|
1866
1858
|
"use strict";
|
|
1867
1859
|
init_shared2();
|
|
1868
|
-
|
|
1869
|
-
init_timeline();
|
|
1860
|
+
init_labels();
|
|
1870
1861
|
init_log();
|
|
1862
|
+
MAX_BATCH_IDS = 50;
|
|
1871
1863
|
}
|
|
1872
1864
|
});
|
|
1873
1865
|
|
|
@@ -1877,7 +1869,7 @@ var init_api = __esm({
|
|
|
1877
1869
|
"use strict";
|
|
1878
1870
|
init_handlers();
|
|
1879
1871
|
init_ingest();
|
|
1880
|
-
|
|
1872
|
+
init_metrics();
|
|
1881
1873
|
init_metrics_live();
|
|
1882
1874
|
init_activity();
|
|
1883
1875
|
}
|
|
@@ -1952,25 +1944,12 @@ var init_issues = __esm({
|
|
|
1952
1944
|
"use strict";
|
|
1953
1945
|
init_shared2();
|
|
1954
1946
|
init_type_guards();
|
|
1955
|
-
|
|
1956
|
-
}
|
|
1957
|
-
});
|
|
1958
|
-
|
|
1959
|
-
// src/constants/events.ts
|
|
1960
|
-
var SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES;
|
|
1961
|
-
var init_events = __esm({
|
|
1962
|
-
"src/constants/events.ts"() {
|
|
1963
|
-
"use strict";
|
|
1964
|
-
SSE_EVENT_FETCH = "fetch";
|
|
1965
|
-
SSE_EVENT_LOG = "log";
|
|
1966
|
-
SSE_EVENT_ERROR = "error_event";
|
|
1967
|
-
SSE_EVENT_QUERY = "query";
|
|
1968
|
-
SSE_EVENT_ISSUES = "issues";
|
|
1947
|
+
init_labels();
|
|
1969
1948
|
}
|
|
1970
1949
|
});
|
|
1971
1950
|
|
|
1972
1951
|
// src/dashboard/sse.ts
|
|
1973
|
-
function createSSEHandler(
|
|
1952
|
+
function createSSEHandler(services) {
|
|
1974
1953
|
const clients = /* @__PURE__ */ new Set();
|
|
1975
1954
|
function broadcast(eventType, data) {
|
|
1976
1955
|
if (clients.size === 0) return;
|
|
@@ -1992,7 +1971,7 @@ data: ${data}
|
|
|
1992
1971
|
}
|
|
1993
1972
|
}
|
|
1994
1973
|
}
|
|
1995
|
-
const bus =
|
|
1974
|
+
const bus = services.bus;
|
|
1996
1975
|
bus.on("request:completed", (r) => broadcast(null, JSON.stringify(r)));
|
|
1997
1976
|
bus.on("telemetry:fetch", (e) => broadcast(SSE_EVENT_FETCH, JSON.stringify(e)));
|
|
1998
1977
|
bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
|
|
@@ -2042,8 +2021,7 @@ var init_sse = __esm({
|
|
|
2042
2021
|
"src/dashboard/sse.ts"() {
|
|
2043
2022
|
"use strict";
|
|
2044
2023
|
init_constants();
|
|
2045
|
-
|
|
2046
|
-
init_events();
|
|
2024
|
+
init_labels();
|
|
2047
2025
|
init_shared2();
|
|
2048
2026
|
}
|
|
2049
2027
|
});
|
|
@@ -2098,7 +2076,7 @@ async function ensureGitignoreAsync(dir, entry) {
|
|
|
2098
2076
|
var init_fs = __esm({
|
|
2099
2077
|
"src/utils/fs.ts"() {
|
|
2100
2078
|
"use strict";
|
|
2101
|
-
|
|
2079
|
+
init_config();
|
|
2102
2080
|
init_log();
|
|
2103
2081
|
init_type_guards();
|
|
2104
2082
|
}
|
|
@@ -2187,7 +2165,7 @@ function computeIssueId(issue) {
|
|
|
2187
2165
|
var init_issue_id = __esm({
|
|
2188
2166
|
"src/utils/issue-id.ts"() {
|
|
2189
2167
|
"use strict";
|
|
2190
|
-
|
|
2168
|
+
init_config();
|
|
2191
2169
|
}
|
|
2192
2170
|
});
|
|
2193
2171
|
|
|
@@ -2200,10 +2178,7 @@ var init_issue_store = __esm({
|
|
|
2200
2178
|
"src/store/issue-store.ts"() {
|
|
2201
2179
|
"use strict";
|
|
2202
2180
|
init_fs();
|
|
2203
|
-
|
|
2204
|
-
init_limits();
|
|
2205
|
-
init_thresholds();
|
|
2206
|
-
init_limits();
|
|
2181
|
+
init_config();
|
|
2207
2182
|
init_atomic_writer();
|
|
2208
2183
|
init_log();
|
|
2209
2184
|
init_type_guards();
|
|
@@ -2446,7 +2421,7 @@ function unwrapResponse(parsed) {
|
|
|
2446
2421
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
2447
2422
|
const obj = parsed;
|
|
2448
2423
|
const keys = Object.keys(obj);
|
|
2449
|
-
if (keys.length >
|
|
2424
|
+
if (keys.length > MAX_WRAPPER_KEYS) return parsed;
|
|
2450
2425
|
let best = null;
|
|
2451
2426
|
let bestSize = 0;
|
|
2452
2427
|
for (const key of keys) {
|
|
@@ -2464,10 +2439,12 @@ function unwrapResponse(parsed) {
|
|
|
2464
2439
|
}
|
|
2465
2440
|
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
2466
2441
|
}
|
|
2442
|
+
var MAX_WRAPPER_KEYS;
|
|
2467
2443
|
var init_response = __esm({
|
|
2468
2444
|
"src/utils/response.ts"() {
|
|
2469
2445
|
"use strict";
|
|
2470
|
-
|
|
2446
|
+
init_config();
|
|
2447
|
+
MAX_WRAPPER_KEYS = 3;
|
|
2471
2448
|
}
|
|
2472
2449
|
});
|
|
2473
2450
|
|
|
@@ -2505,92 +2482,154 @@ var init_patterns = __esm({
|
|
|
2505
2482
|
}
|
|
2506
2483
|
});
|
|
2507
2484
|
|
|
2508
|
-
// src/
|
|
2509
|
-
function
|
|
2510
|
-
|
|
2511
|
-
if (
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
|
|
2515
|
-
found.push(...findSecretKeys(obj[i], prefix, depth + 1));
|
|
2516
|
-
}
|
|
2517
|
-
return found;
|
|
2485
|
+
// src/utils/collections.ts
|
|
2486
|
+
function getOrCreate(map, key, create) {
|
|
2487
|
+
let value = map.get(key);
|
|
2488
|
+
if (value === void 0) {
|
|
2489
|
+
value = create();
|
|
2490
|
+
map.set(key, value);
|
|
2518
2491
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2492
|
+
return value;
|
|
2493
|
+
}
|
|
2494
|
+
function deduplicateFindings(items, extract) {
|
|
2495
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2496
|
+
const findings = [];
|
|
2497
|
+
for (const item of items) {
|
|
2498
|
+
const result = extract(item);
|
|
2499
|
+
if (!result) continue;
|
|
2500
|
+
const existing = seen.get(result.key);
|
|
2501
|
+
if (existing) {
|
|
2502
|
+
existing.count++;
|
|
2503
|
+
continue;
|
|
2526
2504
|
}
|
|
2505
|
+
seen.set(result.key, result.finding);
|
|
2506
|
+
findings.push(result.finding);
|
|
2527
2507
|
}
|
|
2528
|
-
return
|
|
2508
|
+
return findings;
|
|
2529
2509
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2510
|
+
function groupBy(items, keyFn) {
|
|
2511
|
+
const map = /* @__PURE__ */ new Map();
|
|
2512
|
+
for (const item of items) {
|
|
2513
|
+
const key = keyFn(item);
|
|
2514
|
+
if (key == null) continue;
|
|
2515
|
+
let arr = map.get(key);
|
|
2516
|
+
if (!arr) {
|
|
2517
|
+
arr = [];
|
|
2518
|
+
map.set(key, arr);
|
|
2519
|
+
}
|
|
2520
|
+
arr.push(item);
|
|
2521
|
+
}
|
|
2522
|
+
return map;
|
|
2523
|
+
}
|
|
2524
|
+
var init_collections = __esm({
|
|
2525
|
+
"src/utils/collections.ts"() {
|
|
2526
|
+
"use strict";
|
|
2527
|
+
}
|
|
2528
|
+
});
|
|
2529
|
+
|
|
2530
|
+
// src/utils/object-scan.ts
|
|
2531
|
+
function walkObject(obj, visitor, options) {
|
|
2532
|
+
const opts = { ...DEFAULTS, ...options };
|
|
2533
|
+
walk(obj, visitor, opts, 0);
|
|
2534
|
+
}
|
|
2535
|
+
function walk(obj, visitor, opts, depth) {
|
|
2536
|
+
if (depth >= opts.maxDepth) return;
|
|
2537
|
+
if (!obj || typeof obj !== "object") return;
|
|
2538
|
+
if (Array.isArray(obj)) {
|
|
2539
|
+
for (let i = 0; i < Math.min(obj.length, opts.arrayLimit); i++) {
|
|
2540
|
+
walk(obj[i], visitor, opts, depth + 1);
|
|
2541
|
+
}
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
for (const key of Object.keys(obj)) {
|
|
2545
|
+
const val = obj[key];
|
|
2546
|
+
visitor(key, val, depth);
|
|
2547
|
+
if (typeof val === "object" && val !== null) {
|
|
2548
|
+
walk(val, visitor, opts, depth + 1);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
function collectFromObject(obj, match, options) {
|
|
2553
|
+
const results = [];
|
|
2554
|
+
walkObject(obj, (key, value) => {
|
|
2555
|
+
const result = match(key, value);
|
|
2556
|
+
if (result !== null) results.push(result);
|
|
2557
|
+
}, options);
|
|
2558
|
+
return results;
|
|
2559
|
+
}
|
|
2560
|
+
var DEFAULTS;
|
|
2561
|
+
var init_object_scan = __esm({
|
|
2562
|
+
"src/utils/object-scan.ts"() {
|
|
2563
|
+
"use strict";
|
|
2564
|
+
init_config();
|
|
2565
|
+
DEFAULTS = {
|
|
2566
|
+
maxDepth: MAX_OBJECT_SCAN_DEPTH,
|
|
2567
|
+
arrayLimit: SECRET_SCAN_ARRAY_LIMIT
|
|
2572
2568
|
};
|
|
2573
2569
|
}
|
|
2574
2570
|
});
|
|
2575
2571
|
|
|
2576
|
-
// src/analysis/rules/
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2572
|
+
// src/analysis/rules/auth-rules.ts
|
|
2573
|
+
function findSecretKeys(obj) {
|
|
2574
|
+
return collectFromObject(
|
|
2575
|
+
obj,
|
|
2576
|
+
(key, val) => SECRET_KEYS.test(key) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val) ? key : null
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
function isFrameworkResponse(request) {
|
|
2580
|
+
if (isRedirect(request.statusCode)) return true;
|
|
2581
|
+
if (request.path?.startsWith("/__")) return true;
|
|
2582
|
+
if (request.responseHeaders?.["x-middleware-rewrite"]) return true;
|
|
2583
|
+
return false;
|
|
2584
|
+
}
|
|
2585
|
+
var exposedSecretRule, tokenInUrlRule, insecureCookieRule, corsCredentialsRule;
|
|
2586
|
+
var init_auth_rules = __esm({
|
|
2587
|
+
"src/analysis/rules/auth-rules.ts"() {
|
|
2580
2588
|
"use strict";
|
|
2581
2589
|
init_patterns();
|
|
2590
|
+
init_config();
|
|
2591
|
+
init_http_status();
|
|
2592
|
+
init_collections();
|
|
2593
|
+
init_object_scan();
|
|
2594
|
+
exposedSecretRule = {
|
|
2595
|
+
id: "exposed-secret",
|
|
2596
|
+
severity: "critical",
|
|
2597
|
+
name: "Exposed Secret in Response",
|
|
2598
|
+
hint: RULE_HINTS["exposed-secret"],
|
|
2599
|
+
check(ctx) {
|
|
2600
|
+
return deduplicateFindings(ctx.requests, (request) => {
|
|
2601
|
+
if (isErrorStatus(request.statusCode)) return null;
|
|
2602
|
+
const parsed = ctx.parsedBodies.response.get(request.id);
|
|
2603
|
+
if (!parsed) return null;
|
|
2604
|
+
const keys = findSecretKeys(parsed);
|
|
2605
|
+
if (keys.length === 0) return null;
|
|
2606
|
+
const ep = `${request.method} ${request.path}`;
|
|
2607
|
+
return {
|
|
2608
|
+
key: `${ep}:${keys.sort().join(",")}`,
|
|
2609
|
+
finding: {
|
|
2610
|
+
severity: "critical",
|
|
2611
|
+
rule: "exposed-secret",
|
|
2612
|
+
title: "Exposed Secret in Response",
|
|
2613
|
+
desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
|
|
2614
|
+
hint: this.hint,
|
|
2615
|
+
detail: `Exposed fields: ${keys.join(", ")}. ${keys.length} unmasked secret value${keys.length !== 1 ? "s" : ""} in response body.`,
|
|
2616
|
+
endpoint: ep,
|
|
2617
|
+
count: 1
|
|
2618
|
+
}
|
|
2619
|
+
};
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2582
2623
|
tokenInUrlRule = {
|
|
2583
2624
|
id: "token-in-url",
|
|
2584
2625
|
severity: "critical",
|
|
2585
2626
|
name: "Auth Token in URL",
|
|
2586
2627
|
hint: RULE_HINTS["token-in-url"],
|
|
2587
2628
|
check(ctx) {
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
const
|
|
2592
|
-
if (qIdx === -1) continue;
|
|
2593
|
-
const params = r.url.substring(qIdx + 1).split("&");
|
|
2629
|
+
return deduplicateFindings(ctx.requests, (request) => {
|
|
2630
|
+
const qIdx = request.url.indexOf("?");
|
|
2631
|
+
if (qIdx === -1) return null;
|
|
2632
|
+
const params = request.url.substring(qIdx + 1).split("&");
|
|
2594
2633
|
const flagged = [];
|
|
2595
2634
|
for (const param of params) {
|
|
2596
2635
|
const [name, ...rest] = param.split("=");
|
|
@@ -2600,222 +2639,64 @@ var init_token_in_url = __esm({
|
|
|
2600
2639
|
flagged.push(name);
|
|
2601
2640
|
}
|
|
2602
2641
|
}
|
|
2603
|
-
if (flagged.length === 0)
|
|
2604
|
-
const ep = `${
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
existing.count++;
|
|
2609
|
-
continue;
|
|
2610
|
-
}
|
|
2611
|
-
const finding = {
|
|
2612
|
-
severity: "critical",
|
|
2613
|
-
rule: "token-in-url",
|
|
2614
|
-
title: "Auth Token in URL",
|
|
2615
|
-
desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
|
|
2616
|
-
hint: this.hint,
|
|
2617
|
-
endpoint: ep,
|
|
2618
|
-
count: 1
|
|
2619
|
-
};
|
|
2620
|
-
seen.set(dedupKey, finding);
|
|
2621
|
-
findings.push(finding);
|
|
2622
|
-
}
|
|
2623
|
-
return findings;
|
|
2624
|
-
}
|
|
2625
|
-
};
|
|
2626
|
-
}
|
|
2627
|
-
});
|
|
2628
|
-
|
|
2629
|
-
// src/analysis/rules/stack-trace-leak.ts
|
|
2630
|
-
var stackTraceLeakRule;
|
|
2631
|
-
var init_stack_trace_leak = __esm({
|
|
2632
|
-
"src/analysis/rules/stack-trace-leak.ts"() {
|
|
2633
|
-
"use strict";
|
|
2634
|
-
init_patterns();
|
|
2635
|
-
stackTraceLeakRule = {
|
|
2636
|
-
id: "stack-trace-leak",
|
|
2637
|
-
severity: "critical",
|
|
2638
|
-
name: "Stack Trace Leaked to Client",
|
|
2639
|
-
hint: RULE_HINTS["stack-trace-leak"],
|
|
2640
|
-
check(ctx) {
|
|
2641
|
-
const findings = [];
|
|
2642
|
-
const seen = /* @__PURE__ */ new Map();
|
|
2643
|
-
for (const r of ctx.requests) {
|
|
2644
|
-
if (!r.responseBody) continue;
|
|
2645
|
-
if (!STACK_TRACE_RE.test(r.responseBody)) continue;
|
|
2646
|
-
const ep = `${r.method} ${r.path}`;
|
|
2647
|
-
const existing = seen.get(ep);
|
|
2648
|
-
if (existing) {
|
|
2649
|
-
existing.count++;
|
|
2650
|
-
continue;
|
|
2651
|
-
}
|
|
2652
|
-
const finding = {
|
|
2653
|
-
severity: "critical",
|
|
2654
|
-
rule: "stack-trace-leak",
|
|
2655
|
-
title: "Stack Trace Leaked to Client",
|
|
2656
|
-
desc: `${ep} \u2014 response exposes internal stack trace`,
|
|
2657
|
-
hint: this.hint,
|
|
2658
|
-
endpoint: ep,
|
|
2659
|
-
count: 1
|
|
2660
|
-
};
|
|
2661
|
-
seen.set(ep, finding);
|
|
2662
|
-
findings.push(finding);
|
|
2663
|
-
}
|
|
2664
|
-
return findings;
|
|
2665
|
-
}
|
|
2666
|
-
};
|
|
2667
|
-
}
|
|
2668
|
-
});
|
|
2669
|
-
|
|
2670
|
-
// src/analysis/rules/error-info-leak.ts
|
|
2671
|
-
var CRITICAL_PATTERNS, errorInfoLeakRule;
|
|
2672
|
-
var init_error_info_leak = __esm({
|
|
2673
|
-
"src/analysis/rules/error-info-leak.ts"() {
|
|
2674
|
-
"use strict";
|
|
2675
|
-
init_patterns();
|
|
2676
|
-
CRITICAL_PATTERNS = [
|
|
2677
|
-
{ re: DB_CONN_RE, label: "database connection string" },
|
|
2678
|
-
{ re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
|
|
2679
|
-
{ re: SECRET_VAL_RE, label: "secret value" }
|
|
2680
|
-
];
|
|
2681
|
-
errorInfoLeakRule = {
|
|
2682
|
-
id: "error-info-leak",
|
|
2683
|
-
severity: "critical",
|
|
2684
|
-
name: "Sensitive Data in Error Response",
|
|
2685
|
-
hint: RULE_HINTS["error-info-leak"],
|
|
2686
|
-
check(ctx) {
|
|
2687
|
-
const findings = [];
|
|
2688
|
-
const seen = /* @__PURE__ */ new Map();
|
|
2689
|
-
for (const r of ctx.requests) {
|
|
2690
|
-
if (r.statusCode < 400) continue;
|
|
2691
|
-
if (!r.responseBody) continue;
|
|
2692
|
-
if (r.responseHeaders["x-nextjs-error"] || r.responseHeaders["x-nextjs-matched-path"]) continue;
|
|
2693
|
-
const ep = `${r.method} ${r.path}`;
|
|
2694
|
-
for (const p of CRITICAL_PATTERNS) {
|
|
2695
|
-
if (!p.re.test(r.responseBody)) continue;
|
|
2696
|
-
const dedupKey = `${ep}:${p.label}`;
|
|
2697
|
-
const existing = seen.get(dedupKey);
|
|
2698
|
-
if (existing) {
|
|
2699
|
-
existing.count++;
|
|
2700
|
-
continue;
|
|
2701
|
-
}
|
|
2702
|
-
const finding = {
|
|
2642
|
+
if (flagged.length === 0) return null;
|
|
2643
|
+
const ep = `${request.method} ${request.path}`;
|
|
2644
|
+
return {
|
|
2645
|
+
key: `${ep}:${flagged.sort().join(",")}`,
|
|
2646
|
+
finding: {
|
|
2703
2647
|
severity: "critical",
|
|
2704
|
-
rule: "
|
|
2705
|
-
title: "
|
|
2706
|
-
desc: `${ep} \u2014
|
|
2648
|
+
rule: "token-in-url",
|
|
2649
|
+
title: "Auth Token in URL",
|
|
2650
|
+
desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
|
|
2707
2651
|
hint: this.hint,
|
|
2652
|
+
detail: `Parameters in URL: ${flagged.join(", ")}. Auth tokens in URLs are logged by proxies, browsers, and CDNs.`,
|
|
2708
2653
|
endpoint: ep,
|
|
2709
2654
|
count: 1
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
}
|
|
2714
|
-
}
|
|
2715
|
-
return findings;
|
|
2655
|
+
}
|
|
2656
|
+
};
|
|
2657
|
+
});
|
|
2716
2658
|
}
|
|
2717
2659
|
};
|
|
2718
|
-
}
|
|
2719
|
-
});
|
|
2720
|
-
|
|
2721
|
-
// src/analysis/rules/insecure-cookie.ts
|
|
2722
|
-
function isFrameworkResponse(r) {
|
|
2723
|
-
if (isRedirect(r.statusCode)) return true;
|
|
2724
|
-
if (r.path?.startsWith("/__")) return true;
|
|
2725
|
-
if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
|
|
2726
|
-
return false;
|
|
2727
|
-
}
|
|
2728
|
-
var insecureCookieRule;
|
|
2729
|
-
var init_insecure_cookie = __esm({
|
|
2730
|
-
"src/analysis/rules/insecure-cookie.ts"() {
|
|
2731
|
-
"use strict";
|
|
2732
|
-
init_patterns();
|
|
2733
|
-
init_http_status();
|
|
2734
2660
|
insecureCookieRule = {
|
|
2735
2661
|
id: "insecure-cookie",
|
|
2736
2662
|
severity: "warning",
|
|
2737
2663
|
name: "Insecure Cookie",
|
|
2738
2664
|
hint: RULE_HINTS["insecure-cookie"],
|
|
2739
2665
|
check(ctx) {
|
|
2740
|
-
const
|
|
2741
|
-
const
|
|
2742
|
-
|
|
2743
|
-
if (
|
|
2744
|
-
|
|
2745
|
-
const setCookie = r.responseHeaders["set-cookie"];
|
|
2666
|
+
const cookieEntries = [];
|
|
2667
|
+
for (const request of ctx.requests) {
|
|
2668
|
+
if (!request.responseHeaders) continue;
|
|
2669
|
+
if (isFrameworkResponse(request)) continue;
|
|
2670
|
+
const setCookie = request.responseHeaders["set-cookie"];
|
|
2746
2671
|
if (!setCookie) continue;
|
|
2747
2672
|
const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
|
|
2748
2673
|
for (const cookie of cookies) {
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2674
|
+
cookieEntries.push({ cookie });
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
return deduplicateFindings(cookieEntries, ({ cookie }) => {
|
|
2678
|
+
const cookieName = cookie.trim().split("=")[0].trim();
|
|
2679
|
+
const lower = cookie.toLowerCase();
|
|
2680
|
+
const issues = [];
|
|
2681
|
+
if (!lower.includes("httponly")) issues.push("HttpOnly");
|
|
2682
|
+
if (!lower.includes("samesite")) issues.push("SameSite");
|
|
2683
|
+
if (issues.length === 0) return null;
|
|
2684
|
+
return {
|
|
2685
|
+
key: `${cookieName}:${issues.join(",")}`,
|
|
2686
|
+
finding: {
|
|
2762
2687
|
severity: "warning",
|
|
2763
2688
|
rule: "insecure-cookie",
|
|
2764
2689
|
title: "Insecure Cookie",
|
|
2765
2690
|
desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
|
|
2766
2691
|
hint: this.hint,
|
|
2692
|
+
detail: `Missing: ${issues.join(", ")}. ${issues.includes("HttpOnly") ? "Cookie accessible via JavaScript (XSS risk). " : ""}${issues.includes("SameSite") ? "Cookie sent on cross-site requests (CSRF risk)." : ""}`,
|
|
2767
2693
|
endpoint: cookieName,
|
|
2768
2694
|
count: 1
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
|
-
return findings;
|
|
2775
|
-
}
|
|
2776
|
-
};
|
|
2777
|
-
}
|
|
2778
|
-
});
|
|
2779
|
-
|
|
2780
|
-
// src/analysis/rules/sensitive-logs.ts
|
|
2781
|
-
var sensitiveLogsRule;
|
|
2782
|
-
var init_sensitive_logs = __esm({
|
|
2783
|
-
"src/analysis/rules/sensitive-logs.ts"() {
|
|
2784
|
-
"use strict";
|
|
2785
|
-
init_patterns();
|
|
2786
|
-
sensitiveLogsRule = {
|
|
2787
|
-
id: "sensitive-logs",
|
|
2788
|
-
severity: "warning",
|
|
2789
|
-
name: "Sensitive Data in Logs",
|
|
2790
|
-
hint: RULE_HINTS["sensitive-logs"],
|
|
2791
|
-
check(ctx) {
|
|
2792
|
-
let count = 0;
|
|
2793
|
-
for (const log of ctx.logs) {
|
|
2794
|
-
if (!log.message) continue;
|
|
2795
|
-
if (log.message.startsWith("[brakit]")) continue;
|
|
2796
|
-
if (LOG_SECRET_RE.test(log.message)) count++;
|
|
2797
|
-
}
|
|
2798
|
-
if (count === 0) return [];
|
|
2799
|
-
return [{
|
|
2800
|
-
severity: "warning",
|
|
2801
|
-
rule: "sensitive-logs",
|
|
2802
|
-
title: "Sensitive Data in Logs",
|
|
2803
|
-
desc: `Console output contains secret/token values \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
|
|
2804
|
-
hint: this.hint,
|
|
2805
|
-
endpoint: "console",
|
|
2806
|
-
count
|
|
2807
|
-
}];
|
|
2695
|
+
}
|
|
2696
|
+
};
|
|
2697
|
+
});
|
|
2808
2698
|
}
|
|
2809
2699
|
};
|
|
2810
|
-
}
|
|
2811
|
-
});
|
|
2812
|
-
|
|
2813
|
-
// src/analysis/rules/cors-credentials.ts
|
|
2814
|
-
var corsCredentialsRule;
|
|
2815
|
-
var init_cors_credentials = __esm({
|
|
2816
|
-
"src/analysis/rules/cors-credentials.ts"() {
|
|
2817
|
-
"use strict";
|
|
2818
|
-
init_patterns();
|
|
2819
2700
|
corsCredentialsRule = {
|
|
2820
2701
|
id: "cors-credentials",
|
|
2821
2702
|
severity: "warning",
|
|
@@ -2824,12 +2705,12 @@ var init_cors_credentials = __esm({
|
|
|
2824
2705
|
check(ctx) {
|
|
2825
2706
|
const findings = [];
|
|
2826
2707
|
const seen = /* @__PURE__ */ new Set();
|
|
2827
|
-
for (const
|
|
2828
|
-
if (!
|
|
2829
|
-
const origin =
|
|
2830
|
-
const creds =
|
|
2708
|
+
for (const request of ctx.requests) {
|
|
2709
|
+
if (!request.responseHeaders) continue;
|
|
2710
|
+
const origin = request.responseHeaders["access-control-allow-origin"];
|
|
2711
|
+
const creds = request.responseHeaders["access-control-allow-credentials"];
|
|
2831
2712
|
if (origin !== "*" || creds !== "true") continue;
|
|
2832
|
-
const ep = `${
|
|
2713
|
+
const ep = `${request.method} ${request.path}`;
|
|
2833
2714
|
if (seen.has(ep)) continue;
|
|
2834
2715
|
seen.add(ep);
|
|
2835
2716
|
findings.push({
|
|
@@ -2848,25 +2729,13 @@ var init_cors_credentials = __esm({
|
|
|
2848
2729
|
}
|
|
2849
2730
|
});
|
|
2850
2731
|
|
|
2851
|
-
// src/analysis/rules/
|
|
2852
|
-
function findEmails(obj
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
emails.push(...findEmails(obj[i], depth + 1));
|
|
2859
|
-
}
|
|
2860
|
-
return emails;
|
|
2861
|
-
}
|
|
2862
|
-
for (const v of Object.values(obj)) {
|
|
2863
|
-
if (typeof v === "string" && EMAIL_RE.test(v)) {
|
|
2864
|
-
emails.push(v);
|
|
2865
|
-
} else if (typeof v === "object" && v !== null) {
|
|
2866
|
-
emails.push(...findEmails(v, depth + 1));
|
|
2867
|
-
}
|
|
2868
|
-
}
|
|
2869
|
-
return emails;
|
|
2732
|
+
// src/analysis/rules/data-rules.ts
|
|
2733
|
+
function findEmails(obj) {
|
|
2734
|
+
return collectFromObject(
|
|
2735
|
+
obj,
|
|
2736
|
+
(_key, val) => typeof val === "string" && EMAIL_RE.test(val) ? val : null,
|
|
2737
|
+
{ arrayLimit: PII_SCAN_ARRAY_LIMIT }
|
|
2738
|
+
);
|
|
2870
2739
|
}
|
|
2871
2740
|
function topLevelFieldCount(obj) {
|
|
2872
2741
|
if (Array.isArray(obj)) {
|
|
@@ -2939,14 +2808,107 @@ function detectPII(method, reqBody, resBody) {
|
|
|
2939
2808
|
const target = unwrapResponse(resBody);
|
|
2940
2809
|
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
|
|
2941
2810
|
}
|
|
2942
|
-
var WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
|
|
2943
|
-
var
|
|
2944
|
-
"src/analysis/rules/
|
|
2811
|
+
var stackTraceLeakRule, CRITICAL_PATTERNS, errorInfoLeakRule, sensitiveLogsRule, WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
|
|
2812
|
+
var init_data_rules = __esm({
|
|
2813
|
+
"src/analysis/rules/data-rules.ts"() {
|
|
2945
2814
|
"use strict";
|
|
2815
|
+
init_collections();
|
|
2946
2816
|
init_patterns();
|
|
2947
2817
|
init_response();
|
|
2948
|
-
|
|
2818
|
+
init_config();
|
|
2949
2819
|
init_http_status();
|
|
2820
|
+
init_object_scan();
|
|
2821
|
+
stackTraceLeakRule = {
|
|
2822
|
+
id: "stack-trace-leak",
|
|
2823
|
+
severity: "critical",
|
|
2824
|
+
name: "Stack Trace Leaked to Client",
|
|
2825
|
+
hint: RULE_HINTS["stack-trace-leak"],
|
|
2826
|
+
check(ctx) {
|
|
2827
|
+
return deduplicateFindings(ctx.requests, (request) => {
|
|
2828
|
+
if (!request.responseBody) return null;
|
|
2829
|
+
if (!STACK_TRACE_RE.test(request.responseBody)) return null;
|
|
2830
|
+
const ep = `${request.method} ${request.path}`;
|
|
2831
|
+
const firstLine = request.responseBody.split("\n").find((l) => STACK_TRACE_RE.test(l))?.trim() ?? "";
|
|
2832
|
+
return {
|
|
2833
|
+
key: ep,
|
|
2834
|
+
finding: {
|
|
2835
|
+
severity: "critical",
|
|
2836
|
+
rule: "stack-trace-leak",
|
|
2837
|
+
title: "Stack Trace Leaked to Client",
|
|
2838
|
+
desc: `${ep} \u2014 response exposes internal stack trace`,
|
|
2839
|
+
hint: this.hint,
|
|
2840
|
+
detail: firstLine ? `Stack trace: ${firstLine.slice(0, 120)}` : void 0,
|
|
2841
|
+
endpoint: ep,
|
|
2842
|
+
count: 1
|
|
2843
|
+
}
|
|
2844
|
+
};
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
CRITICAL_PATTERNS = [
|
|
2849
|
+
{ re: DB_CONN_RE, label: "database connection string" },
|
|
2850
|
+
{ re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
|
|
2851
|
+
{ re: SECRET_VAL_RE, label: "secret value" }
|
|
2852
|
+
];
|
|
2853
|
+
errorInfoLeakRule = {
|
|
2854
|
+
id: "error-info-leak",
|
|
2855
|
+
severity: "critical",
|
|
2856
|
+
name: "Sensitive Data in Error Response",
|
|
2857
|
+
hint: RULE_HINTS["error-info-leak"],
|
|
2858
|
+
check(ctx) {
|
|
2859
|
+
const entries = [];
|
|
2860
|
+
for (const request of ctx.requests) {
|
|
2861
|
+
if (request.statusCode < 400) continue;
|
|
2862
|
+
if (!request.responseBody) continue;
|
|
2863
|
+
if (request.responseHeaders["x-nextjs-error"] || request.responseHeaders["x-nextjs-matched-path"]) continue;
|
|
2864
|
+
const ep = `${request.method} ${request.path}`;
|
|
2865
|
+
for (const pattern of CRITICAL_PATTERNS) {
|
|
2866
|
+
if (pattern.re.test(request.responseBody)) {
|
|
2867
|
+
entries.push({ ep, pattern, body: request.responseBody });
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
return deduplicateFindings(entries, ({ ep, pattern }) => {
|
|
2872
|
+
return {
|
|
2873
|
+
key: `${ep}:${pattern.label}`,
|
|
2874
|
+
finding: {
|
|
2875
|
+
severity: "critical",
|
|
2876
|
+
rule: "error-info-leak",
|
|
2877
|
+
title: "Sensitive Data in Error Response",
|
|
2878
|
+
desc: `${ep} \u2014 error response exposes ${pattern.label}`,
|
|
2879
|
+
hint: this.hint,
|
|
2880
|
+
detail: `Detected: ${pattern.label} in error response body`,
|
|
2881
|
+
endpoint: ep,
|
|
2882
|
+
count: 1
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
};
|
|
2888
|
+
sensitiveLogsRule = {
|
|
2889
|
+
id: "sensitive-logs",
|
|
2890
|
+
severity: "warning",
|
|
2891
|
+
name: "Sensitive Data in Logs",
|
|
2892
|
+
hint: RULE_HINTS["sensitive-logs"],
|
|
2893
|
+
check(ctx) {
|
|
2894
|
+
let count = 0;
|
|
2895
|
+
for (const log of ctx.logs) {
|
|
2896
|
+
if (!log.message) continue;
|
|
2897
|
+
if (log.message.startsWith("[brakit]")) continue;
|
|
2898
|
+
if (LOG_SECRET_RE.test(log.message)) count++;
|
|
2899
|
+
}
|
|
2900
|
+
if (count === 0) return [];
|
|
2901
|
+
return [{
|
|
2902
|
+
severity: "warning",
|
|
2903
|
+
rule: "sensitive-logs",
|
|
2904
|
+
title: "Sensitive Data in Logs",
|
|
2905
|
+
desc: `Console output contains secret/token values \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
|
|
2906
|
+
hint: this.hint,
|
|
2907
|
+
endpoint: "console",
|
|
2908
|
+
count
|
|
2909
|
+
}];
|
|
2910
|
+
}
|
|
2911
|
+
};
|
|
2950
2912
|
WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
2951
2913
|
REASON_LABELS = {
|
|
2952
2914
|
echo: "echoes back PII from the request body",
|
|
@@ -2960,35 +2922,33 @@ var init_response_pii_leak = __esm({
|
|
|
2960
2922
|
name: "PII Leak in Response",
|
|
2961
2923
|
hint: RULE_HINTS["response-pii-leak"],
|
|
2962
2924
|
check(ctx) {
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
if (
|
|
2968
|
-
const
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
const
|
|
2972
|
-
|
|
2973
|
-
const
|
|
2974
|
-
|
|
2975
|
-
if (
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2925
|
+
return deduplicateFindings(ctx.requests, (request) => {
|
|
2926
|
+
if (isErrorStatus(request.statusCode)) return null;
|
|
2927
|
+
if (SELF_SERVICE_PATH.test(request.path)) return null;
|
|
2928
|
+
const resJson = ctx.parsedBodies.response.get(request.id);
|
|
2929
|
+
if (!resJson) return null;
|
|
2930
|
+
const reqJson = ctx.parsedBodies.request.get(request.id) ?? null;
|
|
2931
|
+
const detection = detectPII(request.method, reqJson, resJson);
|
|
2932
|
+
if (!detection) return null;
|
|
2933
|
+
const ep = `${request.method} ${request.path}`;
|
|
2934
|
+
const fieldCount = topLevelFieldCount(resJson);
|
|
2935
|
+
const detailParts = [`Pattern: ${REASON_LABELS[detection.reason]}`];
|
|
2936
|
+
if (detection.emailCount > 0) detailParts.push(`${detection.emailCount} email${detection.emailCount !== 1 ? "s" : ""} detected`);
|
|
2937
|
+
if (fieldCount > 0) detailParts.push(`${fieldCount} fields per record`);
|
|
2938
|
+
return {
|
|
2939
|
+
key: ep,
|
|
2940
|
+
finding: {
|
|
2941
|
+
severity: "warning",
|
|
2942
|
+
rule: "response-pii-leak",
|
|
2943
|
+
title: "PII Leak in Response",
|
|
2944
|
+
desc: `${ep} \u2014 exposes PII in response`,
|
|
2945
|
+
hint: this.hint,
|
|
2946
|
+
detail: detailParts.join(". "),
|
|
2947
|
+
endpoint: ep,
|
|
2948
|
+
count: 1
|
|
2949
|
+
}
|
|
2987
2950
|
};
|
|
2988
|
-
|
|
2989
|
-
findings.push(finding);
|
|
2990
|
-
}
|
|
2991
|
-
return findings;
|
|
2951
|
+
});
|
|
2992
2952
|
}
|
|
2993
2953
|
};
|
|
2994
2954
|
}
|
|
@@ -2998,14 +2958,14 @@ var init_response_pii_leak = __esm({
|
|
|
2998
2958
|
function buildBodyCache(requests) {
|
|
2999
2959
|
const response = /* @__PURE__ */ new Map();
|
|
3000
2960
|
const request = /* @__PURE__ */ new Map();
|
|
3001
|
-
for (const
|
|
3002
|
-
if (
|
|
3003
|
-
const parsed = tryParseJson(
|
|
3004
|
-
if (parsed != null) response.set(
|
|
2961
|
+
for (const req of requests) {
|
|
2962
|
+
if (req.responseBody) {
|
|
2963
|
+
const parsed = tryParseJson(req.responseBody);
|
|
2964
|
+
if (parsed != null) response.set(req.id, parsed);
|
|
3005
2965
|
}
|
|
3006
|
-
if (
|
|
3007
|
-
const parsed = tryParseJson(
|
|
3008
|
-
if (parsed != null) request.set(
|
|
2966
|
+
if (req.requestBody) {
|
|
2967
|
+
const parsed = tryParseJson(req.requestBody);
|
|
2968
|
+
if (parsed != null) request.set(req.id, parsed);
|
|
3009
2969
|
}
|
|
3010
2970
|
}
|
|
3011
2971
|
return { response, request };
|
|
@@ -3027,14 +2987,10 @@ var init_scanner = __esm({
|
|
|
3027
2987
|
"src/analysis/rules/scanner.ts"() {
|
|
3028
2988
|
"use strict";
|
|
3029
2989
|
init_response();
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
init_insecure_cookie();
|
|
3035
|
-
init_sensitive_logs();
|
|
3036
|
-
init_cors_credentials();
|
|
3037
|
-
init_response_pii_leak();
|
|
2990
|
+
init_log();
|
|
2991
|
+
init_type_guards();
|
|
2992
|
+
init_auth_rules();
|
|
2993
|
+
init_data_rules();
|
|
3038
2994
|
SecurityScanner = class {
|
|
3039
2995
|
constructor() {
|
|
3040
2996
|
this.rules = [];
|
|
@@ -3051,7 +3007,8 @@ var init_scanner = __esm({
|
|
|
3051
3007
|
for (const rule of this.rules) {
|
|
3052
3008
|
try {
|
|
3053
3009
|
findings.push(...rule.check(ctx));
|
|
3054
|
-
} catch {
|
|
3010
|
+
} catch (e) {
|
|
3011
|
+
brakitDebug(`rule ${rule.id} failed: ${getErrorMessage(e)}`);
|
|
3055
3012
|
}
|
|
3056
3013
|
}
|
|
3057
3014
|
return findings;
|
|
@@ -3068,76 +3025,28 @@ var init_rules = __esm({
|
|
|
3068
3025
|
"src/analysis/rules/index.ts"() {
|
|
3069
3026
|
"use strict";
|
|
3070
3027
|
init_scanner();
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
init_stack_trace_leak();
|
|
3074
|
-
init_error_info_leak();
|
|
3075
|
-
init_insecure_cookie();
|
|
3076
|
-
init_sensitive_logs();
|
|
3077
|
-
init_cors_credentials();
|
|
3078
|
-
init_response_pii_leak();
|
|
3028
|
+
init_auth_rules();
|
|
3029
|
+
init_data_rules();
|
|
3079
3030
|
}
|
|
3080
|
-
});
|
|
3081
|
-
|
|
3082
|
-
// src/core/disposable.ts
|
|
3083
|
-
var SubscriptionBag;
|
|
3084
|
-
var init_disposable = __esm({
|
|
3085
|
-
"src/core/disposable.ts"() {
|
|
3086
|
-
"use strict";
|
|
3087
|
-
SubscriptionBag = class {
|
|
3088
|
-
constructor() {
|
|
3089
|
-
this.items = [];
|
|
3090
|
-
}
|
|
3091
|
-
add(teardown) {
|
|
3092
|
-
this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
|
|
3093
|
-
}
|
|
3094
|
-
dispose() {
|
|
3095
|
-
for (const d of this.items) d.dispose();
|
|
3096
|
-
this.items.length = 0;
|
|
3097
|
-
}
|
|
3098
|
-
};
|
|
3099
|
-
}
|
|
3100
|
-
});
|
|
3101
|
-
|
|
3102
|
-
// src/utils/collections.ts
|
|
3103
|
-
function groupBy(items, keyFn) {
|
|
3104
|
-
const map = /* @__PURE__ */ new Map();
|
|
3105
|
-
for (const item of items) {
|
|
3106
|
-
const key = keyFn(item);
|
|
3107
|
-
if (key == null) continue;
|
|
3108
|
-
let arr = map.get(key);
|
|
3109
|
-
if (!arr) {
|
|
3110
|
-
arr = [];
|
|
3111
|
-
map.set(key, arr);
|
|
3112
|
-
}
|
|
3113
|
-
arr.push(item);
|
|
3114
|
-
}
|
|
3115
|
-
return map;
|
|
3116
|
-
}
|
|
3117
|
-
var init_collections = __esm({
|
|
3118
|
-
"src/utils/collections.ts"() {
|
|
3119
|
-
"use strict";
|
|
3120
|
-
}
|
|
3121
|
-
});
|
|
3122
|
-
|
|
3123
|
-
// src/utils/endpoint.ts
|
|
3124
|
-
function normalizePath(path) {
|
|
3125
|
-
const qIdx = path.indexOf("?");
|
|
3126
|
-
const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
|
|
3127
|
-
return pathname.split("/").map((seg) => seg && DYNAMIC_SEGMENT_RE.test(seg) ? ":id" : seg).join("/");
|
|
3128
|
-
}
|
|
3129
|
-
function getEndpointKey(method, path) {
|
|
3130
|
-
return `${method} ${normalizePath(path)}`;
|
|
3131
|
-
}
|
|
3132
|
-
function extractEndpointFromDesc(desc) {
|
|
3133
|
-
return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
|
|
3134
|
-
}
|
|
3135
|
-
var DYNAMIC_SEGMENT_RE, ENDPOINT_PREFIX_RE;
|
|
3136
|
-
var init_endpoint = __esm({
|
|
3137
|
-
"src/utils/endpoint.ts"() {
|
|
3031
|
+
});
|
|
3032
|
+
|
|
3033
|
+
// src/core/disposable.ts
|
|
3034
|
+
var SubscriptionBag;
|
|
3035
|
+
var init_disposable = __esm({
|
|
3036
|
+
"src/core/disposable.ts"() {
|
|
3138
3037
|
"use strict";
|
|
3139
|
-
|
|
3140
|
-
|
|
3038
|
+
SubscriptionBag = class {
|
|
3039
|
+
constructor() {
|
|
3040
|
+
this.items = [];
|
|
3041
|
+
}
|
|
3042
|
+
add(teardown) {
|
|
3043
|
+
this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
|
|
3044
|
+
}
|
|
3045
|
+
dispose() {
|
|
3046
|
+
for (const d of this.items) d.dispose();
|
|
3047
|
+
this.items.length = 0;
|
|
3048
|
+
}
|
|
3049
|
+
};
|
|
3141
3050
|
}
|
|
3142
3051
|
});
|
|
3143
3052
|
|
|
@@ -3161,7 +3070,7 @@ var init_query_helpers = __esm({
|
|
|
3161
3070
|
});
|
|
3162
3071
|
|
|
3163
3072
|
// src/analysis/insights/prepare.ts
|
|
3164
|
-
function
|
|
3073
|
+
function emptyEndpointGroup() {
|
|
3165
3074
|
return {
|
|
3166
3075
|
total: 0,
|
|
3167
3076
|
errors: 0,
|
|
@@ -3173,16 +3082,12 @@ function createEndpointGroup() {
|
|
|
3173
3082
|
queryShapeDurations: /* @__PURE__ */ new Map()
|
|
3174
3083
|
};
|
|
3175
3084
|
}
|
|
3176
|
-
function
|
|
3085
|
+
function keepRecentPerEndpoint(requests) {
|
|
3177
3086
|
const byEndpoint = /* @__PURE__ */ new Map();
|
|
3178
|
-
for (const
|
|
3179
|
-
const
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
list = [];
|
|
3183
|
-
byEndpoint.set(ep, list);
|
|
3184
|
-
}
|
|
3185
|
-
list.push(r);
|
|
3087
|
+
for (const request of requests) {
|
|
3088
|
+
const endpointKey = getEndpointKey(request.method, request.path);
|
|
3089
|
+
const list = getOrCreate(byEndpoint, endpointKey, () => []);
|
|
3090
|
+
list.push(request);
|
|
3186
3091
|
}
|
|
3187
3092
|
const windowed = [];
|
|
3188
3093
|
for (const [, reqs] of byEndpoint) {
|
|
@@ -3190,54 +3095,67 @@ function windowByEndpoint(requests) {
|
|
|
3190
3095
|
}
|
|
3191
3096
|
return windowed;
|
|
3192
3097
|
}
|
|
3098
|
+
function filterUserRequests(requests) {
|
|
3099
|
+
return requests.filter(
|
|
3100
|
+
(request) => !request.isStatic && !request.isHealthCheck && (!request.path || !request.path.startsWith(DASHBOARD_PREFIX))
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3193
3103
|
function extractActiveEndpoints(requests) {
|
|
3194
3104
|
const endpoints = /* @__PURE__ */ new Set();
|
|
3195
|
-
for (const
|
|
3196
|
-
|
|
3197
|
-
endpoints.add(getEndpointKey(r.method, r.path));
|
|
3198
|
-
}
|
|
3105
|
+
for (const request of filterUserRequests(requests)) {
|
|
3106
|
+
endpoints.add(getEndpointKey(request.method, request.path));
|
|
3199
3107
|
}
|
|
3200
3108
|
return endpoints;
|
|
3201
3109
|
}
|
|
3202
|
-
function
|
|
3203
|
-
const nonStatic = ctx.requests.filter(
|
|
3204
|
-
(r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
|
|
3205
|
-
);
|
|
3206
|
-
const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
|
|
3207
|
-
const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
|
|
3208
|
-
const reqById = new Map(nonStatic.map((r) => [r.id, r]));
|
|
3209
|
-
const recent = windowByEndpoint(nonStatic);
|
|
3110
|
+
function aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq) {
|
|
3210
3111
|
const endpointGroups = /* @__PURE__ */ new Map();
|
|
3211
|
-
for (const
|
|
3212
|
-
const
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3112
|
+
for (const request of recent) {
|
|
3113
|
+
const endpointKey = getEndpointKey(request.method, request.path);
|
|
3114
|
+
const group = getOrCreate(endpointGroups, endpointKey, emptyEndpointGroup);
|
|
3115
|
+
group.total++;
|
|
3116
|
+
if (isErrorStatus(request.statusCode)) group.errors++;
|
|
3117
|
+
group.totalDuration += request.durationMs;
|
|
3118
|
+
group.totalSize += request.responseSize ?? 0;
|
|
3119
|
+
const reqQueries = queriesByReq.get(request.id) ?? [];
|
|
3120
|
+
group.queryCount += reqQueries.length;
|
|
3121
|
+
for (const query of reqQueries) {
|
|
3122
|
+
group.totalQueryTimeMs += query.durationMs;
|
|
3123
|
+
const shape = getQueryShape(query);
|
|
3124
|
+
const info = getQueryInfo(query);
|
|
3125
|
+
const shapeDuration = getOrCreate(group.queryShapeDurations, shape, () => ({
|
|
3126
|
+
totalMs: 0,
|
|
3127
|
+
count: 0,
|
|
3128
|
+
label: info.op + (info.table ? ` ${info.table}` : "")
|
|
3129
|
+
}));
|
|
3130
|
+
shapeDuration.totalMs += query.durationMs;
|
|
3131
|
+
shapeDuration.count++;
|
|
3217
3132
|
}
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
g.totalSize += r.responseSize ?? 0;
|
|
3222
|
-
const reqQueries = queriesByReq.get(r.id) ?? [];
|
|
3223
|
-
g.queryCount += reqQueries.length;
|
|
3224
|
-
for (const q of reqQueries) {
|
|
3225
|
-
g.totalQueryTimeMs += q.durationMs;
|
|
3226
|
-
const shape = getQueryShape(q);
|
|
3227
|
-
const info = getQueryInfo(q);
|
|
3228
|
-
let sd = g.queryShapeDurations.get(shape);
|
|
3229
|
-
if (!sd) {
|
|
3230
|
-
sd = { totalMs: 0, count: 0, label: info.op + (info.table ? ` ${info.table}` : "") };
|
|
3231
|
-
g.queryShapeDurations.set(shape, sd);
|
|
3232
|
-
}
|
|
3233
|
-
sd.totalMs += q.durationMs;
|
|
3234
|
-
sd.count++;
|
|
3133
|
+
const reqFetches = fetchesByReq.get(request.id) ?? [];
|
|
3134
|
+
for (const fetch of reqFetches) {
|
|
3135
|
+
group.totalFetchTimeMs += fetch.durationMs;
|
|
3235
3136
|
}
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3137
|
+
}
|
|
3138
|
+
return endpointGroups;
|
|
3139
|
+
}
|
|
3140
|
+
function collectStrictModeDupeIds(ctx) {
|
|
3141
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3142
|
+
for (const flow of ctx.flows) {
|
|
3143
|
+
for (const req of flow.requests) {
|
|
3144
|
+
if (req.isStrictModeDupe) ids.add(req.id);
|
|
3239
3145
|
}
|
|
3240
3146
|
}
|
|
3147
|
+
return ids;
|
|
3148
|
+
}
|
|
3149
|
+
function buildInsightContext(ctx) {
|
|
3150
|
+
const strictModeDupeIds = collectStrictModeDupeIds(ctx);
|
|
3151
|
+
const nonStatic = filterUserRequests(ctx.requests).filter((req) => !strictModeDupeIds.has(req.id));
|
|
3152
|
+
const filteredQueries = strictModeDupeIds.size > 0 ? ctx.queries.filter((q) => !q.parentRequestId || !strictModeDupeIds.has(q.parentRequestId)) : ctx.queries;
|
|
3153
|
+
const filteredFetches = strictModeDupeIds.size > 0 ? ctx.fetches.filter((f) => !f.parentRequestId || !strictModeDupeIds.has(f.parentRequestId)) : ctx.fetches;
|
|
3154
|
+
const queriesByReq = groupBy(filteredQueries, (query) => query.parentRequestId);
|
|
3155
|
+
const fetchesByReq = groupBy(filteredFetches, (fetch) => fetch.parentRequestId);
|
|
3156
|
+
const reqById = new Map(nonStatic.map((request) => [request.id, request]));
|
|
3157
|
+
const recent = keepRecentPerEndpoint(nonStatic);
|
|
3158
|
+
const endpointGroups = aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq);
|
|
3241
3159
|
return {
|
|
3242
3160
|
...ctx,
|
|
3243
3161
|
nonStatic,
|
|
@@ -3254,7 +3172,7 @@ var init_prepare = __esm({
|
|
|
3254
3172
|
init_endpoint();
|
|
3255
3173
|
init_constants();
|
|
3256
3174
|
init_http_status();
|
|
3257
|
-
|
|
3175
|
+
init_config();
|
|
3258
3176
|
init_query_helpers();
|
|
3259
3177
|
}
|
|
3260
3178
|
});
|
|
@@ -3265,6 +3183,8 @@ var init_runner = __esm({
|
|
|
3265
3183
|
"src/analysis/insights/runner.ts"() {
|
|
3266
3184
|
"use strict";
|
|
3267
3185
|
init_prepare();
|
|
3186
|
+
init_log();
|
|
3187
|
+
init_type_guards();
|
|
3268
3188
|
SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
3269
3189
|
InsightRunner = class {
|
|
3270
3190
|
constructor() {
|
|
@@ -3274,12 +3194,13 @@ var init_runner = __esm({
|
|
|
3274
3194
|
this.rules.push(rule);
|
|
3275
3195
|
}
|
|
3276
3196
|
run(ctx) {
|
|
3277
|
-
const prepared =
|
|
3197
|
+
const prepared = buildInsightContext(ctx);
|
|
3278
3198
|
const insights = [];
|
|
3279
3199
|
for (const rule of this.rules) {
|
|
3280
3200
|
try {
|
|
3281
3201
|
insights.push(...rule.check(prepared));
|
|
3282
|
-
} catch {
|
|
3202
|
+
} catch (e) {
|
|
3203
|
+
brakitDebug(`insight rule ${rule.id} failed: ${getErrorMessage(e)}`);
|
|
3283
3204
|
}
|
|
3284
3205
|
}
|
|
3285
3206
|
insights.sort(
|
|
@@ -3291,420 +3212,129 @@ var init_runner = __esm({
|
|
|
3291
3212
|
}
|
|
3292
3213
|
});
|
|
3293
3214
|
|
|
3294
|
-
// src/analysis/insights/rules/
|
|
3295
|
-
var n1Rule;
|
|
3296
|
-
var
|
|
3297
|
-
"src/analysis/insights/rules/
|
|
3215
|
+
// src/analysis/insights/rules/query-rules.ts
|
|
3216
|
+
var n1Rule, redundantQueryRule, selectStarRule, highRowsRule, queryHeavyRule;
|
|
3217
|
+
var init_query_rules = __esm({
|
|
3218
|
+
"src/analysis/insights/rules/query-rules.ts"() {
|
|
3298
3219
|
"use strict";
|
|
3299
3220
|
init_query_helpers();
|
|
3300
3221
|
init_endpoint();
|
|
3301
3222
|
init_constants();
|
|
3223
|
+
init_patterns();
|
|
3302
3224
|
n1Rule = {
|
|
3303
3225
|
id: "n1",
|
|
3304
3226
|
check(ctx) {
|
|
3305
3227
|
const insights = [];
|
|
3306
|
-
const
|
|
3228
|
+
const reportedKeys = /* @__PURE__ */ new Set();
|
|
3307
3229
|
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3308
3230
|
const req = ctx.reqById.get(reqId);
|
|
3309
3231
|
if (!req) continue;
|
|
3310
3232
|
const endpoint = getEndpointKey(req.method, req.path);
|
|
3311
3233
|
const shapeGroups = /* @__PURE__ */ new Map();
|
|
3312
|
-
for (const
|
|
3313
|
-
const shape = getQueryShape(
|
|
3234
|
+
for (const query of reqQueries) {
|
|
3235
|
+
const shape = getQueryShape(query);
|
|
3314
3236
|
let group = shapeGroups.get(shape);
|
|
3315
3237
|
if (!group) {
|
|
3316
|
-
group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first:
|
|
3238
|
+
group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: query };
|
|
3317
3239
|
shapeGroups.set(shape, group);
|
|
3318
3240
|
}
|
|
3319
3241
|
group.count++;
|
|
3320
|
-
group.distinctSql.add(
|
|
3242
|
+
group.distinctSql.add(query.sql ?? shape);
|
|
3321
3243
|
}
|
|
3322
|
-
for (const [,
|
|
3323
|
-
if (
|
|
3324
|
-
const info = getQueryInfo(
|
|
3244
|
+
for (const [, shapeGroup] of shapeGroups) {
|
|
3245
|
+
if (shapeGroup.count <= N1_QUERY_THRESHOLD || shapeGroup.distinctSql.size <= 1) continue;
|
|
3246
|
+
const info = getQueryInfo(shapeGroup.first);
|
|
3325
3247
|
const key = `${endpoint}:${info.op}:${info.table || "unknown"}`;
|
|
3326
|
-
if (
|
|
3327
|
-
|
|
3248
|
+
if (reportedKeys.has(key)) continue;
|
|
3249
|
+
reportedKeys.add(key);
|
|
3328
3250
|
insights.push({
|
|
3329
3251
|
severity: "critical",
|
|
3330
3252
|
type: "n1",
|
|
3331
3253
|
title: "N+1 Query Pattern",
|
|
3332
|
-
desc: `${endpoint} runs ${
|
|
3254
|
+
desc: `${endpoint} runs ${shapeGroup.count}x ${info.op} ${info.table} with different params in a single request`,
|
|
3333
3255
|
hint: "This typically happens when fetching related data in a loop. Use a batch query, JOIN, or include/eager-load to fetch all records at once.",
|
|
3334
|
-
|
|
3335
|
-
});
|
|
3336
|
-
}
|
|
3337
|
-
}
|
|
3338
|
-
return insights;
|
|
3339
|
-
}
|
|
3340
|
-
};
|
|
3341
|
-
}
|
|
3342
|
-
});
|
|
3343
|
-
|
|
3344
|
-
// src/analysis/insights/rules/cross-endpoint.ts
|
|
3345
|
-
var crossEndpointRule;
|
|
3346
|
-
var init_cross_endpoint = __esm({
|
|
3347
|
-
"src/analysis/insights/rules/cross-endpoint.ts"() {
|
|
3348
|
-
"use strict";
|
|
3349
|
-
init_query_helpers();
|
|
3350
|
-
init_endpoint();
|
|
3351
|
-
init_constants();
|
|
3352
|
-
crossEndpointRule = {
|
|
3353
|
-
id: "cross-endpoint",
|
|
3354
|
-
check(ctx) {
|
|
3355
|
-
const insights = [];
|
|
3356
|
-
const queryMap = /* @__PURE__ */ new Map();
|
|
3357
|
-
const allEndpoints = /* @__PURE__ */ new Set();
|
|
3358
|
-
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3359
|
-
const req = ctx.reqById.get(reqId);
|
|
3360
|
-
if (!req) continue;
|
|
3361
|
-
const endpoint = getEndpointKey(req.method, req.path);
|
|
3362
|
-
allEndpoints.add(endpoint);
|
|
3363
|
-
const seenInReq = /* @__PURE__ */ new Set();
|
|
3364
|
-
for (const q of reqQueries) {
|
|
3365
|
-
const shape = getQueryShape(q);
|
|
3366
|
-
let entry = queryMap.get(shape);
|
|
3367
|
-
if (!entry) {
|
|
3368
|
-
entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: q };
|
|
3369
|
-
queryMap.set(shape, entry);
|
|
3370
|
-
}
|
|
3371
|
-
entry.count++;
|
|
3372
|
-
if (!seenInReq.has(shape)) {
|
|
3373
|
-
seenInReq.add(shape);
|
|
3374
|
-
entry.endpoints.add(endpoint);
|
|
3375
|
-
}
|
|
3376
|
-
}
|
|
3377
|
-
}
|
|
3378
|
-
if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
|
|
3379
|
-
for (const [, cem] of queryMap) {
|
|
3380
|
-
if (cem.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
|
|
3381
|
-
if (cem.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
|
|
3382
|
-
const p = Math.round(cem.endpoints.size / allEndpoints.size * 100);
|
|
3383
|
-
if (p < CROSS_ENDPOINT_PCT) continue;
|
|
3384
|
-
const info = getQueryInfo(cem.first);
|
|
3385
|
-
const label = info.op + (info.table ? ` ${info.table}` : "");
|
|
3386
|
-
insights.push({
|
|
3387
|
-
severity: "warning",
|
|
3388
|
-
type: "cross-endpoint",
|
|
3389
|
-
title: "Repeated Query Across Endpoints",
|
|
3390
|
-
desc: `${label} runs on ${cem.endpoints.size} of ${allEndpoints.size} endpoints (${p}%).`,
|
|
3391
|
-
hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
|
|
3392
|
-
nav: "queries"
|
|
3256
|
+
detail: `${shapeGroup.count} queries with ${shapeGroup.distinctSql.size} distinct param variations. Example: ${[...shapeGroup.distinctSql][0]?.slice(0, 100) ?? info.op + " " + info.table}`
|
|
3393
3257
|
});
|
|
3394
3258
|
}
|
|
3395
3259
|
}
|
|
3396
3260
|
return insights;
|
|
3397
3261
|
}
|
|
3398
3262
|
};
|
|
3399
|
-
}
|
|
3400
|
-
});
|
|
3401
|
-
|
|
3402
|
-
// src/analysis/insights/rules/redundant-query.ts
|
|
3403
|
-
var redundantQueryRule;
|
|
3404
|
-
var init_redundant_query = __esm({
|
|
3405
|
-
"src/analysis/insights/rules/redundant-query.ts"() {
|
|
3406
|
-
"use strict";
|
|
3407
|
-
init_query_helpers();
|
|
3408
|
-
init_endpoint();
|
|
3409
|
-
init_constants();
|
|
3410
3263
|
redundantQueryRule = {
|
|
3411
3264
|
id: "redundant-query",
|
|
3412
3265
|
check(ctx) {
|
|
3413
3266
|
const insights = [];
|
|
3414
|
-
const
|
|
3267
|
+
const reportedKeys = /* @__PURE__ */ new Set();
|
|
3415
3268
|
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3416
3269
|
const req = ctx.reqById.get(reqId);
|
|
3417
3270
|
if (!req) continue;
|
|
3418
3271
|
const endpoint = getEndpointKey(req.method, req.path);
|
|
3419
|
-
const
|
|
3420
|
-
for (const
|
|
3421
|
-
if (!
|
|
3422
|
-
let entry =
|
|
3272
|
+
const identicalQueryMap = /* @__PURE__ */ new Map();
|
|
3273
|
+
for (const query of reqQueries) {
|
|
3274
|
+
if (!query.sql) continue;
|
|
3275
|
+
let entry = identicalQueryMap.get(query.sql);
|
|
3423
3276
|
if (!entry) {
|
|
3424
|
-
entry = { count: 0, first:
|
|
3425
|
-
|
|
3277
|
+
entry = { count: 0, first: query };
|
|
3278
|
+
identicalQueryMap.set(query.sql, entry);
|
|
3426
3279
|
}
|
|
3427
3280
|
entry.count++;
|
|
3428
3281
|
}
|
|
3429
|
-
for (const [,
|
|
3430
|
-
if (
|
|
3431
|
-
const info = getQueryInfo(
|
|
3282
|
+
for (const [, entry] of identicalQueryMap) {
|
|
3283
|
+
if (entry.count < REDUNDANT_QUERY_MIN_COUNT) continue;
|
|
3284
|
+
const info = getQueryInfo(entry.first);
|
|
3432
3285
|
const label = info.op + (info.table ? ` ${info.table}` : "");
|
|
3433
|
-
const
|
|
3434
|
-
if (
|
|
3435
|
-
|
|
3286
|
+
const deduplicationKey = `${endpoint}:${label}`;
|
|
3287
|
+
if (reportedKeys.has(deduplicationKey)) continue;
|
|
3288
|
+
reportedKeys.add(deduplicationKey);
|
|
3436
3289
|
insights.push({
|
|
3437
3290
|
severity: "warning",
|
|
3438
3291
|
type: "redundant-query",
|
|
3439
3292
|
title: "Redundant Query",
|
|
3440
|
-
desc: `${label} runs ${
|
|
3293
|
+
desc: `${label} runs ${entry.count}x with identical params in ${endpoint}.`,
|
|
3441
3294
|
hint: "The exact same query with identical parameters runs multiple times in one request. Cache the first result or lift the query to a shared function.",
|
|
3442
|
-
|
|
3443
|
-
});
|
|
3444
|
-
}
|
|
3445
|
-
}
|
|
3446
|
-
return insights;
|
|
3447
|
-
}
|
|
3448
|
-
};
|
|
3449
|
-
}
|
|
3450
|
-
});
|
|
3451
|
-
|
|
3452
|
-
// src/analysis/insights/rules/error.ts
|
|
3453
|
-
var errorRule;
|
|
3454
|
-
var init_error = __esm({
|
|
3455
|
-
"src/analysis/insights/rules/error.ts"() {
|
|
3456
|
-
"use strict";
|
|
3457
|
-
errorRule = {
|
|
3458
|
-
id: "error",
|
|
3459
|
-
check(ctx) {
|
|
3460
|
-
if (ctx.errors.length === 0) return [];
|
|
3461
|
-
const insights = [];
|
|
3462
|
-
const groups = /* @__PURE__ */ new Map();
|
|
3463
|
-
for (const e of ctx.errors) {
|
|
3464
|
-
const name = e.name || "Error";
|
|
3465
|
-
groups.set(name, (groups.get(name) ?? 0) + 1);
|
|
3466
|
-
}
|
|
3467
|
-
for (const [name, cnt] of groups) {
|
|
3468
|
-
insights.push({
|
|
3469
|
-
severity: "critical",
|
|
3470
|
-
type: "error",
|
|
3471
|
-
title: "Unhandled Error",
|
|
3472
|
-
desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
|
|
3473
|
-
hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
|
|
3474
|
-
nav: "errors"
|
|
3475
|
-
});
|
|
3476
|
-
}
|
|
3477
|
-
return insights;
|
|
3478
|
-
}
|
|
3479
|
-
};
|
|
3480
|
-
}
|
|
3481
|
-
});
|
|
3482
|
-
|
|
3483
|
-
// src/analysis/insights/rules/error-hotspot.ts
|
|
3484
|
-
var errorHotspotRule;
|
|
3485
|
-
var init_error_hotspot = __esm({
|
|
3486
|
-
"src/analysis/insights/rules/error-hotspot.ts"() {
|
|
3487
|
-
"use strict";
|
|
3488
|
-
init_constants();
|
|
3489
|
-
errorHotspotRule = {
|
|
3490
|
-
id: "error-hotspot",
|
|
3491
|
-
check(ctx) {
|
|
3492
|
-
const insights = [];
|
|
3493
|
-
for (const [ep, g] of ctx.endpointGroups) {
|
|
3494
|
-
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3495
|
-
const errorRate = Math.round(g.errors / g.total * 100);
|
|
3496
|
-
if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
|
|
3497
|
-
insights.push({
|
|
3498
|
-
severity: "critical",
|
|
3499
|
-
type: "error-hotspot",
|
|
3500
|
-
title: "Error Hotspot",
|
|
3501
|
-
desc: `${ep} \u2014 ${errorRate}% error rate (${g.errors}/${g.total} requests)`,
|
|
3502
|
-
hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces.",
|
|
3503
|
-
nav: "requests"
|
|
3504
|
-
});
|
|
3505
|
-
}
|
|
3506
|
-
}
|
|
3507
|
-
return insights;
|
|
3508
|
-
}
|
|
3509
|
-
};
|
|
3510
|
-
}
|
|
3511
|
-
});
|
|
3512
|
-
|
|
3513
|
-
// src/analysis/insights/rules/duplicate.ts
|
|
3514
|
-
var duplicateRule;
|
|
3515
|
-
var init_duplicate = __esm({
|
|
3516
|
-
"src/analysis/insights/rules/duplicate.ts"() {
|
|
3517
|
-
"use strict";
|
|
3518
|
-
init_constants();
|
|
3519
|
-
duplicateRule = {
|
|
3520
|
-
id: "duplicate",
|
|
3521
|
-
check(ctx) {
|
|
3522
|
-
const dupCounts = /* @__PURE__ */ new Map();
|
|
3523
|
-
const flowCount = /* @__PURE__ */ new Map();
|
|
3524
|
-
for (const flow of ctx.flows) {
|
|
3525
|
-
if (!flow.requests) continue;
|
|
3526
|
-
const seenInFlow = /* @__PURE__ */ new Set();
|
|
3527
|
-
for (const fr of flow.requests) {
|
|
3528
|
-
if (!fr.isDuplicate) continue;
|
|
3529
|
-
const dupKey = `${fr.method} ${fr.label ?? fr.path ?? fr.url}`;
|
|
3530
|
-
dupCounts.set(dupKey, (dupCounts.get(dupKey) ?? 0) + 1);
|
|
3531
|
-
if (!seenInFlow.has(dupKey)) {
|
|
3532
|
-
seenInFlow.add(dupKey);
|
|
3533
|
-
flowCount.set(dupKey, (flowCount.get(dupKey) ?? 0) + 1);
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
|
-
}
|
|
3537
|
-
const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
3538
|
-
const insights = [];
|
|
3539
|
-
for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
|
|
3540
|
-
const d = dupEntries[i];
|
|
3541
|
-
insights.push({
|
|
3542
|
-
severity: "warning",
|
|
3543
|
-
type: "duplicate",
|
|
3544
|
-
title: "Duplicate API Call",
|
|
3545
|
-
desc: `${d.key} loaded ${d.count}x as duplicate across ${d.flows} action${d.flows !== 1 ? "s" : ""}`,
|
|
3546
|
-
hint: "Multiple components independently fetch the same endpoint. Lift the fetch to a parent component, use a data cache, or deduplicate with React Query / SWR.",
|
|
3547
|
-
nav: "actions"
|
|
3548
|
-
});
|
|
3549
|
-
}
|
|
3550
|
-
return insights;
|
|
3551
|
-
}
|
|
3552
|
-
};
|
|
3553
|
-
}
|
|
3554
|
-
});
|
|
3555
|
-
|
|
3556
|
-
// src/utils/format.ts
|
|
3557
|
-
function formatDuration(ms) {
|
|
3558
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
3559
|
-
return `${(ms / 1e3).toFixed(1)}s`;
|
|
3560
|
-
}
|
|
3561
|
-
function formatSize(bytes) {
|
|
3562
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
3563
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
3564
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
3565
|
-
}
|
|
3566
|
-
function pct(part, total) {
|
|
3567
|
-
return total > 0 ? Math.round(part / total * 100) : 0;
|
|
3568
|
-
}
|
|
3569
|
-
var init_format = __esm({
|
|
3570
|
-
"src/utils/format.ts"() {
|
|
3571
|
-
"use strict";
|
|
3572
|
-
}
|
|
3573
|
-
});
|
|
3574
|
-
|
|
3575
|
-
// src/analysis/insights/rules/slow.ts
|
|
3576
|
-
var slowRule;
|
|
3577
|
-
var init_slow = __esm({
|
|
3578
|
-
"src/analysis/insights/rules/slow.ts"() {
|
|
3579
|
-
"use strict";
|
|
3580
|
-
init_format();
|
|
3581
|
-
init_constants();
|
|
3582
|
-
slowRule = {
|
|
3583
|
-
id: "slow",
|
|
3584
|
-
check(ctx) {
|
|
3585
|
-
const insights = [];
|
|
3586
|
-
for (const [ep, g] of ctx.endpointGroups) {
|
|
3587
|
-
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3588
|
-
const avgMs = Math.round(g.totalDuration / g.total);
|
|
3589
|
-
if (avgMs < SLOW_ENDPOINT_THRESHOLD_MS) continue;
|
|
3590
|
-
const avgQueryMs = Math.round(g.totalQueryTimeMs / g.total);
|
|
3591
|
-
const avgFetchMs = Math.round(g.totalFetchTimeMs / g.total);
|
|
3592
|
-
const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
|
|
3593
|
-
const parts = [];
|
|
3594
|
-
if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
|
|
3595
|
-
if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
|
|
3596
|
-
if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
|
|
3597
|
-
const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
|
|
3598
|
-
let detail;
|
|
3599
|
-
let slowestMs = 0;
|
|
3600
|
-
for (const [, sd] of g.queryShapeDurations) {
|
|
3601
|
-
const avgShapeMs = sd.totalMs / sd.count;
|
|
3602
|
-
if (avgShapeMs > slowestMs) {
|
|
3603
|
-
slowestMs = avgShapeMs;
|
|
3604
|
-
detail = `Slowest query: ${sd.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${sd.count}x)`;
|
|
3605
|
-
}
|
|
3606
|
-
}
|
|
3607
|
-
insights.push({
|
|
3608
|
-
severity: "warning",
|
|
3609
|
-
type: "slow",
|
|
3610
|
-
title: "Slow Endpoint",
|
|
3611
|
-
desc: `${ep} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
|
|
3612
|
-
hint: avgQueryMs >= avgFetchMs && avgQueryMs >= avgAppMs ? "Most time is in database queries. Check the Queries tab for slow or redundant queries." : avgFetchMs >= avgQueryMs && avgFetchMs >= avgAppMs ? "Most time is in outbound HTTP calls. Check if upstream services are slow or if calls can be parallelized." : "Most time is in application code. Profile the handler for CPU-heavy operations or blocking calls.",
|
|
3613
|
-
detail,
|
|
3614
|
-
nav: "requests"
|
|
3615
|
-
});
|
|
3616
|
-
}
|
|
3617
|
-
return insights;
|
|
3618
|
-
}
|
|
3619
|
-
};
|
|
3620
|
-
}
|
|
3621
|
-
});
|
|
3622
|
-
|
|
3623
|
-
// src/analysis/insights/rules/query-heavy.ts
|
|
3624
|
-
var queryHeavyRule;
|
|
3625
|
-
var init_query_heavy = __esm({
|
|
3626
|
-
"src/analysis/insights/rules/query-heavy.ts"() {
|
|
3627
|
-
"use strict";
|
|
3628
|
-
init_constants();
|
|
3629
|
-
queryHeavyRule = {
|
|
3630
|
-
id: "query-heavy",
|
|
3631
|
-
check(ctx) {
|
|
3632
|
-
const insights = [];
|
|
3633
|
-
for (const [ep, g] of ctx.endpointGroups) {
|
|
3634
|
-
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3635
|
-
const avgQueries = Math.round(g.queryCount / g.total);
|
|
3636
|
-
if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
|
|
3637
|
-
insights.push({
|
|
3638
|
-
severity: "warning",
|
|
3639
|
-
type: "query-heavy",
|
|
3640
|
-
title: "Query-Heavy Endpoint",
|
|
3641
|
-
desc: `${ep} \u2014 avg ${avgQueries} queries/request`,
|
|
3642
|
-
hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches.",
|
|
3643
|
-
nav: "queries"
|
|
3295
|
+
detail: entry.first.sql ? `Query: ${entry.first.sql.slice(0, 120)}` : void 0
|
|
3644
3296
|
});
|
|
3645
3297
|
}
|
|
3646
3298
|
}
|
|
3647
3299
|
return insights;
|
|
3648
3300
|
}
|
|
3649
3301
|
};
|
|
3650
|
-
}
|
|
3651
|
-
});
|
|
3652
|
-
|
|
3653
|
-
// src/analysis/insights/rules/select-star.ts
|
|
3654
|
-
var selectStarRule;
|
|
3655
|
-
var init_select_star = __esm({
|
|
3656
|
-
"src/analysis/insights/rules/select-star.ts"() {
|
|
3657
|
-
"use strict";
|
|
3658
|
-
init_query_helpers();
|
|
3659
|
-
init_constants();
|
|
3660
|
-
init_patterns();
|
|
3661
3302
|
selectStarRule = {
|
|
3662
3303
|
id: "select-star",
|
|
3663
3304
|
check(ctx) {
|
|
3664
|
-
const
|
|
3305
|
+
const tableCounts = /* @__PURE__ */ new Map();
|
|
3665
3306
|
for (const [, reqQueries] of ctx.queriesByReq) {
|
|
3666
|
-
for (const
|
|
3667
|
-
if (!
|
|
3668
|
-
const isSelectStar = SELECT_STAR_RE.test(
|
|
3307
|
+
for (const query of reqQueries) {
|
|
3308
|
+
if (!query.sql) continue;
|
|
3309
|
+
const isSelectStar = SELECT_STAR_RE.test(query.sql.trim()) || SELECT_DOT_STAR_RE.test(query.sql);
|
|
3669
3310
|
if (!isSelectStar) continue;
|
|
3670
|
-
const info = getQueryInfo(
|
|
3671
|
-
const
|
|
3672
|
-
|
|
3311
|
+
const info = getQueryInfo(query);
|
|
3312
|
+
const table = info.table || "unknown";
|
|
3313
|
+
tableCounts.set(table, (tableCounts.get(table) ?? 0) + 1);
|
|
3673
3314
|
}
|
|
3674
3315
|
}
|
|
3675
3316
|
const insights = [];
|
|
3676
|
-
for (const [table, count] of
|
|
3317
|
+
for (const [table, count] of tableCounts) {
|
|
3677
3318
|
if (count < OVERFETCH_MIN_REQUESTS) continue;
|
|
3678
3319
|
insights.push({
|
|
3679
3320
|
severity: "warning",
|
|
3680
3321
|
type: "select-star",
|
|
3681
3322
|
title: "SELECT * Query",
|
|
3682
3323
|
desc: `SELECT * on ${table} \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
|
|
3683
|
-
hint: "SELECT * fetches all columns including ones you don\u2019t need. Specify only required columns to reduce data transfer and memory usage."
|
|
3684
|
-
nav: "queries"
|
|
3324
|
+
hint: "SELECT * fetches all columns including ones you don\u2019t need. Specify only required columns to reduce data transfer and memory usage."
|
|
3685
3325
|
});
|
|
3686
3326
|
}
|
|
3687
3327
|
return insights;
|
|
3688
3328
|
}
|
|
3689
3329
|
};
|
|
3690
|
-
}
|
|
3691
|
-
});
|
|
3692
|
-
|
|
3693
|
-
// src/analysis/insights/rules/high-rows.ts
|
|
3694
|
-
var highRowsRule;
|
|
3695
|
-
var init_high_rows = __esm({
|
|
3696
|
-
"src/analysis/insights/rules/high-rows.ts"() {
|
|
3697
|
-
"use strict";
|
|
3698
|
-
init_query_helpers();
|
|
3699
|
-
init_constants();
|
|
3700
3330
|
highRowsRule = {
|
|
3701
3331
|
id: "high-rows",
|
|
3702
3332
|
check(ctx) {
|
|
3703
3333
|
const seen = /* @__PURE__ */ new Map();
|
|
3704
3334
|
for (const [, reqQueries] of ctx.queriesByReq) {
|
|
3705
|
-
for (const
|
|
3706
|
-
if (!
|
|
3707
|
-
const info = getQueryInfo(
|
|
3335
|
+
for (const query of reqQueries) {
|
|
3336
|
+
if (!query.rowCount || query.rowCount <= HIGH_ROW_COUNT) continue;
|
|
3337
|
+
const info = getQueryInfo(query);
|
|
3708
3338
|
const key = `${info.op} ${info.table || "unknown"}`;
|
|
3709
3339
|
let entry = seen.get(key);
|
|
3710
3340
|
if (!entry) {
|
|
@@ -3712,7 +3342,7 @@ var init_high_rows = __esm({
|
|
|
3712
3342
|
seen.set(key, entry);
|
|
3713
3343
|
}
|
|
3714
3344
|
entry.count++;
|
|
3715
|
-
if (
|
|
3345
|
+
if (query.rowCount > entry.max) entry.max = query.rowCount;
|
|
3716
3346
|
}
|
|
3717
3347
|
}
|
|
3718
3348
|
const insights = [];
|
|
@@ -3723,39 +3353,81 @@ var init_high_rows = __esm({
|
|
|
3723
3353
|
type: "high-rows",
|
|
3724
3354
|
title: "Large Result Set",
|
|
3725
3355
|
desc: `${key} returns ${hrs.max}+ rows (${hrs.count}x)`,
|
|
3726
|
-
hint: "Fetching many rows slows responses and wastes memory. Add a LIMIT clause, implement pagination, or filter with a WHERE condition."
|
|
3727
|
-
nav: "queries"
|
|
3356
|
+
hint: "Fetching many rows slows responses and wastes memory. Add a LIMIT clause, implement pagination, or filter with a WHERE condition."
|
|
3728
3357
|
});
|
|
3729
3358
|
}
|
|
3730
3359
|
return insights;
|
|
3731
3360
|
}
|
|
3732
3361
|
};
|
|
3362
|
+
queryHeavyRule = {
|
|
3363
|
+
id: "query-heavy",
|
|
3364
|
+
check(ctx) {
|
|
3365
|
+
const insights = [];
|
|
3366
|
+
for (const [endpointKey, group] of ctx.endpointGroups) {
|
|
3367
|
+
if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3368
|
+
const avgQueries = Math.round(group.queryCount / group.total);
|
|
3369
|
+
if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
|
|
3370
|
+
insights.push({
|
|
3371
|
+
severity: "warning",
|
|
3372
|
+
type: "query-heavy",
|
|
3373
|
+
title: "Query-Heavy Endpoint",
|
|
3374
|
+
desc: `${endpointKey} \u2014 avg ${avgQueries} queries/request`,
|
|
3375
|
+
hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches."
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
return insights;
|
|
3380
|
+
}
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
});
|
|
3384
|
+
|
|
3385
|
+
// src/utils/format.ts
|
|
3386
|
+
function formatDuration(ms) {
|
|
3387
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
3388
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
3389
|
+
}
|
|
3390
|
+
function formatSize(bytes) {
|
|
3391
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
3392
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
3393
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
3394
|
+
}
|
|
3395
|
+
function pct(part, total) {
|
|
3396
|
+
return total > 0 ? Math.round(part / total * 100) : 0;
|
|
3397
|
+
}
|
|
3398
|
+
var init_format = __esm({
|
|
3399
|
+
"src/utils/format.ts"() {
|
|
3400
|
+
"use strict";
|
|
3733
3401
|
}
|
|
3734
3402
|
});
|
|
3735
3403
|
|
|
3736
|
-
// src/analysis/insights/rules/response-
|
|
3737
|
-
var responseOverfetchRule;
|
|
3738
|
-
var
|
|
3739
|
-
"src/analysis/insights/rules/response-
|
|
3404
|
+
// src/analysis/insights/rules/response-rules.ts
|
|
3405
|
+
var responseOverfetchRule, largeResponseRule;
|
|
3406
|
+
var init_response_rules = __esm({
|
|
3407
|
+
"src/analysis/insights/rules/response-rules.ts"() {
|
|
3740
3408
|
"use strict";
|
|
3741
3409
|
init_endpoint();
|
|
3742
3410
|
init_response();
|
|
3743
3411
|
init_http_status();
|
|
3412
|
+
init_format();
|
|
3744
3413
|
init_patterns();
|
|
3414
|
+
init_log();
|
|
3415
|
+
init_type_guards();
|
|
3745
3416
|
init_constants();
|
|
3746
3417
|
responseOverfetchRule = {
|
|
3747
3418
|
id: "response-overfetch",
|
|
3748
3419
|
check(ctx) {
|
|
3749
3420
|
const insights = [];
|
|
3750
3421
|
const seen = /* @__PURE__ */ new Set();
|
|
3751
|
-
for (const
|
|
3752
|
-
if (isErrorStatus(
|
|
3753
|
-
const
|
|
3754
|
-
if (seen.has(
|
|
3422
|
+
for (const request of ctx.nonStatic) {
|
|
3423
|
+
if (isErrorStatus(request.statusCode) || !request.responseBody) continue;
|
|
3424
|
+
const endpointKey = getEndpointKey(request.method, request.path);
|
|
3425
|
+
if (seen.has(endpointKey)) continue;
|
|
3755
3426
|
let parsed;
|
|
3756
3427
|
try {
|
|
3757
|
-
parsed = JSON.parse(
|
|
3758
|
-
} catch {
|
|
3428
|
+
parsed = JSON.parse(request.responseBody);
|
|
3429
|
+
} catch (e) {
|
|
3430
|
+
brakitDebug(`json parse: ${getErrorMessage(e)}`);
|
|
3759
3431
|
continue;
|
|
3760
3432
|
}
|
|
3761
3433
|
const target = unwrapResponse(parsed);
|
|
@@ -3778,45 +3450,33 @@ var init_response_overfetch = __esm({
|
|
|
3778
3450
|
reasons.push(`${fields.length} fields returned`);
|
|
3779
3451
|
}
|
|
3780
3452
|
if (reasons.length > 0) {
|
|
3781
|
-
seen.add(
|
|
3453
|
+
seen.add(endpointKey);
|
|
3782
3454
|
insights.push({
|
|
3783
3455
|
severity: "info",
|
|
3784
3456
|
type: "response-overfetch",
|
|
3785
3457
|
title: "Response Overfetch",
|
|
3786
|
-
desc: `${
|
|
3787
|
-
hint: "This response returns more data than the client likely needs. Use a DTO or select only required fields to reduce payload size and avoid leaking internal structure."
|
|
3788
|
-
nav: "requests"
|
|
3458
|
+
desc: `${endpointKey} \u2014 ${reasons.join(", ")}`,
|
|
3459
|
+
hint: "This response returns more data than the client likely needs. Use a DTO or select only required fields to reduce payload size and avoid leaking internal structure."
|
|
3789
3460
|
});
|
|
3790
3461
|
}
|
|
3791
3462
|
}
|
|
3792
3463
|
return insights;
|
|
3793
3464
|
}
|
|
3794
3465
|
};
|
|
3795
|
-
}
|
|
3796
|
-
});
|
|
3797
|
-
|
|
3798
|
-
// src/analysis/insights/rules/large-response.ts
|
|
3799
|
-
var largeResponseRule;
|
|
3800
|
-
var init_large_response = __esm({
|
|
3801
|
-
"src/analysis/insights/rules/large-response.ts"() {
|
|
3802
|
-
"use strict";
|
|
3803
|
-
init_format();
|
|
3804
|
-
init_constants();
|
|
3805
3466
|
largeResponseRule = {
|
|
3806
3467
|
id: "large-response",
|
|
3807
3468
|
check(ctx) {
|
|
3808
3469
|
const insights = [];
|
|
3809
|
-
for (const [
|
|
3810
|
-
if (
|
|
3811
|
-
const avgSize = Math.round(
|
|
3470
|
+
for (const [endpointKey, group] of ctx.endpointGroups) {
|
|
3471
|
+
if (group.total < OVERFETCH_MIN_REQUESTS) continue;
|
|
3472
|
+
const avgSize = Math.round(group.totalSize / group.total);
|
|
3812
3473
|
if (avgSize > LARGE_RESPONSE_BYTES) {
|
|
3813
3474
|
insights.push({
|
|
3814
3475
|
severity: "info",
|
|
3815
3476
|
type: "large-response",
|
|
3816
3477
|
title: "Large Response",
|
|
3817
|
-
desc: `${
|
|
3818
|
-
hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression."
|
|
3819
|
-
nav: "requests"
|
|
3478
|
+
desc: `${endpointKey} \u2014 avg ${formatSize(avgSize)} response`,
|
|
3479
|
+
hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression."
|
|
3820
3480
|
});
|
|
3821
3481
|
}
|
|
3822
3482
|
}
|
|
@@ -3826,13 +3486,66 @@ var init_large_response = __esm({
|
|
|
3826
3486
|
}
|
|
3827
3487
|
});
|
|
3828
3488
|
|
|
3829
|
-
// src/analysis/insights/rules/
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3489
|
+
// src/analysis/insights/rules/reliability-rules.ts
|
|
3490
|
+
function getAdaptiveSlowThreshold(endpointKey, previousMetrics) {
|
|
3491
|
+
if (!previousMetrics) return null;
|
|
3492
|
+
const ep = previousMetrics.find((m) => m.endpoint === endpointKey);
|
|
3493
|
+
if (!ep || ep.sessions.length < BASELINE_MIN_SESSIONS) return null;
|
|
3494
|
+
const valid = ep.sessions.filter((s) => s.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION);
|
|
3495
|
+
if (valid.length < BASELINE_MIN_SESSIONS) return null;
|
|
3496
|
+
const p95s = valid.map((s) => s.p95DurationMs).sort((a, b) => a - b);
|
|
3497
|
+
const medianP95 = p95s[Math.floor(p95s.length / 2)];
|
|
3498
|
+
return medianP95 * 2;
|
|
3499
|
+
}
|
|
3500
|
+
var errorRule, errorHotspotRule, regressionRule, slowRule;
|
|
3501
|
+
var init_reliability_rules = __esm({
|
|
3502
|
+
"src/analysis/insights/rules/reliability-rules.ts"() {
|
|
3833
3503
|
"use strict";
|
|
3834
3504
|
init_format();
|
|
3835
3505
|
init_constants();
|
|
3506
|
+
errorRule = {
|
|
3507
|
+
id: "error",
|
|
3508
|
+
check(ctx) {
|
|
3509
|
+
if (ctx.errors.length === 0) return [];
|
|
3510
|
+
const insights = [];
|
|
3511
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3512
|
+
for (const error of ctx.errors) {
|
|
3513
|
+
const name = error.name || "Error";
|
|
3514
|
+
groups.set(name, (groups.get(name) ?? 0) + 1);
|
|
3515
|
+
}
|
|
3516
|
+
for (const [name, cnt] of groups) {
|
|
3517
|
+
insights.push({
|
|
3518
|
+
severity: "critical",
|
|
3519
|
+
type: "error",
|
|
3520
|
+
title: "Unhandled Error",
|
|
3521
|
+
desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
|
|
3522
|
+
hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
|
|
3523
|
+
detail: ctx.errors.find((e) => e.name === name)?.message
|
|
3524
|
+
});
|
|
3525
|
+
}
|
|
3526
|
+
return insights;
|
|
3527
|
+
}
|
|
3528
|
+
};
|
|
3529
|
+
errorHotspotRule = {
|
|
3530
|
+
id: "error-hotspot",
|
|
3531
|
+
check(ctx) {
|
|
3532
|
+
const insights = [];
|
|
3533
|
+
for (const [endpointKey, group] of ctx.endpointGroups) {
|
|
3534
|
+
if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3535
|
+
const errorRate = Math.round(group.errors / group.total * 100);
|
|
3536
|
+
if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
|
|
3537
|
+
insights.push({
|
|
3538
|
+
severity: "critical",
|
|
3539
|
+
type: "error-hotspot",
|
|
3540
|
+
title: "Error Hotspot",
|
|
3541
|
+
desc: `${endpointKey} \u2014 ${errorRate}% error rate (${group.errors}/${group.total} requests)`,
|
|
3542
|
+
hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces."
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
return insights;
|
|
3547
|
+
}
|
|
3548
|
+
};
|
|
3836
3549
|
regressionRule = {
|
|
3837
3550
|
id: "regression",
|
|
3838
3551
|
check(ctx) {
|
|
@@ -3851,8 +3564,7 @@ var init_regression = __esm({
|
|
|
3851
3564
|
type: "regression",
|
|
3852
3565
|
title: "Performance Regression",
|
|
3853
3566
|
desc: `${epMetrics.endpoint} p95 degraded ${formatDuration(prev.p95DurationMs)} \u2192 ${formatDuration(current.p95DurationMs)} (+${p95PctChange}%)`,
|
|
3854
|
-
hint: "This endpoint is slower than the previous session. Check if recent code changes added queries or processing."
|
|
3855
|
-
nav: "graph"
|
|
3567
|
+
hint: "This endpoint is slower than the previous session. Check if recent code changes added queries or processing."
|
|
3856
3568
|
});
|
|
3857
3569
|
}
|
|
3858
3570
|
if (prev.avgQueryCount > 0 && current.avgQueryCount > prev.avgQueryCount * QUERY_COUNT_REGRESSION_RATIO) {
|
|
@@ -3861,8 +3573,136 @@ var init_regression = __esm({
|
|
|
3861
3573
|
type: "regression",
|
|
3862
3574
|
title: "Query Count Regression",
|
|
3863
3575
|
desc: `${epMetrics.endpoint} queries/request increased ${prev.avgQueryCount} \u2192 ${current.avgQueryCount}`,
|
|
3864
|
-
hint: "This endpoint is making more database queries than before. Check for new N+1 patterns or removed query optimizations."
|
|
3865
|
-
|
|
3576
|
+
hint: "This endpoint is making more database queries than before. Check for new N+1 patterns or removed query optimizations."
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
return insights;
|
|
3581
|
+
}
|
|
3582
|
+
};
|
|
3583
|
+
slowRule = {
|
|
3584
|
+
id: "slow",
|
|
3585
|
+
check(ctx) {
|
|
3586
|
+
const insights = [];
|
|
3587
|
+
for (const [endpointKey, group] of ctx.endpointGroups) {
|
|
3588
|
+
if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3589
|
+
const avgMs = Math.round(group.totalDuration / group.total);
|
|
3590
|
+
const threshold = getAdaptiveSlowThreshold(endpointKey, ctx.previousMetrics);
|
|
3591
|
+
if (threshold === null || avgMs < threshold) continue;
|
|
3592
|
+
const avgQueryMs = Math.round(group.totalQueryTimeMs / group.total);
|
|
3593
|
+
const avgFetchMs = Math.round(group.totalFetchTimeMs / group.total);
|
|
3594
|
+
const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
|
|
3595
|
+
const parts = [];
|
|
3596
|
+
if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
|
|
3597
|
+
if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
|
|
3598
|
+
if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
|
|
3599
|
+
const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
|
|
3600
|
+
let detail;
|
|
3601
|
+
let slowestMs = 0;
|
|
3602
|
+
for (const [, shapeDuration] of group.queryShapeDurations) {
|
|
3603
|
+
const avgShapeMs = shapeDuration.totalMs / shapeDuration.count;
|
|
3604
|
+
if (avgShapeMs > slowestMs) {
|
|
3605
|
+
slowestMs = avgShapeMs;
|
|
3606
|
+
detail = `Slowest query: ${shapeDuration.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${shapeDuration.count}x)`;
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
insights.push({
|
|
3610
|
+
severity: "warning",
|
|
3611
|
+
type: "slow",
|
|
3612
|
+
title: "Slow Endpoint",
|
|
3613
|
+
desc: `${endpointKey} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
|
|
3614
|
+
hint: avgQueryMs >= avgFetchMs && avgQueryMs >= avgAppMs ? "Most time is in database queries. Check the Queries tab for slow or redundant queries." : avgFetchMs >= avgQueryMs && avgFetchMs >= avgAppMs ? "Most time is in outbound HTTP calls. Check if upstream services are slow or if calls can be parallelized." : "Most time is in application code. Profile the handler for CPU-heavy operations or blocking calls.",
|
|
3615
|
+
detail
|
|
3616
|
+
});
|
|
3617
|
+
}
|
|
3618
|
+
return insights;
|
|
3619
|
+
}
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
});
|
|
3623
|
+
|
|
3624
|
+
// src/analysis/insights/rules/pattern-rules.ts
|
|
3625
|
+
var duplicateRule, crossEndpointRule;
|
|
3626
|
+
var init_pattern_rules = __esm({
|
|
3627
|
+
"src/analysis/insights/rules/pattern-rules.ts"() {
|
|
3628
|
+
"use strict";
|
|
3629
|
+
init_query_helpers();
|
|
3630
|
+
init_endpoint();
|
|
3631
|
+
init_constants();
|
|
3632
|
+
duplicateRule = {
|
|
3633
|
+
id: "duplicate",
|
|
3634
|
+
check(ctx) {
|
|
3635
|
+
const dupCounts = /* @__PURE__ */ new Map();
|
|
3636
|
+
const flowCount = /* @__PURE__ */ new Map();
|
|
3637
|
+
for (const flow of ctx.flows) {
|
|
3638
|
+
if (!flow.requests) continue;
|
|
3639
|
+
const seenInFlow = /* @__PURE__ */ new Set();
|
|
3640
|
+
for (const request of flow.requests) {
|
|
3641
|
+
if (!request.isDuplicate) continue;
|
|
3642
|
+
const deduplicationKey = `${request.method} ${request.label ?? request.path ?? request.url}`;
|
|
3643
|
+
dupCounts.set(deduplicationKey, (dupCounts.get(deduplicationKey) ?? 0) + 1);
|
|
3644
|
+
if (!seenInFlow.has(deduplicationKey)) {
|
|
3645
|
+
seenInFlow.add(deduplicationKey);
|
|
3646
|
+
flowCount.set(deduplicationKey, (flowCount.get(deduplicationKey) ?? 0) + 1);
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
3651
|
+
const insights = [];
|
|
3652
|
+
for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
|
|
3653
|
+
const duplicate = dupEntries[i];
|
|
3654
|
+
insights.push({
|
|
3655
|
+
severity: "warning",
|
|
3656
|
+
type: "duplicate",
|
|
3657
|
+
title: "Duplicate API Call",
|
|
3658
|
+
desc: `${duplicate.key} loaded ${duplicate.count}x as duplicate across ${duplicate.flows} action${duplicate.flows !== 1 ? "s" : ""}`,
|
|
3659
|
+
hint: "Multiple components independently fetch the same endpoint. Lift the fetch to a parent component, use a data cache, or deduplicate with React Query / SWR."
|
|
3660
|
+
});
|
|
3661
|
+
}
|
|
3662
|
+
return insights;
|
|
3663
|
+
}
|
|
3664
|
+
};
|
|
3665
|
+
crossEndpointRule = {
|
|
3666
|
+
id: "cross-endpoint",
|
|
3667
|
+
check(ctx) {
|
|
3668
|
+
const insights = [];
|
|
3669
|
+
const queryMap = /* @__PURE__ */ new Map();
|
|
3670
|
+
const allEndpoints = /* @__PURE__ */ new Set();
|
|
3671
|
+
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3672
|
+
const req = ctx.reqById.get(reqId);
|
|
3673
|
+
if (!req) continue;
|
|
3674
|
+
const endpoint = getEndpointKey(req.method, req.path);
|
|
3675
|
+
allEndpoints.add(endpoint);
|
|
3676
|
+
const seenInReq = /* @__PURE__ */ new Set();
|
|
3677
|
+
for (const query of reqQueries) {
|
|
3678
|
+
const shape = getQueryShape(query);
|
|
3679
|
+
let entry = queryMap.get(shape);
|
|
3680
|
+
if (!entry) {
|
|
3681
|
+
entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: query };
|
|
3682
|
+
queryMap.set(shape, entry);
|
|
3683
|
+
}
|
|
3684
|
+
entry.count++;
|
|
3685
|
+
if (!seenInReq.has(shape)) {
|
|
3686
|
+
seenInReq.add(shape);
|
|
3687
|
+
entry.endpoints.add(endpoint);
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
|
|
3692
|
+
for (const [, queryMetric] of queryMap) {
|
|
3693
|
+
if (queryMetric.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
|
|
3694
|
+
if (queryMetric.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
|
|
3695
|
+
const coveragePct = Math.round(queryMetric.endpoints.size / allEndpoints.size * 100);
|
|
3696
|
+
if (coveragePct < CROSS_ENDPOINT_PCT) continue;
|
|
3697
|
+
const info = getQueryInfo(queryMetric.first);
|
|
3698
|
+
const label = info.op + (info.table ? ` ${info.table}` : "");
|
|
3699
|
+
insights.push({
|
|
3700
|
+
severity: "warning",
|
|
3701
|
+
type: "cross-endpoint",
|
|
3702
|
+
title: "Repeated Query Across Endpoints",
|
|
3703
|
+
desc: `${label} runs on ${queryMetric.endpoints.size} of ${allEndpoints.size} endpoints (${coveragePct}%).`,
|
|
3704
|
+
hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
|
|
3705
|
+
detail: `Endpoints: ${[...queryMetric.endpoints].slice(0, 5).join(", ")}${queryMetric.endpoints.size > 5 ? ` +${queryMetric.endpoints.size - 5} more` : ""}. Total: ${queryMetric.count} executions.`
|
|
3866
3706
|
});
|
|
3867
3707
|
}
|
|
3868
3708
|
}
|
|
@@ -3881,13 +3721,13 @@ var init_security = __esm({
|
|
|
3881
3721
|
id: "security",
|
|
3882
3722
|
check(ctx) {
|
|
3883
3723
|
if (!ctx.securityFindings) return [];
|
|
3884
|
-
return ctx.securityFindings.map((
|
|
3885
|
-
severity:
|
|
3724
|
+
return ctx.securityFindings.map((finding) => ({
|
|
3725
|
+
severity: finding.severity,
|
|
3886
3726
|
type: "security",
|
|
3887
|
-
title:
|
|
3888
|
-
desc:
|
|
3889
|
-
hint:
|
|
3890
|
-
|
|
3727
|
+
title: finding.title,
|
|
3728
|
+
desc: finding.desc,
|
|
3729
|
+
hint: finding.hint,
|
|
3730
|
+
detail: finding.detail
|
|
3891
3731
|
}));
|
|
3892
3732
|
}
|
|
3893
3733
|
};
|
|
@@ -3898,19 +3738,10 @@ var init_security = __esm({
|
|
|
3898
3738
|
var init_rules2 = __esm({
|
|
3899
3739
|
"src/analysis/insights/rules/index.ts"() {
|
|
3900
3740
|
"use strict";
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
init_error_hotspot();
|
|
3906
|
-
init_duplicate();
|
|
3907
|
-
init_slow();
|
|
3908
|
-
init_query_heavy();
|
|
3909
|
-
init_select_star();
|
|
3910
|
-
init_high_rows();
|
|
3911
|
-
init_response_overfetch();
|
|
3912
|
-
init_large_response();
|
|
3913
|
-
init_regression();
|
|
3741
|
+
init_query_rules();
|
|
3742
|
+
init_response_rules();
|
|
3743
|
+
init_reliability_rules();
|
|
3744
|
+
init_pattern_rules();
|
|
3914
3745
|
init_security();
|
|
3915
3746
|
}
|
|
3916
3747
|
});
|
|
@@ -3969,8 +3800,7 @@ function insightToIssue(insight) {
|
|
|
3969
3800
|
desc: insight.desc,
|
|
3970
3801
|
hint: insight.hint,
|
|
3971
3802
|
detail: insight.detail,
|
|
3972
|
-
endpoint: extractEndpointFromDesc(insight.desc) ?? void 0
|
|
3973
|
-
nav: insight.nav
|
|
3803
|
+
endpoint: extractEndpointFromDesc(insight.desc) ?? void 0
|
|
3974
3804
|
};
|
|
3975
3805
|
}
|
|
3976
3806
|
function securityFindingToIssue(finding) {
|
|
@@ -3981,8 +3811,8 @@ function securityFindingToIssue(finding) {
|
|
|
3981
3811
|
title: finding.title,
|
|
3982
3812
|
desc: finding.desc,
|
|
3983
3813
|
hint: finding.hint,
|
|
3984
|
-
|
|
3985
|
-
|
|
3814
|
+
detail: finding.detail,
|
|
3815
|
+
endpoint: finding.endpoint
|
|
3986
3816
|
};
|
|
3987
3817
|
}
|
|
3988
3818
|
var init_issue_mappers = __esm({
|
|
@@ -3997,7 +3827,7 @@ var AnalysisEngine;
|
|
|
3997
3827
|
var init_engine = __esm({
|
|
3998
3828
|
"src/analysis/engine.ts"() {
|
|
3999
3829
|
"use strict";
|
|
4000
|
-
|
|
3830
|
+
init_config();
|
|
4001
3831
|
init_disposable();
|
|
4002
3832
|
init_group();
|
|
4003
3833
|
init_rules();
|
|
@@ -4006,8 +3836,8 @@ var init_engine = __esm({
|
|
|
4006
3836
|
init_issue_id();
|
|
4007
3837
|
init_prepare();
|
|
4008
3838
|
AnalysisEngine = class {
|
|
4009
|
-
constructor(
|
|
4010
|
-
this.
|
|
3839
|
+
constructor(services, debounceMs = ANALYSIS_DEBOUNCE_MS) {
|
|
3840
|
+
this.services = services;
|
|
4011
3841
|
this.debounceMs = debounceMs;
|
|
4012
3842
|
this.cachedInsights = [];
|
|
4013
3843
|
this.cachedFindings = [];
|
|
@@ -4016,7 +3846,7 @@ var init_engine = __esm({
|
|
|
4016
3846
|
this.scanner = createDefaultScanner();
|
|
4017
3847
|
}
|
|
4018
3848
|
start() {
|
|
4019
|
-
const bus = this.
|
|
3849
|
+
const bus = this.services.bus;
|
|
4020
3850
|
this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
|
|
4021
3851
|
this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
|
|
4022
3852
|
this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
|
|
@@ -4043,12 +3873,12 @@ var init_engine = __esm({
|
|
|
4043
3873
|
}, this.debounceMs);
|
|
4044
3874
|
}
|
|
4045
3875
|
recompute() {
|
|
4046
|
-
const allRequests = this.
|
|
4047
|
-
const queries = this.
|
|
4048
|
-
const errors = this.
|
|
4049
|
-
const logs = this.
|
|
4050
|
-
const fetches = this.
|
|
4051
|
-
const requests =
|
|
3876
|
+
const allRequests = this.services.requestStore.getAll();
|
|
3877
|
+
const queries = this.services.queryStore.getAll();
|
|
3878
|
+
const errors = this.services.errorStore.getAll();
|
|
3879
|
+
const logs = this.services.logStore.getAll();
|
|
3880
|
+
const fetches = this.services.fetchStore.getAll();
|
|
3881
|
+
const requests = keepRecentPerEndpoint(allRequests);
|
|
4052
3882
|
const flows = groupRequestsIntoFlows(requests);
|
|
4053
3883
|
this.cachedFindings = this.scanner.scan({ requests, logs });
|
|
4054
3884
|
this.cachedInsights = computeInsights({
|
|
@@ -4057,33 +3887,29 @@ var init_engine = __esm({
|
|
|
4057
3887
|
errors,
|
|
4058
3888
|
flows,
|
|
4059
3889
|
fetches,
|
|
4060
|
-
previousMetrics: this.
|
|
3890
|
+
previousMetrics: this.services.metricsStore.getAll(),
|
|
4061
3891
|
securityFindings: this.cachedFindings
|
|
4062
3892
|
});
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
issueStore.upsert(insightToIssue(insight), "passive");
|
|
4070
|
-
}
|
|
4071
|
-
const currentIssueIds = /* @__PURE__ */ new Set();
|
|
4072
|
-
for (const finding of this.cachedFindings) {
|
|
4073
|
-
currentIssueIds.add(computeIssueId(securityFindingToIssue(finding)));
|
|
4074
|
-
}
|
|
4075
|
-
for (const insight of this.cachedInsights) {
|
|
4076
|
-
currentIssueIds.add(computeIssueId(insightToIssue(insight)));
|
|
4077
|
-
}
|
|
4078
|
-
const activeEndpoints = extractActiveEndpoints(allRequests);
|
|
4079
|
-
issueStore.reconcile(currentIssueIds, activeEndpoints);
|
|
4080
|
-
const update = {
|
|
4081
|
-
insights: this.cachedInsights,
|
|
4082
|
-
findings: this.cachedFindings,
|
|
4083
|
-
issues: issueStore.getAll()
|
|
4084
|
-
};
|
|
4085
|
-
this.registry.get("event-bus").emit("analysis:updated", update);
|
|
3893
|
+
const issueStore = this.services.issueStore;
|
|
3894
|
+
const currentIssueIds = /* @__PURE__ */ new Set();
|
|
3895
|
+
for (const finding of this.cachedFindings) {
|
|
3896
|
+
const issue = securityFindingToIssue(finding);
|
|
3897
|
+
issueStore.upsert(issue, "passive");
|
|
3898
|
+
currentIssueIds.add(computeIssueId(issue));
|
|
4086
3899
|
}
|
|
3900
|
+
for (const insight of this.cachedInsights) {
|
|
3901
|
+
const issue = insightToIssue(insight);
|
|
3902
|
+
issueStore.upsert(issue, "passive");
|
|
3903
|
+
currentIssueIds.add(computeIssueId(issue));
|
|
3904
|
+
}
|
|
3905
|
+
const activeEndpoints = extractActiveEndpoints(allRequests);
|
|
3906
|
+
issueStore.reconcile(currentIssueIds, activeEndpoints);
|
|
3907
|
+
const update = {
|
|
3908
|
+
insights: this.cachedInsights,
|
|
3909
|
+
findings: this.cachedFindings,
|
|
3910
|
+
issues: issueStore.getAll()
|
|
3911
|
+
};
|
|
3912
|
+
this.services.bus.emit("analysis:updated", update);
|
|
4087
3913
|
}
|
|
4088
3914
|
};
|
|
4089
3915
|
}
|
|
@@ -4101,7 +3927,7 @@ var init_src = __esm({
|
|
|
4101
3927
|
init_engine();
|
|
4102
3928
|
init_insights2();
|
|
4103
3929
|
init_insights();
|
|
4104
|
-
VERSION = "0.
|
|
3930
|
+
VERSION = "0.9.0";
|
|
4105
3931
|
}
|
|
4106
3932
|
});
|
|
4107
3933
|
|
|
@@ -4121,11 +3947,13 @@ function getBaseStyles() {
|
|
|
4121
3947
|
--red:#dc2626;
|
|
4122
3948
|
--cyan:#0891b2;
|
|
4123
3949
|
--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);
|
|
3950
|
+
--amber-bg:rgba(217,119,6,0.07);--red-bg:rgba(220,38,38,0.07);--blue-bg:rgba(37,99,235,0.08);--cyan-bg:rgba(8,145,178,0.07);
|
|
4124
3951
|
--sidebar-width:232px;--header-height:52px;
|
|
4125
3952
|
--radius:8px;--radius-sm:6px;
|
|
4126
|
-
--shadow-sm:0 1px 2px rgba(0,0,0,0.
|
|
4127
|
-
--shadow-md:0
|
|
4128
|
-
--shadow-lg:0 4px
|
|
3953
|
+
--shadow-sm:0 1px 3px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.03);
|
|
3954
|
+
--shadow-md:0 2px 6px rgba(0,0,0,0.08),0 1px 3px rgba(0,0,0,0.04);
|
|
3955
|
+
--shadow-lg:0 4px 16px rgba(0,0,0,0.1),0 2px 6px rgba(0,0,0,0.05);
|
|
3956
|
+
--transition:0.15s ease;
|
|
4129
3957
|
--breakdown-db:#6366f1;--breakdown-fetch:#f59e0b;--breakdown-app:#94a3b8;
|
|
4130
3958
|
--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
|
|
4131
3959
|
--sans:Inter,system-ui,-apple-system,sans-serif;
|
|
@@ -4243,8 +4071,8 @@ function getFlowStyles() {
|
|
|
4243
4071
|
.flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
|
|
4244
4072
|
.flow-badge-pill{font-size:11px;flex-shrink:0;font-family:var(--mono);font-weight:600;padding:2px 10px;border-radius:10px;text-align:center}
|
|
4245
4073
|
.flow-badge-pill.badge-clean{background:var(--green-bg);color:var(--green)}
|
|
4246
|
-
.flow-badge-pill.badge-warn{background:
|
|
4247
|
-
.flow-badge-pill.badge-error{background:
|
|
4074
|
+
.flow-badge-pill.badge-warn{background:var(--amber-bg);color:var(--amber)}
|
|
4075
|
+
.flow-badge-pill.badge-error{background:var(--red-bg);color:var(--red)}
|
|
4248
4076
|
.flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
|
|
4249
4077
|
|
|
4250
4078
|
/* Flow expand panel */
|
|
@@ -4263,23 +4091,23 @@ function getFlowStyles() {
|
|
|
4263
4091
|
/* Method badges */
|
|
4264
4092
|
.method-badge{display:inline-flex;align-items:center;justify-content:center;padding:3px 8px;border-radius:5px;font-size:10px;font-weight:700;font-family:var(--mono);letter-spacing:.3px;flex-shrink:0}
|
|
4265
4093
|
.method-badge-GET{background:var(--green-bg);color:var(--green)}
|
|
4266
|
-
.method-badge-POST{background:
|
|
4267
|
-
.method-badge-PUT,.method-badge-PATCH{background:
|
|
4268
|
-
.method-badge-DELETE{background:
|
|
4094
|
+
.method-badge-POST{background:var(--blue-bg);color:var(--blue)}
|
|
4095
|
+
.method-badge-PUT,.method-badge-PATCH{background:var(--amber-bg);color:var(--amber)}
|
|
4096
|
+
.method-badge-DELETE{background:var(--red-bg);color:var(--red)}
|
|
4269
4097
|
.method-badge-HEAD,.method-badge-OPTIONS{background:var(--bg-muted);color:var(--text-muted)}
|
|
4270
4098
|
|
|
4271
4099
|
/* Status pills */
|
|
4272
4100
|
.status-pill{display:inline-flex;align-items:center;padding:1px 7px;border-radius:4px;font-size:11px;font-weight:600;font-family:var(--mono);flex-shrink:0}
|
|
4273
4101
|
.status-pill-2xx{background:var(--green-bg);color:var(--green)}
|
|
4274
|
-
.status-pill-3xx{background:
|
|
4275
|
-
.status-pill-4xx{background:
|
|
4276
|
-
.status-pill-5xx{background:
|
|
4102
|
+
.status-pill-3xx{background:var(--cyan-bg);color:var(--cyan)}
|
|
4103
|
+
.status-pill-4xx{background:var(--amber-bg);color:var(--amber)}
|
|
4104
|
+
.status-pill-5xx{background:var(--red-bg);color:var(--red)}
|
|
4277
4105
|
|
|
4278
4106
|
.traffic-card-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;font-size:13px}
|
|
4279
4107
|
.traffic-card-path.is-dup{color:var(--text-muted);font-weight:400}
|
|
4280
4108
|
.traffic-card-dur{color:var(--text-muted);font-size:12px;flex-shrink:0}
|
|
4281
4109
|
.traffic-card-size{color:var(--text-muted);font-size:11px;flex-shrink:0}
|
|
4282
|
-
.traffic-card-dup{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:
|
|
4110
|
+
.traffic-card-dup{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:var(--amber-bg);padding:1px 7px;border-radius:4px}
|
|
4283
4111
|
|
|
4284
4112
|
/* Body toggles */
|
|
4285
4113
|
.traffic-body{padding:0;margin-top:8px}
|
|
@@ -4308,7 +4136,7 @@ function getFlowStyles() {
|
|
|
4308
4136
|
.flow-subreq .subreq-label.is-dup{color:var(--text-muted);font-weight:400}
|
|
4309
4137
|
.flow-subreq .subreq-status{flex-shrink:0}
|
|
4310
4138
|
.flow-subreq .subreq-dur{color:var(--text-muted);font-size:12px;text-align:right;flex-shrink:0}
|
|
4311
|
-
.flow-subreq .subreq-dup-tag{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:
|
|
4139
|
+
.flow-subreq .subreq-dup-tag{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:var(--amber-bg);padding:1px 7px;border-radius:4px}
|
|
4312
4140
|
.flow-subreq-detail{display:none;padding:12px 0;border-bottom:1px solid var(--border-subtle)}
|
|
4313
4141
|
.flow-subreq-detail.open{display:block}
|
|
4314
4142
|
|
|
@@ -4337,6 +4165,41 @@ function getFlowStyles() {
|
|
|
4337
4165
|
/* Strict Mode duplicate banner */
|
|
4338
4166
|
.strict-mode-dupe{opacity:0.55}
|
|
4339
4167
|
.strict-mode-banner{font-size:11px;color:var(--text-muted);padding:6px 0 0;font-family:var(--mono)}
|
|
4168
|
+
|
|
4169
|
+
/* Flow detail tabs */
|
|
4170
|
+
.flow-detail-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--border)}
|
|
4171
|
+
.flow-tab{padding:8px 16px;font-size:12px;font-family:var(--mono);font-weight:600;color:var(--text-muted);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .15s;letter-spacing:.3px}
|
|
4172
|
+
.flow-tab:hover{color:var(--text)}
|
|
4173
|
+
.flow-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
|
4174
|
+
|
|
4175
|
+
/* Waterfall chart \u2014 request bars on time axis, sub-events as text rows */
|
|
4176
|
+
.flow-waterfall{padding:0;font-family:var(--mono);font-size:11px}
|
|
4177
|
+
.wf-time-axis{display:flex;justify-content:space-between;font-size:9px;color:var(--text-muted);padding:0 0 6px;margin-left:180px;margin-right:56px;border-bottom:1px solid var(--border);margin-bottom:2px}
|
|
4178
|
+
.wf-rows{display:flex;flex-direction:column;gap:0}
|
|
4179
|
+
|
|
4180
|
+
/* Request group \u2014 request bar + its sub-events */
|
|
4181
|
+
.wf-request-group{border-bottom:1px solid var(--border-subtle);padding:2px 0}
|
|
4182
|
+
.wf-request-group:last-child{border-bottom:none}
|
|
4183
|
+
|
|
4184
|
+
/* Request row \u2014 label | bar on time axis | duration */
|
|
4185
|
+
.wf-req-row{display:flex;align-items:center;gap:0;height:24px;transition:background .1s}
|
|
4186
|
+
.wf-req-row:hover{background:var(--bg-hover)}
|
|
4187
|
+
.wf-req-label{width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;padding:0 10px 0 0;font-size:11px}
|
|
4188
|
+
.wf-bar-track{flex:1;position:relative;height:14px;min-width:0;overflow:hidden}
|
|
4189
|
+
.wf-bar{position:absolute;top:1px;height:12px;border-radius:3px;opacity:0.8;min-width:3px}
|
|
4190
|
+
.wf-req-row:hover .wf-bar{opacity:1}
|
|
4191
|
+
.wf-req-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-muted);font-size:10px;padding-left:8px}
|
|
4192
|
+
|
|
4193
|
+
/* Sub-event rows \u2014 same layout as request rows: label | bar track | duration */
|
|
4194
|
+
.wf-sub-row{display:flex;align-items:center;gap:0;height:20px;transition:background .1s}
|
|
4195
|
+
.wf-sub-row:hover{background:var(--bg-hover)}
|
|
4196
|
+
.wf-sub-label{width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);font-size:10px;padding-left:14px;display:flex;align-items:center;gap:6px}
|
|
4197
|
+
.wf-sub-dot{width:6px;height:6px;border-radius:2px;flex-shrink:0}
|
|
4198
|
+
.wf-sub-bar-sized{height:8px !important;top:3px !important;opacity:0.65}
|
|
4199
|
+
.wf-sub-row:hover .wf-sub-bar-sized{opacity:0.9}
|
|
4200
|
+
.wf-sub-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-dim);font-size:9px;padding-left:8px}
|
|
4201
|
+
|
|
4202
|
+
.wf-loading{color:var(--text-muted);padding:12px 0;font-size:11px;font-family:var(--mono)}
|
|
4340
4203
|
`;
|
|
4341
4204
|
}
|
|
4342
4205
|
var init_flows = __esm({
|
|
@@ -4445,22 +4308,39 @@ function getPerformanceStyles() {
|
|
|
4445
4308
|
.perf-badge-lg{padding:4px 12px;font-size:13px;border-radius:var(--radius-sm)}
|
|
4446
4309
|
.perf-badge-sm{padding:1px 6px;font-size:9px}
|
|
4447
4310
|
|
|
4448
|
-
/* Overview:
|
|
4449
|
-
.perf-
|
|
4450
|
-
.perf-
|
|
4451
|
-
.perf-
|
|
4452
|
-
.perf-
|
|
4453
|
-
.perf-
|
|
4454
|
-
.perf-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
.perf-
|
|
4458
|
-
.perf-
|
|
4459
|
-
.perf-
|
|
4311
|
+
/* Overview: summary cards */
|
|
4312
|
+
.perf-overview{padding:16px 28px}
|
|
4313
|
+
.perf-summary-row{display:flex;gap:8px;margin-bottom:16px}
|
|
4314
|
+
.perf-summary-card{flex:1;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;display:flex;flex-direction:column;gap:4px;box-shadow:var(--shadow-sm)}
|
|
4315
|
+
.perf-summary-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-family:var(--sans);font-weight:600}
|
|
4316
|
+
.perf-summary-value{font-size:20px;font-weight:700;font-family:var(--mono);color:var(--text)}
|
|
4317
|
+
.perf-summary-value-sm{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4318
|
+
|
|
4319
|
+
/* Shared table styles */
|
|
4320
|
+
.perf-table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px}
|
|
4321
|
+
.perf-table thead th{text-align:left;font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600;font-family:var(--sans);padding:10px 14px;border-bottom:2px solid var(--border);white-space:nowrap}
|
|
4322
|
+
.perf-table tbody td{padding:11px 14px;border-bottom:1px solid var(--border-subtle);color:var(--text)}
|
|
4323
|
+
.perf-table-row{cursor:pointer;transition:background var(--transition, .15s ease)}
|
|
4324
|
+
.perf-table-row:hover{background:var(--bg-hover)}
|
|
4325
|
+
.perf-table tbody tr:last-child td{border-bottom:none}
|
|
4326
|
+
.perf-th-right{text-align:right !important}
|
|
4327
|
+
.perf-th-center{text-align:center !important}
|
|
4328
|
+
.perf-td-right{text-align:right}
|
|
4329
|
+
.perf-td-center{text-align:center}
|
|
4330
|
+
.perf-td-name{font-weight:600;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4331
|
+
.perf-td-muted{color:var(--text-dim)}
|
|
4332
|
+
.perf-row-err{background:var(--red-bg)}
|
|
4333
|
+
.perf-row-err:hover{background:rgba(220,38,38,0.1)}
|
|
4334
|
+
|
|
4335
|
+
/* Heat map table wrapper */
|
|
4336
|
+
.perf-heatmap{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm)}
|
|
4337
|
+
.perf-hm-p95{display:inline-flex;align-items:center;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;border:1px solid}
|
|
4338
|
+
.perf-hm-split-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;background:var(--bg-muted);width:100%;min-width:80px}
|
|
4460
4339
|
|
|
4461
4340
|
/* Detail view */
|
|
4462
4341
|
.perf-detail-header{padding:20px 28px 16px;border-bottom:1px solid var(--border-subtle)}
|
|
4463
4342
|
.perf-detail-title{display:flex;align-items:center;gap:12px;font-size:17px;font-weight:600;color:var(--text);font-family:var(--mono)}
|
|
4343
|
+
.perf-baseline-hint{font-size:11px;font-weight:400;color:var(--text-muted);padding:2px 8px;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius-sm)}
|
|
4464
4344
|
.perf-metric-row{display:flex;gap:4px;padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
4465
4345
|
.perf-metric-card{flex:1;background:var(--bg-muted);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px 16px;display:flex;flex-direction:column;gap:4px}
|
|
4466
4346
|
.perf-metric-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-family:var(--sans);font-weight:600}
|
|
@@ -4495,22 +4375,42 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
|
|
|
4495
4375
|
.perf-canvas{border-radius:var(--radius);background:var(--bg-muted);border:1px solid var(--border)}
|
|
4496
4376
|
.perf-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:10px}
|
|
4497
4377
|
|
|
4498
|
-
/* Request history
|
|
4378
|
+
/* Request history */
|
|
4499
4379
|
.perf-history-wrap{padding:0 28px 20px}
|
|
4500
|
-
.perf-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
.perf-
|
|
4504
|
-
.perf-
|
|
4505
|
-
.perf-
|
|
4506
|
-
.perf-
|
|
4507
|
-
.perf-
|
|
4508
|
-
.perf-
|
|
4509
|
-
.perf-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
.perf-
|
|
4513
|
-
.perf-
|
|
4380
|
+
.perf-hist-row-hl{background:rgba(37,99,235,0.1) !important;border-left:3px solid #4ade80}
|
|
4381
|
+
|
|
4382
|
+
/* Callers section */
|
|
4383
|
+
.perf-callers{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
4384
|
+
.perf-callers-list{display:flex;flex-direction:column;gap:0}
|
|
4385
|
+
.perf-caller-row{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
|
|
4386
|
+
.perf-caller-row:last-child{border-bottom:none}
|
|
4387
|
+
.perf-caller-name{flex:1;font-weight:500;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4388
|
+
.perf-caller-count{color:var(--text-muted);font-size:11px;flex-shrink:0}
|
|
4389
|
+
.perf-caller-avg{color:var(--text-dim);font-size:11px;flex-shrink:0}
|
|
4390
|
+
|
|
4391
|
+
/* Query breakdown section */
|
|
4392
|
+
.perf-queries{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
4393
|
+
.perf-queries-loading{font-size:11px;color:var(--text-muted);font-family:var(--mono)}
|
|
4394
|
+
.perf-queries-list{display:flex;flex-direction:column;gap:0}
|
|
4395
|
+
.perf-query-row{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
|
|
4396
|
+
.perf-query-row:last-child{border-bottom:none}
|
|
4397
|
+
.perf-query-label{flex:1;font-weight:500;color:var(--accent);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4398
|
+
.perf-query-avg{color:var(--text-muted);font-size:11px;flex-shrink:0}
|
|
4399
|
+
.perf-query-count{color:var(--text-dim);font-size:11px;flex-shrink:0}
|
|
4400
|
+
|
|
4401
|
+
/* Session trends */
|
|
4402
|
+
.perf-trends{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
4403
|
+
.perf-trends-list{display:flex;flex-direction:column;gap:0}
|
|
4404
|
+
.perf-trend-row{display:flex;align-items:center;gap:14px;padding:8px 12px;border-bottom:1px solid var(--border-subtle);font-family:var(--mono);font-size:12px}
|
|
4405
|
+
.perf-trend-row:last-child{border-bottom:none}
|
|
4406
|
+
.perf-trend-current{background:var(--bg-muted);border-radius:var(--radius-sm);font-weight:600}
|
|
4407
|
+
.perf-trend-time{width:80px;color:var(--text-dim);font-size:11px;flex-shrink:0}
|
|
4408
|
+
.perf-trend-p95{flex-shrink:0}
|
|
4409
|
+
.perf-trend-reqs{color:var(--text-muted);font-size:11px;flex-shrink:0}
|
|
4410
|
+
.perf-trend-errs{font-size:11px;flex-shrink:0}
|
|
4411
|
+
.perf-trend-arrow{font-size:10px;font-weight:600;flex-shrink:0}
|
|
4412
|
+
.perf-trend-slower{color:var(--red)}
|
|
4413
|
+
.perf-trend-faster{color:var(--green)}
|
|
4514
4414
|
`;
|
|
4515
4415
|
}
|
|
4516
4416
|
var init_graph = __esm({
|
|
@@ -4526,9 +4426,10 @@ function getOverviewStyles() {
|
|
|
4526
4426
|
.ov-container{padding:24px 28px}
|
|
4527
4427
|
|
|
4528
4428
|
/* Summary banner */
|
|
4529
|
-
.ov-summary{display:flex;gap:
|
|
4530
|
-
.ov-stat{display:flex;flex-direction:column;gap:
|
|
4531
|
-
.ov-stat
|
|
4429
|
+
.ov-summary{display:flex;gap:10px;margin-bottom:24px;flex-wrap:wrap}
|
|
4430
|
+
.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)}
|
|
4431
|
+
.ov-stat:hover{box-shadow:var(--shadow-md)}
|
|
4432
|
+
.ov-stat-value{font-size:22px;font-weight:700;font-family:var(--mono);color:var(--text);line-height:1.2}
|
|
4532
4433
|
.ov-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
|
|
4533
4434
|
|
|
4534
4435
|
/* Section header */
|
|
@@ -4537,8 +4438,8 @@ function getOverviewStyles() {
|
|
|
4537
4438
|
|
|
4538
4439
|
/* Insight cards */
|
|
4539
4440
|
.ov-cards{display:flex;flex-direction:column;gap:8px}
|
|
4540
|
-
.ov-card{display:flex;align-items:flex-start;gap:14px;padding:
|
|
4541
|
-
.ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md)}
|
|
4441
|
+
.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)}
|
|
4442
|
+
.ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md);transform:translateY(-1px)}
|
|
4542
4443
|
.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}
|
|
4543
4444
|
.ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
|
|
4544
4445
|
.ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
|
|
@@ -4547,6 +4448,7 @@ function getOverviewStyles() {
|
|
|
4547
4448
|
.ov-card-body{flex:1;min-width:0}
|
|
4548
4449
|
.ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
|
|
4549
4450
|
.ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
|
|
4451
|
+
.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}
|
|
4550
4452
|
.ov-card-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
|
|
4551
4453
|
.ov-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;margin-top:2px;font-family:var(--mono);transition:transform .15s}
|
|
4552
4454
|
|
|
@@ -4842,7 +4744,7 @@ function getTimelineStyles() {
|
|
|
4842
4744
|
}
|
|
4843
4745
|
`;
|
|
4844
4746
|
}
|
|
4845
|
-
var
|
|
4747
|
+
var init_timeline = __esm({
|
|
4846
4748
|
"src/dashboard/styles/timeline.ts"() {
|
|
4847
4749
|
"use strict";
|
|
4848
4750
|
}
|
|
@@ -4862,7 +4764,7 @@ var init_styles = __esm({
|
|
|
4862
4764
|
init_graph();
|
|
4863
4765
|
init_overview();
|
|
4864
4766
|
init_security2();
|
|
4865
|
-
|
|
4767
|
+
init_timeline();
|
|
4866
4768
|
}
|
|
4867
4769
|
});
|
|
4868
4770
|
|
|
@@ -4957,10 +4859,10 @@ function isTelemetryEnabled() {
|
|
|
4957
4859
|
return cachedEnabled;
|
|
4958
4860
|
}
|
|
4959
4861
|
var CONFIG_DIR, CONFIG_PATH, cachedEnabled;
|
|
4960
|
-
var
|
|
4862
|
+
var init_config2 = __esm({
|
|
4961
4863
|
"src/telemetry/config.ts"() {
|
|
4962
4864
|
"use strict";
|
|
4963
|
-
|
|
4865
|
+
init_features();
|
|
4964
4866
|
CONFIG_DIR = join3(homedir2(), ".brakit");
|
|
4965
4867
|
CONFIG_PATH = join3(CONFIG_DIR, "config.json");
|
|
4966
4868
|
cachedEnabled = null;
|
|
@@ -5001,12 +4903,12 @@ function speedBucket(ms) {
|
|
|
5001
4903
|
}
|
|
5002
4904
|
return `>${t[t.length - 1]}ms`;
|
|
5003
4905
|
}
|
|
5004
|
-
function trackSession(
|
|
4906
|
+
function trackSession(services) {
|
|
5005
4907
|
if (!isTelemetryEnabled()) return;
|
|
5006
4908
|
const isFirstSession = readConfig() === null;
|
|
5007
4909
|
const config = getOrCreateConfig();
|
|
5008
|
-
const metricsStore =
|
|
5009
|
-
const analysisEngine =
|
|
4910
|
+
const metricsStore = services.metricsStore;
|
|
4911
|
+
const analysisEngine = services.analysisEngine;
|
|
5010
4912
|
const live = metricsStore.getLiveEndpoints();
|
|
5011
4913
|
const insights = analysisEngine.getInsights();
|
|
5012
4914
|
const findings = analysisEngine.getFindings();
|
|
@@ -5034,9 +4936,9 @@ function trackSession(registry) {
|
|
|
5034
4936
|
first_session: isFirstSession,
|
|
5035
4937
|
adapters_detected: session.adapters,
|
|
5036
4938
|
request_count: session.requestCount,
|
|
5037
|
-
error_count:
|
|
5038
|
-
query_count:
|
|
5039
|
-
fetch_count:
|
|
4939
|
+
error_count: services.errorStore.getAll().length,
|
|
4940
|
+
query_count: services.queryStore.getAll().length,
|
|
4941
|
+
fetch_count: services.fetchStore.getAll().length,
|
|
5040
4942
|
insight_count: insights.length,
|
|
5041
4943
|
finding_count: findings.length,
|
|
5042
4944
|
insight_types: [...session.insightTypes],
|
|
@@ -5069,13 +4971,13 @@ function trackSession(registry) {
|
|
|
5069
4971
|
}
|
|
5070
4972
|
}
|
|
5071
4973
|
var POSTHOG_KEY, session;
|
|
5072
|
-
var
|
|
4974
|
+
var init_telemetry = __esm({
|
|
5073
4975
|
"src/telemetry/index.ts"() {
|
|
5074
4976
|
"use strict";
|
|
5075
4977
|
init_src();
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
4978
|
+
init_config2();
|
|
4979
|
+
init_labels();
|
|
4980
|
+
init_config2();
|
|
5079
4981
|
POSTHOG_KEY = "phc_E9TwydCGnSfPLIUhNxChpeg32TSowjk31KiPhnLPP0x";
|
|
5080
4982
|
session = {
|
|
5081
4983
|
startTime: 0,
|
|
@@ -5097,32 +4999,30 @@ var init_telemetry2 = __esm({
|
|
|
5097
4999
|
function isDashboardRequest(url) {
|
|
5098
5000
|
return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
|
|
5099
5001
|
}
|
|
5100
|
-
function createDashboardHandler(
|
|
5101
|
-
const metricsStore =
|
|
5002
|
+
function createDashboardHandler(services) {
|
|
5003
|
+
const metricsStore = services.metricsStore;
|
|
5102
5004
|
const routes = {
|
|
5103
|
-
[DASHBOARD_API_REQUESTS]: createRequestsHandler(
|
|
5104
|
-
[DASHBOARD_API_EVENTS]: createSSEHandler(
|
|
5105
|
-
[DASHBOARD_API_FLOWS]: createFlowsHandler(
|
|
5106
|
-
[DASHBOARD_API_CLEAR]: createClearHandler(
|
|
5107
|
-
[DASHBOARD_API_LOGS]: createLogsHandler(
|
|
5108
|
-
[DASHBOARD_API_FETCHES]: createFetchesHandler(
|
|
5109
|
-
[DASHBOARD_API_ERRORS]: createErrorsHandler(
|
|
5110
|
-
[DASHBOARD_API_QUERIES]: createQueriesHandler(
|
|
5005
|
+
[DASHBOARD_API_REQUESTS]: createRequestsHandler(services),
|
|
5006
|
+
[DASHBOARD_API_EVENTS]: createSSEHandler(services),
|
|
5007
|
+
[DASHBOARD_API_FLOWS]: createFlowsHandler(services),
|
|
5008
|
+
[DASHBOARD_API_CLEAR]: createClearHandler(services),
|
|
5009
|
+
[DASHBOARD_API_LOGS]: createLogsHandler(services),
|
|
5010
|
+
[DASHBOARD_API_FETCHES]: createFetchesHandler(services),
|
|
5011
|
+
[DASHBOARD_API_ERRORS]: createErrorsHandler(services),
|
|
5012
|
+
[DASHBOARD_API_QUERIES]: createQueriesHandler(services),
|
|
5111
5013
|
[DASHBOARD_API_METRICS]: createMetricsHandler(metricsStore),
|
|
5112
5014
|
[DASHBOARD_API_METRICS_LIVE]: createLiveMetricsHandler(metricsStore),
|
|
5113
|
-
[DASHBOARD_API_INGEST]: createIngestHandler(
|
|
5114
|
-
[DASHBOARD_API_ACTIVITY]: createActivityHandler(
|
|
5015
|
+
[DASHBOARD_API_INGEST]: createIngestHandler(services),
|
|
5016
|
+
[DASHBOARD_API_ACTIVITY]: createActivityHandler(services)
|
|
5115
5017
|
};
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
);
|
|
5125
|
-
}
|
|
5018
|
+
const issueStore = services.issueStore;
|
|
5019
|
+
routes[DASHBOARD_API_INSIGHTS] = createIssuesHandler(issueStore);
|
|
5020
|
+
routes[DASHBOARD_API_SECURITY] = createIssuesHandler(issueStore);
|
|
5021
|
+
routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(issueStore);
|
|
5022
|
+
routes[DASHBOARD_API_FINDINGS_REPORT] = createIssuesReportHandler(
|
|
5023
|
+
issueStore,
|
|
5024
|
+
services.bus
|
|
5025
|
+
);
|
|
5126
5026
|
routes[DASHBOARD_API_TAB] = (req, res) => {
|
|
5127
5027
|
const raw = (req.url ?? "").split("tab=")[1];
|
|
5128
5028
|
if (raw) {
|
|
@@ -5133,7 +5033,7 @@ function createDashboardHandler(registry) {
|
|
|
5133
5033
|
res.end();
|
|
5134
5034
|
};
|
|
5135
5035
|
return (req, res, config) => {
|
|
5136
|
-
const path = (req.url ?? "/")
|
|
5036
|
+
const path = stripQueryString(req.url ?? "/");
|
|
5137
5037
|
const handler = routes[path];
|
|
5138
5038
|
if (handler) {
|
|
5139
5039
|
handler(req, res);
|
|
@@ -5151,13 +5051,14 @@ function createDashboardHandler(registry) {
|
|
|
5151
5051
|
var init_router = __esm({
|
|
5152
5052
|
"src/dashboard/router.ts"() {
|
|
5153
5053
|
"use strict";
|
|
5054
|
+
init_endpoint();
|
|
5154
5055
|
init_constants();
|
|
5155
|
-
|
|
5056
|
+
init_labels();
|
|
5156
5057
|
init_api();
|
|
5157
5058
|
init_issues();
|
|
5158
5059
|
init_sse();
|
|
5159
5060
|
init_page();
|
|
5160
|
-
|
|
5061
|
+
init_telemetry();
|
|
5161
5062
|
}
|
|
5162
5063
|
});
|
|
5163
5064
|
|
|
@@ -5198,51 +5099,6 @@ var init_event_bus = __esm({
|
|
|
5198
5099
|
}
|
|
5199
5100
|
});
|
|
5200
5101
|
|
|
5201
|
-
// src/core/service-registry.ts
|
|
5202
|
-
var ServiceRegistry;
|
|
5203
|
-
var init_service_registry = __esm({
|
|
5204
|
-
"src/core/service-registry.ts"() {
|
|
5205
|
-
"use strict";
|
|
5206
|
-
ServiceRegistry = class {
|
|
5207
|
-
constructor() {
|
|
5208
|
-
this.services = /* @__PURE__ */ new Map();
|
|
5209
|
-
}
|
|
5210
|
-
register(name, service) {
|
|
5211
|
-
this.services.set(name, service);
|
|
5212
|
-
}
|
|
5213
|
-
get(name) {
|
|
5214
|
-
const service = this.services.get(name);
|
|
5215
|
-
if (!service) throw new Error(`Service "${name}" not registered`);
|
|
5216
|
-
return service;
|
|
5217
|
-
}
|
|
5218
|
-
has(name) {
|
|
5219
|
-
return this.services.has(name);
|
|
5220
|
-
}
|
|
5221
|
-
};
|
|
5222
|
-
}
|
|
5223
|
-
});
|
|
5224
|
-
|
|
5225
|
-
// src/utils/static-patterns.ts
|
|
5226
|
-
function isStaticPath(urlPath) {
|
|
5227
|
-
return STATIC_PATTERNS.some((p) => p.test(urlPath));
|
|
5228
|
-
}
|
|
5229
|
-
var STATIC_PATTERNS;
|
|
5230
|
-
var init_static_patterns = __esm({
|
|
5231
|
-
"src/utils/static-patterns.ts"() {
|
|
5232
|
-
"use strict";
|
|
5233
|
-
STATIC_PATTERNS = [
|
|
5234
|
-
/\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
|
|
5235
|
-
/^\/favicon/,
|
|
5236
|
-
/^\/node_modules\//,
|
|
5237
|
-
// Framework-specific static/internal paths
|
|
5238
|
-
/^\/_next\//,
|
|
5239
|
-
/^\/__nextjs/,
|
|
5240
|
-
/^\/@vite\//,
|
|
5241
|
-
/^\/__vite/
|
|
5242
|
-
];
|
|
5243
|
-
}
|
|
5244
|
-
});
|
|
5245
|
-
|
|
5246
5102
|
// src/store/request-store.ts
|
|
5247
5103
|
function flattenHeaders(headers2) {
|
|
5248
5104
|
const flat = {};
|
|
@@ -5258,6 +5114,7 @@ var init_request_store = __esm({
|
|
|
5258
5114
|
"use strict";
|
|
5259
5115
|
init_constants();
|
|
5260
5116
|
init_static_patterns();
|
|
5117
|
+
init_endpoint();
|
|
5261
5118
|
RequestStore = class {
|
|
5262
5119
|
constructor(maxEntries = MAX_REQUEST_ENTRIES) {
|
|
5263
5120
|
this.maxEntries = maxEntries;
|
|
@@ -5266,7 +5123,7 @@ var init_request_store = __esm({
|
|
|
5266
5123
|
}
|
|
5267
5124
|
capture(input) {
|
|
5268
5125
|
const url = input.url;
|
|
5269
|
-
const path = url
|
|
5126
|
+
const path = stripQueryString(url);
|
|
5270
5127
|
let requestBodyStr = null;
|
|
5271
5128
|
if (input.requestBody && input.requestBody.length > 0) {
|
|
5272
5129
|
requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
|
|
@@ -5291,7 +5148,8 @@ var init_request_store = __esm({
|
|
|
5291
5148
|
startedAt: input.startTime,
|
|
5292
5149
|
durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
|
|
5293
5150
|
responseSize: input.responseBody?.length ?? 0,
|
|
5294
|
-
isStatic: isStaticPath(path)
|
|
5151
|
+
isStatic: isStaticPath(path),
|
|
5152
|
+
isHealthCheck: isHealthCheckPath(path)
|
|
5295
5153
|
};
|
|
5296
5154
|
this.requests.push(entry);
|
|
5297
5155
|
if (this.requests.length > this.maxEntries) {
|
|
@@ -5368,50 +5226,6 @@ var init_telemetry_store = __esm({
|
|
|
5368
5226
|
}
|
|
5369
5227
|
});
|
|
5370
5228
|
|
|
5371
|
-
// src/store/fetch-store.ts
|
|
5372
|
-
var FetchStore;
|
|
5373
|
-
var init_fetch_store = __esm({
|
|
5374
|
-
"src/store/fetch-store.ts"() {
|
|
5375
|
-
"use strict";
|
|
5376
|
-
init_telemetry_store();
|
|
5377
|
-
FetchStore = class extends TelemetryStore {
|
|
5378
|
-
};
|
|
5379
|
-
}
|
|
5380
|
-
});
|
|
5381
|
-
|
|
5382
|
-
// src/store/log-store.ts
|
|
5383
|
-
var LogStore;
|
|
5384
|
-
var init_log_store = __esm({
|
|
5385
|
-
"src/store/log-store.ts"() {
|
|
5386
|
-
"use strict";
|
|
5387
|
-
init_telemetry_store();
|
|
5388
|
-
LogStore = class extends TelemetryStore {
|
|
5389
|
-
};
|
|
5390
|
-
}
|
|
5391
|
-
});
|
|
5392
|
-
|
|
5393
|
-
// src/store/error-store.ts
|
|
5394
|
-
var ErrorStore;
|
|
5395
|
-
var init_error_store = __esm({
|
|
5396
|
-
"src/store/error-store.ts"() {
|
|
5397
|
-
"use strict";
|
|
5398
|
-
init_telemetry_store();
|
|
5399
|
-
ErrorStore = class extends TelemetryStore {
|
|
5400
|
-
};
|
|
5401
|
-
}
|
|
5402
|
-
});
|
|
5403
|
-
|
|
5404
|
-
// src/store/query-store.ts
|
|
5405
|
-
var QueryStore;
|
|
5406
|
-
var init_query_store = __esm({
|
|
5407
|
-
"src/store/query-store.ts"() {
|
|
5408
|
-
"use strict";
|
|
5409
|
-
init_telemetry_store();
|
|
5410
|
-
QueryStore = class extends TelemetryStore {
|
|
5411
|
-
};
|
|
5412
|
-
}
|
|
5413
|
-
});
|
|
5414
|
-
|
|
5415
5229
|
// src/utils/math.ts
|
|
5416
5230
|
function percentile(values, p) {
|
|
5417
5231
|
if (values.length === 0) return 0;
|
|
@@ -5458,6 +5272,17 @@ var init_metrics_store = __esm({
|
|
|
5458
5272
|
this.dirty = false;
|
|
5459
5273
|
this.accumulators = /* @__PURE__ */ new Map();
|
|
5460
5274
|
this.pendingPoints = /* @__PURE__ */ new Map();
|
|
5275
|
+
/**
|
|
5276
|
+
* Compute the adaptive performance baseline for an endpoint.
|
|
5277
|
+
* Returns the median p95 across historical sessions, or null when
|
|
5278
|
+
* there isn't enough data to establish a meaningful baseline.
|
|
5279
|
+
*/
|
|
5280
|
+
/**
|
|
5281
|
+
* Cached baselines — invalidated on flush (when sessions change) and
|
|
5282
|
+
* on new request recordings (when pending points grow). Avoids recomputing
|
|
5283
|
+
* on every getLiveEndpoints() API call.
|
|
5284
|
+
*/
|
|
5285
|
+
this.baselineCache = /* @__PURE__ */ new Map();
|
|
5461
5286
|
this.data = { version: 1, endpoints: [] };
|
|
5462
5287
|
}
|
|
5463
5288
|
start() {
|
|
@@ -5482,7 +5307,7 @@ var init_metrics_store = __esm({
|
|
|
5482
5307
|
this.flush(true);
|
|
5483
5308
|
}
|
|
5484
5309
|
recordRequest(req, metrics) {
|
|
5485
|
-
if (req.isStatic) return;
|
|
5310
|
+
if (req.isStatic || req.isHealthCheck) return;
|
|
5486
5311
|
this.dirty = true;
|
|
5487
5312
|
const key = getEndpointKey(req.method, req.path);
|
|
5488
5313
|
let acc = this.accumulators.get(key);
|
|
@@ -5531,6 +5356,38 @@ var init_metrics_store = __esm({
|
|
|
5531
5356
|
getEndpoint(endpoint) {
|
|
5532
5357
|
return this.endpointIndex.get(endpoint);
|
|
5533
5358
|
}
|
|
5359
|
+
getEndpointBaseline(endpoint) {
|
|
5360
|
+
const pending2 = this.pendingPoints.get(endpoint);
|
|
5361
|
+
const pointCount = pending2?.length ?? 0;
|
|
5362
|
+
const cached = this.baselineCache.get(endpoint);
|
|
5363
|
+
if (cached && cached.pointCount === pointCount) return cached.value;
|
|
5364
|
+
const value = this.computeBaseline(endpoint, pending2);
|
|
5365
|
+
this.baselineCache.set(endpoint, { value, pointCount });
|
|
5366
|
+
return value;
|
|
5367
|
+
}
|
|
5368
|
+
computeBaseline(endpoint, pending2) {
|
|
5369
|
+
const ep = this.endpointIndex.get(endpoint);
|
|
5370
|
+
if (ep && ep.sessions.length >= BASELINE_MIN_SESSIONS) {
|
|
5371
|
+
const validSessions = ep.sessions.filter(
|
|
5372
|
+
(s) => s.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION
|
|
5373
|
+
);
|
|
5374
|
+
if (validSessions.length >= BASELINE_MIN_SESSIONS) {
|
|
5375
|
+
const p95s = validSessions.map((s) => s.p95DurationMs).sort((a, b) => a - b);
|
|
5376
|
+
return p95s[Math.floor(p95s.length / 2)];
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
if (ep && ep.sessions.length === 1) {
|
|
5380
|
+
const session2 = ep.sessions[0];
|
|
5381
|
+
if (session2.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION) {
|
|
5382
|
+
return session2.p95DurationMs;
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
5385
|
+
if (pending2 && pending2.length >= BASELINE_PENDING_POINTS_MIN) {
|
|
5386
|
+
const warmDurations = pending2.slice(1).map((r) => r.durationMs).sort((a, b) => a - b);
|
|
5387
|
+
return warmDurations[Math.floor(warmDurations.length / 2)];
|
|
5388
|
+
}
|
|
5389
|
+
return null;
|
|
5390
|
+
}
|
|
5534
5391
|
getLiveEndpoints() {
|
|
5535
5392
|
const merged = /* @__PURE__ */ new Map();
|
|
5536
5393
|
for (const ep of this.data.endpoints) {
|
|
@@ -5545,27 +5402,35 @@ var init_metrics_store = __esm({
|
|
|
5545
5402
|
const endpoints = [];
|
|
5546
5403
|
for (const [endpoint, requests] of merged) {
|
|
5547
5404
|
if (requests.length === 0) continue;
|
|
5548
|
-
const
|
|
5405
|
+
const warmRequests = requests.length > 1 ? requests.slice(1) : requests;
|
|
5406
|
+
const warmDurations = warmRequests.map((r) => r.durationMs);
|
|
5549
5407
|
const errors = requests.filter((r) => isErrorStatus(r.statusCode)).length;
|
|
5550
|
-
const totalQueries =
|
|
5551
|
-
const totalQueryTime =
|
|
5552
|
-
const totalFetchTime =
|
|
5553
|
-
const n =
|
|
5554
|
-
const avgDurationMs = Math.round(
|
|
5408
|
+
const totalQueries = warmRequests.reduce((s, r) => s + r.queryCount, 0);
|
|
5409
|
+
const totalQueryTime = warmRequests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
|
|
5410
|
+
const totalFetchTime = warmRequests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
|
|
5411
|
+
const n = warmRequests.length;
|
|
5412
|
+
const avgDurationMs = Math.round(warmDurations.reduce((s, d) => s + d, 0) / n);
|
|
5555
5413
|
const avgQueryTimeMs = Math.round(totalQueryTime / n);
|
|
5556
5414
|
const avgFetchTimeMs = Math.round(totalFetchTime / n);
|
|
5415
|
+
const p95Ms = percentile(warmDurations, 0.95);
|
|
5416
|
+
const medianMs = percentile(warmDurations, 0.5);
|
|
5417
|
+
const epData = this.endpointIndex.get(endpoint);
|
|
5557
5418
|
endpoints.push({
|
|
5558
5419
|
endpoint,
|
|
5559
5420
|
requests,
|
|
5560
5421
|
summary: {
|
|
5561
|
-
p95Ms
|
|
5562
|
-
|
|
5422
|
+
p95Ms,
|
|
5423
|
+
medianMs,
|
|
5424
|
+
errorRate: errors / requests.length,
|
|
5425
|
+
// Error rate uses ALL requests
|
|
5563
5426
|
avgQueryCount: Math.round(totalQueries / n),
|
|
5564
|
-
totalRequests:
|
|
5427
|
+
totalRequests: requests.length,
|
|
5565
5428
|
avgQueryTimeMs,
|
|
5566
5429
|
avgFetchTimeMs,
|
|
5567
5430
|
avgAppTimeMs: Math.max(0, avgDurationMs - avgQueryTimeMs - avgFetchTimeMs)
|
|
5568
|
-
}
|
|
5431
|
+
},
|
|
5432
|
+
sessions: epData?.sessions,
|
|
5433
|
+
baselineP95Ms: this.getEndpointBaseline(endpoint)
|
|
5569
5434
|
});
|
|
5570
5435
|
}
|
|
5571
5436
|
endpoints.sort((a, b) => b.summary.p95Ms - a.summary.p95Ms);
|
|
@@ -5576,6 +5441,7 @@ var init_metrics_store = __esm({
|
|
|
5576
5441
|
this.endpointIndex.clear();
|
|
5577
5442
|
this.accumulators.clear();
|
|
5578
5443
|
this.pendingPoints.clear();
|
|
5444
|
+
this.baselineCache.clear();
|
|
5579
5445
|
this.dirty = false;
|
|
5580
5446
|
this.persistence.remove();
|
|
5581
5447
|
}
|
|
@@ -5617,6 +5483,7 @@ var init_metrics_store = __esm({
|
|
|
5617
5483
|
epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
|
|
5618
5484
|
}
|
|
5619
5485
|
this.pendingPoints.clear();
|
|
5486
|
+
this.baselineCache.clear();
|
|
5620
5487
|
if (!this.dirty) return;
|
|
5621
5488
|
if (sync) {
|
|
5622
5489
|
this.persistence.saveSync(this.data);
|
|
@@ -5710,10 +5577,6 @@ var init_store = __esm({
|
|
|
5710
5577
|
"use strict";
|
|
5711
5578
|
init_request_store();
|
|
5712
5579
|
init_telemetry_store();
|
|
5713
|
-
init_fetch_store();
|
|
5714
|
-
init_log_store();
|
|
5715
|
-
init_error_store();
|
|
5716
|
-
init_query_store();
|
|
5717
5580
|
init_metrics_store();
|
|
5718
5581
|
init_persistence();
|
|
5719
5582
|
}
|
|
@@ -5745,9 +5608,9 @@ function formatConsoleLine(issue, suffix) {
|
|
|
5745
5608
|
}
|
|
5746
5609
|
return line;
|
|
5747
5610
|
}
|
|
5748
|
-
function startTerminalInsights(
|
|
5749
|
-
const bus =
|
|
5750
|
-
const metricsStore =
|
|
5611
|
+
function startTerminalInsights(services, proxyPort) {
|
|
5612
|
+
const bus = services.bus;
|
|
5613
|
+
const metricsStore = services.metricsStore;
|
|
5751
5614
|
const printedKeys = /* @__PURE__ */ new Set();
|
|
5752
5615
|
const resolvedKeys = /* @__PURE__ */ new Set();
|
|
5753
5616
|
const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
|
|
@@ -5818,8 +5681,8 @@ var init_terminal = __esm({
|
|
|
5818
5681
|
"use strict";
|
|
5819
5682
|
init_src();
|
|
5820
5683
|
init_constants();
|
|
5821
|
-
|
|
5822
|
-
|
|
5684
|
+
init_config();
|
|
5685
|
+
init_labels();
|
|
5823
5686
|
SEVERITY_COLOR = {
|
|
5824
5687
|
critical: pc.red,
|
|
5825
5688
|
warning: pc.yellow,
|
|
@@ -5914,8 +5777,14 @@ function outgoingToIncoming(headers2) {
|
|
|
5914
5777
|
}
|
|
5915
5778
|
return result;
|
|
5916
5779
|
}
|
|
5780
|
+
function getDecompressor(encoding) {
|
|
5781
|
+
if (encoding === CONTENT_ENCODING_GZIP) return gunzip;
|
|
5782
|
+
if (encoding === CONTENT_ENCODING_BR) return brotliDecompress;
|
|
5783
|
+
if (encoding === CONTENT_ENCODING_DEFLATE) return inflate;
|
|
5784
|
+
return null;
|
|
5785
|
+
}
|
|
5917
5786
|
function decompressAsync(body, encoding) {
|
|
5918
|
-
const decompressor = encoding
|
|
5787
|
+
const decompressor = getDecompressor(encoding);
|
|
5919
5788
|
if (!decompressor) return Promise.resolve(body);
|
|
5920
5789
|
return new Promise((resolve6) => {
|
|
5921
5790
|
decompressor(body, (err, result) => {
|
|
@@ -5929,7 +5798,7 @@ function toBuffer(chunk) {
|
|
|
5929
5798
|
if (typeof chunk === "string") return Buffer.from(chunk);
|
|
5930
5799
|
return null;
|
|
5931
5800
|
}
|
|
5932
|
-
function captureInProcess(req, res, requestId, requestStore) {
|
|
5801
|
+
function captureInProcess(req, res, requestId, requestStore, isChild = false) {
|
|
5933
5802
|
const startTime = performance.now();
|
|
5934
5803
|
const method = req.method ?? "GET";
|
|
5935
5804
|
const resChunks = [];
|
|
@@ -5975,30 +5844,32 @@ function captureInProcess(req, res, requestId, requestStore) {
|
|
|
5975
5844
|
const responseHeaders = outgoingToIncoming(res.getHeaders());
|
|
5976
5845
|
const responseContentType = String(res.getHeader("content-type") ?? "");
|
|
5977
5846
|
const capturedChunks = resChunks.slice();
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
body
|
|
5847
|
+
if (!isChild) {
|
|
5848
|
+
void (async () => {
|
|
5849
|
+
try {
|
|
5850
|
+
let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
|
|
5851
|
+
if (body && encoding && !truncated) {
|
|
5852
|
+
body = await decompressAsync(body, encoding);
|
|
5853
|
+
}
|
|
5854
|
+
requestStore.capture({
|
|
5855
|
+
requestId,
|
|
5856
|
+
method,
|
|
5857
|
+
url: req.url ?? "/",
|
|
5858
|
+
requestHeaders: req.headers,
|
|
5859
|
+
requestBody: null,
|
|
5860
|
+
statusCode,
|
|
5861
|
+
responseHeaders,
|
|
5862
|
+
responseBody: body,
|
|
5863
|
+
responseContentType,
|
|
5864
|
+
startTime,
|
|
5865
|
+
endTime,
|
|
5866
|
+
config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
|
|
5867
|
+
});
|
|
5868
|
+
} catch (e) {
|
|
5869
|
+
brakitDebug(`capture store: ${getErrorMessage(e)}`);
|
|
5983
5870
|
}
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
method,
|
|
5987
|
-
url: req.url ?? "/",
|
|
5988
|
-
requestHeaders: req.headers,
|
|
5989
|
-
requestBody: null,
|
|
5990
|
-
statusCode,
|
|
5991
|
-
responseHeaders,
|
|
5992
|
-
responseBody: body,
|
|
5993
|
-
responseContentType,
|
|
5994
|
-
startTime,
|
|
5995
|
-
endTime,
|
|
5996
|
-
config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
|
|
5997
|
-
});
|
|
5998
|
-
} catch (e) {
|
|
5999
|
-
brakitDebug(`capture store: ${getErrorMessage(e)}`);
|
|
6000
|
-
}
|
|
6001
|
-
})();
|
|
5871
|
+
})();
|
|
5872
|
+
}
|
|
6002
5873
|
return result;
|
|
6003
5874
|
};
|
|
6004
5875
|
}
|
|
@@ -6046,13 +5917,15 @@ function installInterceptor(deps) {
|
|
|
6046
5917
|
deps.handleDashboard(req, res, deps.config);
|
|
6047
5918
|
return true;
|
|
6048
5919
|
}
|
|
6049
|
-
const
|
|
5920
|
+
const propagated = req.headers[BRAKIT_REQUEST_ID_HEADER];
|
|
5921
|
+
const requestId = propagated ?? randomUUID8();
|
|
5922
|
+
const isChild = propagated !== void 0;
|
|
6050
5923
|
const ctx = {
|
|
6051
5924
|
requestId,
|
|
6052
5925
|
url,
|
|
6053
5926
|
method: req.method ?? "GET"
|
|
6054
5927
|
};
|
|
6055
|
-
captureInProcess(req, res, requestId, deps.requestStore);
|
|
5928
|
+
captureInProcess(req, res, requestId, deps.requestStore, isChild);
|
|
6056
5929
|
return storage.run(
|
|
6057
5930
|
ctx,
|
|
6058
5931
|
() => original.apply(this, [event, ...args])
|
|
@@ -6075,7 +5948,8 @@ var init_interceptor = __esm({
|
|
|
6075
5948
|
init_safe_wrap();
|
|
6076
5949
|
init_guard();
|
|
6077
5950
|
init_capture();
|
|
6078
|
-
|
|
5951
|
+
init_labels();
|
|
5952
|
+
init_constants();
|
|
6079
5953
|
originalEmit = null;
|
|
6080
5954
|
}
|
|
6081
5955
|
});
|
|
@@ -6093,18 +5967,12 @@ function setup() {
|
|
|
6093
5967
|
initPromise = doSetup();
|
|
6094
5968
|
return initPromise;
|
|
6095
5969
|
}
|
|
6096
|
-
function createStores(bus
|
|
5970
|
+
function createStores(bus) {
|
|
6097
5971
|
const requestStore = new RequestStore();
|
|
6098
|
-
const fetchStore = new
|
|
6099
|
-
const logStore = new
|
|
6100
|
-
const errorStore = new
|
|
6101
|
-
const queryStore = new
|
|
6102
|
-
registry.register("event-bus", bus);
|
|
6103
|
-
registry.register("request-store", requestStore);
|
|
6104
|
-
registry.register("fetch-store", fetchStore);
|
|
6105
|
-
registry.register("log-store", logStore);
|
|
6106
|
-
registry.register("error-store", errorStore);
|
|
6107
|
-
registry.register("query-store", queryStore);
|
|
5972
|
+
const fetchStore = new TelemetryStore();
|
|
5973
|
+
const logStore = new TelemetryStore();
|
|
5974
|
+
const errorStore = new TelemetryStore();
|
|
5975
|
+
const queryStore = new TelemetryStore();
|
|
6108
5976
|
bus.on("telemetry:fetch", (data) => fetchStore.add(data));
|
|
6109
5977
|
bus.on("telemetry:query", (data) => queryStore.add(data));
|
|
6110
5978
|
bus.on("telemetry:log", (data) => logStore.add(data));
|
|
@@ -6138,17 +6006,16 @@ function installHooks(bus) {
|
|
|
6138
6006
|
adapterNames: adapterRegistry.getActive().map((a) => a.name)
|
|
6139
6007
|
};
|
|
6140
6008
|
}
|
|
6141
|
-
function startAnalysis(
|
|
6142
|
-
const bus = registry.get("event-bus");
|
|
6009
|
+
function startAnalysis(bus, stores, dataDir, services) {
|
|
6143
6010
|
const metricsStore = new MetricsStore(new FileMetricsPersistence(dataDir));
|
|
6144
6011
|
metricsStore.start();
|
|
6145
|
-
registry.register("metrics-store", metricsStore);
|
|
6146
6012
|
const issueStore = new IssueStore(dataDir);
|
|
6147
6013
|
issueStore.start();
|
|
6148
|
-
|
|
6149
|
-
|
|
6014
|
+
services.metricsStore = metricsStore;
|
|
6015
|
+
services.issueStore = issueStore;
|
|
6016
|
+
const analysisEngine = new AnalysisEngine(services);
|
|
6150
6017
|
analysisEngine.start();
|
|
6151
|
-
|
|
6018
|
+
services.analysisEngine = analysisEngine;
|
|
6152
6019
|
bus.on("request:completed", (req) => {
|
|
6153
6020
|
const queries = stores.queryStore.getByRequest(req.id);
|
|
6154
6021
|
const fetches = stores.fetchStore.getByRequest(req.id);
|
|
@@ -6160,7 +6027,7 @@ function startAnalysis(registry, stores, dataDir) {
|
|
|
6160
6027
|
});
|
|
6161
6028
|
return { analysisEngine, metricsStore, issueStore };
|
|
6162
6029
|
}
|
|
6163
|
-
function registerLifecycle(
|
|
6030
|
+
function registerLifecycle(allServices, stores, services, cwd) {
|
|
6164
6031
|
let telemetrySent = false;
|
|
6165
6032
|
const sendTelemetry = () => {
|
|
6166
6033
|
if (telemetrySent) return;
|
|
@@ -6172,7 +6039,7 @@ function registerLifecycle(registry, stores, services, cwd) {
|
|
|
6172
6039
|
recordRulesTriggered(
|
|
6173
6040
|
services.analysisEngine.getFindings().map((f) => f.rule)
|
|
6174
6041
|
);
|
|
6175
|
-
trackSession(
|
|
6042
|
+
trackSession(allServices);
|
|
6176
6043
|
};
|
|
6177
6044
|
let teardownCalled = false;
|
|
6178
6045
|
const runTeardown = () => {
|
|
@@ -6201,20 +6068,23 @@ function registerLifecycle(registry, stores, services, cwd) {
|
|
|
6201
6068
|
async function doSetup() {
|
|
6202
6069
|
brakitDebug(`[setup] doSetup called at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
6203
6070
|
const bus = new EventBus();
|
|
6204
|
-
const registry = new ServiceRegistry();
|
|
6205
6071
|
const cwd = process.cwd();
|
|
6206
|
-
const stores = createStores(bus
|
|
6072
|
+
const stores = createStores(bus);
|
|
6073
|
+
const services = {
|
|
6074
|
+
bus,
|
|
6075
|
+
...stores
|
|
6076
|
+
};
|
|
6207
6077
|
const { framework, adapterNames } = installHooks(bus);
|
|
6208
6078
|
initSession(framework, detectPackageManagerSync(cwd), false, adapterNames);
|
|
6209
6079
|
const dataDir = getProjectDataDir(cwd);
|
|
6210
|
-
const
|
|
6080
|
+
const analysisServices = startAnalysis(bus, stores, dataDir, services);
|
|
6211
6081
|
const config = {
|
|
6212
6082
|
proxyPort: 0,
|
|
6213
6083
|
targetPort: 0,
|
|
6214
6084
|
showStatic: false,
|
|
6215
6085
|
maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
|
|
6216
6086
|
};
|
|
6217
|
-
const handleDashboard = createDashboardHandler(
|
|
6087
|
+
const handleDashboard = createDashboardHandler(services);
|
|
6218
6088
|
installInterceptor({
|
|
6219
6089
|
handleDashboard,
|
|
6220
6090
|
config,
|
|
@@ -6247,14 +6117,14 @@ async function doSetup() {
|
|
|
6247
6117
|
brakitDebug(`port file write failed: ${getErrorMessage(err)}`);
|
|
6248
6118
|
}
|
|
6249
6119
|
})();
|
|
6250
|
-
startTerminalInsights(
|
|
6120
|
+
startTerminalInsights(services, port);
|
|
6251
6121
|
process.stdout.write(
|
|
6252
6122
|
` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
|
|
6253
6123
|
`
|
|
6254
6124
|
);
|
|
6255
6125
|
}
|
|
6256
6126
|
});
|
|
6257
|
-
registerLifecycle(
|
|
6127
|
+
registerLifecycle(services, stores, analysisServices, cwd);
|
|
6258
6128
|
}
|
|
6259
6129
|
var initPromise;
|
|
6260
6130
|
var init_setup = __esm({
|
|
@@ -6266,12 +6136,8 @@ var init_setup = __esm({
|
|
|
6266
6136
|
init_adapters();
|
|
6267
6137
|
init_router();
|
|
6268
6138
|
init_event_bus();
|
|
6269
|
-
init_service_registry();
|
|
6270
6139
|
init_request_store();
|
|
6271
|
-
|
|
6272
|
-
init_log_store();
|
|
6273
|
-
init_error_store();
|
|
6274
|
-
init_query_store();
|
|
6140
|
+
init_telemetry_store();
|
|
6275
6141
|
init_store();
|
|
6276
6142
|
init_issue_store();
|
|
6277
6143
|
init_engine();
|
|
@@ -6284,7 +6150,7 @@ var init_setup = __esm({
|
|
|
6284
6150
|
init_type_guards();
|
|
6285
6151
|
init_fs();
|
|
6286
6152
|
init_project();
|
|
6287
|
-
|
|
6153
|
+
init_telemetry();
|
|
6288
6154
|
initPromise = null;
|
|
6289
6155
|
}
|
|
6290
6156
|
});
|