brakit 0.8.7 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/dist/api.d.ts +85 -46
- package/dist/api.js +782 -767
- package/dist/bin/brakit.js +456 -450
- 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 +1747 -1770
- 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, TELEMETRY_EVENT_SETUP_COMPLETED, TELEMETRY_EVENT_FIRST_REQUEST, TELEMETRY_EVENT_DASHBOARD_VIEWED, TELEMETRY_EVENT_SESSION, EXIT_REASON_CLEAN, EXIT_REASON_SIGINT, EXIT_REASON_SIGTERM, DETAIL_PREVIEW_LENGTH, KNOWN_DEPENDENCY_NAMES;
|
|
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,74 @@ 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"]);
|
|
93
|
+
TELEMETRY_EVENT_SETUP_COMPLETED = "setup_completed";
|
|
94
|
+
TELEMETRY_EVENT_FIRST_REQUEST = "first_request";
|
|
95
|
+
TELEMETRY_EVENT_DASHBOARD_VIEWED = "dashboard_viewed";
|
|
96
|
+
TELEMETRY_EVENT_SESSION = "session";
|
|
97
|
+
EXIT_REASON_CLEAN = "clean";
|
|
98
|
+
EXIT_REASON_SIGINT = "sigint";
|
|
99
|
+
EXIT_REASON_SIGTERM = "sigterm";
|
|
100
|
+
DETAIL_PREVIEW_LENGTH = 120;
|
|
101
|
+
KNOWN_DEPENDENCY_NAMES = [
|
|
102
|
+
"next",
|
|
103
|
+
"@remix-run/dev",
|
|
104
|
+
"nuxt",
|
|
105
|
+
"vite",
|
|
106
|
+
"astro",
|
|
107
|
+
"express",
|
|
108
|
+
"fastify",
|
|
109
|
+
"hono",
|
|
110
|
+
"koa",
|
|
111
|
+
"nest",
|
|
112
|
+
"prisma",
|
|
113
|
+
"drizzle-orm",
|
|
114
|
+
"typeorm",
|
|
115
|
+
"sequelize"
|
|
116
|
+
];
|
|
149
117
|
}
|
|
150
118
|
});
|
|
151
119
|
|
|
152
|
-
// src/constants/
|
|
153
|
-
var BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES;
|
|
154
|
-
var
|
|
155
|
-
"src/constants/
|
|
120
|
+
// src/constants/labels.ts
|
|
121
|
+
var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, VALID_TABS_TUPLE, VALID_TABS, BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES, HTTP_OK, HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, HTTP_INTERNAL_ERROR, SECURITY_HEADERS, CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE, SEVERITY_ICON, SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES, SDK_EVENT_REQUEST, SDK_EVENT_DB_QUERY, SDK_EVENT_FETCH, SDK_EVENT_LOG, SDK_EVENT_ERROR, SDK_EVENT_AUTH_CHECK, POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS, TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY;
|
|
122
|
+
var init_labels = __esm({
|
|
123
|
+
"src/constants/labels.ts"() {
|
|
156
124
|
"use strict";
|
|
125
|
+
DASHBOARD_PREFIX = "/__brakit";
|
|
126
|
+
DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
|
|
127
|
+
DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
|
|
128
|
+
DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
|
|
129
|
+
DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
|
|
130
|
+
DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
|
|
131
|
+
DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
|
|
132
|
+
DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
|
|
133
|
+
DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
|
|
134
|
+
DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
|
|
135
|
+
DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
|
|
136
|
+
DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
|
|
137
|
+
DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
|
|
138
|
+
DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
|
|
139
|
+
DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
|
|
140
|
+
DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
141
|
+
DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
142
|
+
DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
143
|
+
VALID_TABS_TUPLE = [
|
|
144
|
+
"overview",
|
|
145
|
+
"actions",
|
|
146
|
+
"requests",
|
|
147
|
+
"fetches",
|
|
148
|
+
"queries",
|
|
149
|
+
"errors",
|
|
150
|
+
"logs",
|
|
151
|
+
"performance",
|
|
152
|
+
"security"
|
|
153
|
+
];
|
|
154
|
+
VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
157
155
|
BRAKIT_REQUEST_ID_HEADER = "x-brakit-request-id";
|
|
158
156
|
BRAKIT_FETCH_ID_HEADER = "x-brakit-fetch-id";
|
|
159
157
|
SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
@@ -164,13 +162,53 @@ var init_headers = __esm({
|
|
|
164
162
|
"x-api-key",
|
|
165
163
|
"x-auth-token"
|
|
166
164
|
]);
|
|
165
|
+
HTTP_OK = 200;
|
|
166
|
+
HTTP_NO_CONTENT = 204;
|
|
167
|
+
HTTP_BAD_REQUEST = 400;
|
|
168
|
+
HTTP_NOT_FOUND = 404;
|
|
169
|
+
HTTP_METHOD_NOT_ALLOWED = 405;
|
|
170
|
+
HTTP_PAYLOAD_TOO_LARGE = 413;
|
|
171
|
+
HTTP_INTERNAL_ERROR = 500;
|
|
172
|
+
SECURITY_HEADERS = {
|
|
173
|
+
"x-content-type-options": "nosniff",
|
|
174
|
+
"x-frame-options": "DENY",
|
|
175
|
+
"referrer-policy": "no-referrer",
|
|
176
|
+
"content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
|
|
177
|
+
};
|
|
178
|
+
CONTENT_ENCODING_GZIP = "gzip";
|
|
179
|
+
CONTENT_ENCODING_BR = "br";
|
|
180
|
+
CONTENT_ENCODING_DEFLATE = "deflate";
|
|
181
|
+
SEVERITY_ICON = {
|
|
182
|
+
critical: "\u2717",
|
|
183
|
+
warning: "\u26A0",
|
|
184
|
+
info: "\u2139"
|
|
185
|
+
};
|
|
186
|
+
SSE_EVENT_FETCH = "fetch";
|
|
187
|
+
SSE_EVENT_LOG = "log";
|
|
188
|
+
SSE_EVENT_ERROR = "error_event";
|
|
189
|
+
SSE_EVENT_QUERY = "query";
|
|
190
|
+
SSE_EVENT_ISSUES = "issues";
|
|
191
|
+
SDK_EVENT_REQUEST = "request";
|
|
192
|
+
SDK_EVENT_DB_QUERY = "db.query";
|
|
193
|
+
SDK_EVENT_FETCH = "fetch";
|
|
194
|
+
SDK_EVENT_LOG = "log";
|
|
195
|
+
SDK_EVENT_ERROR = "error";
|
|
196
|
+
SDK_EVENT_AUTH_CHECK = "auth.check";
|
|
197
|
+
POSTHOG_HOST = "https://us.i.posthog.com";
|
|
198
|
+
POSTHOG_CAPTURE_PATH = "/i/v0/e/";
|
|
199
|
+
POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
|
|
200
|
+
SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
|
|
201
|
+
TIMELINE_FETCH = "fetch";
|
|
202
|
+
TIMELINE_LOG = "log";
|
|
203
|
+
TIMELINE_ERROR = "error";
|
|
204
|
+
TIMELINE_QUERY = "query";
|
|
167
205
|
}
|
|
168
206
|
});
|
|
169
207
|
|
|
170
|
-
// src/constants/
|
|
208
|
+
// src/constants/features.ts
|
|
171
209
|
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/
|
|
210
|
+
var init_features = __esm({
|
|
211
|
+
"src/constants/features.ts"() {
|
|
174
212
|
"use strict";
|
|
175
213
|
CLOUD_SIGNALS = [
|
|
176
214
|
"VERCEL",
|
|
@@ -203,112 +241,13 @@ var init_network = __esm({
|
|
|
203
241
|
}
|
|
204
242
|
});
|
|
205
243
|
|
|
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
244
|
// src/constants/index.ts
|
|
294
245
|
var init_constants = __esm({
|
|
295
246
|
"src/constants/index.ts"() {
|
|
296
247
|
"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();
|
|
248
|
+
init_config();
|
|
249
|
+
init_labels();
|
|
250
|
+
init_features();
|
|
312
251
|
}
|
|
313
252
|
});
|
|
314
253
|
|
|
@@ -377,7 +316,7 @@ function setupFetchHook(emit) {
|
|
|
377
316
|
statusCode: msg.response.statusCode ?? 0,
|
|
378
317
|
durationMs: Math.round(performance.now() - info.startTime),
|
|
379
318
|
parentRequestId: info.parentRequestId,
|
|
380
|
-
timestamp:
|
|
319
|
+
timestamp: performance.now()
|
|
381
320
|
}
|
|
382
321
|
});
|
|
383
322
|
});
|
|
@@ -407,7 +346,7 @@ function setupConsoleHook(emit) {
|
|
|
407
346
|
const ctx = getRequestContext();
|
|
408
347
|
if (!ctx) return;
|
|
409
348
|
const message = format(...args);
|
|
410
|
-
const timestamp =
|
|
349
|
+
const timestamp = performance.now();
|
|
411
350
|
const parentRequestId = ctx.requestId;
|
|
412
351
|
if (level === "error") {
|
|
413
352
|
const errorArg = args.find((a) => a instanceof Error);
|
|
@@ -474,7 +413,7 @@ function createCaptureError(emit) {
|
|
|
474
413
|
message: error.message,
|
|
475
414
|
stack: error.stack ?? "",
|
|
476
415
|
parentRequestId: ctx?.requestId ?? null,
|
|
477
|
-
timestamp:
|
|
416
|
+
timestamp: performance.now()
|
|
478
417
|
}
|
|
479
418
|
});
|
|
480
419
|
};
|
|
@@ -507,6 +446,7 @@ var init_adapter_registry = __esm({
|
|
|
507
446
|
constructor() {
|
|
508
447
|
this.adapters = [];
|
|
509
448
|
this.active = [];
|
|
449
|
+
this.failed = [];
|
|
510
450
|
}
|
|
511
451
|
register(adapter) {
|
|
512
452
|
this.adapters.push(adapter);
|
|
@@ -519,6 +459,7 @@ var init_adapter_registry = __esm({
|
|
|
519
459
|
this.active.push(adapter);
|
|
520
460
|
}
|
|
521
461
|
} catch {
|
|
462
|
+
this.failed.push(adapter.name);
|
|
522
463
|
}
|
|
523
464
|
}
|
|
524
465
|
}
|
|
@@ -534,69 +475,35 @@ var init_adapter_registry = __esm({
|
|
|
534
475
|
getActive() {
|
|
535
476
|
return this.active;
|
|
536
477
|
}
|
|
478
|
+
getFailed() {
|
|
479
|
+
return this.failed;
|
|
480
|
+
}
|
|
537
481
|
};
|
|
538
482
|
}
|
|
539
483
|
});
|
|
540
484
|
|
|
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
485
|
// src/instrument/adapters/normalize.ts
|
|
563
486
|
function normalizeSQL(sql) {
|
|
564
487
|
if (!sql) return { op: "OTHER", table: "" };
|
|
565
488
|
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
|
-
}
|
|
489
|
+
const keyword = trimmed.split(/\s+/, 1)[0].toUpperCase();
|
|
490
|
+
const op = VALID_OPS.has(keyword) ? keyword : "OTHER";
|
|
491
|
+
const table = trimmed.match(TABLE_RE)?.[1] ?? "";
|
|
492
|
+
return { op, table };
|
|
585
493
|
}
|
|
586
494
|
function normalizePrismaOp(operation) {
|
|
587
495
|
return PRISMA_OP_MAP[operation] ?? "OTHER";
|
|
588
496
|
}
|
|
589
497
|
function normalizeQueryParams(sql) {
|
|
590
498
|
if (!sql) return null;
|
|
591
|
-
|
|
592
|
-
n = n.replace(/\b\d+(\.\d+)?\b/g, "?");
|
|
593
|
-
n = n.replace(/\$\d+/g, "?");
|
|
594
|
-
return n;
|
|
499
|
+
return sql.replace(SQL_PARAM_MARKER, "?").replace(SQL_STRING_LITERAL, "?").replace(SQL_NUMBER_LITERAL, "?");
|
|
595
500
|
}
|
|
596
|
-
var PRISMA_OP_MAP;
|
|
501
|
+
var VALID_OPS, TABLE_RE, PRISMA_OP_MAP, SQL_PARAM_MARKER, SQL_STRING_LITERAL, SQL_NUMBER_LITERAL;
|
|
597
502
|
var init_normalize = __esm({
|
|
598
503
|
"src/instrument/adapters/normalize.ts"() {
|
|
599
504
|
"use strict";
|
|
505
|
+
VALID_OPS = /* @__PURE__ */ new Set(["SELECT", "INSERT", "UPDATE", "DELETE"]);
|
|
506
|
+
TABLE_RE = /(?:FROM|INTO|UPDATE)\s+(?:"?\w+"?\.)?"?(\w+)"?/i;
|
|
600
507
|
PRISMA_OP_MAP = {
|
|
601
508
|
findUnique: "SELECT",
|
|
602
509
|
findUniqueOrThrow: "SELECT",
|
|
@@ -615,156 +522,240 @@ var init_normalize = __esm({
|
|
|
615
522
|
delete: "DELETE",
|
|
616
523
|
deleteMany: "DELETE"
|
|
617
524
|
};
|
|
525
|
+
SQL_PARAM_MARKER = /\$\d+/g;
|
|
526
|
+
SQL_STRING_LITERAL = /'[^']*'/g;
|
|
527
|
+
SQL_NUMBER_LITERAL = /\b\d+(\.\d+)?\b/g;
|
|
618
528
|
}
|
|
619
529
|
});
|
|
620
530
|
|
|
621
|
-
// src/
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
531
|
+
// src/utils/type-guards.ts
|
|
532
|
+
function isString(val) {
|
|
533
|
+
return typeof val === "string";
|
|
534
|
+
}
|
|
535
|
+
function isNumber(val) {
|
|
536
|
+
return typeof val === "number" && !isNaN(val);
|
|
537
|
+
}
|
|
538
|
+
function isBoolean(val) {
|
|
539
|
+
return typeof val === "boolean";
|
|
540
|
+
}
|
|
541
|
+
function isThenable(value) {
|
|
542
|
+
return value != null && typeof value.then === "function";
|
|
543
|
+
}
|
|
544
|
+
function getErrorMessage(err) {
|
|
545
|
+
if (err instanceof Error) return err.message;
|
|
546
|
+
if (typeof err === "string") return err;
|
|
547
|
+
return String(err);
|
|
548
|
+
}
|
|
549
|
+
function isValidIssueState(val) {
|
|
550
|
+
return typeof val === "string" && VALID_ISSUE_STATES.has(val);
|
|
551
|
+
}
|
|
552
|
+
function isValidIssueCategory(val) {
|
|
553
|
+
return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
|
|
554
|
+
}
|
|
555
|
+
function isValidAiFixStatus(val) {
|
|
556
|
+
return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
|
|
557
|
+
}
|
|
558
|
+
function validateIssuesData(parsed) {
|
|
559
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
560
|
+
const obj = parsed;
|
|
561
|
+
if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
|
|
562
|
+
return parsed;
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
function validateMetricsData(parsed) {
|
|
567
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
568
|
+
const obj = parsed;
|
|
569
|
+
if (obj.version === 1 && Array.isArray(obj.endpoints)) {
|
|
570
|
+
return parsed;
|
|
571
|
+
}
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
var init_type_guards = __esm({
|
|
575
|
+
"src/utils/type-guards.ts"() {
|
|
576
|
+
"use strict";
|
|
577
|
+
init_config();
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// src/utils/log.ts
|
|
582
|
+
function brakitWarn(message) {
|
|
583
|
+
process.stderr.write(`${PREFIX} ${message}
|
|
584
|
+
`);
|
|
585
|
+
}
|
|
586
|
+
function brakitDebug(message) {
|
|
587
|
+
if (process.env.DEBUG_BRAKIT) {
|
|
588
|
+
process.stderr.write(`${PREFIX}:debug ${message}
|
|
589
|
+
`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
var PREFIX;
|
|
593
|
+
var init_log = __esm({
|
|
594
|
+
"src/utils/log.ts"() {
|
|
595
|
+
"use strict";
|
|
596
|
+
PREFIX = "[brakit]";
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// src/instrument/adapters/shared.ts
|
|
601
|
+
import { createRequire } from "module";
|
|
602
|
+
function tryRequire(id) {
|
|
603
|
+
try {
|
|
604
|
+
return appRequire(id);
|
|
605
|
+
} catch {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function getActiveRequestId() {
|
|
610
|
+
return getRequestContext()?.requestId ?? null;
|
|
611
|
+
}
|
|
612
|
+
function getPrototype(lib, className) {
|
|
613
|
+
const defaultExport = lib.default;
|
|
614
|
+
const cls = defaultExport?.[className] ?? lib[className];
|
|
615
|
+
if (!cls || typeof cls !== "function") return null;
|
|
616
|
+
return cls.prototype ?? null;
|
|
617
|
+
}
|
|
618
|
+
function buildQueryEvent(config, sql, op, table, start, requestId, rowCount) {
|
|
619
|
+
return {
|
|
620
|
+
type: "query",
|
|
621
|
+
data: {
|
|
622
|
+
driver: config.driver,
|
|
623
|
+
source: config.driver,
|
|
624
|
+
sql,
|
|
625
|
+
normalizedOp: op,
|
|
626
|
+
table,
|
|
627
|
+
durationMs: Math.round(performance.now() - start),
|
|
628
|
+
rowCount: rowCount ?? void 0,
|
|
629
|
+
parentRequestId: requestId,
|
|
630
|
+
timestamp: performance.now()
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function wrapQueryMethod(original, emit, config) {
|
|
635
|
+
return function(...args) {
|
|
636
|
+
const sql = config.extractSql(args);
|
|
637
|
+
const start = performance.now();
|
|
638
|
+
const requestId = getActiveRequestId();
|
|
639
|
+
const { op, table } = normalizeSQL(sql ?? "");
|
|
640
|
+
const emitQuery = (result2) => {
|
|
641
|
+
const rowCount = config.extractRowCount?.(result2);
|
|
642
|
+
emit(buildQueryEvent(config, sql, op, table, start, requestId, rowCount));
|
|
643
|
+
};
|
|
644
|
+
const lastIdx = args.length - 1;
|
|
645
|
+
if (lastIdx >= 0 && typeof args[lastIdx] === "function") {
|
|
646
|
+
const originalCallback = args[lastIdx];
|
|
647
|
+
args[lastIdx] = function(...callbackArgs) {
|
|
648
|
+
emitQuery(callbackArgs[1]);
|
|
649
|
+
return originalCallback.apply(this, callbackArgs);
|
|
650
|
+
};
|
|
651
|
+
return original.apply(this, args);
|
|
652
|
+
}
|
|
653
|
+
const result = original.apply(this, args);
|
|
654
|
+
if (isThenable(result)) {
|
|
655
|
+
return result.then((res) => {
|
|
656
|
+
try {
|
|
657
|
+
emitQuery(res);
|
|
658
|
+
} catch (e) {
|
|
659
|
+
brakitDebug(`query telemetry: ${getErrorMessage(e)}`);
|
|
660
|
+
}
|
|
661
|
+
return res;
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
if (config.supportsEventEmitter && result && typeof result.on === "function") {
|
|
665
|
+
result.on(
|
|
666
|
+
"end",
|
|
667
|
+
(res) => emitQuery(res)
|
|
668
|
+
);
|
|
669
|
+
return result;
|
|
670
|
+
}
|
|
671
|
+
return result;
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
var appRequire;
|
|
675
|
+
var init_shared = __esm({
|
|
676
|
+
"src/instrument/adapters/shared.ts"() {
|
|
677
|
+
"use strict";
|
|
678
|
+
init_context();
|
|
679
|
+
init_normalize();
|
|
680
|
+
init_type_guards();
|
|
681
|
+
init_log();
|
|
682
|
+
appRequire = createRequire(process.cwd() + "/index.js");
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// src/instrument/adapters/pg.ts
|
|
687
|
+
var origQuery, proto, pgConfig, pgAdapter;
|
|
688
|
+
var init_pg = __esm({
|
|
689
|
+
"src/instrument/adapters/pg.ts"() {
|
|
690
|
+
"use strict";
|
|
691
|
+
init_shared();
|
|
692
|
+
origQuery = null;
|
|
693
|
+
proto = null;
|
|
694
|
+
pgConfig = {
|
|
695
|
+
driver: "pg",
|
|
696
|
+
extractSql: (args) => {
|
|
697
|
+
const q = args[0];
|
|
698
|
+
if (typeof q === "string") return q;
|
|
699
|
+
if (typeof q === "object" && q !== null && "text" in q) return q.text;
|
|
700
|
+
return void 0;
|
|
701
|
+
},
|
|
702
|
+
extractRowCount: (result) => result?.rowCount,
|
|
703
|
+
supportsEventEmitter: true
|
|
704
|
+
};
|
|
705
|
+
pgAdapter = {
|
|
706
|
+
name: "pg",
|
|
707
|
+
detect() {
|
|
708
|
+
return tryRequire("pg") !== null;
|
|
709
|
+
},
|
|
710
|
+
/** Monkeypatches pg's Client prototype to intercept database queries and emit telemetry events. */
|
|
711
|
+
patch(emit) {
|
|
712
|
+
const pg = tryRequire("pg");
|
|
713
|
+
if (!pg) return;
|
|
714
|
+
proto = getPrototype(pg, "Client");
|
|
715
|
+
if (!proto?.query) return;
|
|
716
|
+
origQuery = proto.query;
|
|
717
|
+
proto.query = wrapQueryMethod(origQuery, emit, pgConfig);
|
|
718
|
+
},
|
|
719
|
+
unpatch() {
|
|
720
|
+
if (proto && origQuery) {
|
|
721
|
+
proto.query = origQuery;
|
|
722
|
+
origQuery = null;
|
|
723
|
+
proto = null;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// src/instrument/adapters/mysql2.ts
|
|
731
|
+
var PATCHED_METHODS, originals2, proto2, mysql2Config, mysql2Adapter;
|
|
732
|
+
var init_mysql2 = __esm({
|
|
706
733
|
"src/instrument/adapters/mysql2.ts"() {
|
|
707
734
|
"use strict";
|
|
708
735
|
init_shared();
|
|
709
|
-
|
|
736
|
+
PATCHED_METHODS = ["query", "execute"];
|
|
710
737
|
originals2 = /* @__PURE__ */ new Map();
|
|
711
738
|
proto2 = null;
|
|
739
|
+
mysql2Config = {
|
|
740
|
+
driver: "mysql2",
|
|
741
|
+
extractSql: (args) => typeof args[0] === "string" ? args[0] : void 0
|
|
742
|
+
};
|
|
712
743
|
mysql2Adapter = {
|
|
713
744
|
name: "mysql2",
|
|
714
745
|
detect() {
|
|
715
746
|
return tryRequire("mysql2") !== null;
|
|
716
747
|
},
|
|
748
|
+
/** Monkeypatches mysql2's Connection prototype to intercept database queries and emit telemetry events. */
|
|
717
749
|
patch(emit) {
|
|
718
750
|
const mysql2 = tryRequire("mysql2");
|
|
719
751
|
if (!mysql2) return;
|
|
720
|
-
proto2 = mysql2
|
|
752
|
+
proto2 = getPrototype(mysql2, "Connection");
|
|
721
753
|
if (!proto2) return;
|
|
722
|
-
for (const method of
|
|
754
|
+
for (const method of PATCHED_METHODS) {
|
|
723
755
|
const orig = proto2[method];
|
|
724
756
|
if (typeof orig !== "function") continue;
|
|
725
757
|
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
|
-
};
|
|
758
|
+
proto2[method] = wrapQueryMethod(orig, emit, mysql2Config);
|
|
768
759
|
}
|
|
769
760
|
},
|
|
770
761
|
unpatch() {
|
|
@@ -797,9 +788,8 @@ var init_prisma = __esm({
|
|
|
797
788
|
patch(emit) {
|
|
798
789
|
const prismaModule = tryRequire("@prisma/client");
|
|
799
790
|
if (!prismaModule) return;
|
|
800
|
-
|
|
801
|
-
if (!
|
|
802
|
-
prismaProto = PrismaClient.prototype;
|
|
791
|
+
prismaProto = getPrototype(prismaModule, "PrismaClient");
|
|
792
|
+
if (!prismaProto) return;
|
|
803
793
|
origConnect = prismaProto.$connect;
|
|
804
794
|
if (typeof origConnect !== "function") return;
|
|
805
795
|
const saved = origConnect;
|
|
@@ -815,7 +805,7 @@ var init_prisma = __esm({
|
|
|
815
805
|
args: opArgs,
|
|
816
806
|
query
|
|
817
807
|
}) {
|
|
818
|
-
const requestId =
|
|
808
|
+
const requestId = getActiveRequestId();
|
|
819
809
|
const start = performance.now();
|
|
820
810
|
const result = await query(opArgs);
|
|
821
811
|
emit({
|
|
@@ -829,7 +819,7 @@ var init_prisma = __esm({
|
|
|
829
819
|
table: model,
|
|
830
820
|
durationMs: Math.round(performance.now() - start),
|
|
831
821
|
parentRequestId: requestId,
|
|
832
|
-
timestamp:
|
|
822
|
+
timestamp: performance.now()
|
|
833
823
|
}
|
|
834
824
|
});
|
|
835
825
|
return result;
|
|
@@ -874,24 +864,35 @@ var init_adapters = __esm({
|
|
|
874
864
|
}
|
|
875
865
|
});
|
|
876
866
|
|
|
877
|
-
// src/
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
867
|
+
// src/utils/endpoint.ts
|
|
868
|
+
function isDynamicSegment(segment) {
|
|
869
|
+
return UUID_RE.test(segment) || NUMERIC_ID_RE.test(segment) || HEX_HASH_RE.test(segment) || ALPHA_TOKEN_RE.test(segment);
|
|
870
|
+
}
|
|
871
|
+
function normalizePath(path) {
|
|
872
|
+
const qIdx = path.indexOf("?");
|
|
873
|
+
const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
|
|
874
|
+
return pathname.split("/").map((seg) => seg && isDynamicSegment(seg) ? DYNAMIC_SEGMENT_PLACEHOLDER : seg).join("/");
|
|
875
|
+
}
|
|
876
|
+
function getEndpointKey(method, path) {
|
|
877
|
+
return `${method} ${normalizePath(path)}`;
|
|
878
|
+
}
|
|
879
|
+
function extractEndpointFromDesc(desc) {
|
|
880
|
+
return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
|
|
881
|
+
}
|
|
882
|
+
function stripQueryString(path) {
|
|
883
|
+
const i = path.indexOf("?");
|
|
884
|
+
return i === -1 ? path : path.slice(0, i);
|
|
885
|
+
}
|
|
886
|
+
var UUID_RE, NUMERIC_ID_RE, HEX_HASH_RE, ALPHA_TOKEN_RE, DYNAMIC_SEGMENT_PLACEHOLDER, ENDPOINT_PREFIX_RE;
|
|
887
|
+
var init_endpoint = __esm({
|
|
888
|
+
"src/utils/endpoint.ts"() {
|
|
881
889
|
"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
|
-
};
|
|
890
|
+
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
891
|
+
NUMERIC_ID_RE = /^\d+$/;
|
|
892
|
+
HEX_HASH_RE = /^[0-9a-f]{12,}$/i;
|
|
893
|
+
ALPHA_TOKEN_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/;
|
|
894
|
+
DYNAMIC_SEGMENT_PLACEHOLDER = ":id";
|
|
895
|
+
ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
895
896
|
}
|
|
896
897
|
});
|
|
897
898
|
|
|
@@ -915,6 +916,7 @@ var init_http_status = __esm({
|
|
|
915
916
|
function detectCategory(req) {
|
|
916
917
|
const { method, url, statusCode, responseHeaders } = req;
|
|
917
918
|
if (req.isStatic) return "static";
|
|
919
|
+
if (req.isHealthCheck) return "health-check";
|
|
918
920
|
if (statusCode === 307 && (url.includes("__clerk_handshake") || url.includes("__clerk_db_jwt"))) {
|
|
919
921
|
return "auth-handshake";
|
|
920
922
|
}
|
|
@@ -1079,20 +1081,41 @@ var init_label = __esm({
|
|
|
1079
1081
|
});
|
|
1080
1082
|
|
|
1081
1083
|
// src/analysis/transforms.ts
|
|
1082
|
-
function
|
|
1084
|
+
function isDuplicateCandidate(req) {
|
|
1085
|
+
return DUPLICATE_CATEGORIES.has(req.category);
|
|
1086
|
+
}
|
|
1087
|
+
function buildRequestKey(req) {
|
|
1088
|
+
return `${req.method} ${stripQueryString(getEffectivePath(req))}`;
|
|
1089
|
+
}
|
|
1090
|
+
function isStrictModePattern(requests, counts) {
|
|
1091
|
+
if (counts.size === 0 || ![...counts.values()].every((c) => c === 2)) {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
const firstByKey = /* @__PURE__ */ new Map();
|
|
1095
|
+
for (const req of requests) {
|
|
1096
|
+
if (!isDuplicateCandidate(req)) continue;
|
|
1097
|
+
const key = buildRequestKey(req);
|
|
1098
|
+
const first = firstByKey.get(key);
|
|
1099
|
+
if (!first) {
|
|
1100
|
+
firstByKey.set(key, req);
|
|
1101
|
+
} else if (Math.abs(req.startedAt - first.startedAt) > STRICT_MODE_MAX_GAP_MS) {
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return true;
|
|
1106
|
+
}
|
|
1107
|
+
function flagDuplicateRequests(requests) {
|
|
1083
1108
|
const counts = /* @__PURE__ */ new Map();
|
|
1084
1109
|
for (const req of requests) {
|
|
1085
|
-
if (req
|
|
1086
|
-
|
|
1087
|
-
const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
|
|
1110
|
+
if (!isDuplicateCandidate(req)) continue;
|
|
1111
|
+
const key = buildRequestKey(req);
|
|
1088
1112
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1089
1113
|
}
|
|
1090
|
-
const isStrictMode =
|
|
1114
|
+
const isStrictMode = isStrictModePattern(requests, counts);
|
|
1091
1115
|
const seen = /* @__PURE__ */ new Set();
|
|
1092
1116
|
for (const req of requests) {
|
|
1093
|
-
if (req
|
|
1094
|
-
|
|
1095
|
-
const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
|
|
1117
|
+
if (!isDuplicateCandidate(req)) continue;
|
|
1118
|
+
const key = buildRequestKey(req);
|
|
1096
1119
|
if (seen.has(key)) {
|
|
1097
1120
|
if (isStrictMode) {
|
|
1098
1121
|
req.isStrictModeDupe = true;
|
|
@@ -1104,20 +1127,20 @@ function markDuplicates(requests) {
|
|
|
1104
1127
|
}
|
|
1105
1128
|
}
|
|
1106
1129
|
}
|
|
1107
|
-
function
|
|
1130
|
+
function mergePollingSequences(requests) {
|
|
1108
1131
|
const result = [];
|
|
1109
1132
|
let i = 0;
|
|
1110
1133
|
while (i < requests.length) {
|
|
1111
1134
|
const current = requests[i];
|
|
1112
|
-
const currentEffective = getEffectivePath(current)
|
|
1135
|
+
const currentEffective = stripQueryString(getEffectivePath(current));
|
|
1113
1136
|
if (current.method === "GET" && current.category === "data-fetch") {
|
|
1114
|
-
let
|
|
1115
|
-
while (
|
|
1116
|
-
|
|
1137
|
+
let nextIndex = i + 1;
|
|
1138
|
+
while (nextIndex < requests.length && requests[nextIndex].method === "GET" && stripQueryString(getEffectivePath(requests[nextIndex])) === currentEffective) {
|
|
1139
|
+
nextIndex++;
|
|
1117
1140
|
}
|
|
1118
|
-
const count =
|
|
1141
|
+
const count = nextIndex - i;
|
|
1119
1142
|
if (count >= MIN_POLLING_SEQUENCE) {
|
|
1120
|
-
const last = requests[
|
|
1143
|
+
const last = requests[nextIndex - 1];
|
|
1121
1144
|
const pollingDuration = last.startedAt + last.durationMs - current.startedAt;
|
|
1122
1145
|
const endpointName = prettifyEndpoint(currentEffective);
|
|
1123
1146
|
result.push({
|
|
@@ -1128,7 +1151,7 @@ function collapsePolling(requests) {
|
|
|
1128
1151
|
pollingDurationMs: pollingDuration,
|
|
1129
1152
|
isDuplicate: false
|
|
1130
1153
|
});
|
|
1131
|
-
i =
|
|
1154
|
+
i = nextIndex;
|
|
1132
1155
|
continue;
|
|
1133
1156
|
}
|
|
1134
1157
|
}
|
|
@@ -1141,18 +1164,18 @@ function formatDurationLabel(ms) {
|
|
|
1141
1164
|
if (ms < 1e3) return `${ms}ms`;
|
|
1142
1165
|
return `${(ms / 1e3).toFixed(1)}s`;
|
|
1143
1166
|
}
|
|
1144
|
-
function
|
|
1167
|
+
function collectRequestWarnings(requests) {
|
|
1145
1168
|
const warnings = [];
|
|
1146
1169
|
const duplicateCount = requests.filter((r) => r.isDuplicate).length;
|
|
1147
1170
|
if (duplicateCount > 0) {
|
|
1148
1171
|
const unique = new Set(
|
|
1149
|
-
requests.filter((r) => r.isDuplicate).map((r) =>
|
|
1172
|
+
requests.filter((r) => r.isDuplicate).map((r) => buildRequestKey(r))
|
|
1150
1173
|
);
|
|
1151
1174
|
const endpoints = unique.size;
|
|
1152
1175
|
const sameData = requests.filter((r) => r.isDuplicate).every((r) => {
|
|
1153
|
-
const key =
|
|
1176
|
+
const key = buildRequestKey(r);
|
|
1154
1177
|
const first = requests.find(
|
|
1155
|
-
(o) => !o.isDuplicate &&
|
|
1178
|
+
(o) => !o.isDuplicate && buildRequestKey(o) === key
|
|
1156
1179
|
);
|
|
1157
1180
|
return first && first.responseBody === r.responseBody;
|
|
1158
1181
|
});
|
|
@@ -1173,18 +1196,30 @@ function detectWarnings(requests) {
|
|
|
1173
1196
|
}
|
|
1174
1197
|
return warnings;
|
|
1175
1198
|
}
|
|
1199
|
+
var DUPLICATE_CATEGORIES;
|
|
1176
1200
|
var init_transforms = __esm({
|
|
1177
1201
|
"src/analysis/transforms.ts"() {
|
|
1178
1202
|
"use strict";
|
|
1179
1203
|
init_constants();
|
|
1204
|
+
init_config();
|
|
1180
1205
|
init_categorize();
|
|
1181
1206
|
init_label();
|
|
1182
1207
|
init_http_status();
|
|
1208
|
+
init_endpoint();
|
|
1209
|
+
DUPLICATE_CATEGORIES = /* @__PURE__ */ new Set(["data-fetch", "auth-check"]);
|
|
1183
1210
|
}
|
|
1184
1211
|
});
|
|
1185
1212
|
|
|
1186
1213
|
// src/analysis/group.ts
|
|
1187
1214
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1215
|
+
function shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, startedAt) {
|
|
1216
|
+
if (currentRequests.length === 0) return false;
|
|
1217
|
+
const sourcePage = labeled.sourcePage;
|
|
1218
|
+
const isNewPage = sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
|
|
1219
|
+
const isTimeGap = startedAt - lastEndTime > FLOW_GAP_MS;
|
|
1220
|
+
const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
|
|
1221
|
+
return isNewPage || isTimeGap || isPageLoad;
|
|
1222
|
+
}
|
|
1188
1223
|
function groupRequestsIntoFlows(requests) {
|
|
1189
1224
|
if (requests.length === 0) return [];
|
|
1190
1225
|
const flows = [];
|
|
@@ -1195,17 +1230,12 @@ function groupRequestsIntoFlows(requests) {
|
|
|
1195
1230
|
if (req.path.startsWith(DASHBOARD_PREFIX)) continue;
|
|
1196
1231
|
const labeled = labelRequest(req);
|
|
1197
1232
|
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)) {
|
|
1233
|
+
if (shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, req.startedAt)) {
|
|
1204
1234
|
flows.push(buildFlow(currentRequests));
|
|
1205
1235
|
currentRequests = [];
|
|
1206
1236
|
}
|
|
1207
1237
|
currentRequests.push(labeled);
|
|
1208
|
-
currentSourcePage = sourcePage ?? currentSourcePage;
|
|
1238
|
+
currentSourcePage = labeled.sourcePage ?? currentSourcePage;
|
|
1209
1239
|
lastEndTime = Math.max(lastEndTime, req.startedAt + req.durationMs);
|
|
1210
1240
|
}
|
|
1211
1241
|
if (currentRequests.length > 0) {
|
|
@@ -1214,8 +1244,8 @@ function groupRequestsIntoFlows(requests) {
|
|
|
1214
1244
|
return flows;
|
|
1215
1245
|
}
|
|
1216
1246
|
function buildFlow(rawRequests) {
|
|
1217
|
-
|
|
1218
|
-
const requests =
|
|
1247
|
+
flagDuplicateRequests(rawRequests);
|
|
1248
|
+
const requests = mergePollingSequences(rawRequests);
|
|
1219
1249
|
const first = requests[0];
|
|
1220
1250
|
const startTime = first.startedAt;
|
|
1221
1251
|
const endTime = Math.max(
|
|
@@ -1234,7 +1264,7 @@ function buildFlow(rawRequests) {
|
|
|
1234
1264
|
startTime,
|
|
1235
1265
|
totalDurationMs: Math.round(endTime - startTime),
|
|
1236
1266
|
hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
|
|
1237
|
-
warnings:
|
|
1267
|
+
warnings: collectRequestWarnings(rawRequests),
|
|
1238
1268
|
sourcePage,
|
|
1239
1269
|
redundancyPct
|
|
1240
1270
|
};
|
|
@@ -1246,20 +1276,20 @@ function getDominantSourcePage(requests) {
|
|
|
1246
1276
|
counts.set(req.sourcePage, (counts.get(req.sourcePage) ?? 0) + 1);
|
|
1247
1277
|
}
|
|
1248
1278
|
}
|
|
1249
|
-
let
|
|
1250
|
-
let
|
|
1279
|
+
let mostCommonPage = "";
|
|
1280
|
+
let highestCount = 0;
|
|
1251
1281
|
for (const [page, count] of counts) {
|
|
1252
|
-
if (count >
|
|
1253
|
-
|
|
1254
|
-
|
|
1282
|
+
if (count > highestCount) {
|
|
1283
|
+
mostCommonPage = page;
|
|
1284
|
+
highestCount = count;
|
|
1255
1285
|
}
|
|
1256
1286
|
}
|
|
1257
|
-
return
|
|
1287
|
+
return mostCommonPage || (requests[0]?.path ? stripQueryString(requests[0].path) : "") || "/";
|
|
1258
1288
|
}
|
|
1259
1289
|
function deriveFlowLabel(requests, sourcePage) {
|
|
1260
1290
|
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
1291
|
if (trigger.category === "page-load" || trigger.category === "navigation") {
|
|
1262
|
-
const pageName = prettifyPageName(trigger.path
|
|
1292
|
+
const pageName = prettifyPageName(stripQueryString(trigger.path));
|
|
1263
1293
|
return `${pageName} Page`;
|
|
1264
1294
|
}
|
|
1265
1295
|
if (trigger.category === "api-call") {
|
|
@@ -1288,6 +1318,7 @@ var init_group = __esm({
|
|
|
1288
1318
|
"use strict";
|
|
1289
1319
|
init_constants();
|
|
1290
1320
|
init_http_status();
|
|
1321
|
+
init_endpoint();
|
|
1291
1322
|
init_label();
|
|
1292
1323
|
init_categorize();
|
|
1293
1324
|
init_transforms();
|
|
@@ -1385,12 +1416,28 @@ var init_shared2 = __esm({
|
|
|
1385
1416
|
"src/dashboard/api/shared.ts"() {
|
|
1386
1417
|
"use strict";
|
|
1387
1418
|
init_constants();
|
|
1388
|
-
|
|
1389
|
-
|
|
1419
|
+
init_config();
|
|
1420
|
+
init_labels();
|
|
1390
1421
|
}
|
|
1391
1422
|
});
|
|
1392
1423
|
|
|
1393
1424
|
// src/dashboard/api/handlers.ts
|
|
1425
|
+
function filterByStatusRange(requests, statusStr) {
|
|
1426
|
+
if (statusStr.endsWith("xx")) {
|
|
1427
|
+
const prefix = parseInt(statusStr[0], 10);
|
|
1428
|
+
return requests.filter(
|
|
1429
|
+
(r) => Math.floor(r.statusCode / 100) === prefix
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
const code = parseInt(statusStr, 10);
|
|
1433
|
+
return requests.filter((r) => r.statusCode === code);
|
|
1434
|
+
}
|
|
1435
|
+
function filterBySearch(requests, searchQuery) {
|
|
1436
|
+
const lower = searchQuery.toLowerCase();
|
|
1437
|
+
return requests.filter(
|
|
1438
|
+
(r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1394
1441
|
function sanitizeRequest(r) {
|
|
1395
1442
|
return {
|
|
1396
1443
|
...r,
|
|
@@ -1398,7 +1445,7 @@ function sanitizeRequest(r) {
|
|
|
1398
1445
|
responseHeaders: maskSensitiveHeaders(r.responseHeaders)
|
|
1399
1446
|
};
|
|
1400
1447
|
}
|
|
1401
|
-
function createRequestsHandler(
|
|
1448
|
+
function createRequestsHandler(services) {
|
|
1402
1449
|
return (req, res) => {
|
|
1403
1450
|
if (!requireGet(req, res)) return;
|
|
1404
1451
|
const url = parseRequestUrl(req);
|
|
@@ -1408,26 +1455,15 @@ function createRequestsHandler(registry) {
|
|
|
1408
1455
|
const rawLimit = parseInt(url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT), 10);
|
|
1409
1456
|
const limit = Math.min(Math.max(rawLimit || DEFAULT_API_LIMIT, 1), MAX_API_LIMIT);
|
|
1410
1457
|
const offset = Math.max(parseInt(url.searchParams.get("offset") ?? "0", 10) || 0, 0);
|
|
1411
|
-
let results = [...
|
|
1458
|
+
let results = [...services.requestStore.getAll()].reverse();
|
|
1412
1459
|
if (method) {
|
|
1413
1460
|
results = results.filter((r) => r.method === method.toUpperCase());
|
|
1414
1461
|
}
|
|
1415
1462
|
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
|
-
}
|
|
1463
|
+
results = filterByStatusRange(results, status);
|
|
1425
1464
|
}
|
|
1426
1465
|
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
|
-
);
|
|
1466
|
+
results = filterBySearch(results, search);
|
|
1431
1467
|
}
|
|
1432
1468
|
const total = results.length;
|
|
1433
1469
|
results = results.slice(offset, offset + limit);
|
|
@@ -1435,96 +1471,84 @@ function createRequestsHandler(registry) {
|
|
|
1435
1471
|
sendJson(req, res, HTTP_OK, { total, requests: sanitized });
|
|
1436
1472
|
};
|
|
1437
1473
|
}
|
|
1438
|
-
function createFlowsHandler(
|
|
1474
|
+
function createFlowsHandler(services) {
|
|
1439
1475
|
return (req, res) => {
|
|
1440
1476
|
if (!requireGet(req, res)) return;
|
|
1441
|
-
const flows = groupRequestsIntoFlows(
|
|
1477
|
+
const flows = groupRequestsIntoFlows(services.requestStore.getAll()).reverse().map((flow) => ({
|
|
1442
1478
|
...flow,
|
|
1443
1479
|
requests: flow.requests.map(sanitizeRequest)
|
|
1444
1480
|
}));
|
|
1445
1481
|
sendJson(req, res, HTTP_OK, { total: flows.length, flows });
|
|
1446
1482
|
};
|
|
1447
1483
|
}
|
|
1448
|
-
function createClearHandler(
|
|
1484
|
+
function createClearHandler(services) {
|
|
1449
1485
|
return (req, res) => {
|
|
1450
1486
|
if (req.method !== "POST") {
|
|
1451
1487
|
sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
|
|
1452
1488
|
return;
|
|
1453
1489
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1490
|
+
services.requestStore.clear();
|
|
1491
|
+
services.fetchStore.clear();
|
|
1492
|
+
services.logStore.clear();
|
|
1493
|
+
services.errorStore.clear();
|
|
1494
|
+
services.queryStore.clear();
|
|
1495
|
+
services.metricsStore.reset();
|
|
1496
|
+
services.issueStore.clear();
|
|
1497
|
+
services.bus.emit("store:cleared", void 0);
|
|
1462
1498
|
sendJson(req, res, HTTP_OK, { cleared: true });
|
|
1463
1499
|
};
|
|
1464
1500
|
}
|
|
1465
|
-
function createFetchesHandler(
|
|
1466
|
-
return (req, res) => handleTelemetryGet(req, res,
|
|
1501
|
+
function createFetchesHandler(services) {
|
|
1502
|
+
return (req, res) => handleTelemetryGet(req, res, services.fetchStore);
|
|
1467
1503
|
}
|
|
1468
|
-
function createLogsHandler(
|
|
1469
|
-
return (req, res) => handleTelemetryGet(req, res,
|
|
1504
|
+
function createLogsHandler(services) {
|
|
1505
|
+
return (req, res) => handleTelemetryGet(req, res, services.logStore);
|
|
1470
1506
|
}
|
|
1471
|
-
function createErrorsHandler(
|
|
1472
|
-
return (req, res) => handleTelemetryGet(req, res,
|
|
1507
|
+
function createErrorsHandler(services) {
|
|
1508
|
+
return (req, res) => handleTelemetryGet(req, res, services.errorStore);
|
|
1473
1509
|
}
|
|
1474
|
-
function createQueriesHandler(
|
|
1475
|
-
return (req, res) => handleTelemetryGet(req, res,
|
|
1510
|
+
function createQueriesHandler(services) {
|
|
1511
|
+
return (req, res) => handleTelemetryGet(req, res, services.queryStore);
|
|
1476
1512
|
}
|
|
1477
1513
|
var init_handlers = __esm({
|
|
1478
1514
|
"src/dashboard/api/handlers.ts"() {
|
|
1479
1515
|
"use strict";
|
|
1480
1516
|
init_group();
|
|
1481
1517
|
init_constants();
|
|
1482
|
-
|
|
1518
|
+
init_labels();
|
|
1483
1519
|
init_shared2();
|
|
1484
1520
|
}
|
|
1485
1521
|
});
|
|
1486
1522
|
|
|
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;
|
|
1523
|
+
// src/utils/static-patterns.ts
|
|
1524
|
+
function isStaticPath(urlPath) {
|
|
1525
|
+
return STATIC_PATTERNS.some((p) => p.test(urlPath));
|
|
1516
1526
|
}
|
|
1517
|
-
function
|
|
1518
|
-
|
|
1519
|
-
return parsed;
|
|
1520
|
-
}
|
|
1521
|
-
return null;
|
|
1527
|
+
function isHealthCheckPath(urlPath) {
|
|
1528
|
+
return HEALTH_CHECK_PATTERNS.some((p) => p.test(urlPath));
|
|
1522
1529
|
}
|
|
1523
|
-
var
|
|
1524
|
-
|
|
1530
|
+
var STATIC_PATTERNS, HEALTH_CHECK_PATTERNS;
|
|
1531
|
+
var init_static_patterns = __esm({
|
|
1532
|
+
"src/utils/static-patterns.ts"() {
|
|
1525
1533
|
"use strict";
|
|
1526
|
-
|
|
1527
|
-
|
|
1534
|
+
STATIC_PATTERNS = [
|
|
1535
|
+
/\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
|
|
1536
|
+
/^\/favicon/,
|
|
1537
|
+
/^\/node_modules\//,
|
|
1538
|
+
// Framework-specific static/internal paths
|
|
1539
|
+
/^\/_next\//,
|
|
1540
|
+
/^\/__nextjs/,
|
|
1541
|
+
/^\/@vite\//,
|
|
1542
|
+
/^\/__vite/
|
|
1543
|
+
];
|
|
1544
|
+
HEALTH_CHECK_PATTERNS = [
|
|
1545
|
+
/^\/health(z|check)?$/i,
|
|
1546
|
+
/^\/ping$/i,
|
|
1547
|
+
/^\/(ready|readiness|liveness)$/i,
|
|
1548
|
+
/^\/status$/i,
|
|
1549
|
+
/^\/__health$/i,
|
|
1550
|
+
/^\/api\/health(z|check)?$/i
|
|
1551
|
+
];
|
|
1528
1552
|
}
|
|
1529
1553
|
});
|
|
1530
1554
|
|
|
@@ -1545,8 +1569,8 @@ function numOrUndef(val) {
|
|
|
1545
1569
|
function headers(val) {
|
|
1546
1570
|
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1547
1571
|
const result = {};
|
|
1548
|
-
for (const [
|
|
1549
|
-
if (typeof
|
|
1572
|
+
for (const [key, value] of Object.entries(val)) {
|
|
1573
|
+
if (typeof value === "string") result[key] = value;
|
|
1550
1574
|
}
|
|
1551
1575
|
return result;
|
|
1552
1576
|
}
|
|
@@ -1610,7 +1634,7 @@ function parseRequestEvent(data, ts) {
|
|
|
1610
1634
|
id: str(data.id, randomUUID4()),
|
|
1611
1635
|
method: str(data.method, "GET"),
|
|
1612
1636
|
url,
|
|
1613
|
-
path: url
|
|
1637
|
+
path: stripQueryString(url),
|
|
1614
1638
|
headers: headers(data.headers),
|
|
1615
1639
|
requestBody: isString(data.requestBody) ? data.requestBody : null,
|
|
1616
1640
|
statusCode: num(data.statusCode, 200),
|
|
@@ -1619,7 +1643,8 @@ function parseRequestEvent(data, ts) {
|
|
|
1619
1643
|
startedAt: ts,
|
|
1620
1644
|
durationMs: num(data.durationMs, 0),
|
|
1621
1645
|
responseSize: num(data.responseSize, 0),
|
|
1622
|
-
isStatic: isBoolean(data.isStatic) ? data.isStatic : false
|
|
1646
|
+
isStatic: isBoolean(data.isStatic) ? data.isStatic : false,
|
|
1647
|
+
isHealthCheck: isBoolean(data.isHealthCheck) ? data.isHealthCheck : isHealthCheckPath(stripQueryString(url))
|
|
1623
1648
|
};
|
|
1624
1649
|
}
|
|
1625
1650
|
function routeSDKEvent(event, stores) {
|
|
@@ -1653,7 +1678,9 @@ var init_sdk_event_parser = __esm({
|
|
|
1653
1678
|
"src/dashboard/api/sdk-event-parser.ts"() {
|
|
1654
1679
|
"use strict";
|
|
1655
1680
|
init_type_guards();
|
|
1656
|
-
|
|
1681
|
+
init_labels();
|
|
1682
|
+
init_static_patterns();
|
|
1683
|
+
init_endpoint();
|
|
1657
1684
|
LOG_LEVEL_MAP = {
|
|
1658
1685
|
debug: "debug",
|
|
1659
1686
|
info: "info",
|
|
@@ -1673,28 +1700,28 @@ function isBrakitBatch(msg) {
|
|
|
1673
1700
|
function isSDKPayload(msg) {
|
|
1674
1701
|
return typeof msg === "object" && msg !== null && "_brakit" in msg && "version" in msg && typeof msg.version === "number";
|
|
1675
1702
|
}
|
|
1676
|
-
function createIngestHandler(
|
|
1703
|
+
function createIngestHandler(services) {
|
|
1677
1704
|
const routeEvent = (event) => {
|
|
1678
1705
|
switch (event.type) {
|
|
1679
1706
|
case TIMELINE_FETCH:
|
|
1680
|
-
|
|
1707
|
+
services.fetchStore.add(event.data);
|
|
1681
1708
|
break;
|
|
1682
1709
|
case TIMELINE_LOG:
|
|
1683
|
-
|
|
1710
|
+
services.logStore.add(event.data);
|
|
1684
1711
|
break;
|
|
1685
1712
|
case TIMELINE_ERROR:
|
|
1686
|
-
|
|
1713
|
+
services.errorStore.add(event.data);
|
|
1687
1714
|
break;
|
|
1688
1715
|
case TIMELINE_QUERY:
|
|
1689
|
-
|
|
1716
|
+
services.queryStore.add(event.data);
|
|
1690
1717
|
break;
|
|
1691
1718
|
}
|
|
1692
1719
|
};
|
|
1693
|
-
const queryStore =
|
|
1694
|
-
const fetchStore =
|
|
1695
|
-
const logStore =
|
|
1696
|
-
const errorStore =
|
|
1697
|
-
const requestStore =
|
|
1720
|
+
const queryStore = services.queryStore;
|
|
1721
|
+
const fetchStore = services.fetchStore;
|
|
1722
|
+
const logStore = services.logStore;
|
|
1723
|
+
const errorStore = services.errorStore;
|
|
1724
|
+
const requestStore = services.requestStore;
|
|
1698
1725
|
const stores = {
|
|
1699
1726
|
addQuery: (data) => queryStore.add(data),
|
|
1700
1727
|
addFetch: (data) => fetchStore.add(data),
|
|
@@ -1754,9 +1781,8 @@ function createIngestHandler(registry) {
|
|
|
1754
1781
|
var init_ingest = __esm({
|
|
1755
1782
|
"src/dashboard/api/ingest.ts"() {
|
|
1756
1783
|
"use strict";
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
init_timeline();
|
|
1784
|
+
init_config();
|
|
1785
|
+
init_labels();
|
|
1760
1786
|
init_shared2();
|
|
1761
1787
|
init_sdk_event_parser();
|
|
1762
1788
|
}
|
|
@@ -1776,11 +1802,11 @@ function createMetricsHandler(metricsStore) {
|
|
|
1776
1802
|
sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
|
|
1777
1803
|
};
|
|
1778
1804
|
}
|
|
1779
|
-
var
|
|
1805
|
+
var init_metrics = __esm({
|
|
1780
1806
|
"src/dashboard/api/metrics.ts"() {
|
|
1781
1807
|
"use strict";
|
|
1782
1808
|
init_shared2();
|
|
1783
|
-
|
|
1809
|
+
init_labels();
|
|
1784
1810
|
}
|
|
1785
1811
|
});
|
|
1786
1812
|
|
|
@@ -1798,61 +1824,55 @@ var init_metrics_live = __esm({
|
|
|
1798
1824
|
}
|
|
1799
1825
|
});
|
|
1800
1826
|
|
|
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
1827
|
// src/dashboard/api/activity.ts
|
|
1821
|
-
function
|
|
1828
|
+
function buildTimeline(services, requestId) {
|
|
1829
|
+
const fetches = services.fetchStore.getByRequest(requestId);
|
|
1830
|
+
const logs = services.logStore.getByRequest(requestId);
|
|
1831
|
+
const errors = services.errorStore.getByRequest(requestId);
|
|
1832
|
+
const queries = services.queryStore.getByRequest(requestId);
|
|
1833
|
+
const timeline = [];
|
|
1834
|
+
for (const fetch of fetches)
|
|
1835
|
+
timeline.push({ type: TIMELINE_FETCH, timestamp: fetch.timestamp, data: fetch });
|
|
1836
|
+
for (const log of logs)
|
|
1837
|
+
timeline.push({ type: TIMELINE_LOG, timestamp: log.timestamp, data: log });
|
|
1838
|
+
for (const error of errors)
|
|
1839
|
+
timeline.push({ type: TIMELINE_ERROR, timestamp: error.timestamp, data: error });
|
|
1840
|
+
for (const query of queries)
|
|
1841
|
+
timeline.push({ type: TIMELINE_QUERY, timestamp: query.timestamp, data: query });
|
|
1842
|
+
timeline.sort((a, b) => a.timestamp - b.timestamp);
|
|
1843
|
+
return {
|
|
1844
|
+
total: timeline.length,
|
|
1845
|
+
timeline,
|
|
1846
|
+
counts: {
|
|
1847
|
+
fetches: fetches.length,
|
|
1848
|
+
logs: logs.length,
|
|
1849
|
+
errors: errors.length,
|
|
1850
|
+
queries: queries.length
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
function createActivityHandler(services) {
|
|
1822
1855
|
return (req, res) => {
|
|
1823
1856
|
if (!requireGet(req, res)) return;
|
|
1824
1857
|
try {
|
|
1825
1858
|
const url = parseRequestUrl(req);
|
|
1826
1859
|
const requestId = url.searchParams.get("requestId");
|
|
1827
|
-
|
|
1828
|
-
|
|
1860
|
+
const requestIds = url.searchParams.get("requestIds");
|
|
1861
|
+
if (!requestId && !requestIds) {
|
|
1862
|
+
sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId or requestIds parameter required" });
|
|
1829
1863
|
return;
|
|
1830
1864
|
}
|
|
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
|
-
});
|
|
1865
|
+
if (requestId) {
|
|
1866
|
+
const result = buildTimeline(services, requestId);
|
|
1867
|
+
sendJson(req, res, HTTP_OK, { requestId, ...result });
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
const ids = (requestIds || "").split(",").filter(Boolean).slice(0, MAX_BATCH_IDS);
|
|
1871
|
+
const activities = {};
|
|
1872
|
+
for (const id of ids) {
|
|
1873
|
+
activities[id] = buildTimeline(services, id);
|
|
1874
|
+
}
|
|
1875
|
+
sendJson(req, res, HTTP_OK, { requestIds: ids, activities });
|
|
1856
1876
|
} catch (err) {
|
|
1857
1877
|
brakitDebug(`activity handler error: ${err}`);
|
|
1858
1878
|
if (!res.headersSent) {
|
|
@@ -1861,13 +1881,14 @@ function createActivityHandler(registry) {
|
|
|
1861
1881
|
}
|
|
1862
1882
|
};
|
|
1863
1883
|
}
|
|
1884
|
+
var MAX_BATCH_IDS;
|
|
1864
1885
|
var init_activity = __esm({
|
|
1865
1886
|
"src/dashboard/api/activity.ts"() {
|
|
1866
1887
|
"use strict";
|
|
1867
1888
|
init_shared2();
|
|
1868
|
-
|
|
1869
|
-
init_timeline();
|
|
1889
|
+
init_labels();
|
|
1870
1890
|
init_log();
|
|
1891
|
+
MAX_BATCH_IDS = 50;
|
|
1871
1892
|
}
|
|
1872
1893
|
});
|
|
1873
1894
|
|
|
@@ -1877,7 +1898,7 @@ var init_api = __esm({
|
|
|
1877
1898
|
"use strict";
|
|
1878
1899
|
init_handlers();
|
|
1879
1900
|
init_ingest();
|
|
1880
|
-
|
|
1901
|
+
init_metrics();
|
|
1881
1902
|
init_metrics_live();
|
|
1882
1903
|
init_activity();
|
|
1883
1904
|
}
|
|
@@ -1952,25 +1973,12 @@ var init_issues = __esm({
|
|
|
1952
1973
|
"use strict";
|
|
1953
1974
|
init_shared2();
|
|
1954
1975
|
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";
|
|
1976
|
+
init_labels();
|
|
1969
1977
|
}
|
|
1970
1978
|
});
|
|
1971
1979
|
|
|
1972
1980
|
// src/dashboard/sse.ts
|
|
1973
|
-
function createSSEHandler(
|
|
1981
|
+
function createSSEHandler(services) {
|
|
1974
1982
|
const clients = /* @__PURE__ */ new Set();
|
|
1975
1983
|
function broadcast(eventType, data) {
|
|
1976
1984
|
if (clients.size === 0) return;
|
|
@@ -1992,7 +2000,7 @@ data: ${data}
|
|
|
1992
2000
|
}
|
|
1993
2001
|
}
|
|
1994
2002
|
}
|
|
1995
|
-
const bus =
|
|
2003
|
+
const bus = services.bus;
|
|
1996
2004
|
bus.on("request:completed", (r) => broadcast(null, JSON.stringify(r)));
|
|
1997
2005
|
bus.on("telemetry:fetch", (e) => broadcast(SSE_EVENT_FETCH, JSON.stringify(e)));
|
|
1998
2006
|
bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
|
|
@@ -2042,8 +2050,7 @@ var init_sse = __esm({
|
|
|
2042
2050
|
"src/dashboard/sse.ts"() {
|
|
2043
2051
|
"use strict";
|
|
2044
2052
|
init_constants();
|
|
2045
|
-
|
|
2046
|
-
init_events();
|
|
2053
|
+
init_labels();
|
|
2047
2054
|
init_shared2();
|
|
2048
2055
|
}
|
|
2049
2056
|
});
|
|
@@ -2098,7 +2105,7 @@ async function ensureGitignoreAsync(dir, entry) {
|
|
|
2098
2105
|
var init_fs = __esm({
|
|
2099
2106
|
"src/utils/fs.ts"() {
|
|
2100
2107
|
"use strict";
|
|
2101
|
-
|
|
2108
|
+
init_config();
|
|
2102
2109
|
init_log();
|
|
2103
2110
|
init_type_guards();
|
|
2104
2111
|
}
|
|
@@ -2187,7 +2194,7 @@ function computeIssueId(issue) {
|
|
|
2187
2194
|
var init_issue_id = __esm({
|
|
2188
2195
|
"src/utils/issue-id.ts"() {
|
|
2189
2196
|
"use strict";
|
|
2190
|
-
|
|
2197
|
+
init_config();
|
|
2191
2198
|
}
|
|
2192
2199
|
});
|
|
2193
2200
|
|
|
@@ -2200,10 +2207,7 @@ var init_issue_store = __esm({
|
|
|
2200
2207
|
"src/store/issue-store.ts"() {
|
|
2201
2208
|
"use strict";
|
|
2202
2209
|
init_fs();
|
|
2203
|
-
|
|
2204
|
-
init_limits();
|
|
2205
|
-
init_thresholds();
|
|
2206
|
-
init_limits();
|
|
2210
|
+
init_config();
|
|
2207
2211
|
init_atomic_writer();
|
|
2208
2212
|
init_log();
|
|
2209
2213
|
init_type_guards();
|
|
@@ -2446,7 +2450,7 @@ function unwrapResponse(parsed) {
|
|
|
2446
2450
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
2447
2451
|
const obj = parsed;
|
|
2448
2452
|
const keys = Object.keys(obj);
|
|
2449
|
-
if (keys.length >
|
|
2453
|
+
if (keys.length > MAX_WRAPPER_KEYS) return parsed;
|
|
2450
2454
|
let best = null;
|
|
2451
2455
|
let bestSize = 0;
|
|
2452
2456
|
for (const key of keys) {
|
|
@@ -2464,10 +2468,12 @@ function unwrapResponse(parsed) {
|
|
|
2464
2468
|
}
|
|
2465
2469
|
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
2466
2470
|
}
|
|
2471
|
+
var MAX_WRAPPER_KEYS;
|
|
2467
2472
|
var init_response = __esm({
|
|
2468
2473
|
"src/utils/response.ts"() {
|
|
2469
2474
|
"use strict";
|
|
2470
|
-
|
|
2475
|
+
init_config();
|
|
2476
|
+
MAX_WRAPPER_KEYS = 3;
|
|
2471
2477
|
}
|
|
2472
2478
|
});
|
|
2473
2479
|
|
|
@@ -2505,92 +2511,154 @@ var init_patterns = __esm({
|
|
|
2505
2511
|
}
|
|
2506
2512
|
});
|
|
2507
2513
|
|
|
2508
|
-
// src/
|
|
2509
|
-
function
|
|
2510
|
-
|
|
2511
|
-
if (
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2514
|
+
// src/utils/collections.ts
|
|
2515
|
+
function getOrCreate(map, key, create) {
|
|
2516
|
+
let value = map.get(key);
|
|
2517
|
+
if (value === void 0) {
|
|
2518
|
+
value = create();
|
|
2519
|
+
map.set(key, value);
|
|
2520
|
+
}
|
|
2521
|
+
return value;
|
|
2522
|
+
}
|
|
2523
|
+
function deduplicateFindings(items, extract) {
|
|
2524
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2525
|
+
const findings = [];
|
|
2526
|
+
for (const item of items) {
|
|
2527
|
+
const result = extract(item);
|
|
2528
|
+
if (!result) continue;
|
|
2529
|
+
const existing = seen.get(result.key);
|
|
2530
|
+
if (existing) {
|
|
2531
|
+
existing.count++;
|
|
2532
|
+
continue;
|
|
2533
|
+
}
|
|
2534
|
+
seen.set(result.key, result.finding);
|
|
2535
|
+
findings.push(result.finding);
|
|
2536
|
+
}
|
|
2537
|
+
return findings;
|
|
2538
|
+
}
|
|
2539
|
+
function groupBy(items, keyFn) {
|
|
2540
|
+
const map = /* @__PURE__ */ new Map();
|
|
2541
|
+
for (const item of items) {
|
|
2542
|
+
const key = keyFn(item);
|
|
2543
|
+
if (key == null) continue;
|
|
2544
|
+
let arr = map.get(key);
|
|
2545
|
+
if (!arr) {
|
|
2546
|
+
arr = [];
|
|
2547
|
+
map.set(key, arr);
|
|
2516
2548
|
}
|
|
2517
|
-
|
|
2549
|
+
arr.push(item);
|
|
2550
|
+
}
|
|
2551
|
+
return map;
|
|
2552
|
+
}
|
|
2553
|
+
var init_collections = __esm({
|
|
2554
|
+
"src/utils/collections.ts"() {
|
|
2555
|
+
"use strict";
|
|
2518
2556
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2557
|
+
});
|
|
2558
|
+
|
|
2559
|
+
// src/utils/object-scan.ts
|
|
2560
|
+
function walkObject(obj, visitor, options) {
|
|
2561
|
+
const opts = { ...DEFAULTS, ...options };
|
|
2562
|
+
walk(obj, visitor, opts, 0);
|
|
2563
|
+
}
|
|
2564
|
+
function walk(obj, visitor, opts, depth) {
|
|
2565
|
+
if (depth >= opts.maxDepth) return;
|
|
2566
|
+
if (!obj || typeof obj !== "object") return;
|
|
2567
|
+
if (Array.isArray(obj)) {
|
|
2568
|
+
for (let i = 0; i < Math.min(obj.length, opts.arrayLimit); i++) {
|
|
2569
|
+
walk(obj[i], visitor, opts, depth + 1);
|
|
2523
2570
|
}
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
for (const key of Object.keys(obj)) {
|
|
2574
|
+
const val = obj[key];
|
|
2575
|
+
visitor(key, val, depth);
|
|
2524
2576
|
if (typeof val === "object" && val !== null) {
|
|
2525
|
-
|
|
2577
|
+
walk(val, visitor, opts, depth + 1);
|
|
2526
2578
|
}
|
|
2527
2579
|
}
|
|
2528
|
-
return found;
|
|
2529
2580
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2581
|
+
function collectFromObject(obj, match, options) {
|
|
2582
|
+
const results = [];
|
|
2583
|
+
walkObject(obj, (key, value) => {
|
|
2584
|
+
const result = match(key, value);
|
|
2585
|
+
if (result !== null) results.push(result);
|
|
2586
|
+
}, options);
|
|
2587
|
+
return results;
|
|
2588
|
+
}
|
|
2589
|
+
var DEFAULTS;
|
|
2590
|
+
var init_object_scan = __esm({
|
|
2591
|
+
"src/utils/object-scan.ts"() {
|
|
2592
|
+
"use strict";
|
|
2593
|
+
init_config();
|
|
2594
|
+
DEFAULTS = {
|
|
2595
|
+
maxDepth: MAX_OBJECT_SCAN_DEPTH,
|
|
2596
|
+
arrayLimit: SECRET_SCAN_ARRAY_LIMIT
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
|
|
2601
|
+
// src/analysis/rules/auth-rules.ts
|
|
2602
|
+
function findSecretKeys(obj) {
|
|
2603
|
+
return collectFromObject(
|
|
2604
|
+
obj,
|
|
2605
|
+
(key, val) => SECRET_KEYS.test(key) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val) ? key : null
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
function isFrameworkResponse(request) {
|
|
2609
|
+
if (isRedirect(request.statusCode)) return true;
|
|
2610
|
+
if (request.path?.startsWith("/__")) return true;
|
|
2611
|
+
if (request.responseHeaders?.["x-middleware-rewrite"]) return true;
|
|
2612
|
+
return false;
|
|
2613
|
+
}
|
|
2614
|
+
var exposedSecretRule, tokenInUrlRule, insecureCookieRule, corsCredentialsRule;
|
|
2615
|
+
var init_auth_rules = __esm({
|
|
2616
|
+
"src/analysis/rules/auth-rules.ts"() {
|
|
2533
2617
|
"use strict";
|
|
2534
2618
|
init_patterns();
|
|
2535
|
-
|
|
2619
|
+
init_config();
|
|
2536
2620
|
init_http_status();
|
|
2621
|
+
init_collections();
|
|
2622
|
+
init_object_scan();
|
|
2537
2623
|
exposedSecretRule = {
|
|
2538
2624
|
id: "exposed-secret",
|
|
2539
2625
|
severity: "critical",
|
|
2540
2626
|
name: "Exposed Secret in Response",
|
|
2541
2627
|
hint: RULE_HINTS["exposed-secret"],
|
|
2542
2628
|
check(ctx) {
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
if (
|
|
2547
|
-
const
|
|
2548
|
-
if (
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
|
|
2563
|
-
hint: this.hint,
|
|
2564
|
-
endpoint: ep,
|
|
2565
|
-
count: 1
|
|
2629
|
+
return deduplicateFindings(ctx.requests, (request) => {
|
|
2630
|
+
if (isErrorStatus(request.statusCode)) return null;
|
|
2631
|
+
const parsed = ctx.parsedBodies.response.get(request.id);
|
|
2632
|
+
if (!parsed) return null;
|
|
2633
|
+
const keys = findSecretKeys(parsed);
|
|
2634
|
+
if (keys.length === 0) return null;
|
|
2635
|
+
const ep = `${request.method} ${request.path}`;
|
|
2636
|
+
return {
|
|
2637
|
+
key: `${ep}:${keys.sort().join(",")}`,
|
|
2638
|
+
finding: {
|
|
2639
|
+
severity: "critical",
|
|
2640
|
+
rule: "exposed-secret",
|
|
2641
|
+
title: "Exposed Secret in Response",
|
|
2642
|
+
desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
|
|
2643
|
+
hint: this.hint,
|
|
2644
|
+
detail: `Exposed fields: ${keys.join(", ")}. ${keys.length} unmasked secret value${keys.length !== 1 ? "s" : ""} in response body.`,
|
|
2645
|
+
endpoint: ep,
|
|
2646
|
+
count: 1
|
|
2647
|
+
}
|
|
2566
2648
|
};
|
|
2567
|
-
|
|
2568
|
-
findings.push(finding);
|
|
2569
|
-
}
|
|
2570
|
-
return findings;
|
|
2649
|
+
});
|
|
2571
2650
|
}
|
|
2572
2651
|
};
|
|
2573
|
-
}
|
|
2574
|
-
});
|
|
2575
|
-
|
|
2576
|
-
// src/analysis/rules/token-in-url.ts
|
|
2577
|
-
var tokenInUrlRule;
|
|
2578
|
-
var init_token_in_url = __esm({
|
|
2579
|
-
"src/analysis/rules/token-in-url.ts"() {
|
|
2580
|
-
"use strict";
|
|
2581
|
-
init_patterns();
|
|
2582
2652
|
tokenInUrlRule = {
|
|
2583
2653
|
id: "token-in-url",
|
|
2584
2654
|
severity: "critical",
|
|
2585
2655
|
name: "Auth Token in URL",
|
|
2586
2656
|
hint: RULE_HINTS["token-in-url"],
|
|
2587
2657
|
check(ctx) {
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
const
|
|
2592
|
-
if (qIdx === -1) continue;
|
|
2593
|
-
const params = r.url.substring(qIdx + 1).split("&");
|
|
2658
|
+
return deduplicateFindings(ctx.requests, (request) => {
|
|
2659
|
+
const qIdx = request.url.indexOf("?");
|
|
2660
|
+
if (qIdx === -1) return null;
|
|
2661
|
+
const params = request.url.substring(qIdx + 1).split("&");
|
|
2594
2662
|
const flagged = [];
|
|
2595
2663
|
for (const param of params) {
|
|
2596
2664
|
const [name, ...rest] = param.split("=");
|
|
@@ -2600,222 +2668,64 @@ var init_token_in_url = __esm({
|
|
|
2600
2668
|
flagged.push(name);
|
|
2601
2669
|
}
|
|
2602
2670
|
}
|
|
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 = {
|
|
2671
|
+
if (flagged.length === 0) return null;
|
|
2672
|
+
const ep = `${request.method} ${request.path}`;
|
|
2673
|
+
return {
|
|
2674
|
+
key: `${ep}:${flagged.sort().join(",")}`,
|
|
2675
|
+
finding: {
|
|
2703
2676
|
severity: "critical",
|
|
2704
|
-
rule: "
|
|
2705
|
-
title: "
|
|
2706
|
-
desc: `${ep} \u2014
|
|
2677
|
+
rule: "token-in-url",
|
|
2678
|
+
title: "Auth Token in URL",
|
|
2679
|
+
desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
|
|
2707
2680
|
hint: this.hint,
|
|
2681
|
+
detail: `Parameters in URL: ${flagged.join(", ")}. Auth tokens in URLs are logged by proxies, browsers, and CDNs.`,
|
|
2708
2682
|
endpoint: ep,
|
|
2709
2683
|
count: 1
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
}
|
|
2714
|
-
}
|
|
2715
|
-
return findings;
|
|
2684
|
+
}
|
|
2685
|
+
};
|
|
2686
|
+
});
|
|
2716
2687
|
}
|
|
2717
2688
|
};
|
|
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
2689
|
insecureCookieRule = {
|
|
2735
2690
|
id: "insecure-cookie",
|
|
2736
2691
|
severity: "warning",
|
|
2737
2692
|
name: "Insecure Cookie",
|
|
2738
2693
|
hint: RULE_HINTS["insecure-cookie"],
|
|
2739
2694
|
check(ctx) {
|
|
2740
|
-
const
|
|
2741
|
-
const
|
|
2742
|
-
|
|
2743
|
-
if (
|
|
2744
|
-
|
|
2745
|
-
const setCookie = r.responseHeaders["set-cookie"];
|
|
2695
|
+
const cookieEntries = [];
|
|
2696
|
+
for (const request of ctx.requests) {
|
|
2697
|
+
if (!request.responseHeaders) continue;
|
|
2698
|
+
if (isFrameworkResponse(request)) continue;
|
|
2699
|
+
const setCookie = request.responseHeaders["set-cookie"];
|
|
2746
2700
|
if (!setCookie) continue;
|
|
2747
2701
|
const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
|
|
2748
2702
|
for (const cookie of cookies) {
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2703
|
+
cookieEntries.push({ cookie });
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
return deduplicateFindings(cookieEntries, ({ cookie }) => {
|
|
2707
|
+
const cookieName = cookie.trim().split("=")[0].trim();
|
|
2708
|
+
const lower = cookie.toLowerCase();
|
|
2709
|
+
const issues = [];
|
|
2710
|
+
if (!lower.includes("httponly")) issues.push("HttpOnly");
|
|
2711
|
+
if (!lower.includes("samesite")) issues.push("SameSite");
|
|
2712
|
+
if (issues.length === 0) return null;
|
|
2713
|
+
return {
|
|
2714
|
+
key: `${cookieName}:${issues.join(",")}`,
|
|
2715
|
+
finding: {
|
|
2762
2716
|
severity: "warning",
|
|
2763
2717
|
rule: "insecure-cookie",
|
|
2764
2718
|
title: "Insecure Cookie",
|
|
2765
2719
|
desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
|
|
2766
2720
|
hint: this.hint,
|
|
2721
|
+
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
2722
|
endpoint: cookieName,
|
|
2768
2723
|
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
|
-
}];
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
});
|
|
2808
2727
|
}
|
|
2809
2728
|
};
|
|
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
2729
|
corsCredentialsRule = {
|
|
2820
2730
|
id: "cors-credentials",
|
|
2821
2731
|
severity: "warning",
|
|
@@ -2824,12 +2734,12 @@ var init_cors_credentials = __esm({
|
|
|
2824
2734
|
check(ctx) {
|
|
2825
2735
|
const findings = [];
|
|
2826
2736
|
const seen = /* @__PURE__ */ new Set();
|
|
2827
|
-
for (const
|
|
2828
|
-
if (!
|
|
2829
|
-
const origin =
|
|
2830
|
-
const creds =
|
|
2737
|
+
for (const request of ctx.requests) {
|
|
2738
|
+
if (!request.responseHeaders) continue;
|
|
2739
|
+
const origin = request.responseHeaders["access-control-allow-origin"];
|
|
2740
|
+
const creds = request.responseHeaders["access-control-allow-credentials"];
|
|
2831
2741
|
if (origin !== "*" || creds !== "true") continue;
|
|
2832
|
-
const ep = `${
|
|
2742
|
+
const ep = `${request.method} ${request.path}`;
|
|
2833
2743
|
if (seen.has(ep)) continue;
|
|
2834
2744
|
seen.add(ep);
|
|
2835
2745
|
findings.push({
|
|
@@ -2848,25 +2758,13 @@ var init_cors_credentials = __esm({
|
|
|
2848
2758
|
}
|
|
2849
2759
|
});
|
|
2850
2760
|
|
|
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;
|
|
2761
|
+
// src/analysis/rules/data-rules.ts
|
|
2762
|
+
function findEmails(obj) {
|
|
2763
|
+
return collectFromObject(
|
|
2764
|
+
obj,
|
|
2765
|
+
(_key, val) => typeof val === "string" && EMAIL_RE.test(val) ? val : null,
|
|
2766
|
+
{ arrayLimit: PII_SCAN_ARRAY_LIMIT }
|
|
2767
|
+
);
|
|
2870
2768
|
}
|
|
2871
2769
|
function topLevelFieldCount(obj) {
|
|
2872
2770
|
if (Array.isArray(obj)) {
|
|
@@ -2939,56 +2837,147 @@ function detectPII(method, reqBody, resBody) {
|
|
|
2939
2837
|
const target = unwrapResponse(resBody);
|
|
2940
2838
|
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
|
|
2941
2839
|
}
|
|
2942
|
-
var WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
|
|
2943
|
-
var
|
|
2944
|
-
"src/analysis/rules/
|
|
2840
|
+
var stackTraceLeakRule, CRITICAL_PATTERNS, errorInfoLeakRule, sensitiveLogsRule, WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
|
|
2841
|
+
var init_data_rules = __esm({
|
|
2842
|
+
"src/analysis/rules/data-rules.ts"() {
|
|
2945
2843
|
"use strict";
|
|
2844
|
+
init_collections();
|
|
2946
2845
|
init_patterns();
|
|
2947
2846
|
init_response();
|
|
2948
|
-
|
|
2847
|
+
init_config();
|
|
2949
2848
|
init_http_status();
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
"
|
|
2956
|
-
};
|
|
2957
|
-
responsePiiLeakRule = {
|
|
2958
|
-
id: "response-pii-leak",
|
|
2959
|
-
severity: "warning",
|
|
2960
|
-
name: "PII Leak in Response",
|
|
2961
|
-
hint: RULE_HINTS["response-pii-leak"],
|
|
2849
|
+
init_object_scan();
|
|
2850
|
+
stackTraceLeakRule = {
|
|
2851
|
+
id: "stack-trace-leak",
|
|
2852
|
+
severity: "critical",
|
|
2853
|
+
name: "Stack Trace Leaked to Client",
|
|
2854
|
+
hint: RULE_HINTS["stack-trace-leak"],
|
|
2962
2855
|
check(ctx) {
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
severity: "warning",
|
|
2981
|
-
rule: "response-pii-leak",
|
|
2982
|
-
title: "PII Leak in Response",
|
|
2983
|
-
desc: `${ep} \u2014 exposes PII in response`,
|
|
2984
|
-
hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
|
|
2985
|
-
endpoint: ep,
|
|
2986
|
-
count: 1
|
|
2856
|
+
return deduplicateFindings(ctx.requests, (request) => {
|
|
2857
|
+
if (!request.responseBody) return null;
|
|
2858
|
+
if (!STACK_TRACE_RE.test(request.responseBody)) return null;
|
|
2859
|
+
const ep = `${request.method} ${request.path}`;
|
|
2860
|
+
const firstLine = request.responseBody.split("\n").find((l) => STACK_TRACE_RE.test(l))?.trim() ?? "";
|
|
2861
|
+
return {
|
|
2862
|
+
key: ep,
|
|
2863
|
+
finding: {
|
|
2864
|
+
severity: "critical",
|
|
2865
|
+
rule: "stack-trace-leak",
|
|
2866
|
+
title: "Stack Trace Leaked to Client",
|
|
2867
|
+
desc: `${ep} \u2014 response exposes internal stack trace`,
|
|
2868
|
+
hint: this.hint,
|
|
2869
|
+
detail: firstLine ? `Stack trace: ${firstLine.slice(0, DETAIL_PREVIEW_LENGTH)}` : void 0,
|
|
2870
|
+
endpoint: ep,
|
|
2871
|
+
count: 1
|
|
2872
|
+
}
|
|
2987
2873
|
};
|
|
2988
|
-
|
|
2989
|
-
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
CRITICAL_PATTERNS = [
|
|
2878
|
+
{ re: DB_CONN_RE, label: "database connection string" },
|
|
2879
|
+
{ re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
|
|
2880
|
+
{ re: SECRET_VAL_RE, label: "secret value" }
|
|
2881
|
+
];
|
|
2882
|
+
errorInfoLeakRule = {
|
|
2883
|
+
id: "error-info-leak",
|
|
2884
|
+
severity: "critical",
|
|
2885
|
+
name: "Sensitive Data in Error Response",
|
|
2886
|
+
hint: RULE_HINTS["error-info-leak"],
|
|
2887
|
+
check(ctx) {
|
|
2888
|
+
const entries = [];
|
|
2889
|
+
for (const request of ctx.requests) {
|
|
2890
|
+
if (request.statusCode < 400) continue;
|
|
2891
|
+
if (!request.responseBody) continue;
|
|
2892
|
+
if (request.responseHeaders["x-nextjs-error"] || request.responseHeaders["x-nextjs-matched-path"]) continue;
|
|
2893
|
+
const ep = `${request.method} ${request.path}`;
|
|
2894
|
+
for (const pattern of CRITICAL_PATTERNS) {
|
|
2895
|
+
if (pattern.re.test(request.responseBody)) {
|
|
2896
|
+
entries.push({ ep, pattern, body: request.responseBody });
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2990
2899
|
}
|
|
2991
|
-
return
|
|
2900
|
+
return deduplicateFindings(entries, ({ ep, pattern }) => {
|
|
2901
|
+
return {
|
|
2902
|
+
key: `${ep}:${pattern.label}`,
|
|
2903
|
+
finding: {
|
|
2904
|
+
severity: "critical",
|
|
2905
|
+
rule: "error-info-leak",
|
|
2906
|
+
title: "Sensitive Data in Error Response",
|
|
2907
|
+
desc: `${ep} \u2014 error response exposes ${pattern.label}`,
|
|
2908
|
+
hint: this.hint,
|
|
2909
|
+
detail: `Detected: ${pattern.label} in error response body`,
|
|
2910
|
+
endpoint: ep,
|
|
2911
|
+
count: 1
|
|
2912
|
+
}
|
|
2913
|
+
};
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
sensitiveLogsRule = {
|
|
2918
|
+
id: "sensitive-logs",
|
|
2919
|
+
severity: "warning",
|
|
2920
|
+
name: "Sensitive Data in Logs",
|
|
2921
|
+
hint: RULE_HINTS["sensitive-logs"],
|
|
2922
|
+
check(ctx) {
|
|
2923
|
+
let count = 0;
|
|
2924
|
+
for (const log of ctx.logs) {
|
|
2925
|
+
if (!log.message) continue;
|
|
2926
|
+
if (log.message.startsWith("[brakit]")) continue;
|
|
2927
|
+
if (LOG_SECRET_RE.test(log.message)) count++;
|
|
2928
|
+
}
|
|
2929
|
+
if (count === 0) return [];
|
|
2930
|
+
return [{
|
|
2931
|
+
severity: "warning",
|
|
2932
|
+
rule: "sensitive-logs",
|
|
2933
|
+
title: "Sensitive Data in Logs",
|
|
2934
|
+
desc: `Console output contains secret/token values \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
|
|
2935
|
+
hint: this.hint,
|
|
2936
|
+
endpoint: "console",
|
|
2937
|
+
count
|
|
2938
|
+
}];
|
|
2939
|
+
}
|
|
2940
|
+
};
|
|
2941
|
+
WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
2942
|
+
REASON_LABELS = {
|
|
2943
|
+
echo: "echoes back PII from the request body",
|
|
2944
|
+
"full-record": "returns a full record with email and internal IDs",
|
|
2945
|
+
"list-pii": "returns a list of records containing email addresses",
|
|
2946
|
+
"sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
|
|
2947
|
+
};
|
|
2948
|
+
responsePiiLeakRule = {
|
|
2949
|
+
id: "response-pii-leak",
|
|
2950
|
+
severity: "warning",
|
|
2951
|
+
name: "PII Leak in Response",
|
|
2952
|
+
hint: RULE_HINTS["response-pii-leak"],
|
|
2953
|
+
check(ctx) {
|
|
2954
|
+
return deduplicateFindings(ctx.requests, (request) => {
|
|
2955
|
+
if (isErrorStatus(request.statusCode)) return null;
|
|
2956
|
+
if (SELF_SERVICE_PATH.test(request.path)) return null;
|
|
2957
|
+
const resJson = ctx.parsedBodies.response.get(request.id);
|
|
2958
|
+
if (!resJson) return null;
|
|
2959
|
+
const reqJson = ctx.parsedBodies.request.get(request.id) ?? null;
|
|
2960
|
+
const detection = detectPII(request.method, reqJson, resJson);
|
|
2961
|
+
if (!detection) return null;
|
|
2962
|
+
const ep = `${request.method} ${request.path}`;
|
|
2963
|
+
const fieldCount = topLevelFieldCount(resJson);
|
|
2964
|
+
const detailParts = [`Pattern: ${REASON_LABELS[detection.reason]}`];
|
|
2965
|
+
if (detection.emailCount > 0) detailParts.push(`${detection.emailCount} email${detection.emailCount !== 1 ? "s" : ""} detected`);
|
|
2966
|
+
if (fieldCount > 0) detailParts.push(`${fieldCount} fields per record`);
|
|
2967
|
+
return {
|
|
2968
|
+
key: ep,
|
|
2969
|
+
finding: {
|
|
2970
|
+
severity: "warning",
|
|
2971
|
+
rule: "response-pii-leak",
|
|
2972
|
+
title: "PII Leak in Response",
|
|
2973
|
+
desc: `${ep} \u2014 exposes PII in response`,
|
|
2974
|
+
hint: this.hint,
|
|
2975
|
+
detail: detailParts.join(". "),
|
|
2976
|
+
endpoint: ep,
|
|
2977
|
+
count: 1
|
|
2978
|
+
}
|
|
2979
|
+
};
|
|
2980
|
+
});
|
|
2992
2981
|
}
|
|
2993
2982
|
};
|
|
2994
2983
|
}
|
|
@@ -2998,14 +2987,14 @@ var init_response_pii_leak = __esm({
|
|
|
2998
2987
|
function buildBodyCache(requests) {
|
|
2999
2988
|
const response = /* @__PURE__ */ new Map();
|
|
3000
2989
|
const request = /* @__PURE__ */ new Map();
|
|
3001
|
-
for (const
|
|
3002
|
-
if (
|
|
3003
|
-
const parsed = tryParseJson(
|
|
3004
|
-
if (parsed != null) response.set(
|
|
2990
|
+
for (const req of requests) {
|
|
2991
|
+
if (req.responseBody) {
|
|
2992
|
+
const parsed = tryParseJson(req.responseBody);
|
|
2993
|
+
if (parsed != null) response.set(req.id, parsed);
|
|
3005
2994
|
}
|
|
3006
|
-
if (
|
|
3007
|
-
const parsed = tryParseJson(
|
|
3008
|
-
if (parsed != null) request.set(
|
|
2995
|
+
if (req.requestBody) {
|
|
2996
|
+
const parsed = tryParseJson(req.requestBody);
|
|
2997
|
+
if (parsed != null) request.set(req.id, parsed);
|
|
3009
2998
|
}
|
|
3010
2999
|
}
|
|
3011
3000
|
return { response, request };
|
|
@@ -3027,14 +3016,10 @@ var init_scanner = __esm({
|
|
|
3027
3016
|
"src/analysis/rules/scanner.ts"() {
|
|
3028
3017
|
"use strict";
|
|
3029
3018
|
init_response();
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
init_insecure_cookie();
|
|
3035
|
-
init_sensitive_logs();
|
|
3036
|
-
init_cors_credentials();
|
|
3037
|
-
init_response_pii_leak();
|
|
3019
|
+
init_log();
|
|
3020
|
+
init_type_guards();
|
|
3021
|
+
init_auth_rules();
|
|
3022
|
+
init_data_rules();
|
|
3038
3023
|
SecurityScanner = class {
|
|
3039
3024
|
constructor() {
|
|
3040
3025
|
this.rules = [];
|
|
@@ -3051,7 +3036,8 @@ var init_scanner = __esm({
|
|
|
3051
3036
|
for (const rule of this.rules) {
|
|
3052
3037
|
try {
|
|
3053
3038
|
findings.push(...rule.check(ctx));
|
|
3054
|
-
} catch {
|
|
3039
|
+
} catch (e) {
|
|
3040
|
+
brakitDebug(`rule ${rule.id} failed: ${getErrorMessage(e)}`);
|
|
3055
3041
|
}
|
|
3056
3042
|
}
|
|
3057
3043
|
return findings;
|
|
@@ -3068,14 +3054,8 @@ var init_rules = __esm({
|
|
|
3068
3054
|
"src/analysis/rules/index.ts"() {
|
|
3069
3055
|
"use strict";
|
|
3070
3056
|
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();
|
|
3057
|
+
init_auth_rules();
|
|
3058
|
+
init_data_rules();
|
|
3079
3059
|
}
|
|
3080
3060
|
});
|
|
3081
3061
|
|
|
@@ -3099,48 +3079,6 @@ var init_disposable = __esm({
|
|
|
3099
3079
|
}
|
|
3100
3080
|
});
|
|
3101
3081
|
|
|
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"() {
|
|
3138
|
-
"use strict";
|
|
3139
|
-
DYNAMIC_SEGMENT_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$|^\d+$|^[0-9a-f]{12,}$|^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/i;
|
|
3140
|
-
ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
3141
|
-
}
|
|
3142
|
-
});
|
|
3143
|
-
|
|
3144
3082
|
// src/analysis/insights/query-helpers.ts
|
|
3145
3083
|
function getQueryShape(q) {
|
|
3146
3084
|
if (q.sql) return normalizeQueryParams(q.sql) ?? "";
|
|
@@ -3161,7 +3099,7 @@ var init_query_helpers = __esm({
|
|
|
3161
3099
|
});
|
|
3162
3100
|
|
|
3163
3101
|
// src/analysis/insights/prepare.ts
|
|
3164
|
-
function
|
|
3102
|
+
function emptyEndpointGroup() {
|
|
3165
3103
|
return {
|
|
3166
3104
|
total: 0,
|
|
3167
3105
|
errors: 0,
|
|
@@ -3173,16 +3111,12 @@ function createEndpointGroup() {
|
|
|
3173
3111
|
queryShapeDurations: /* @__PURE__ */ new Map()
|
|
3174
3112
|
};
|
|
3175
3113
|
}
|
|
3176
|
-
function
|
|
3114
|
+
function keepRecentPerEndpoint(requests) {
|
|
3177
3115
|
const byEndpoint = /* @__PURE__ */ new Map();
|
|
3178
|
-
for (const
|
|
3179
|
-
const
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
list = [];
|
|
3183
|
-
byEndpoint.set(ep, list);
|
|
3184
|
-
}
|
|
3185
|
-
list.push(r);
|
|
3116
|
+
for (const request of requests) {
|
|
3117
|
+
const endpointKey = getEndpointKey(request.method, request.path);
|
|
3118
|
+
const list = getOrCreate(byEndpoint, endpointKey, () => []);
|
|
3119
|
+
list.push(request);
|
|
3186
3120
|
}
|
|
3187
3121
|
const windowed = [];
|
|
3188
3122
|
for (const [, reqs] of byEndpoint) {
|
|
@@ -3190,54 +3124,67 @@ function windowByEndpoint(requests) {
|
|
|
3190
3124
|
}
|
|
3191
3125
|
return windowed;
|
|
3192
3126
|
}
|
|
3127
|
+
function filterUserRequests(requests) {
|
|
3128
|
+
return requests.filter(
|
|
3129
|
+
(request) => !request.isStatic && !request.isHealthCheck && (!request.path || !request.path.startsWith(DASHBOARD_PREFIX))
|
|
3130
|
+
);
|
|
3131
|
+
}
|
|
3193
3132
|
function extractActiveEndpoints(requests) {
|
|
3194
3133
|
const endpoints = /* @__PURE__ */ new Set();
|
|
3195
|
-
for (const
|
|
3196
|
-
|
|
3197
|
-
endpoints.add(getEndpointKey(r.method, r.path));
|
|
3198
|
-
}
|
|
3134
|
+
for (const request of filterUserRequests(requests)) {
|
|
3135
|
+
endpoints.add(getEndpointKey(request.method, request.path));
|
|
3199
3136
|
}
|
|
3200
3137
|
return endpoints;
|
|
3201
3138
|
}
|
|
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);
|
|
3139
|
+
function aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq) {
|
|
3210
3140
|
const endpointGroups = /* @__PURE__ */ new Map();
|
|
3211
|
-
for (const
|
|
3212
|
-
const
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3141
|
+
for (const request of recent) {
|
|
3142
|
+
const endpointKey = getEndpointKey(request.method, request.path);
|
|
3143
|
+
const group = getOrCreate(endpointGroups, endpointKey, emptyEndpointGroup);
|
|
3144
|
+
group.total++;
|
|
3145
|
+
if (isErrorStatus(request.statusCode)) group.errors++;
|
|
3146
|
+
group.totalDuration += request.durationMs;
|
|
3147
|
+
group.totalSize += request.responseSize ?? 0;
|
|
3148
|
+
const reqQueries = queriesByReq.get(request.id) ?? [];
|
|
3149
|
+
group.queryCount += reqQueries.length;
|
|
3150
|
+
for (const query of reqQueries) {
|
|
3151
|
+
group.totalQueryTimeMs += query.durationMs;
|
|
3152
|
+
const shape = getQueryShape(query);
|
|
3153
|
+
const info = getQueryInfo(query);
|
|
3154
|
+
const shapeDuration = getOrCreate(group.queryShapeDurations, shape, () => ({
|
|
3155
|
+
totalMs: 0,
|
|
3156
|
+
count: 0,
|
|
3157
|
+
label: info.op + (info.table ? ` ${info.table}` : "")
|
|
3158
|
+
}));
|
|
3159
|
+
shapeDuration.totalMs += query.durationMs;
|
|
3160
|
+
shapeDuration.count++;
|
|
3217
3161
|
}
|
|
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++;
|
|
3162
|
+
const reqFetches = fetchesByReq.get(request.id) ?? [];
|
|
3163
|
+
for (const fetch of reqFetches) {
|
|
3164
|
+
group.totalFetchTimeMs += fetch.durationMs;
|
|
3235
3165
|
}
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3166
|
+
}
|
|
3167
|
+
return endpointGroups;
|
|
3168
|
+
}
|
|
3169
|
+
function collectStrictModeDupeIds(ctx) {
|
|
3170
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3171
|
+
for (const flow of ctx.flows) {
|
|
3172
|
+
for (const req of flow.requests) {
|
|
3173
|
+
if (req.isStrictModeDupe) ids.add(req.id);
|
|
3239
3174
|
}
|
|
3240
3175
|
}
|
|
3176
|
+
return ids;
|
|
3177
|
+
}
|
|
3178
|
+
function buildInsightContext(ctx) {
|
|
3179
|
+
const strictModeDupeIds = collectStrictModeDupeIds(ctx);
|
|
3180
|
+
const nonStatic = filterUserRequests(ctx.requests).filter((req) => !strictModeDupeIds.has(req.id));
|
|
3181
|
+
const filteredQueries = strictModeDupeIds.size > 0 ? ctx.queries.filter((q) => !q.parentRequestId || !strictModeDupeIds.has(q.parentRequestId)) : ctx.queries;
|
|
3182
|
+
const filteredFetches = strictModeDupeIds.size > 0 ? ctx.fetches.filter((f) => !f.parentRequestId || !strictModeDupeIds.has(f.parentRequestId)) : ctx.fetches;
|
|
3183
|
+
const queriesByReq = groupBy(filteredQueries, (query) => query.parentRequestId);
|
|
3184
|
+
const fetchesByReq = groupBy(filteredFetches, (fetch) => fetch.parentRequestId);
|
|
3185
|
+
const reqById = new Map(nonStatic.map((request) => [request.id, request]));
|
|
3186
|
+
const recent = keepRecentPerEndpoint(nonStatic);
|
|
3187
|
+
const endpointGroups = aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq);
|
|
3241
3188
|
return {
|
|
3242
3189
|
...ctx,
|
|
3243
3190
|
nonStatic,
|
|
@@ -3254,7 +3201,7 @@ var init_prepare = __esm({
|
|
|
3254
3201
|
init_endpoint();
|
|
3255
3202
|
init_constants();
|
|
3256
3203
|
init_http_status();
|
|
3257
|
-
|
|
3204
|
+
init_config();
|
|
3258
3205
|
init_query_helpers();
|
|
3259
3206
|
}
|
|
3260
3207
|
});
|
|
@@ -3265,6 +3212,8 @@ var init_runner = __esm({
|
|
|
3265
3212
|
"src/analysis/insights/runner.ts"() {
|
|
3266
3213
|
"use strict";
|
|
3267
3214
|
init_prepare();
|
|
3215
|
+
init_log();
|
|
3216
|
+
init_type_guards();
|
|
3268
3217
|
SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
3269
3218
|
InsightRunner = class {
|
|
3270
3219
|
constructor() {
|
|
@@ -3274,12 +3223,13 @@ var init_runner = __esm({
|
|
|
3274
3223
|
this.rules.push(rule);
|
|
3275
3224
|
}
|
|
3276
3225
|
run(ctx) {
|
|
3277
|
-
const prepared =
|
|
3226
|
+
const prepared = buildInsightContext(ctx);
|
|
3278
3227
|
const insights = [];
|
|
3279
3228
|
for (const rule of this.rules) {
|
|
3280
3229
|
try {
|
|
3281
3230
|
insights.push(...rule.check(prepared));
|
|
3282
|
-
} catch {
|
|
3231
|
+
} catch (e) {
|
|
3232
|
+
brakitDebug(`insight rule ${rule.id} failed: ${getErrorMessage(e)}`);
|
|
3283
3233
|
}
|
|
3284
3234
|
}
|
|
3285
3235
|
insights.sort(
|
|
@@ -3291,420 +3241,129 @@ var init_runner = __esm({
|
|
|
3291
3241
|
}
|
|
3292
3242
|
});
|
|
3293
3243
|
|
|
3294
|
-
// src/analysis/insights/rules/
|
|
3295
|
-
var n1Rule;
|
|
3296
|
-
var
|
|
3297
|
-
"src/analysis/insights/rules/
|
|
3244
|
+
// src/analysis/insights/rules/query-rules.ts
|
|
3245
|
+
var n1Rule, redundantQueryRule, selectStarRule, highRowsRule, queryHeavyRule;
|
|
3246
|
+
var init_query_rules = __esm({
|
|
3247
|
+
"src/analysis/insights/rules/query-rules.ts"() {
|
|
3298
3248
|
"use strict";
|
|
3299
3249
|
init_query_helpers();
|
|
3300
3250
|
init_endpoint();
|
|
3301
3251
|
init_constants();
|
|
3252
|
+
init_patterns();
|
|
3302
3253
|
n1Rule = {
|
|
3303
3254
|
id: "n1",
|
|
3304
3255
|
check(ctx) {
|
|
3305
3256
|
const insights = [];
|
|
3306
|
-
const
|
|
3257
|
+
const reportedKeys = /* @__PURE__ */ new Set();
|
|
3307
3258
|
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3308
3259
|
const req = ctx.reqById.get(reqId);
|
|
3309
3260
|
if (!req) continue;
|
|
3310
3261
|
const endpoint = getEndpointKey(req.method, req.path);
|
|
3311
3262
|
const shapeGroups = /* @__PURE__ */ new Map();
|
|
3312
|
-
for (const
|
|
3313
|
-
const shape = getQueryShape(
|
|
3263
|
+
for (const query of reqQueries) {
|
|
3264
|
+
const shape = getQueryShape(query);
|
|
3314
3265
|
let group = shapeGroups.get(shape);
|
|
3315
3266
|
if (!group) {
|
|
3316
|
-
group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first:
|
|
3267
|
+
group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: query };
|
|
3317
3268
|
shapeGroups.set(shape, group);
|
|
3318
3269
|
}
|
|
3319
3270
|
group.count++;
|
|
3320
|
-
group.distinctSql.add(
|
|
3271
|
+
group.distinctSql.add(query.sql ?? shape);
|
|
3321
3272
|
}
|
|
3322
|
-
for (const [,
|
|
3323
|
-
if (
|
|
3324
|
-
const info = getQueryInfo(
|
|
3273
|
+
for (const [, shapeGroup] of shapeGroups) {
|
|
3274
|
+
if (shapeGroup.count <= N1_QUERY_THRESHOLD || shapeGroup.distinctSql.size <= 1) continue;
|
|
3275
|
+
const info = getQueryInfo(shapeGroup.first);
|
|
3325
3276
|
const key = `${endpoint}:${info.op}:${info.table || "unknown"}`;
|
|
3326
|
-
if (
|
|
3327
|
-
|
|
3277
|
+
if (reportedKeys.has(key)) continue;
|
|
3278
|
+
reportedKeys.add(key);
|
|
3328
3279
|
insights.push({
|
|
3329
3280
|
severity: "critical",
|
|
3330
3281
|
type: "n1",
|
|
3331
3282
|
title: "N+1 Query Pattern",
|
|
3332
|
-
desc: `${endpoint} runs ${
|
|
3283
|
+
desc: `${endpoint} runs ${shapeGroup.count}x ${info.op} ${info.table} with different params in a single request`,
|
|
3333
3284
|
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
|
-
|
|
3285
|
+
detail: `${shapeGroup.count} queries with ${shapeGroup.distinctSql.size} distinct param variations. Example: ${[...shapeGroup.distinctSql][0]?.slice(0, DETAIL_PREVIEW_LENGTH) ?? info.op + " " + info.table}`
|
|
3335
3286
|
});
|
|
3336
3287
|
}
|
|
3337
3288
|
}
|
|
3338
3289
|
return insights;
|
|
3339
3290
|
}
|
|
3340
3291
|
};
|
|
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"
|
|
3393
|
-
});
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
|
-
return insights;
|
|
3397
|
-
}
|
|
3398
|
-
};
|
|
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
3292
|
redundantQueryRule = {
|
|
3411
3293
|
id: "redundant-query",
|
|
3412
3294
|
check(ctx) {
|
|
3413
3295
|
const insights = [];
|
|
3414
|
-
const
|
|
3296
|
+
const reportedKeys = /* @__PURE__ */ new Set();
|
|
3415
3297
|
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3416
3298
|
const req = ctx.reqById.get(reqId);
|
|
3417
3299
|
if (!req) continue;
|
|
3418
3300
|
const endpoint = getEndpointKey(req.method, req.path);
|
|
3419
|
-
const
|
|
3420
|
-
for (const
|
|
3421
|
-
if (!
|
|
3422
|
-
let entry =
|
|
3301
|
+
const identicalQueryMap = /* @__PURE__ */ new Map();
|
|
3302
|
+
for (const query of reqQueries) {
|
|
3303
|
+
if (!query.sql) continue;
|
|
3304
|
+
let entry = identicalQueryMap.get(query.sql);
|
|
3423
3305
|
if (!entry) {
|
|
3424
|
-
entry = { count: 0, first:
|
|
3425
|
-
|
|
3306
|
+
entry = { count: 0, first: query };
|
|
3307
|
+
identicalQueryMap.set(query.sql, entry);
|
|
3426
3308
|
}
|
|
3427
3309
|
entry.count++;
|
|
3428
3310
|
}
|
|
3429
|
-
for (const [,
|
|
3430
|
-
if (
|
|
3431
|
-
const info = getQueryInfo(
|
|
3311
|
+
for (const [, entry] of identicalQueryMap) {
|
|
3312
|
+
if (entry.count < REDUNDANT_QUERY_MIN_COUNT) continue;
|
|
3313
|
+
const info = getQueryInfo(entry.first);
|
|
3432
3314
|
const label = info.op + (info.table ? ` ${info.table}` : "");
|
|
3433
|
-
const
|
|
3434
|
-
if (
|
|
3435
|
-
|
|
3315
|
+
const deduplicationKey = `${endpoint}:${label}`;
|
|
3316
|
+
if (reportedKeys.has(deduplicationKey)) continue;
|
|
3317
|
+
reportedKeys.add(deduplicationKey);
|
|
3436
3318
|
insights.push({
|
|
3437
3319
|
severity: "warning",
|
|
3438
3320
|
type: "redundant-query",
|
|
3439
3321
|
title: "Redundant Query",
|
|
3440
|
-
desc: `${label} runs ${
|
|
3322
|
+
desc: `${label} runs ${entry.count}x with identical params in ${endpoint}.`,
|
|
3441
3323
|
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
|
-
|
|
3324
|
+
detail: entry.first.sql ? `Query: ${entry.first.sql.slice(0, DETAIL_PREVIEW_LENGTH)}` : void 0
|
|
3443
3325
|
});
|
|
3444
3326
|
}
|
|
3445
3327
|
}
|
|
3446
3328
|
return insights;
|
|
3447
3329
|
}
|
|
3448
3330
|
};
|
|
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"
|
|
3644
|
-
});
|
|
3645
|
-
}
|
|
3646
|
-
}
|
|
3647
|
-
return insights;
|
|
3648
|
-
}
|
|
3649
|
-
};
|
|
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
3331
|
selectStarRule = {
|
|
3662
3332
|
id: "select-star",
|
|
3663
3333
|
check(ctx) {
|
|
3664
|
-
const
|
|
3334
|
+
const tableCounts = /* @__PURE__ */ new Map();
|
|
3665
3335
|
for (const [, reqQueries] of ctx.queriesByReq) {
|
|
3666
|
-
for (const
|
|
3667
|
-
if (!
|
|
3668
|
-
const isSelectStar = SELECT_STAR_RE.test(
|
|
3336
|
+
for (const query of reqQueries) {
|
|
3337
|
+
if (!query.sql) continue;
|
|
3338
|
+
const isSelectStar = SELECT_STAR_RE.test(query.sql.trim()) || SELECT_DOT_STAR_RE.test(query.sql);
|
|
3669
3339
|
if (!isSelectStar) continue;
|
|
3670
|
-
const info = getQueryInfo(
|
|
3671
|
-
const
|
|
3672
|
-
|
|
3340
|
+
const info = getQueryInfo(query);
|
|
3341
|
+
const table = info.table || "unknown";
|
|
3342
|
+
tableCounts.set(table, (tableCounts.get(table) ?? 0) + 1);
|
|
3673
3343
|
}
|
|
3674
3344
|
}
|
|
3675
3345
|
const insights = [];
|
|
3676
|
-
for (const [table, count] of
|
|
3346
|
+
for (const [table, count] of tableCounts) {
|
|
3677
3347
|
if (count < OVERFETCH_MIN_REQUESTS) continue;
|
|
3678
3348
|
insights.push({
|
|
3679
3349
|
severity: "warning",
|
|
3680
3350
|
type: "select-star",
|
|
3681
3351
|
title: "SELECT * Query",
|
|
3682
3352
|
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"
|
|
3353
|
+
hint: "SELECT * fetches all columns including ones you don\u2019t need. Specify only required columns to reduce data transfer and memory usage."
|
|
3685
3354
|
});
|
|
3686
3355
|
}
|
|
3687
3356
|
return insights;
|
|
3688
3357
|
}
|
|
3689
3358
|
};
|
|
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
3359
|
highRowsRule = {
|
|
3701
3360
|
id: "high-rows",
|
|
3702
3361
|
check(ctx) {
|
|
3703
3362
|
const seen = /* @__PURE__ */ new Map();
|
|
3704
3363
|
for (const [, reqQueries] of ctx.queriesByReq) {
|
|
3705
|
-
for (const
|
|
3706
|
-
if (!
|
|
3707
|
-
const info = getQueryInfo(
|
|
3364
|
+
for (const query of reqQueries) {
|
|
3365
|
+
if (!query.rowCount || query.rowCount <= HIGH_ROW_COUNT) continue;
|
|
3366
|
+
const info = getQueryInfo(query);
|
|
3708
3367
|
const key = `${info.op} ${info.table || "unknown"}`;
|
|
3709
3368
|
let entry = seen.get(key);
|
|
3710
3369
|
if (!entry) {
|
|
@@ -3712,7 +3371,7 @@ var init_high_rows = __esm({
|
|
|
3712
3371
|
seen.set(key, entry);
|
|
3713
3372
|
}
|
|
3714
3373
|
entry.count++;
|
|
3715
|
-
if (
|
|
3374
|
+
if (query.rowCount > entry.max) entry.max = query.rowCount;
|
|
3716
3375
|
}
|
|
3717
3376
|
}
|
|
3718
3377
|
const insights = [];
|
|
@@ -3723,39 +3382,81 @@ var init_high_rows = __esm({
|
|
|
3723
3382
|
type: "high-rows",
|
|
3724
3383
|
title: "Large Result Set",
|
|
3725
3384
|
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"
|
|
3385
|
+
hint: "Fetching many rows slows responses and wastes memory. Add a LIMIT clause, implement pagination, or filter with a WHERE condition."
|
|
3728
3386
|
});
|
|
3729
3387
|
}
|
|
3730
3388
|
return insights;
|
|
3731
3389
|
}
|
|
3732
3390
|
};
|
|
3391
|
+
queryHeavyRule = {
|
|
3392
|
+
id: "query-heavy",
|
|
3393
|
+
check(ctx) {
|
|
3394
|
+
const insights = [];
|
|
3395
|
+
for (const [endpointKey, group] of ctx.endpointGroups) {
|
|
3396
|
+
if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3397
|
+
const avgQueries = Math.round(group.queryCount / group.total);
|
|
3398
|
+
if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
|
|
3399
|
+
insights.push({
|
|
3400
|
+
severity: "warning",
|
|
3401
|
+
type: "query-heavy",
|
|
3402
|
+
title: "Query-Heavy Endpoint",
|
|
3403
|
+
desc: `${endpointKey} \u2014 avg ${avgQueries} queries/request`,
|
|
3404
|
+
hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches."
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
return insights;
|
|
3409
|
+
}
|
|
3410
|
+
};
|
|
3733
3411
|
}
|
|
3734
3412
|
});
|
|
3735
3413
|
|
|
3736
|
-
// src/
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3414
|
+
// src/utils/format.ts
|
|
3415
|
+
function formatDuration(ms) {
|
|
3416
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
3417
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
3418
|
+
}
|
|
3419
|
+
function formatSize(bytes) {
|
|
3420
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
3421
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
3422
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
3423
|
+
}
|
|
3424
|
+
function pct(part, total) {
|
|
3425
|
+
return total > 0 ? Math.round(part / total * 100) : 0;
|
|
3426
|
+
}
|
|
3427
|
+
var init_format = __esm({
|
|
3428
|
+
"src/utils/format.ts"() {
|
|
3429
|
+
"use strict";
|
|
3430
|
+
}
|
|
3431
|
+
});
|
|
3432
|
+
|
|
3433
|
+
// src/analysis/insights/rules/response-rules.ts
|
|
3434
|
+
var responseOverfetchRule, largeResponseRule;
|
|
3435
|
+
var init_response_rules = __esm({
|
|
3436
|
+
"src/analysis/insights/rules/response-rules.ts"() {
|
|
3740
3437
|
"use strict";
|
|
3741
3438
|
init_endpoint();
|
|
3742
3439
|
init_response();
|
|
3743
3440
|
init_http_status();
|
|
3441
|
+
init_format();
|
|
3744
3442
|
init_patterns();
|
|
3443
|
+
init_log();
|
|
3444
|
+
init_type_guards();
|
|
3745
3445
|
init_constants();
|
|
3746
3446
|
responseOverfetchRule = {
|
|
3747
3447
|
id: "response-overfetch",
|
|
3748
3448
|
check(ctx) {
|
|
3749
3449
|
const insights = [];
|
|
3750
3450
|
const seen = /* @__PURE__ */ new Set();
|
|
3751
|
-
for (const
|
|
3752
|
-
if (isErrorStatus(
|
|
3753
|
-
const
|
|
3754
|
-
if (seen.has(
|
|
3451
|
+
for (const request of ctx.nonStatic) {
|
|
3452
|
+
if (isErrorStatus(request.statusCode) || !request.responseBody) continue;
|
|
3453
|
+
const endpointKey = getEndpointKey(request.method, request.path);
|
|
3454
|
+
if (seen.has(endpointKey)) continue;
|
|
3755
3455
|
let parsed;
|
|
3756
3456
|
try {
|
|
3757
|
-
parsed = JSON.parse(
|
|
3758
|
-
} catch {
|
|
3457
|
+
parsed = JSON.parse(request.responseBody);
|
|
3458
|
+
} catch (e) {
|
|
3459
|
+
brakitDebug(`json parse: ${getErrorMessage(e)}`);
|
|
3759
3460
|
continue;
|
|
3760
3461
|
}
|
|
3761
3462
|
const target = unwrapResponse(parsed);
|
|
@@ -3778,45 +3479,33 @@ var init_response_overfetch = __esm({
|
|
|
3778
3479
|
reasons.push(`${fields.length} fields returned`);
|
|
3779
3480
|
}
|
|
3780
3481
|
if (reasons.length > 0) {
|
|
3781
|
-
seen.add(
|
|
3482
|
+
seen.add(endpointKey);
|
|
3782
3483
|
insights.push({
|
|
3783
3484
|
severity: "info",
|
|
3784
3485
|
type: "response-overfetch",
|
|
3785
3486
|
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"
|
|
3487
|
+
desc: `${endpointKey} \u2014 ${reasons.join(", ")}`,
|
|
3488
|
+
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
3489
|
});
|
|
3790
3490
|
}
|
|
3791
3491
|
}
|
|
3792
3492
|
return insights;
|
|
3793
3493
|
}
|
|
3794
3494
|
};
|
|
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
3495
|
largeResponseRule = {
|
|
3806
3496
|
id: "large-response",
|
|
3807
3497
|
check(ctx) {
|
|
3808
3498
|
const insights = [];
|
|
3809
|
-
for (const [
|
|
3810
|
-
if (
|
|
3811
|
-
const avgSize = Math.round(
|
|
3499
|
+
for (const [endpointKey, group] of ctx.endpointGroups) {
|
|
3500
|
+
if (group.total < OVERFETCH_MIN_REQUESTS) continue;
|
|
3501
|
+
const avgSize = Math.round(group.totalSize / group.total);
|
|
3812
3502
|
if (avgSize > LARGE_RESPONSE_BYTES) {
|
|
3813
3503
|
insights.push({
|
|
3814
3504
|
severity: "info",
|
|
3815
3505
|
type: "large-response",
|
|
3816
3506
|
title: "Large Response",
|
|
3817
|
-
desc: `${
|
|
3818
|
-
hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression."
|
|
3819
|
-
nav: "requests"
|
|
3507
|
+
desc: `${endpointKey} \u2014 avg ${formatSize(avgSize)} response`,
|
|
3508
|
+
hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression."
|
|
3820
3509
|
});
|
|
3821
3510
|
}
|
|
3822
3511
|
}
|
|
@@ -3826,13 +3515,66 @@ var init_large_response = __esm({
|
|
|
3826
3515
|
}
|
|
3827
3516
|
});
|
|
3828
3517
|
|
|
3829
|
-
// src/analysis/insights/rules/
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3518
|
+
// src/analysis/insights/rules/reliability-rules.ts
|
|
3519
|
+
function getAdaptiveSlowThreshold(endpointKey, previousMetrics) {
|
|
3520
|
+
if (!previousMetrics) return null;
|
|
3521
|
+
const ep = previousMetrics.find((m) => m.endpoint === endpointKey);
|
|
3522
|
+
if (!ep || ep.sessions.length < BASELINE_MIN_SESSIONS) return null;
|
|
3523
|
+
const valid = ep.sessions.filter((s) => s.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION);
|
|
3524
|
+
if (valid.length < BASELINE_MIN_SESSIONS) return null;
|
|
3525
|
+
const p95s = valid.map((s) => s.p95DurationMs).sort((a, b) => a - b);
|
|
3526
|
+
const medianP95 = p95s[Math.floor(p95s.length / 2)];
|
|
3527
|
+
return medianP95 * 2;
|
|
3528
|
+
}
|
|
3529
|
+
var errorRule, errorHotspotRule, regressionRule, slowRule;
|
|
3530
|
+
var init_reliability_rules = __esm({
|
|
3531
|
+
"src/analysis/insights/rules/reliability-rules.ts"() {
|
|
3833
3532
|
"use strict";
|
|
3834
3533
|
init_format();
|
|
3835
3534
|
init_constants();
|
|
3535
|
+
errorRule = {
|
|
3536
|
+
id: "error",
|
|
3537
|
+
check(ctx) {
|
|
3538
|
+
if (ctx.errors.length === 0) return [];
|
|
3539
|
+
const insights = [];
|
|
3540
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3541
|
+
for (const error of ctx.errors) {
|
|
3542
|
+
const name = error.name || "Error";
|
|
3543
|
+
groups.set(name, (groups.get(name) ?? 0) + 1);
|
|
3544
|
+
}
|
|
3545
|
+
for (const [name, cnt] of groups) {
|
|
3546
|
+
insights.push({
|
|
3547
|
+
severity: "critical",
|
|
3548
|
+
type: "error",
|
|
3549
|
+
title: "Unhandled Error",
|
|
3550
|
+
desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
|
|
3551
|
+
hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
|
|
3552
|
+
detail: ctx.errors.find((e) => e.name === name)?.message
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
return insights;
|
|
3556
|
+
}
|
|
3557
|
+
};
|
|
3558
|
+
errorHotspotRule = {
|
|
3559
|
+
id: "error-hotspot",
|
|
3560
|
+
check(ctx) {
|
|
3561
|
+
const insights = [];
|
|
3562
|
+
for (const [endpointKey, group] of ctx.endpointGroups) {
|
|
3563
|
+
if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3564
|
+
const errorRate = Math.round(group.errors / group.total * 100);
|
|
3565
|
+
if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
|
|
3566
|
+
insights.push({
|
|
3567
|
+
severity: "critical",
|
|
3568
|
+
type: "error-hotspot",
|
|
3569
|
+
title: "Error Hotspot",
|
|
3570
|
+
desc: `${endpointKey} \u2014 ${errorRate}% error rate (${group.errors}/${group.total} requests)`,
|
|
3571
|
+
hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces."
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
return insights;
|
|
3576
|
+
}
|
|
3577
|
+
};
|
|
3836
3578
|
regressionRule = {
|
|
3837
3579
|
id: "regression",
|
|
3838
3580
|
check(ctx) {
|
|
@@ -3851,8 +3593,7 @@ var init_regression = __esm({
|
|
|
3851
3593
|
type: "regression",
|
|
3852
3594
|
title: "Performance Regression",
|
|
3853
3595
|
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"
|
|
3596
|
+
hint: "This endpoint is slower than the previous session. Check if recent code changes added queries or processing."
|
|
3856
3597
|
});
|
|
3857
3598
|
}
|
|
3858
3599
|
if (prev.avgQueryCount > 0 && current.avgQueryCount > prev.avgQueryCount * QUERY_COUNT_REGRESSION_RATIO) {
|
|
@@ -3861,8 +3602,136 @@ var init_regression = __esm({
|
|
|
3861
3602
|
type: "regression",
|
|
3862
3603
|
title: "Query Count Regression",
|
|
3863
3604
|
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
|
-
|
|
3605
|
+
hint: "This endpoint is making more database queries than before. Check for new N+1 patterns or removed query optimizations."
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
return insights;
|
|
3610
|
+
}
|
|
3611
|
+
};
|
|
3612
|
+
slowRule = {
|
|
3613
|
+
id: "slow",
|
|
3614
|
+
check(ctx) {
|
|
3615
|
+
const insights = [];
|
|
3616
|
+
for (const [endpointKey, group] of ctx.endpointGroups) {
|
|
3617
|
+
if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3618
|
+
const avgMs = Math.round(group.totalDuration / group.total);
|
|
3619
|
+
const threshold = getAdaptiveSlowThreshold(endpointKey, ctx.previousMetrics);
|
|
3620
|
+
if (threshold === null || avgMs < threshold) continue;
|
|
3621
|
+
const avgQueryMs = Math.round(group.totalQueryTimeMs / group.total);
|
|
3622
|
+
const avgFetchMs = Math.round(group.totalFetchTimeMs / group.total);
|
|
3623
|
+
const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
|
|
3624
|
+
const parts = [];
|
|
3625
|
+
if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
|
|
3626
|
+
if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
|
|
3627
|
+
if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
|
|
3628
|
+
const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
|
|
3629
|
+
let detail;
|
|
3630
|
+
let slowestMs = 0;
|
|
3631
|
+
for (const [, shapeDuration] of group.queryShapeDurations) {
|
|
3632
|
+
const avgShapeMs = shapeDuration.totalMs / shapeDuration.count;
|
|
3633
|
+
if (avgShapeMs > slowestMs) {
|
|
3634
|
+
slowestMs = avgShapeMs;
|
|
3635
|
+
detail = `Slowest query: ${shapeDuration.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${shapeDuration.count}x)`;
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
insights.push({
|
|
3639
|
+
severity: "warning",
|
|
3640
|
+
type: "slow",
|
|
3641
|
+
title: "Slow Endpoint",
|
|
3642
|
+
desc: `${endpointKey} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
|
|
3643
|
+
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.",
|
|
3644
|
+
detail
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
return insights;
|
|
3648
|
+
}
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
});
|
|
3652
|
+
|
|
3653
|
+
// src/analysis/insights/rules/pattern-rules.ts
|
|
3654
|
+
var duplicateRule, crossEndpointRule;
|
|
3655
|
+
var init_pattern_rules = __esm({
|
|
3656
|
+
"src/analysis/insights/rules/pattern-rules.ts"() {
|
|
3657
|
+
"use strict";
|
|
3658
|
+
init_query_helpers();
|
|
3659
|
+
init_endpoint();
|
|
3660
|
+
init_constants();
|
|
3661
|
+
duplicateRule = {
|
|
3662
|
+
id: "duplicate",
|
|
3663
|
+
check(ctx) {
|
|
3664
|
+
const dupCounts = /* @__PURE__ */ new Map();
|
|
3665
|
+
const flowCount = /* @__PURE__ */ new Map();
|
|
3666
|
+
for (const flow of ctx.flows) {
|
|
3667
|
+
if (!flow.requests) continue;
|
|
3668
|
+
const seenInFlow = /* @__PURE__ */ new Set();
|
|
3669
|
+
for (const request of flow.requests) {
|
|
3670
|
+
if (!request.isDuplicate) continue;
|
|
3671
|
+
const deduplicationKey = `${request.method} ${request.label ?? request.path ?? request.url}`;
|
|
3672
|
+
dupCounts.set(deduplicationKey, (dupCounts.get(deduplicationKey) ?? 0) + 1);
|
|
3673
|
+
if (!seenInFlow.has(deduplicationKey)) {
|
|
3674
|
+
seenInFlow.add(deduplicationKey);
|
|
3675
|
+
flowCount.set(deduplicationKey, (flowCount.get(deduplicationKey) ?? 0) + 1);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
3680
|
+
const insights = [];
|
|
3681
|
+
for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
|
|
3682
|
+
const duplicate = dupEntries[i];
|
|
3683
|
+
insights.push({
|
|
3684
|
+
severity: "warning",
|
|
3685
|
+
type: "duplicate",
|
|
3686
|
+
title: "Duplicate API Call",
|
|
3687
|
+
desc: `${duplicate.key} loaded ${duplicate.count}x as duplicate across ${duplicate.flows} action${duplicate.flows !== 1 ? "s" : ""}`,
|
|
3688
|
+
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."
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
return insights;
|
|
3692
|
+
}
|
|
3693
|
+
};
|
|
3694
|
+
crossEndpointRule = {
|
|
3695
|
+
id: "cross-endpoint",
|
|
3696
|
+
check(ctx) {
|
|
3697
|
+
const insights = [];
|
|
3698
|
+
const queryMap = /* @__PURE__ */ new Map();
|
|
3699
|
+
const allEndpoints = /* @__PURE__ */ new Set();
|
|
3700
|
+
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3701
|
+
const req = ctx.reqById.get(reqId);
|
|
3702
|
+
if (!req) continue;
|
|
3703
|
+
const endpoint = getEndpointKey(req.method, req.path);
|
|
3704
|
+
allEndpoints.add(endpoint);
|
|
3705
|
+
const seenInReq = /* @__PURE__ */ new Set();
|
|
3706
|
+
for (const query of reqQueries) {
|
|
3707
|
+
const shape = getQueryShape(query);
|
|
3708
|
+
let entry = queryMap.get(shape);
|
|
3709
|
+
if (!entry) {
|
|
3710
|
+
entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: query };
|
|
3711
|
+
queryMap.set(shape, entry);
|
|
3712
|
+
}
|
|
3713
|
+
entry.count++;
|
|
3714
|
+
if (!seenInReq.has(shape)) {
|
|
3715
|
+
seenInReq.add(shape);
|
|
3716
|
+
entry.endpoints.add(endpoint);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
|
|
3721
|
+
for (const [, queryMetric] of queryMap) {
|
|
3722
|
+
if (queryMetric.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
|
|
3723
|
+
if (queryMetric.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
|
|
3724
|
+
const coveragePct = Math.round(queryMetric.endpoints.size / allEndpoints.size * 100);
|
|
3725
|
+
if (coveragePct < CROSS_ENDPOINT_PCT) continue;
|
|
3726
|
+
const info = getQueryInfo(queryMetric.first);
|
|
3727
|
+
const label = info.op + (info.table ? ` ${info.table}` : "");
|
|
3728
|
+
insights.push({
|
|
3729
|
+
severity: "warning",
|
|
3730
|
+
type: "cross-endpoint",
|
|
3731
|
+
title: "Repeated Query Across Endpoints",
|
|
3732
|
+
desc: `${label} runs on ${queryMetric.endpoints.size} of ${allEndpoints.size} endpoints (${coveragePct}%).`,
|
|
3733
|
+
hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
|
|
3734
|
+
detail: `Endpoints: ${[...queryMetric.endpoints].slice(0, 5).join(", ")}${queryMetric.endpoints.size > 5 ? ` +${queryMetric.endpoints.size - 5} more` : ""}. Total: ${queryMetric.count} executions.`
|
|
3866
3735
|
});
|
|
3867
3736
|
}
|
|
3868
3737
|
}
|
|
@@ -3881,13 +3750,13 @@ var init_security = __esm({
|
|
|
3881
3750
|
id: "security",
|
|
3882
3751
|
check(ctx) {
|
|
3883
3752
|
if (!ctx.securityFindings) return [];
|
|
3884
|
-
return ctx.securityFindings.map((
|
|
3885
|
-
severity:
|
|
3753
|
+
return ctx.securityFindings.map((finding) => ({
|
|
3754
|
+
severity: finding.severity,
|
|
3886
3755
|
type: "security",
|
|
3887
|
-
title:
|
|
3888
|
-
desc:
|
|
3889
|
-
hint:
|
|
3890
|
-
|
|
3756
|
+
title: finding.title,
|
|
3757
|
+
desc: finding.desc,
|
|
3758
|
+
hint: finding.hint,
|
|
3759
|
+
detail: finding.detail
|
|
3891
3760
|
}));
|
|
3892
3761
|
}
|
|
3893
3762
|
};
|
|
@@ -3898,19 +3767,10 @@ var init_security = __esm({
|
|
|
3898
3767
|
var init_rules2 = __esm({
|
|
3899
3768
|
"src/analysis/insights/rules/index.ts"() {
|
|
3900
3769
|
"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();
|
|
3770
|
+
init_query_rules();
|
|
3771
|
+
init_response_rules();
|
|
3772
|
+
init_reliability_rules();
|
|
3773
|
+
init_pattern_rules();
|
|
3914
3774
|
init_security();
|
|
3915
3775
|
}
|
|
3916
3776
|
});
|
|
@@ -3969,8 +3829,7 @@ function insightToIssue(insight) {
|
|
|
3969
3829
|
desc: insight.desc,
|
|
3970
3830
|
hint: insight.hint,
|
|
3971
3831
|
detail: insight.detail,
|
|
3972
|
-
endpoint: extractEndpointFromDesc(insight.desc) ?? void 0
|
|
3973
|
-
nav: insight.nav
|
|
3832
|
+
endpoint: extractEndpointFromDesc(insight.desc) ?? void 0
|
|
3974
3833
|
};
|
|
3975
3834
|
}
|
|
3976
3835
|
function securityFindingToIssue(finding) {
|
|
@@ -3981,8 +3840,8 @@ function securityFindingToIssue(finding) {
|
|
|
3981
3840
|
title: finding.title,
|
|
3982
3841
|
desc: finding.desc,
|
|
3983
3842
|
hint: finding.hint,
|
|
3984
|
-
|
|
3985
|
-
|
|
3843
|
+
detail: finding.detail,
|
|
3844
|
+
endpoint: finding.endpoint
|
|
3986
3845
|
};
|
|
3987
3846
|
}
|
|
3988
3847
|
var init_issue_mappers = __esm({
|
|
@@ -3997,7 +3856,7 @@ var AnalysisEngine;
|
|
|
3997
3856
|
var init_engine = __esm({
|
|
3998
3857
|
"src/analysis/engine.ts"() {
|
|
3999
3858
|
"use strict";
|
|
4000
|
-
|
|
3859
|
+
init_config();
|
|
4001
3860
|
init_disposable();
|
|
4002
3861
|
init_group();
|
|
4003
3862
|
init_rules();
|
|
@@ -4006,8 +3865,8 @@ var init_engine = __esm({
|
|
|
4006
3865
|
init_issue_id();
|
|
4007
3866
|
init_prepare();
|
|
4008
3867
|
AnalysisEngine = class {
|
|
4009
|
-
constructor(
|
|
4010
|
-
this.
|
|
3868
|
+
constructor(services, debounceMs = ANALYSIS_DEBOUNCE_MS) {
|
|
3869
|
+
this.services = services;
|
|
4011
3870
|
this.debounceMs = debounceMs;
|
|
4012
3871
|
this.cachedInsights = [];
|
|
4013
3872
|
this.cachedFindings = [];
|
|
@@ -4016,7 +3875,7 @@ var init_engine = __esm({
|
|
|
4016
3875
|
this.scanner = createDefaultScanner();
|
|
4017
3876
|
}
|
|
4018
3877
|
start() {
|
|
4019
|
-
const bus = this.
|
|
3878
|
+
const bus = this.services.bus;
|
|
4020
3879
|
this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
|
|
4021
3880
|
this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
|
|
4022
3881
|
this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
|
|
@@ -4043,12 +3902,12 @@ var init_engine = __esm({
|
|
|
4043
3902
|
}, this.debounceMs);
|
|
4044
3903
|
}
|
|
4045
3904
|
recompute() {
|
|
4046
|
-
const allRequests = this.
|
|
4047
|
-
const queries = this.
|
|
4048
|
-
const errors = this.
|
|
4049
|
-
const logs = this.
|
|
4050
|
-
const fetches = this.
|
|
4051
|
-
const requests =
|
|
3905
|
+
const allRequests = this.services.requestStore.getAll();
|
|
3906
|
+
const queries = this.services.queryStore.getAll();
|
|
3907
|
+
const errors = this.services.errorStore.getAll();
|
|
3908
|
+
const logs = this.services.logStore.getAll();
|
|
3909
|
+
const fetches = this.services.fetchStore.getAll();
|
|
3910
|
+
const requests = keepRecentPerEndpoint(allRequests);
|
|
4052
3911
|
const flows = groupRequestsIntoFlows(requests);
|
|
4053
3912
|
this.cachedFindings = this.scanner.scan({ requests, logs });
|
|
4054
3913
|
this.cachedInsights = computeInsights({
|
|
@@ -4057,33 +3916,29 @@ var init_engine = __esm({
|
|
|
4057
3916
|
errors,
|
|
4058
3917
|
flows,
|
|
4059
3918
|
fetches,
|
|
4060
|
-
previousMetrics: this.
|
|
3919
|
+
previousMetrics: this.services.metricsStore.getAll(),
|
|
4061
3920
|
securityFindings: this.cachedFindings
|
|
4062
3921
|
});
|
|
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);
|
|
3922
|
+
const issueStore = this.services.issueStore;
|
|
3923
|
+
const currentIssueIds = /* @__PURE__ */ new Set();
|
|
3924
|
+
for (const finding of this.cachedFindings) {
|
|
3925
|
+
const issue = securityFindingToIssue(finding);
|
|
3926
|
+
issueStore.upsert(issue, "passive");
|
|
3927
|
+
currentIssueIds.add(computeIssueId(issue));
|
|
4086
3928
|
}
|
|
3929
|
+
for (const insight of this.cachedInsights) {
|
|
3930
|
+
const issue = insightToIssue(insight);
|
|
3931
|
+
issueStore.upsert(issue, "passive");
|
|
3932
|
+
currentIssueIds.add(computeIssueId(issue));
|
|
3933
|
+
}
|
|
3934
|
+
const activeEndpoints = extractActiveEndpoints(allRequests);
|
|
3935
|
+
issueStore.reconcile(currentIssueIds, activeEndpoints);
|
|
3936
|
+
const update = {
|
|
3937
|
+
insights: this.cachedInsights,
|
|
3938
|
+
findings: this.cachedFindings,
|
|
3939
|
+
issues: issueStore.getAll()
|
|
3940
|
+
};
|
|
3941
|
+
this.services.bus.emit("analysis:updated", update);
|
|
4087
3942
|
}
|
|
4088
3943
|
};
|
|
4089
3944
|
}
|
|
@@ -4101,7 +3956,7 @@ var init_src = __esm({
|
|
|
4101
3956
|
init_engine();
|
|
4102
3957
|
init_insights2();
|
|
4103
3958
|
init_insights();
|
|
4104
|
-
VERSION = "0.
|
|
3959
|
+
VERSION = "0.9.1";
|
|
4105
3960
|
}
|
|
4106
3961
|
});
|
|
4107
3962
|
|
|
@@ -4121,11 +3976,13 @@ function getBaseStyles() {
|
|
|
4121
3976
|
--red:#dc2626;
|
|
4122
3977
|
--cyan:#0891b2;
|
|
4123
3978
|
--green-bg:rgba(22,163,74,0.08);--green-bg-subtle:rgba(22,163,74,0.05);--green-border:rgba(22,163,74,0.2);--green-border-subtle:rgba(22,163,74,0.15);
|
|
3979
|
+
--amber-bg:rgba(217,119,6,0.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
3980
|
--sidebar-width:232px;--header-height:52px;
|
|
4125
3981
|
--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
|
|
3982
|
+
--shadow-sm:0 1px 3px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.03);
|
|
3983
|
+
--shadow-md:0 2px 6px rgba(0,0,0,0.08),0 1px 3px rgba(0,0,0,0.04);
|
|
3984
|
+
--shadow-lg:0 4px 16px rgba(0,0,0,0.1),0 2px 6px rgba(0,0,0,0.05);
|
|
3985
|
+
--transition:0.15s ease;
|
|
4129
3986
|
--breakdown-db:#6366f1;--breakdown-fetch:#f59e0b;--breakdown-app:#94a3b8;
|
|
4130
3987
|
--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
|
|
4131
3988
|
--sans:Inter,system-ui,-apple-system,sans-serif;
|
|
@@ -4243,8 +4100,8 @@ function getFlowStyles() {
|
|
|
4243
4100
|
.flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
|
|
4244
4101
|
.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
4102
|
.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:
|
|
4103
|
+
.flow-badge-pill.badge-warn{background:var(--amber-bg);color:var(--amber)}
|
|
4104
|
+
.flow-badge-pill.badge-error{background:var(--red-bg);color:var(--red)}
|
|
4248
4105
|
.flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
|
|
4249
4106
|
|
|
4250
4107
|
/* Flow expand panel */
|
|
@@ -4263,23 +4120,23 @@ function getFlowStyles() {
|
|
|
4263
4120
|
/* Method badges */
|
|
4264
4121
|
.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
4122
|
.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:
|
|
4123
|
+
.method-badge-POST{background:var(--blue-bg);color:var(--blue)}
|
|
4124
|
+
.method-badge-PUT,.method-badge-PATCH{background:var(--amber-bg);color:var(--amber)}
|
|
4125
|
+
.method-badge-DELETE{background:var(--red-bg);color:var(--red)}
|
|
4269
4126
|
.method-badge-HEAD,.method-badge-OPTIONS{background:var(--bg-muted);color:var(--text-muted)}
|
|
4270
4127
|
|
|
4271
4128
|
/* Status pills */
|
|
4272
4129
|
.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
4130
|
.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:
|
|
4131
|
+
.status-pill-3xx{background:var(--cyan-bg);color:var(--cyan)}
|
|
4132
|
+
.status-pill-4xx{background:var(--amber-bg);color:var(--amber)}
|
|
4133
|
+
.status-pill-5xx{background:var(--red-bg);color:var(--red)}
|
|
4277
4134
|
|
|
4278
4135
|
.traffic-card-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;font-size:13px}
|
|
4279
4136
|
.traffic-card-path.is-dup{color:var(--text-muted);font-weight:400}
|
|
4280
4137
|
.traffic-card-dur{color:var(--text-muted);font-size:12px;flex-shrink:0}
|
|
4281
4138
|
.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:
|
|
4139
|
+
.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
4140
|
|
|
4284
4141
|
/* Body toggles */
|
|
4285
4142
|
.traffic-body{padding:0;margin-top:8px}
|
|
@@ -4308,7 +4165,7 @@ function getFlowStyles() {
|
|
|
4308
4165
|
.flow-subreq .subreq-label.is-dup{color:var(--text-muted);font-weight:400}
|
|
4309
4166
|
.flow-subreq .subreq-status{flex-shrink:0}
|
|
4310
4167
|
.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:
|
|
4168
|
+
.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
4169
|
.flow-subreq-detail{display:none;padding:12px 0;border-bottom:1px solid var(--border-subtle)}
|
|
4313
4170
|
.flow-subreq-detail.open{display:block}
|
|
4314
4171
|
|
|
@@ -4337,6 +4194,41 @@ function getFlowStyles() {
|
|
|
4337
4194
|
/* Strict Mode duplicate banner */
|
|
4338
4195
|
.strict-mode-dupe{opacity:0.55}
|
|
4339
4196
|
.strict-mode-banner{font-size:11px;color:var(--text-muted);padding:6px 0 0;font-family:var(--mono)}
|
|
4197
|
+
|
|
4198
|
+
/* Flow detail tabs */
|
|
4199
|
+
.flow-detail-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--border)}
|
|
4200
|
+
.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}
|
|
4201
|
+
.flow-tab:hover{color:var(--text)}
|
|
4202
|
+
.flow-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
|
4203
|
+
|
|
4204
|
+
/* Waterfall chart \u2014 request bars on time axis, sub-events as text rows */
|
|
4205
|
+
.flow-waterfall{padding:0;font-family:var(--mono);font-size:11px}
|
|
4206
|
+
.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}
|
|
4207
|
+
.wf-rows{display:flex;flex-direction:column;gap:0}
|
|
4208
|
+
|
|
4209
|
+
/* Request group \u2014 request bar + its sub-events */
|
|
4210
|
+
.wf-request-group{border-bottom:1px solid var(--border-subtle);padding:2px 0}
|
|
4211
|
+
.wf-request-group:last-child{border-bottom:none}
|
|
4212
|
+
|
|
4213
|
+
/* Request row \u2014 label | bar on time axis | duration */
|
|
4214
|
+
.wf-req-row{display:flex;align-items:center;gap:0;height:24px;transition:background .1s}
|
|
4215
|
+
.wf-req-row:hover{background:var(--bg-hover)}
|
|
4216
|
+
.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}
|
|
4217
|
+
.wf-bar-track{flex:1;position:relative;height:14px;min-width:0;overflow:hidden}
|
|
4218
|
+
.wf-bar{position:absolute;top:1px;height:12px;border-radius:3px;opacity:0.8;min-width:3px}
|
|
4219
|
+
.wf-req-row:hover .wf-bar{opacity:1}
|
|
4220
|
+
.wf-req-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-muted);font-size:10px;padding-left:8px}
|
|
4221
|
+
|
|
4222
|
+
/* Sub-event rows \u2014 same layout as request rows: label | bar track | duration */
|
|
4223
|
+
.wf-sub-row{display:flex;align-items:center;gap:0;height:20px;transition:background .1s}
|
|
4224
|
+
.wf-sub-row:hover{background:var(--bg-hover)}
|
|
4225
|
+
.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}
|
|
4226
|
+
.wf-sub-dot{width:6px;height:6px;border-radius:2px;flex-shrink:0}
|
|
4227
|
+
.wf-sub-bar-sized{height:8px !important;top:3px !important;opacity:0.65}
|
|
4228
|
+
.wf-sub-row:hover .wf-sub-bar-sized{opacity:0.9}
|
|
4229
|
+
.wf-sub-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-dim);font-size:9px;padding-left:8px}
|
|
4230
|
+
|
|
4231
|
+
.wf-loading{color:var(--text-muted);padding:12px 0;font-size:11px;font-family:var(--mono)}
|
|
4340
4232
|
`;
|
|
4341
4233
|
}
|
|
4342
4234
|
var init_flows = __esm({
|
|
@@ -4445,22 +4337,39 @@ function getPerformanceStyles() {
|
|
|
4445
4337
|
.perf-badge-lg{padding:4px 12px;font-size:13px;border-radius:var(--radius-sm)}
|
|
4446
4338
|
.perf-badge-sm{padding:1px 6px;font-size:9px}
|
|
4447
4339
|
|
|
4448
|
-
/* Overview:
|
|
4449
|
-
.perf-
|
|
4450
|
-
.perf-
|
|
4451
|
-
.perf-
|
|
4452
|
-
.perf-
|
|
4453
|
-
.perf-
|
|
4454
|
-
.perf-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
.perf-
|
|
4458
|
-
.perf-
|
|
4459
|
-
.perf-
|
|
4340
|
+
/* Overview: summary cards */
|
|
4341
|
+
.perf-overview{padding:16px 28px}
|
|
4342
|
+
.perf-summary-row{display:flex;gap:8px;margin-bottom:16px}
|
|
4343
|
+
.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)}
|
|
4344
|
+
.perf-summary-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-family:var(--sans);font-weight:600}
|
|
4345
|
+
.perf-summary-value{font-size:20px;font-weight:700;font-family:var(--mono);color:var(--text)}
|
|
4346
|
+
.perf-summary-value-sm{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4347
|
+
|
|
4348
|
+
/* Shared table styles */
|
|
4349
|
+
.perf-table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px}
|
|
4350
|
+
.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}
|
|
4351
|
+
.perf-table tbody td{padding:11px 14px;border-bottom:1px solid var(--border-subtle);color:var(--text)}
|
|
4352
|
+
.perf-table-row{cursor:pointer;transition:background var(--transition, .15s ease)}
|
|
4353
|
+
.perf-table-row:hover{background:var(--bg-hover)}
|
|
4354
|
+
.perf-table tbody tr:last-child td{border-bottom:none}
|
|
4355
|
+
.perf-th-right{text-align:right !important}
|
|
4356
|
+
.perf-th-center{text-align:center !important}
|
|
4357
|
+
.perf-td-right{text-align:right}
|
|
4358
|
+
.perf-td-center{text-align:center}
|
|
4359
|
+
.perf-td-name{font-weight:600;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4360
|
+
.perf-td-muted{color:var(--text-dim)}
|
|
4361
|
+
.perf-row-err{background:var(--red-bg)}
|
|
4362
|
+
.perf-row-err:hover{background:rgba(220,38,38,0.1)}
|
|
4363
|
+
|
|
4364
|
+
/* Heat map table wrapper */
|
|
4365
|
+
.perf-heatmap{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm)}
|
|
4366
|
+
.perf-hm-p95{display:inline-flex;align-items:center;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;border:1px solid}
|
|
4367
|
+
.perf-hm-split-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;background:var(--bg-muted);width:100%;min-width:80px}
|
|
4460
4368
|
|
|
4461
4369
|
/* Detail view */
|
|
4462
4370
|
.perf-detail-header{padding:20px 28px 16px;border-bottom:1px solid var(--border-subtle)}
|
|
4463
4371
|
.perf-detail-title{display:flex;align-items:center;gap:12px;font-size:17px;font-weight:600;color:var(--text);font-family:var(--mono)}
|
|
4372
|
+
.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
4373
|
.perf-metric-row{display:flex;gap:4px;padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
4465
4374
|
.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
4375
|
.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 +4404,42 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
|
|
|
4495
4404
|
.perf-canvas{border-radius:var(--radius);background:var(--bg-muted);border:1px solid var(--border)}
|
|
4496
4405
|
.perf-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:10px}
|
|
4497
4406
|
|
|
4498
|
-
/* Request history
|
|
4407
|
+
/* Request history */
|
|
4499
4408
|
.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-
|
|
4409
|
+
.perf-hist-row-hl{background:rgba(37,99,235,0.1) !important;border-left:3px solid #4ade80}
|
|
4410
|
+
|
|
4411
|
+
/* Callers section */
|
|
4412
|
+
.perf-callers{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
4413
|
+
.perf-callers-list{display:flex;flex-direction:column;gap:0}
|
|
4414
|
+
.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}
|
|
4415
|
+
.perf-caller-row:last-child{border-bottom:none}
|
|
4416
|
+
.perf-caller-name{flex:1;font-weight:500;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4417
|
+
.perf-caller-count{color:var(--text-muted);font-size:11px;flex-shrink:0}
|
|
4418
|
+
.perf-caller-avg{color:var(--text-dim);font-size:11px;flex-shrink:0}
|
|
4419
|
+
|
|
4420
|
+
/* Query breakdown section */
|
|
4421
|
+
.perf-queries{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
4422
|
+
.perf-queries-loading{font-size:11px;color:var(--text-muted);font-family:var(--mono)}
|
|
4423
|
+
.perf-queries-list{display:flex;flex-direction:column;gap:0}
|
|
4424
|
+
.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}
|
|
4425
|
+
.perf-query-row:last-child{border-bottom:none}
|
|
4426
|
+
.perf-query-label{flex:1;font-weight:500;color:var(--accent);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4427
|
+
.perf-query-avg{color:var(--text-muted);font-size:11px;flex-shrink:0}
|
|
4428
|
+
.perf-query-count{color:var(--text-dim);font-size:11px;flex-shrink:0}
|
|
4429
|
+
|
|
4430
|
+
/* Session trends */
|
|
4431
|
+
.perf-trends{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
4432
|
+
.perf-trends-list{display:flex;flex-direction:column;gap:0}
|
|
4433
|
+
.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}
|
|
4434
|
+
.perf-trend-row:last-child{border-bottom:none}
|
|
4435
|
+
.perf-trend-current{background:var(--bg-muted);border-radius:var(--radius-sm);font-weight:600}
|
|
4436
|
+
.perf-trend-time{width:80px;color:var(--text-dim);font-size:11px;flex-shrink:0}
|
|
4437
|
+
.perf-trend-p95{flex-shrink:0}
|
|
4438
|
+
.perf-trend-reqs{color:var(--text-muted);font-size:11px;flex-shrink:0}
|
|
4439
|
+
.perf-trend-errs{font-size:11px;flex-shrink:0}
|
|
4440
|
+
.perf-trend-arrow{font-size:10px;font-weight:600;flex-shrink:0}
|
|
4441
|
+
.perf-trend-slower{color:var(--red)}
|
|
4442
|
+
.perf-trend-faster{color:var(--green)}
|
|
4514
4443
|
`;
|
|
4515
4444
|
}
|
|
4516
4445
|
var init_graph = __esm({
|
|
@@ -4526,9 +4455,10 @@ function getOverviewStyles() {
|
|
|
4526
4455
|
.ov-container{padding:24px 28px}
|
|
4527
4456
|
|
|
4528
4457
|
/* Summary banner */
|
|
4529
|
-
.ov-summary{display:flex;gap:
|
|
4530
|
-
.ov-stat{display:flex;flex-direction:column;gap:
|
|
4531
|
-
.ov-stat
|
|
4458
|
+
.ov-summary{display:flex;gap:10px;margin-bottom:24px;flex-wrap:wrap}
|
|
4459
|
+
.ov-stat{display:flex;flex-direction:column;gap:4px;flex:1;min-width:100px;padding:16px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-sm);transition:box-shadow var(--transition, .15s ease)}
|
|
4460
|
+
.ov-stat:hover{box-shadow:var(--shadow-md)}
|
|
4461
|
+
.ov-stat-value{font-size:22px;font-weight:700;font-family:var(--mono);color:var(--text);line-height:1.2}
|
|
4532
4462
|
.ov-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
|
|
4533
4463
|
|
|
4534
4464
|
/* Section header */
|
|
@@ -4537,8 +4467,8 @@ function getOverviewStyles() {
|
|
|
4537
4467
|
|
|
4538
4468
|
/* Insight cards */
|
|
4539
4469
|
.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)}
|
|
4470
|
+
.ov-card{display:flex;align-items:flex-start;gap:14px;padding:16px 20px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;transition:all var(--transition, .15s ease);box-shadow:var(--shadow-sm)}
|
|
4471
|
+
.ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md);transform:translateY(-1px)}
|
|
4542
4472
|
.ov-card-icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:10px;border-radius:50%;margin-top:2px}
|
|
4543
4473
|
.ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
|
|
4544
4474
|
.ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
|
|
@@ -4547,6 +4477,7 @@ function getOverviewStyles() {
|
|
|
4547
4477
|
.ov-card-body{flex:1;min-width:0}
|
|
4548
4478
|
.ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
|
|
4549
4479
|
.ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
|
|
4480
|
+
.ov-card-detail{font-size:11px;font-family:var(--mono);color:var(--text-muted);margin-top:6px;padding:8px 10px;background:var(--bg-muted);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);line-height:1.5}
|
|
4550
4481
|
.ov-card-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
|
|
4551
4482
|
.ov-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;margin-top:2px;font-family:var(--mono);transition:transform .15s}
|
|
4552
4483
|
|
|
@@ -4842,7 +4773,7 @@ function getTimelineStyles() {
|
|
|
4842
4773
|
}
|
|
4843
4774
|
`;
|
|
4844
4775
|
}
|
|
4845
|
-
var
|
|
4776
|
+
var init_timeline = __esm({
|
|
4846
4777
|
"src/dashboard/styles/timeline.ts"() {
|
|
4847
4778
|
"use strict";
|
|
4848
4779
|
}
|
|
@@ -4862,7 +4793,7 @@ var init_styles = __esm({
|
|
|
4862
4793
|
init_graph();
|
|
4863
4794
|
init_overview();
|
|
4864
4795
|
init_security2();
|
|
4865
|
-
|
|
4796
|
+
init_timeline();
|
|
4866
4797
|
}
|
|
4867
4798
|
});
|
|
4868
4799
|
|
|
@@ -4915,7 +4846,7 @@ var init_page = __esm({
|
|
|
4915
4846
|
});
|
|
4916
4847
|
|
|
4917
4848
|
// src/telemetry/config.ts
|
|
4918
|
-
import { homedir as homedir2 } from "os";
|
|
4849
|
+
import { homedir as homedir2, platform } from "os";
|
|
4919
4850
|
import { join as join3 } from "path";
|
|
4920
4851
|
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
4921
4852
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
@@ -4930,11 +4861,17 @@ function readConfig() {
|
|
|
4930
4861
|
function writeConfig(config) {
|
|
4931
4862
|
try {
|
|
4932
4863
|
if (!existsSync5(CONFIG_DIR))
|
|
4933
|
-
mkdirSync3(CONFIG_DIR, { recursive: true, mode: DIR_MODE_OWNER_ONLY });
|
|
4934
|
-
writeFileSync3(
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4864
|
+
mkdirSync3(CONFIG_DIR, { recursive: true, ...IS_WINDOWS ? {} : { mode: DIR_MODE_OWNER_ONLY } });
|
|
4865
|
+
writeFileSync3(
|
|
4866
|
+
CONFIG_PATH,
|
|
4867
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
4868
|
+
IS_WINDOWS ? {} : { mode: FILE_MODE_OWNER_ONLY }
|
|
4869
|
+
);
|
|
4870
|
+
} catch (err) {
|
|
4871
|
+
if (process.env.BRAKIT_DEBUG) {
|
|
4872
|
+
process.stderr.write(`[brakit] config write failed: ${err?.message ?? err}
|
|
4873
|
+
`);
|
|
4874
|
+
}
|
|
4938
4875
|
}
|
|
4939
4876
|
}
|
|
4940
4877
|
function getOrCreateConfig() {
|
|
@@ -4956,11 +4893,12 @@ function isTelemetryEnabled() {
|
|
|
4956
4893
|
cachedEnabled = readConfig()?.telemetry ?? true;
|
|
4957
4894
|
return cachedEnabled;
|
|
4958
4895
|
}
|
|
4959
|
-
var CONFIG_DIR, CONFIG_PATH, cachedEnabled;
|
|
4960
|
-
var
|
|
4896
|
+
var IS_WINDOWS, CONFIG_DIR, CONFIG_PATH, cachedEnabled;
|
|
4897
|
+
var init_config2 = __esm({
|
|
4961
4898
|
"src/telemetry/config.ts"() {
|
|
4962
4899
|
"use strict";
|
|
4963
|
-
|
|
4900
|
+
init_features();
|
|
4901
|
+
IS_WINDOWS = platform() === "win32";
|
|
4964
4902
|
CONFIG_DIR = join3(homedir2(), ".brakit");
|
|
4965
4903
|
CONFIG_PATH = join3(CONFIG_DIR, "config.json");
|
|
4966
4904
|
cachedEnabled = null;
|
|
@@ -4968,9 +4906,49 @@ var init_config = __esm({
|
|
|
4968
4906
|
});
|
|
4969
4907
|
|
|
4970
4908
|
// src/telemetry/index.ts
|
|
4971
|
-
import { platform, release, arch } from "os";
|
|
4909
|
+
import { platform as platform2, release, arch } from "os";
|
|
4972
4910
|
import { spawn } from "child_process";
|
|
4911
|
+
function commonProperties() {
|
|
4912
|
+
return {
|
|
4913
|
+
brakit_version: VERSION,
|
|
4914
|
+
node_version: process.version,
|
|
4915
|
+
os: `${platform2()}-${release()}`,
|
|
4916
|
+
arch: arch(),
|
|
4917
|
+
$lib: "brakit",
|
|
4918
|
+
$process_person_profile: false,
|
|
4919
|
+
$geoip_disable: true
|
|
4920
|
+
};
|
|
4921
|
+
}
|
|
4922
|
+
function sendToPosthog(event, properties) {
|
|
4923
|
+
if (!isTelemetryEnabled()) return;
|
|
4924
|
+
const config = getOrCreateConfig();
|
|
4925
|
+
const payload = {
|
|
4926
|
+
api_key: POSTHOG_KEY,
|
|
4927
|
+
event,
|
|
4928
|
+
distinct_id: config.anonymousId,
|
|
4929
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4930
|
+
properties: { ...commonProperties(), ...properties }
|
|
4931
|
+
};
|
|
4932
|
+
try {
|
|
4933
|
+
const body = JSON.stringify(payload);
|
|
4934
|
+
const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
|
|
4935
|
+
const child = spawn(
|
|
4936
|
+
process.execPath,
|
|
4937
|
+
[
|
|
4938
|
+
"-e",
|
|
4939
|
+
`fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
|
|
4940
|
+
],
|
|
4941
|
+
{ detached: true, stdio: "ignore" }
|
|
4942
|
+
);
|
|
4943
|
+
child.unref();
|
|
4944
|
+
} catch {
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
function trackEvent(event, properties) {
|
|
4948
|
+
sendToPosthog(event, { sdk: "node", ...properties });
|
|
4949
|
+
}
|
|
4973
4950
|
function initSession(framework, packageManager, isCustomCommand, adapters) {
|
|
4951
|
+
getOrCreateConfig();
|
|
4974
4952
|
session.startTime = Date.now();
|
|
4975
4953
|
session.framework = framework;
|
|
4976
4954
|
session.packageManager = packageManager;
|
|
@@ -4990,7 +4968,25 @@ function recordTabViewed(tab) {
|
|
|
4990
4968
|
session.tabsViewed.add(tab);
|
|
4991
4969
|
}
|
|
4992
4970
|
function recordDashboardOpened() {
|
|
4971
|
+
if (session.dashboardOpened) return;
|
|
4993
4972
|
session.dashboardOpened = true;
|
|
4973
|
+
session.dashboardOpenedAt = Date.now();
|
|
4974
|
+
trackEvent(TELEMETRY_EVENT_DASHBOARD_VIEWED, {
|
|
4975
|
+
time_to_dashboard_ms: session.startTime > 0 ? Date.now() - session.startTime : null,
|
|
4976
|
+
request_count_at_open: session.requestCount
|
|
4977
|
+
});
|
|
4978
|
+
}
|
|
4979
|
+
function recordSetupCompleted(info) {
|
|
4980
|
+
session.frameworkCandidates = info.frameworkCandidates;
|
|
4981
|
+
session.adaptersFailed = info.adaptersFailed;
|
|
4982
|
+
session.setupDurationMs = info.setupDurationMs;
|
|
4983
|
+
session.setupSucceeded = true;
|
|
4984
|
+
}
|
|
4985
|
+
function recordFirstRequest() {
|
|
4986
|
+
if (!session.firstRequestAt) session.firstRequestAt = Date.now();
|
|
4987
|
+
}
|
|
4988
|
+
function recordExitReason(reason) {
|
|
4989
|
+
if (session.exitReason === "unknown") session.exitReason = reason;
|
|
4994
4990
|
}
|
|
4995
4991
|
function speedBucket(ms) {
|
|
4996
4992
|
if (ms === 0) return "none";
|
|
@@ -5001,12 +4997,11 @@ function speedBucket(ms) {
|
|
|
5001
4997
|
}
|
|
5002
4998
|
return `>${t[t.length - 1]}ms`;
|
|
5003
4999
|
}
|
|
5004
|
-
function trackSession(
|
|
5000
|
+
function trackSession(services) {
|
|
5005
5001
|
if (!isTelemetryEnabled()) return;
|
|
5006
5002
|
const isFirstSession = readConfig() === null;
|
|
5007
|
-
const
|
|
5008
|
-
const
|
|
5009
|
-
const analysisEngine = registry.get("analysis-engine");
|
|
5003
|
+
const metricsStore = services.metricsStore;
|
|
5004
|
+
const analysisEngine = services.analysisEngine;
|
|
5010
5005
|
const live = metricsStore.getLiveEndpoints();
|
|
5011
5006
|
const insights = analysisEngine.getInsights();
|
|
5012
5007
|
const findings = analysisEngine.getFindings();
|
|
@@ -5018,64 +5013,49 @@ function trackSession(registry) {
|
|
|
5018
5013
|
totalDuration += ep.summary.p95Ms * ep.summary.totalRequests;
|
|
5019
5014
|
if (ep.summary.p95Ms > slowestP95) slowestP95 = ep.summary.p95Ms;
|
|
5020
5015
|
}
|
|
5021
|
-
const
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
}
|
|
5055
|
-
};
|
|
5056
|
-
try {
|
|
5057
|
-
const body = JSON.stringify(payload);
|
|
5058
|
-
const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
|
|
5059
|
-
const child = spawn(
|
|
5060
|
-
process.execPath,
|
|
5061
|
-
[
|
|
5062
|
-
"-e",
|
|
5063
|
-
`fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
|
|
5064
|
-
],
|
|
5065
|
-
{ detached: true, stdio: "ignore" }
|
|
5066
|
-
);
|
|
5067
|
-
child.unref();
|
|
5068
|
-
} catch {
|
|
5069
|
-
}
|
|
5016
|
+
const now = Date.now();
|
|
5017
|
+
sendToPosthog(TELEMETRY_EVENT_SESSION, {
|
|
5018
|
+
sdk: "node",
|
|
5019
|
+
framework: session.framework,
|
|
5020
|
+
package_manager: session.packageManager,
|
|
5021
|
+
is_custom_command: session.isCustomCommand,
|
|
5022
|
+
first_session: isFirstSession,
|
|
5023
|
+
adapters_detected: session.adapters,
|
|
5024
|
+
request_count: session.requestCount,
|
|
5025
|
+
error_count: services.errorStore.getAll().length,
|
|
5026
|
+
query_count: services.queryStore.getAll().length,
|
|
5027
|
+
fetch_count: services.fetchStore.getAll().length,
|
|
5028
|
+
insight_count: insights.length,
|
|
5029
|
+
finding_count: findings.length,
|
|
5030
|
+
insight_types: [...session.insightTypes],
|
|
5031
|
+
rules_triggered: [...session.rulesTriggered],
|
|
5032
|
+
endpoint_count: live.length,
|
|
5033
|
+
avg_duration_ms: totalRequests > 0 ? Math.round(totalDuration / totalRequests) : 0,
|
|
5034
|
+
slowest_endpoint_bucket: speedBucket(slowestP95),
|
|
5035
|
+
tabs_viewed: [...session.tabsViewed],
|
|
5036
|
+
dashboard_opened: session.dashboardOpened,
|
|
5037
|
+
explain_used: session.explainUsed,
|
|
5038
|
+
session_duration_s: Math.round((now - session.startTime) / 1e3),
|
|
5039
|
+
// Enhanced fields
|
|
5040
|
+
setup_succeeded: session.setupSucceeded,
|
|
5041
|
+
setup_duration_ms: session.setupDurationMs,
|
|
5042
|
+
framework_detection_candidates: session.frameworkCandidates,
|
|
5043
|
+
adapters_failed: session.adaptersFailed,
|
|
5044
|
+
time_to_first_request_ms: session.firstRequestAt ? session.firstRequestAt - session.startTime : null,
|
|
5045
|
+
time_to_dashboard_ms: session.dashboardOpenedAt ? session.dashboardOpenedAt - session.startTime : null,
|
|
5046
|
+
exit_reason: session.exitReason
|
|
5047
|
+
});
|
|
5048
|
+
getOrCreateConfig();
|
|
5070
5049
|
}
|
|
5071
5050
|
var POSTHOG_KEY, session;
|
|
5072
|
-
var
|
|
5051
|
+
var init_telemetry = __esm({
|
|
5073
5052
|
"src/telemetry/index.ts"() {
|
|
5074
5053
|
"use strict";
|
|
5075
5054
|
init_src();
|
|
5055
|
+
init_config2();
|
|
5056
|
+
init_labels();
|
|
5076
5057
|
init_config();
|
|
5077
|
-
|
|
5078
|
-
init_config();
|
|
5058
|
+
init_config2();
|
|
5079
5059
|
POSTHOG_KEY = "phc_E9TwydCGnSfPLIUhNxChpeg32TSowjk31KiPhnLPP0x";
|
|
5080
5060
|
session = {
|
|
5081
5061
|
startTime: 0,
|
|
@@ -5088,7 +5068,14 @@ var init_telemetry2 = __esm({
|
|
|
5088
5068
|
rulesTriggered: /* @__PURE__ */ new Set(),
|
|
5089
5069
|
tabsViewed: /* @__PURE__ */ new Set(),
|
|
5090
5070
|
dashboardOpened: false,
|
|
5091
|
-
explainUsed: false
|
|
5071
|
+
explainUsed: false,
|
|
5072
|
+
frameworkCandidates: [],
|
|
5073
|
+
adaptersFailed: [],
|
|
5074
|
+
setupDurationMs: 0,
|
|
5075
|
+
setupSucceeded: false,
|
|
5076
|
+
firstRequestAt: 0,
|
|
5077
|
+
dashboardOpenedAt: 0,
|
|
5078
|
+
exitReason: "unknown"
|
|
5092
5079
|
};
|
|
5093
5080
|
}
|
|
5094
5081
|
});
|
|
@@ -5097,32 +5084,30 @@ var init_telemetry2 = __esm({
|
|
|
5097
5084
|
function isDashboardRequest(url) {
|
|
5098
5085
|
return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
|
|
5099
5086
|
}
|
|
5100
|
-
function createDashboardHandler(
|
|
5101
|
-
const metricsStore =
|
|
5087
|
+
function createDashboardHandler(services) {
|
|
5088
|
+
const metricsStore = services.metricsStore;
|
|
5102
5089
|
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(
|
|
5090
|
+
[DASHBOARD_API_REQUESTS]: createRequestsHandler(services),
|
|
5091
|
+
[DASHBOARD_API_EVENTS]: createSSEHandler(services),
|
|
5092
|
+
[DASHBOARD_API_FLOWS]: createFlowsHandler(services),
|
|
5093
|
+
[DASHBOARD_API_CLEAR]: createClearHandler(services),
|
|
5094
|
+
[DASHBOARD_API_LOGS]: createLogsHandler(services),
|
|
5095
|
+
[DASHBOARD_API_FETCHES]: createFetchesHandler(services),
|
|
5096
|
+
[DASHBOARD_API_ERRORS]: createErrorsHandler(services),
|
|
5097
|
+
[DASHBOARD_API_QUERIES]: createQueriesHandler(services),
|
|
5111
5098
|
[DASHBOARD_API_METRICS]: createMetricsHandler(metricsStore),
|
|
5112
5099
|
[DASHBOARD_API_METRICS_LIVE]: createLiveMetricsHandler(metricsStore),
|
|
5113
|
-
[DASHBOARD_API_INGEST]: createIngestHandler(
|
|
5114
|
-
[DASHBOARD_API_ACTIVITY]: createActivityHandler(
|
|
5100
|
+
[DASHBOARD_API_INGEST]: createIngestHandler(services),
|
|
5101
|
+
[DASHBOARD_API_ACTIVITY]: createActivityHandler(services)
|
|
5115
5102
|
};
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
);
|
|
5125
|
-
}
|
|
5103
|
+
const issueStore = services.issueStore;
|
|
5104
|
+
routes[DASHBOARD_API_INSIGHTS] = createIssuesHandler(issueStore);
|
|
5105
|
+
routes[DASHBOARD_API_SECURITY] = createIssuesHandler(issueStore);
|
|
5106
|
+
routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(issueStore);
|
|
5107
|
+
routes[DASHBOARD_API_FINDINGS_REPORT] = createIssuesReportHandler(
|
|
5108
|
+
issueStore,
|
|
5109
|
+
services.bus
|
|
5110
|
+
);
|
|
5126
5111
|
routes[DASHBOARD_API_TAB] = (req, res) => {
|
|
5127
5112
|
const raw = (req.url ?? "").split("tab=")[1];
|
|
5128
5113
|
if (raw) {
|
|
@@ -5133,7 +5118,7 @@ function createDashboardHandler(registry) {
|
|
|
5133
5118
|
res.end();
|
|
5134
5119
|
};
|
|
5135
5120
|
return (req, res, config) => {
|
|
5136
|
-
const path = (req.url ?? "/")
|
|
5121
|
+
const path = stripQueryString(req.url ?? "/");
|
|
5137
5122
|
const handler = routes[path];
|
|
5138
5123
|
if (handler) {
|
|
5139
5124
|
handler(req, res);
|
|
@@ -5151,13 +5136,14 @@ function createDashboardHandler(registry) {
|
|
|
5151
5136
|
var init_router = __esm({
|
|
5152
5137
|
"src/dashboard/router.ts"() {
|
|
5153
5138
|
"use strict";
|
|
5139
|
+
init_endpoint();
|
|
5154
5140
|
init_constants();
|
|
5155
|
-
|
|
5141
|
+
init_labels();
|
|
5156
5142
|
init_api();
|
|
5157
5143
|
init_issues();
|
|
5158
5144
|
init_sse();
|
|
5159
5145
|
init_page();
|
|
5160
|
-
|
|
5146
|
+
init_telemetry();
|
|
5161
5147
|
}
|
|
5162
5148
|
});
|
|
5163
5149
|
|
|
@@ -5198,51 +5184,6 @@ var init_event_bus = __esm({
|
|
|
5198
5184
|
}
|
|
5199
5185
|
});
|
|
5200
5186
|
|
|
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
5187
|
// src/store/request-store.ts
|
|
5247
5188
|
function flattenHeaders(headers2) {
|
|
5248
5189
|
const flat = {};
|
|
@@ -5258,6 +5199,7 @@ var init_request_store = __esm({
|
|
|
5258
5199
|
"use strict";
|
|
5259
5200
|
init_constants();
|
|
5260
5201
|
init_static_patterns();
|
|
5202
|
+
init_endpoint();
|
|
5261
5203
|
RequestStore = class {
|
|
5262
5204
|
constructor(maxEntries = MAX_REQUEST_ENTRIES) {
|
|
5263
5205
|
this.maxEntries = maxEntries;
|
|
@@ -5266,7 +5208,7 @@ var init_request_store = __esm({
|
|
|
5266
5208
|
}
|
|
5267
5209
|
capture(input) {
|
|
5268
5210
|
const url = input.url;
|
|
5269
|
-
const path = url
|
|
5211
|
+
const path = stripQueryString(url);
|
|
5270
5212
|
let requestBodyStr = null;
|
|
5271
5213
|
if (input.requestBody && input.requestBody.length > 0) {
|
|
5272
5214
|
requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
|
|
@@ -5291,7 +5233,8 @@ var init_request_store = __esm({
|
|
|
5291
5233
|
startedAt: input.startTime,
|
|
5292
5234
|
durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
|
|
5293
5235
|
responseSize: input.responseBody?.length ?? 0,
|
|
5294
|
-
isStatic: isStaticPath(path)
|
|
5236
|
+
isStatic: isStaticPath(path),
|
|
5237
|
+
isHealthCheck: isHealthCheckPath(path)
|
|
5295
5238
|
};
|
|
5296
5239
|
this.requests.push(entry);
|
|
5297
5240
|
if (this.requests.length > this.maxEntries) {
|
|
@@ -5368,50 +5311,6 @@ var init_telemetry_store = __esm({
|
|
|
5368
5311
|
}
|
|
5369
5312
|
});
|
|
5370
5313
|
|
|
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
5314
|
// src/utils/math.ts
|
|
5416
5315
|
function percentile(values, p) {
|
|
5417
5316
|
if (values.length === 0) return 0;
|
|
@@ -5458,6 +5357,17 @@ var init_metrics_store = __esm({
|
|
|
5458
5357
|
this.dirty = false;
|
|
5459
5358
|
this.accumulators = /* @__PURE__ */ new Map();
|
|
5460
5359
|
this.pendingPoints = /* @__PURE__ */ new Map();
|
|
5360
|
+
/**
|
|
5361
|
+
* Compute the adaptive performance baseline for an endpoint.
|
|
5362
|
+
* Returns the median p95 across historical sessions, or null when
|
|
5363
|
+
* there isn't enough data to establish a meaningful baseline.
|
|
5364
|
+
*/
|
|
5365
|
+
/**
|
|
5366
|
+
* Cached baselines — invalidated on flush (when sessions change) and
|
|
5367
|
+
* on new request recordings (when pending points grow). Avoids recomputing
|
|
5368
|
+
* on every getLiveEndpoints() API call.
|
|
5369
|
+
*/
|
|
5370
|
+
this.baselineCache = /* @__PURE__ */ new Map();
|
|
5461
5371
|
this.data = { version: 1, endpoints: [] };
|
|
5462
5372
|
}
|
|
5463
5373
|
start() {
|
|
@@ -5482,7 +5392,7 @@ var init_metrics_store = __esm({
|
|
|
5482
5392
|
this.flush(true);
|
|
5483
5393
|
}
|
|
5484
5394
|
recordRequest(req, metrics) {
|
|
5485
|
-
if (req.isStatic) return;
|
|
5395
|
+
if (req.isStatic || req.isHealthCheck) return;
|
|
5486
5396
|
this.dirty = true;
|
|
5487
5397
|
const key = getEndpointKey(req.method, req.path);
|
|
5488
5398
|
let acc = this.accumulators.get(key);
|
|
@@ -5531,6 +5441,38 @@ var init_metrics_store = __esm({
|
|
|
5531
5441
|
getEndpoint(endpoint) {
|
|
5532
5442
|
return this.endpointIndex.get(endpoint);
|
|
5533
5443
|
}
|
|
5444
|
+
getEndpointBaseline(endpoint) {
|
|
5445
|
+
const pending2 = this.pendingPoints.get(endpoint);
|
|
5446
|
+
const pointCount = pending2?.length ?? 0;
|
|
5447
|
+
const cached = this.baselineCache.get(endpoint);
|
|
5448
|
+
if (cached && cached.pointCount === pointCount) return cached.value;
|
|
5449
|
+
const value = this.computeBaseline(endpoint, pending2);
|
|
5450
|
+
this.baselineCache.set(endpoint, { value, pointCount });
|
|
5451
|
+
return value;
|
|
5452
|
+
}
|
|
5453
|
+
computeBaseline(endpoint, pending2) {
|
|
5454
|
+
const ep = this.endpointIndex.get(endpoint);
|
|
5455
|
+
if (ep && ep.sessions.length >= BASELINE_MIN_SESSIONS) {
|
|
5456
|
+
const validSessions = ep.sessions.filter(
|
|
5457
|
+
(s) => s.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION
|
|
5458
|
+
);
|
|
5459
|
+
if (validSessions.length >= BASELINE_MIN_SESSIONS) {
|
|
5460
|
+
const p95s = validSessions.map((s) => s.p95DurationMs).sort((a, b) => a - b);
|
|
5461
|
+
return p95s[Math.floor(p95s.length / 2)];
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
if (ep && ep.sessions.length === 1) {
|
|
5465
|
+
const session2 = ep.sessions[0];
|
|
5466
|
+
if (session2.requestCount >= BASELINE_MIN_REQUESTS_PER_SESSION) {
|
|
5467
|
+
return session2.p95DurationMs;
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5470
|
+
if (pending2 && pending2.length >= BASELINE_PENDING_POINTS_MIN) {
|
|
5471
|
+
const warmDurations = pending2.slice(1).map((r) => r.durationMs).sort((a, b) => a - b);
|
|
5472
|
+
return warmDurations[Math.floor(warmDurations.length / 2)];
|
|
5473
|
+
}
|
|
5474
|
+
return null;
|
|
5475
|
+
}
|
|
5534
5476
|
getLiveEndpoints() {
|
|
5535
5477
|
const merged = /* @__PURE__ */ new Map();
|
|
5536
5478
|
for (const ep of this.data.endpoints) {
|
|
@@ -5545,27 +5487,35 @@ var init_metrics_store = __esm({
|
|
|
5545
5487
|
const endpoints = [];
|
|
5546
5488
|
for (const [endpoint, requests] of merged) {
|
|
5547
5489
|
if (requests.length === 0) continue;
|
|
5548
|
-
const
|
|
5490
|
+
const warmRequests = requests.length > 1 ? requests.slice(1) : requests;
|
|
5491
|
+
const warmDurations = warmRequests.map((r) => r.durationMs);
|
|
5549
5492
|
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(
|
|
5493
|
+
const totalQueries = warmRequests.reduce((s, r) => s + r.queryCount, 0);
|
|
5494
|
+
const totalQueryTime = warmRequests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
|
|
5495
|
+
const totalFetchTime = warmRequests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
|
|
5496
|
+
const n = warmRequests.length;
|
|
5497
|
+
const avgDurationMs = Math.round(warmDurations.reduce((s, d) => s + d, 0) / n);
|
|
5555
5498
|
const avgQueryTimeMs = Math.round(totalQueryTime / n);
|
|
5556
5499
|
const avgFetchTimeMs = Math.round(totalFetchTime / n);
|
|
5500
|
+
const p95Ms = percentile(warmDurations, 0.95);
|
|
5501
|
+
const medianMs = percentile(warmDurations, 0.5);
|
|
5502
|
+
const epData = this.endpointIndex.get(endpoint);
|
|
5557
5503
|
endpoints.push({
|
|
5558
5504
|
endpoint,
|
|
5559
5505
|
requests,
|
|
5560
5506
|
summary: {
|
|
5561
|
-
p95Ms
|
|
5562
|
-
|
|
5507
|
+
p95Ms,
|
|
5508
|
+
medianMs,
|
|
5509
|
+
errorRate: errors / requests.length,
|
|
5510
|
+
// Error rate uses ALL requests
|
|
5563
5511
|
avgQueryCount: Math.round(totalQueries / n),
|
|
5564
|
-
totalRequests:
|
|
5512
|
+
totalRequests: requests.length,
|
|
5565
5513
|
avgQueryTimeMs,
|
|
5566
5514
|
avgFetchTimeMs,
|
|
5567
5515
|
avgAppTimeMs: Math.max(0, avgDurationMs - avgQueryTimeMs - avgFetchTimeMs)
|
|
5568
|
-
}
|
|
5516
|
+
},
|
|
5517
|
+
sessions: epData?.sessions,
|
|
5518
|
+
baselineP95Ms: this.getEndpointBaseline(endpoint)
|
|
5569
5519
|
});
|
|
5570
5520
|
}
|
|
5571
5521
|
endpoints.sort((a, b) => b.summary.p95Ms - a.summary.p95Ms);
|
|
@@ -5576,6 +5526,7 @@ var init_metrics_store = __esm({
|
|
|
5576
5526
|
this.endpointIndex.clear();
|
|
5577
5527
|
this.accumulators.clear();
|
|
5578
5528
|
this.pendingPoints.clear();
|
|
5529
|
+
this.baselineCache.clear();
|
|
5579
5530
|
this.dirty = false;
|
|
5580
5531
|
this.persistence.remove();
|
|
5581
5532
|
}
|
|
@@ -5617,6 +5568,7 @@ var init_metrics_store = __esm({
|
|
|
5617
5568
|
epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
|
|
5618
5569
|
}
|
|
5619
5570
|
this.pendingPoints.clear();
|
|
5571
|
+
this.baselineCache.clear();
|
|
5620
5572
|
if (!this.dirty) return;
|
|
5621
5573
|
if (sync) {
|
|
5622
5574
|
this.persistence.saveSync(this.data);
|
|
@@ -5710,10 +5662,6 @@ var init_store = __esm({
|
|
|
5710
5662
|
"use strict";
|
|
5711
5663
|
init_request_store();
|
|
5712
5664
|
init_telemetry_store();
|
|
5713
|
-
init_fetch_store();
|
|
5714
|
-
init_log_store();
|
|
5715
|
-
init_error_store();
|
|
5716
|
-
init_query_store();
|
|
5717
5665
|
init_metrics_store();
|
|
5718
5666
|
init_persistence();
|
|
5719
5667
|
}
|
|
@@ -5745,9 +5693,9 @@ function formatConsoleLine(issue, suffix) {
|
|
|
5745
5693
|
}
|
|
5746
5694
|
return line;
|
|
5747
5695
|
}
|
|
5748
|
-
function startTerminalInsights(
|
|
5749
|
-
const bus =
|
|
5750
|
-
const metricsStore =
|
|
5696
|
+
function startTerminalInsights(services, proxyPort) {
|
|
5697
|
+
const bus = services.bus;
|
|
5698
|
+
const metricsStore = services.metricsStore;
|
|
5751
5699
|
const printedKeys = /* @__PURE__ */ new Set();
|
|
5752
5700
|
const resolvedKeys = /* @__PURE__ */ new Set();
|
|
5753
5701
|
const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
|
|
@@ -5818,8 +5766,8 @@ var init_terminal = __esm({
|
|
|
5818
5766
|
"use strict";
|
|
5819
5767
|
init_src();
|
|
5820
5768
|
init_constants();
|
|
5821
|
-
|
|
5822
|
-
|
|
5769
|
+
init_config();
|
|
5770
|
+
init_labels();
|
|
5823
5771
|
SEVERITY_COLOR = {
|
|
5824
5772
|
critical: pc.red,
|
|
5825
5773
|
warning: pc.yellow,
|
|
@@ -5914,8 +5862,14 @@ function outgoingToIncoming(headers2) {
|
|
|
5914
5862
|
}
|
|
5915
5863
|
return result;
|
|
5916
5864
|
}
|
|
5865
|
+
function getDecompressor(encoding) {
|
|
5866
|
+
if (encoding === CONTENT_ENCODING_GZIP) return gunzip;
|
|
5867
|
+
if (encoding === CONTENT_ENCODING_BR) return brotliDecompress;
|
|
5868
|
+
if (encoding === CONTENT_ENCODING_DEFLATE) return inflate;
|
|
5869
|
+
return null;
|
|
5870
|
+
}
|
|
5917
5871
|
function decompressAsync(body, encoding) {
|
|
5918
|
-
const decompressor = encoding
|
|
5872
|
+
const decompressor = getDecompressor(encoding);
|
|
5919
5873
|
if (!decompressor) return Promise.resolve(body);
|
|
5920
5874
|
return new Promise((resolve6) => {
|
|
5921
5875
|
decompressor(body, (err, result) => {
|
|
@@ -5929,7 +5883,7 @@ function toBuffer(chunk) {
|
|
|
5929
5883
|
if (typeof chunk === "string") return Buffer.from(chunk);
|
|
5930
5884
|
return null;
|
|
5931
5885
|
}
|
|
5932
|
-
function captureInProcess(req, res, requestId, requestStore) {
|
|
5886
|
+
function captureInProcess(req, res, requestId, requestStore, isChild = false) {
|
|
5933
5887
|
const startTime = performance.now();
|
|
5934
5888
|
const method = req.method ?? "GET";
|
|
5935
5889
|
const resChunks = [];
|
|
@@ -5975,30 +5929,32 @@ function captureInProcess(req, res, requestId, requestStore) {
|
|
|
5975
5929
|
const responseHeaders = outgoingToIncoming(res.getHeaders());
|
|
5976
5930
|
const responseContentType = String(res.getHeader("content-type") ?? "");
|
|
5977
5931
|
const capturedChunks = resChunks.slice();
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
body
|
|
5932
|
+
if (!isChild) {
|
|
5933
|
+
void (async () => {
|
|
5934
|
+
try {
|
|
5935
|
+
let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
|
|
5936
|
+
if (body && encoding && !truncated) {
|
|
5937
|
+
body = await decompressAsync(body, encoding);
|
|
5938
|
+
}
|
|
5939
|
+
requestStore.capture({
|
|
5940
|
+
requestId,
|
|
5941
|
+
method,
|
|
5942
|
+
url: req.url ?? "/",
|
|
5943
|
+
requestHeaders: req.headers,
|
|
5944
|
+
requestBody: null,
|
|
5945
|
+
statusCode,
|
|
5946
|
+
responseHeaders,
|
|
5947
|
+
responseBody: body,
|
|
5948
|
+
responseContentType,
|
|
5949
|
+
startTime,
|
|
5950
|
+
endTime,
|
|
5951
|
+
config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
|
|
5952
|
+
});
|
|
5953
|
+
} catch (e) {
|
|
5954
|
+
brakitDebug(`capture store: ${getErrorMessage(e)}`);
|
|
5983
5955
|
}
|
|
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
|
-
})();
|
|
5956
|
+
})();
|
|
5957
|
+
}
|
|
6002
5958
|
return result;
|
|
6003
5959
|
};
|
|
6004
5960
|
}
|
|
@@ -6046,13 +6002,15 @@ function installInterceptor(deps) {
|
|
|
6046
6002
|
deps.handleDashboard(req, res, deps.config);
|
|
6047
6003
|
return true;
|
|
6048
6004
|
}
|
|
6049
|
-
const
|
|
6005
|
+
const propagated = req.headers[BRAKIT_REQUEST_ID_HEADER];
|
|
6006
|
+
const requestId = propagated ?? randomUUID8();
|
|
6007
|
+
const isChild = propagated !== void 0;
|
|
6050
6008
|
const ctx = {
|
|
6051
6009
|
requestId,
|
|
6052
6010
|
url,
|
|
6053
6011
|
method: req.method ?? "GET"
|
|
6054
6012
|
};
|
|
6055
|
-
captureInProcess(req, res, requestId, deps.requestStore);
|
|
6013
|
+
captureInProcess(req, res, requestId, deps.requestStore, isChild);
|
|
6056
6014
|
return storage.run(
|
|
6057
6015
|
ctx,
|
|
6058
6016
|
() => original.apply(this, [event, ...args])
|
|
@@ -6075,7 +6033,8 @@ var init_interceptor = __esm({
|
|
|
6075
6033
|
init_safe_wrap();
|
|
6076
6034
|
init_guard();
|
|
6077
6035
|
init_capture();
|
|
6078
|
-
|
|
6036
|
+
init_labels();
|
|
6037
|
+
init_constants();
|
|
6079
6038
|
originalEmit = null;
|
|
6080
6039
|
}
|
|
6081
6040
|
});
|
|
@@ -6093,18 +6052,12 @@ function setup() {
|
|
|
6093
6052
|
initPromise = doSetup();
|
|
6094
6053
|
return initPromise;
|
|
6095
6054
|
}
|
|
6096
|
-
function createStores(bus
|
|
6055
|
+
function createStores(bus) {
|
|
6097
6056
|
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);
|
|
6057
|
+
const fetchStore = new TelemetryStore();
|
|
6058
|
+
const logStore = new TelemetryStore();
|
|
6059
|
+
const errorStore = new TelemetryStore();
|
|
6060
|
+
const queryStore = new TelemetryStore();
|
|
6108
6061
|
bus.on("telemetry:fetch", (data) => fetchStore.add(data));
|
|
6109
6062
|
bus.on("telemetry:query", (data) => queryStore.add(data));
|
|
6110
6063
|
bus.on("telemetry:log", (data) => logStore.add(data));
|
|
@@ -6124,31 +6077,33 @@ function installHooks(bus) {
|
|
|
6124
6077
|
adapterRegistry.patchAll(telemetryEmit);
|
|
6125
6078
|
const cwd = process.cwd();
|
|
6126
6079
|
let framework = "unknown";
|
|
6080
|
+
let frameworkCandidates = [];
|
|
6127
6081
|
try {
|
|
6128
6082
|
const pkg = JSON.parse(
|
|
6129
|
-
// readFileSync is acceptable here — runs once at startup
|
|
6130
6083
|
__require("fs").readFileSync(resolve5(cwd, "package.json"), "utf-8")
|
|
6131
6084
|
);
|
|
6132
6085
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
6133
6086
|
framework = detectFrameworkFromDeps(allDeps);
|
|
6087
|
+
frameworkCandidates = KNOWN_DEPENDENCY_NAMES.filter((dep) => dep in allDeps);
|
|
6134
6088
|
} catch {
|
|
6135
6089
|
}
|
|
6136
6090
|
return {
|
|
6137
6091
|
framework,
|
|
6138
|
-
adapterNames: adapterRegistry.getActive().map((a) => a.name)
|
|
6092
|
+
adapterNames: adapterRegistry.getActive().map((a) => a.name),
|
|
6093
|
+
adaptersFailed: [...adapterRegistry.getFailed()],
|
|
6094
|
+
frameworkCandidates
|
|
6139
6095
|
};
|
|
6140
6096
|
}
|
|
6141
|
-
function startAnalysis(
|
|
6142
|
-
const bus = registry.get("event-bus");
|
|
6097
|
+
function startAnalysis(bus, stores, dataDir, services) {
|
|
6143
6098
|
const metricsStore = new MetricsStore(new FileMetricsPersistence(dataDir));
|
|
6144
6099
|
metricsStore.start();
|
|
6145
|
-
registry.register("metrics-store", metricsStore);
|
|
6146
6100
|
const issueStore = new IssueStore(dataDir);
|
|
6147
6101
|
issueStore.start();
|
|
6148
|
-
|
|
6149
|
-
|
|
6102
|
+
services.metricsStore = metricsStore;
|
|
6103
|
+
services.issueStore = issueStore;
|
|
6104
|
+
const analysisEngine = new AnalysisEngine(services);
|
|
6150
6105
|
analysisEngine.start();
|
|
6151
|
-
|
|
6106
|
+
services.analysisEngine = analysisEngine;
|
|
6152
6107
|
bus.on("request:completed", (req) => {
|
|
6153
6108
|
const queries = stores.queryStore.getByRequest(req.id);
|
|
6154
6109
|
const fetches = stores.fetchStore.getByRequest(req.id);
|
|
@@ -6160,7 +6115,7 @@ function startAnalysis(registry, stores, dataDir) {
|
|
|
6160
6115
|
});
|
|
6161
6116
|
return { analysisEngine, metricsStore, issueStore };
|
|
6162
6117
|
}
|
|
6163
|
-
function registerLifecycle(
|
|
6118
|
+
function registerLifecycle(allServices, stores, services, cwd) {
|
|
6164
6119
|
let telemetrySent = false;
|
|
6165
6120
|
const sendTelemetry = () => {
|
|
6166
6121
|
if (telemetrySent) return;
|
|
@@ -6172,7 +6127,7 @@ function registerLifecycle(registry, stores, services, cwd) {
|
|
|
6172
6127
|
recordRulesTriggered(
|
|
6173
6128
|
services.analysisEngine.getFindings().map((f) => f.rule)
|
|
6174
6129
|
);
|
|
6175
|
-
trackSession(
|
|
6130
|
+
trackSession(allServices);
|
|
6176
6131
|
};
|
|
6177
6132
|
let teardownCalled = false;
|
|
6178
6133
|
const runTeardown = () => {
|
|
@@ -6191,7 +6146,14 @@ function registerLifecycle(registry, stores, services, cwd) {
|
|
|
6191
6146
|
}
|
|
6192
6147
|
};
|
|
6193
6148
|
health.setTeardown(runTeardown);
|
|
6149
|
+
process.on("SIGINT", () => {
|
|
6150
|
+
recordExitReason(EXIT_REASON_SIGINT);
|
|
6151
|
+
});
|
|
6152
|
+
process.on("SIGTERM", () => {
|
|
6153
|
+
recordExitReason(EXIT_REASON_SIGTERM);
|
|
6154
|
+
});
|
|
6194
6155
|
process.on("beforeExit", () => {
|
|
6156
|
+
recordExitReason(EXIT_REASON_CLEAN);
|
|
6195
6157
|
sendTelemetry();
|
|
6196
6158
|
});
|
|
6197
6159
|
process.on("exit", () => {
|
|
@@ -6199,22 +6161,36 @@ function registerLifecycle(registry, stores, services, cwd) {
|
|
|
6199
6161
|
});
|
|
6200
6162
|
}
|
|
6201
6163
|
async function doSetup() {
|
|
6164
|
+
const setupStart = Date.now();
|
|
6202
6165
|
brakitDebug(`[setup] doSetup called at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
6203
6166
|
const bus = new EventBus();
|
|
6204
|
-
const registry = new ServiceRegistry();
|
|
6205
6167
|
const cwd = process.cwd();
|
|
6206
|
-
const stores = createStores(bus
|
|
6207
|
-
const
|
|
6168
|
+
const stores = createStores(bus);
|
|
6169
|
+
const services = {
|
|
6170
|
+
bus,
|
|
6171
|
+
...stores
|
|
6172
|
+
};
|
|
6173
|
+
const { framework, adapterNames, adaptersFailed, frameworkCandidates } = installHooks(bus);
|
|
6208
6174
|
initSession(framework, detectPackageManagerSync(cwd), false, adapterNames);
|
|
6175
|
+
const setupDurationMs = Date.now() - setupStart;
|
|
6176
|
+
recordSetupCompleted({ frameworkCandidates, adaptersFailed, setupDurationMs });
|
|
6177
|
+
trackEvent(TELEMETRY_EVENT_SETUP_COMPLETED, {
|
|
6178
|
+
framework,
|
|
6179
|
+
framework_detection_candidates: frameworkCandidates,
|
|
6180
|
+
adapters_detected: adapterNames,
|
|
6181
|
+
adapters_failed: adaptersFailed,
|
|
6182
|
+
hooks_installed: ["fetch", "console", "error"],
|
|
6183
|
+
setup_duration_ms: setupDurationMs
|
|
6184
|
+
});
|
|
6209
6185
|
const dataDir = getProjectDataDir(cwd);
|
|
6210
|
-
const
|
|
6186
|
+
const analysisServices = startAnalysis(bus, stores, dataDir, services);
|
|
6211
6187
|
const config = {
|
|
6212
6188
|
proxyPort: 0,
|
|
6213
6189
|
targetPort: 0,
|
|
6214
6190
|
showStatic: false,
|
|
6215
6191
|
maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
|
|
6216
6192
|
};
|
|
6217
|
-
const handleDashboard = createDashboardHandler(
|
|
6193
|
+
const handleDashboard = createDashboardHandler(services);
|
|
6218
6194
|
installInterceptor({
|
|
6219
6195
|
handleDashboard,
|
|
6220
6196
|
config,
|
|
@@ -6222,6 +6198,11 @@ async function doSetup() {
|
|
|
6222
6198
|
onFirstRequest(port) {
|
|
6223
6199
|
setBrakitPort(port);
|
|
6224
6200
|
brakitDebug(`[setup] onFirstRequest fired, port=${port}`);
|
|
6201
|
+
recordFirstRequest();
|
|
6202
|
+
trackEvent(TELEMETRY_EVENT_FIRST_REQUEST, {
|
|
6203
|
+
port,
|
|
6204
|
+
time_to_first_request_ms: Date.now() - setupStart
|
|
6205
|
+
});
|
|
6225
6206
|
void (async () => {
|
|
6226
6207
|
try {
|
|
6227
6208
|
const dir = resolve5(cwd, METRICS_DIR);
|
|
@@ -6247,14 +6228,14 @@ async function doSetup() {
|
|
|
6247
6228
|
brakitDebug(`port file write failed: ${getErrorMessage(err)}`);
|
|
6248
6229
|
}
|
|
6249
6230
|
})();
|
|
6250
|
-
startTerminalInsights(
|
|
6231
|
+
startTerminalInsights(services, port);
|
|
6251
6232
|
process.stdout.write(
|
|
6252
6233
|
` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
|
|
6253
6234
|
`
|
|
6254
6235
|
);
|
|
6255
6236
|
}
|
|
6256
6237
|
});
|
|
6257
|
-
registerLifecycle(
|
|
6238
|
+
registerLifecycle(services, stores, analysisServices, cwd);
|
|
6258
6239
|
}
|
|
6259
6240
|
var initPromise;
|
|
6260
6241
|
var init_setup = __esm({
|
|
@@ -6266,12 +6247,8 @@ var init_setup = __esm({
|
|
|
6266
6247
|
init_adapters();
|
|
6267
6248
|
init_router();
|
|
6268
6249
|
init_event_bus();
|
|
6269
|
-
init_service_registry();
|
|
6270
6250
|
init_request_store();
|
|
6271
|
-
|
|
6272
|
-
init_log_store();
|
|
6273
|
-
init_error_store();
|
|
6274
|
-
init_query_store();
|
|
6251
|
+
init_telemetry_store();
|
|
6275
6252
|
init_store();
|
|
6276
6253
|
init_issue_store();
|
|
6277
6254
|
init_engine();
|
|
@@ -6284,7 +6261,7 @@ var init_setup = __esm({
|
|
|
6284
6261
|
init_type_guards();
|
|
6285
6262
|
init_fs();
|
|
6286
6263
|
init_project();
|
|
6287
|
-
|
|
6264
|
+
init_telemetry();
|
|
6288
6265
|
initPromise = null;
|
|
6289
6266
|
}
|
|
6290
6267
|
});
|