brakit 0.7.4 → 0.7.6
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 +78 -50
- package/dist/api.d.ts +116 -3
- package/dist/api.js +615 -353
- package/dist/bin/brakit.js +38 -31
- package/dist/runtime/index.js +1150 -510
- package/package.json +1 -1
package/dist/runtime/index.js
CHANGED
|
@@ -33,7 +33,7 @@ var init_routes = __esm({
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
// src/constants/limits.ts
|
|
36
|
-
var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES;
|
|
36
|
+
var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH;
|
|
37
37
|
var init_limits = __esm({
|
|
38
38
|
"src/constants/limits.ts"() {
|
|
39
39
|
"use strict";
|
|
@@ -41,11 +41,12 @@ var init_limits = __esm({
|
|
|
41
41
|
DEFAULT_MAX_BODY_CAPTURE = 10240;
|
|
42
42
|
DEFAULT_API_LIMIT = 500;
|
|
43
43
|
MAX_TELEMETRY_ENTRIES = 1e3;
|
|
44
|
+
MAX_TAB_NAME_LENGTH = 32;
|
|
44
45
|
}
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
// src/constants/thresholds.ts
|
|
48
|
-
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;
|
|
49
|
+
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;
|
|
49
50
|
var init_thresholds = __esm({
|
|
50
51
|
"src/constants/thresholds.ts"() {
|
|
51
52
|
"use strict";
|
|
@@ -68,6 +69,13 @@ var init_thresholds = __esm({
|
|
|
68
69
|
OVERFETCH_MIN_FIELDS = 8;
|
|
69
70
|
OVERFETCH_MIN_INTERNAL_IDS = 2;
|
|
70
71
|
OVERFETCH_NULL_RATIO = 0.3;
|
|
72
|
+
REGRESSION_PCT_THRESHOLD = 50;
|
|
73
|
+
REGRESSION_MIN_INCREASE_MS = 200;
|
|
74
|
+
REGRESSION_MIN_REQUESTS = 5;
|
|
75
|
+
QUERY_COUNT_REGRESSION_RATIO = 1.5;
|
|
76
|
+
OVERFETCH_MANY_FIELDS = 12;
|
|
77
|
+
OVERFETCH_UNWRAP_MIN_SIZE = 3;
|
|
78
|
+
MAX_DUPLICATE_INSIGHTS = 3;
|
|
71
79
|
}
|
|
72
80
|
});
|
|
73
81
|
|
|
@@ -154,20 +162,6 @@ var init_constants = __esm({
|
|
|
154
162
|
}
|
|
155
163
|
});
|
|
156
164
|
|
|
157
|
-
// src/instrument/hooks/context.ts
|
|
158
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
159
|
-
import { randomUUID } from "crypto";
|
|
160
|
-
function getRequestContext() {
|
|
161
|
-
return storage.getStore();
|
|
162
|
-
}
|
|
163
|
-
var storage;
|
|
164
|
-
var init_context = __esm({
|
|
165
|
-
"src/instrument/hooks/context.ts"() {
|
|
166
|
-
"use strict";
|
|
167
|
-
storage = new AsyncLocalStorage();
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
165
|
// src/instrument/transport.ts
|
|
172
166
|
function setEmitter(fn) {
|
|
173
167
|
emitter = fn;
|
|
@@ -183,6 +177,20 @@ var init_transport2 = __esm({
|
|
|
183
177
|
}
|
|
184
178
|
});
|
|
185
179
|
|
|
180
|
+
// src/instrument/hooks/context.ts
|
|
181
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
182
|
+
import { randomUUID } from "crypto";
|
|
183
|
+
function getRequestContext() {
|
|
184
|
+
return storage.getStore();
|
|
185
|
+
}
|
|
186
|
+
var storage;
|
|
187
|
+
var init_context = __esm({
|
|
188
|
+
"src/instrument/hooks/context.ts"() {
|
|
189
|
+
"use strict";
|
|
190
|
+
storage = new AsyncLocalStorage();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
186
194
|
// src/instrument/hooks/fetch.ts
|
|
187
195
|
import { subscribe } from "diagnostics_channel";
|
|
188
196
|
function isNoise(origin) {
|
|
@@ -770,7 +778,7 @@ var init_request_store = __esm({
|
|
|
770
778
|
responseHeaders: flattenHeaders(input.responseHeaders),
|
|
771
779
|
responseBody: responseBodyStr,
|
|
772
780
|
startedAt: input.startTime,
|
|
773
|
-
durationMs: Math.round(performance.now() - input.startTime),
|
|
781
|
+
durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
|
|
774
782
|
responseSize: input.responseBody?.length ?? 0,
|
|
775
783
|
isStatic: isStaticPath(path)
|
|
776
784
|
};
|
|
@@ -1297,25 +1305,52 @@ var init_math = __esm({
|
|
|
1297
1305
|
}
|
|
1298
1306
|
});
|
|
1299
1307
|
|
|
1308
|
+
// src/utils/endpoint.ts
|
|
1309
|
+
function getEndpointKey(method, path) {
|
|
1310
|
+
return `${method} ${path}`;
|
|
1311
|
+
}
|
|
1312
|
+
var init_endpoint = __esm({
|
|
1313
|
+
"src/utils/endpoint.ts"() {
|
|
1314
|
+
"use strict";
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1300
1318
|
// src/store/metrics/metrics-store.ts
|
|
1301
1319
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
1320
|
+
function createAccumulator() {
|
|
1321
|
+
return {
|
|
1322
|
+
durations: [],
|
|
1323
|
+
queryCounts: [],
|
|
1324
|
+
errorCount: 0,
|
|
1325
|
+
totalDurationSum: 0,
|
|
1326
|
+
totalRequestCount: 0,
|
|
1327
|
+
totalErrorCount: 0,
|
|
1328
|
+
totalQuerySum: 0,
|
|
1329
|
+
totalQueryTimeMs: 0,
|
|
1330
|
+
totalFetchTimeMs: 0
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1302
1333
|
var MetricsStore;
|
|
1303
1334
|
var init_metrics_store = __esm({
|
|
1304
1335
|
"src/store/metrics/metrics-store.ts"() {
|
|
1305
1336
|
"use strict";
|
|
1306
1337
|
init_constants();
|
|
1307
1338
|
init_math();
|
|
1339
|
+
init_endpoint();
|
|
1308
1340
|
MetricsStore = class {
|
|
1309
1341
|
constructor(persistence) {
|
|
1310
1342
|
this.persistence = persistence;
|
|
1311
1343
|
this.data = persistence.load();
|
|
1344
|
+
for (const ep of this.data.endpoints) {
|
|
1345
|
+
this.endpointIndex.set(ep.endpoint, ep);
|
|
1346
|
+
}
|
|
1312
1347
|
}
|
|
1313
1348
|
data;
|
|
1349
|
+
endpointIndex = /* @__PURE__ */ new Map();
|
|
1314
1350
|
sessionId = randomUUID4();
|
|
1315
1351
|
sessionStart = Date.now();
|
|
1316
1352
|
flushTimer = null;
|
|
1317
1353
|
accumulators = /* @__PURE__ */ new Map();
|
|
1318
|
-
/** Pending data points not yet flushed to disk. */
|
|
1319
1354
|
pendingPoints = /* @__PURE__ */ new Map();
|
|
1320
1355
|
start() {
|
|
1321
1356
|
this.flushTimer = setInterval(
|
|
@@ -1329,19 +1364,25 @@ var init_metrics_store = __esm({
|
|
|
1329
1364
|
clearInterval(this.flushTimer);
|
|
1330
1365
|
this.flushTimer = null;
|
|
1331
1366
|
}
|
|
1332
|
-
this.flush();
|
|
1367
|
+
this.flush(true);
|
|
1333
1368
|
}
|
|
1334
|
-
recordRequest(req,
|
|
1369
|
+
recordRequest(req, metrics) {
|
|
1335
1370
|
if (req.isStatic) return;
|
|
1336
|
-
const key = req.method
|
|
1371
|
+
const key = getEndpointKey(req.method, req.path);
|
|
1337
1372
|
let acc = this.accumulators.get(key);
|
|
1338
1373
|
if (!acc) {
|
|
1339
|
-
acc =
|
|
1374
|
+
acc = createAccumulator();
|
|
1340
1375
|
this.accumulators.set(key, acc);
|
|
1341
1376
|
}
|
|
1342
1377
|
acc.durations.push(req.durationMs);
|
|
1343
|
-
acc.queryCounts.push(queryCount);
|
|
1378
|
+
acc.queryCounts.push(metrics.queryCount);
|
|
1344
1379
|
if (req.statusCode >= 400) acc.errorCount++;
|
|
1380
|
+
acc.totalDurationSum += req.durationMs;
|
|
1381
|
+
acc.totalRequestCount++;
|
|
1382
|
+
acc.totalQuerySum += metrics.queryCount;
|
|
1383
|
+
acc.totalQueryTimeMs += metrics.queryTimeMs;
|
|
1384
|
+
acc.totalFetchTimeMs += metrics.fetchTimeMs;
|
|
1385
|
+
if (req.statusCode >= 400) acc.totalErrorCount++;
|
|
1345
1386
|
const timestamp = Math.round(
|
|
1346
1387
|
Date.now() - (performance.now() - req.startedAt)
|
|
1347
1388
|
);
|
|
@@ -1349,7 +1390,9 @@ var init_metrics_store = __esm({
|
|
|
1349
1390
|
timestamp,
|
|
1350
1391
|
durationMs: req.durationMs,
|
|
1351
1392
|
statusCode: req.statusCode,
|
|
1352
|
-
queryCount
|
|
1393
|
+
queryCount: metrics.queryCount,
|
|
1394
|
+
queryTimeMs: metrics.queryTimeMs,
|
|
1395
|
+
fetchTimeMs: metrics.fetchTimeMs
|
|
1353
1396
|
};
|
|
1354
1397
|
let pending2 = this.pendingPoints.get(key);
|
|
1355
1398
|
if (!pending2) {
|
|
@@ -1362,20 +1405,18 @@ var init_metrics_store = __esm({
|
|
|
1362
1405
|
return this.data.endpoints;
|
|
1363
1406
|
}
|
|
1364
1407
|
getEndpoint(endpoint) {
|
|
1365
|
-
return this.
|
|
1408
|
+
return this.endpointIndex.get(endpoint);
|
|
1366
1409
|
}
|
|
1367
|
-
/** Returns live per-request data for the performance tab. */
|
|
1368
1410
|
getLiveEndpoints() {
|
|
1369
1411
|
const merged = /* @__PURE__ */ new Map();
|
|
1370
1412
|
for (const ep of this.data.endpoints) {
|
|
1371
1413
|
if (ep.dataPoints && ep.dataPoints.length > 0) {
|
|
1372
|
-
merged.set(ep.endpoint,
|
|
1414
|
+
merged.set(ep.endpoint, ep.dataPoints);
|
|
1373
1415
|
}
|
|
1374
1416
|
}
|
|
1375
1417
|
for (const [endpoint, points] of this.pendingPoints) {
|
|
1376
|
-
const existing = merged.get(endpoint)
|
|
1377
|
-
existing.
|
|
1378
|
-
merged.set(endpoint, existing);
|
|
1418
|
+
const existing = merged.get(endpoint);
|
|
1419
|
+
merged.set(endpoint, existing ? existing.concat(points) : points);
|
|
1379
1420
|
}
|
|
1380
1421
|
const endpoints = [];
|
|
1381
1422
|
for (const [endpoint, requests] of merged) {
|
|
@@ -1383,14 +1424,23 @@ var init_metrics_store = __esm({
|
|
|
1383
1424
|
const durations = requests.map((r) => r.durationMs);
|
|
1384
1425
|
const errors = requests.filter((r) => r.statusCode >= 400).length;
|
|
1385
1426
|
const totalQueries = requests.reduce((s, r) => s + r.queryCount, 0);
|
|
1427
|
+
const totalQueryTime = requests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
|
|
1428
|
+
const totalFetchTime = requests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
|
|
1429
|
+
const n = requests.length;
|
|
1430
|
+
const avgDurationMs = Math.round(durations.reduce((s, d) => s + d, 0) / n);
|
|
1431
|
+
const avgQueryTimeMs = Math.round(totalQueryTime / n);
|
|
1432
|
+
const avgFetchTimeMs = Math.round(totalFetchTime / n);
|
|
1386
1433
|
endpoints.push({
|
|
1387
1434
|
endpoint,
|
|
1388
1435
|
requests,
|
|
1389
1436
|
summary: {
|
|
1390
1437
|
p95Ms: percentile(durations, 0.95),
|
|
1391
|
-
errorRate:
|
|
1392
|
-
avgQueryCount:
|
|
1393
|
-
totalRequests:
|
|
1438
|
+
errorRate: errors / n,
|
|
1439
|
+
avgQueryCount: Math.round(totalQueries / n),
|
|
1440
|
+
totalRequests: n,
|
|
1441
|
+
avgQueryTimeMs,
|
|
1442
|
+
avgFetchTimeMs,
|
|
1443
|
+
avgAppTimeMs: Math.max(0, avgDurationMs - avgQueryTimeMs - avgFetchTimeMs)
|
|
1394
1444
|
}
|
|
1395
1445
|
});
|
|
1396
1446
|
}
|
|
@@ -1399,33 +1449,27 @@ var init_metrics_store = __esm({
|
|
|
1399
1449
|
}
|
|
1400
1450
|
reset() {
|
|
1401
1451
|
this.data = { version: 1, endpoints: [] };
|
|
1452
|
+
this.endpointIndex.clear();
|
|
1402
1453
|
this.accumulators.clear();
|
|
1403
1454
|
this.pendingPoints.clear();
|
|
1404
1455
|
this.persistence.remove();
|
|
1405
1456
|
}
|
|
1406
|
-
flush() {
|
|
1457
|
+
flush(sync = false) {
|
|
1407
1458
|
for (const [endpoint, acc] of this.accumulators) {
|
|
1408
1459
|
if (acc.durations.length === 0) continue;
|
|
1460
|
+
const n = acc.totalRequestCount;
|
|
1409
1461
|
const session = {
|
|
1410
1462
|
sessionId: this.sessionId,
|
|
1411
1463
|
startedAt: this.sessionStart,
|
|
1412
|
-
avgDurationMs: Math.round(
|
|
1413
|
-
acc.durations.reduce((s, d) => s + d, 0) / acc.durations.length
|
|
1414
|
-
),
|
|
1464
|
+
avgDurationMs: Math.round(acc.totalDurationSum / n),
|
|
1415
1465
|
p95DurationMs: percentile(acc.durations, 0.95),
|
|
1416
|
-
requestCount:
|
|
1417
|
-
errorCount: acc.
|
|
1418
|
-
avgQueryCount:
|
|
1419
|
-
|
|
1420
|
-
) : 0
|
|
1466
|
+
requestCount: n,
|
|
1467
|
+
errorCount: acc.totalErrorCount,
|
|
1468
|
+
avgQueryCount: n > 0 ? Math.round(acc.totalQuerySum / n) : 0,
|
|
1469
|
+
avgQueryTimeMs: n > 0 ? Math.round(acc.totalQueryTimeMs / n) : 0,
|
|
1470
|
+
avgFetchTimeMs: n > 0 ? Math.round(acc.totalFetchTimeMs / n) : 0
|
|
1421
1471
|
};
|
|
1422
|
-
|
|
1423
|
-
(e) => e.endpoint === endpoint
|
|
1424
|
-
);
|
|
1425
|
-
if (!epMetrics) {
|
|
1426
|
-
epMetrics = { endpoint, sessions: [] };
|
|
1427
|
-
this.data.endpoints.push(epMetrics);
|
|
1428
|
-
}
|
|
1472
|
+
const epMetrics = this.getOrCreateEndpoint(endpoint);
|
|
1429
1473
|
const existingIdx = epMetrics.sessions.findIndex(
|
|
1430
1474
|
(s) => s.sessionId === this.sessionId
|
|
1431
1475
|
);
|
|
@@ -1437,23 +1481,31 @@ var init_metrics_store = __esm({
|
|
|
1437
1481
|
if (epMetrics.sessions.length > METRICS_MAX_SESSIONS) {
|
|
1438
1482
|
epMetrics.sessions = epMetrics.sessions.slice(-METRICS_MAX_SESSIONS);
|
|
1439
1483
|
}
|
|
1484
|
+
acc.durations.length = 0;
|
|
1485
|
+
acc.queryCounts.length = 0;
|
|
1486
|
+
acc.errorCount = 0;
|
|
1440
1487
|
}
|
|
1441
1488
|
for (const [endpoint, points] of this.pendingPoints) {
|
|
1442
1489
|
if (points.length === 0) continue;
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
);
|
|
1446
|
-
if (!epMetrics) {
|
|
1447
|
-
epMetrics = { endpoint, sessions: [] };
|
|
1448
|
-
this.data.endpoints.push(epMetrics);
|
|
1449
|
-
}
|
|
1450
|
-
const existing = epMetrics.dataPoints || [];
|
|
1451
|
-
epMetrics.dataPoints = [...existing, ...points].slice(
|
|
1452
|
-
-METRICS_MAX_DATA_POINTS
|
|
1453
|
-
);
|
|
1490
|
+
const epMetrics = this.getOrCreateEndpoint(endpoint);
|
|
1491
|
+
const existing = epMetrics.dataPoints ?? [];
|
|
1492
|
+
epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
|
|
1454
1493
|
}
|
|
1455
1494
|
this.pendingPoints.clear();
|
|
1456
|
-
|
|
1495
|
+
if (sync) {
|
|
1496
|
+
this.persistence.saveSync(this.data);
|
|
1497
|
+
} else {
|
|
1498
|
+
this.persistence.save(this.data);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
getOrCreateEndpoint(endpoint) {
|
|
1502
|
+
let ep = this.endpointIndex.get(endpoint);
|
|
1503
|
+
if (!ep) {
|
|
1504
|
+
ep = { endpoint, sessions: [] };
|
|
1505
|
+
this.data.endpoints.push(ep);
|
|
1506
|
+
this.endpointIndex.set(endpoint, ep);
|
|
1507
|
+
}
|
|
1508
|
+
return ep;
|
|
1457
1509
|
}
|
|
1458
1510
|
};
|
|
1459
1511
|
}
|
|
@@ -1488,8 +1540,10 @@ import {
|
|
|
1488
1540
|
writeFileSync as writeFileSync2,
|
|
1489
1541
|
mkdirSync as mkdirSync2,
|
|
1490
1542
|
existsSync as existsSync2,
|
|
1491
|
-
unlinkSync
|
|
1543
|
+
unlinkSync,
|
|
1544
|
+
renameSync
|
|
1492
1545
|
} from "fs";
|
|
1546
|
+
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
1493
1547
|
import { resolve as resolve2 } from "path";
|
|
1494
1548
|
var FileMetricsPersistence;
|
|
1495
1549
|
var init_persistence = __esm({
|
|
@@ -1500,9 +1554,13 @@ var init_persistence = __esm({
|
|
|
1500
1554
|
FileMetricsPersistence = class {
|
|
1501
1555
|
metricsDir;
|
|
1502
1556
|
metricsPath;
|
|
1557
|
+
tmpPath;
|
|
1558
|
+
writing = false;
|
|
1559
|
+
pendingData = null;
|
|
1503
1560
|
constructor(rootDir) {
|
|
1504
1561
|
this.metricsDir = resolve2(rootDir, METRICS_DIR);
|
|
1505
1562
|
this.metricsPath = resolve2(rootDir, METRICS_FILE);
|
|
1563
|
+
this.tmpPath = this.metricsPath + ".tmp";
|
|
1506
1564
|
}
|
|
1507
1565
|
load() {
|
|
1508
1566
|
try {
|
|
@@ -1513,18 +1571,27 @@ var init_persistence = __esm({
|
|
|
1513
1571
|
return parsed;
|
|
1514
1572
|
}
|
|
1515
1573
|
}
|
|
1516
|
-
} catch {
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
process.stderr.write(`[brakit] failed to load metrics: ${err.message}
|
|
1576
|
+
`);
|
|
1517
1577
|
}
|
|
1518
1578
|
return { version: 1, endpoints: [] };
|
|
1519
1579
|
}
|
|
1520
1580
|
save(data) {
|
|
1581
|
+
if (this.writing) {
|
|
1582
|
+
this.pendingData = data;
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
this.writeAsync(data);
|
|
1586
|
+
}
|
|
1587
|
+
saveSync(data) {
|
|
1521
1588
|
try {
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1589
|
+
this.ensureDir();
|
|
1590
|
+
writeFileSync2(this.tmpPath, JSON.stringify(data));
|
|
1591
|
+
renameSync(this.tmpPath, this.metricsPath);
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
process.stderr.write(`[brakit] failed to save metrics: ${err.message}
|
|
1594
|
+
`);
|
|
1528
1595
|
}
|
|
1529
1596
|
}
|
|
1530
1597
|
remove() {
|
|
@@ -1535,6 +1602,33 @@ var init_persistence = __esm({
|
|
|
1535
1602
|
} catch {
|
|
1536
1603
|
}
|
|
1537
1604
|
}
|
|
1605
|
+
async writeAsync(data) {
|
|
1606
|
+
this.writing = true;
|
|
1607
|
+
try {
|
|
1608
|
+
if (!existsSync2(this.metricsDir)) {
|
|
1609
|
+
await mkdir(this.metricsDir, { recursive: true });
|
|
1610
|
+
ensureGitignore(this.metricsDir, METRICS_DIR);
|
|
1611
|
+
}
|
|
1612
|
+
await writeFile2(this.tmpPath, JSON.stringify(data));
|
|
1613
|
+
await rename(this.tmpPath, this.metricsPath);
|
|
1614
|
+
} catch (err) {
|
|
1615
|
+
process.stderr.write(`[brakit] failed to save metrics: ${err.message}
|
|
1616
|
+
`);
|
|
1617
|
+
} finally {
|
|
1618
|
+
this.writing = false;
|
|
1619
|
+
if (this.pendingData) {
|
|
1620
|
+
const next = this.pendingData;
|
|
1621
|
+
this.pendingData = null;
|
|
1622
|
+
this.writeAsync(next);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
ensureDir() {
|
|
1627
|
+
if (!existsSync2(this.metricsDir)) {
|
|
1628
|
+
mkdirSync2(this.metricsDir, { recursive: true });
|
|
1629
|
+
ensureGitignore(this.metricsDir, METRICS_DIR);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1538
1632
|
};
|
|
1539
1633
|
}
|
|
1540
1634
|
});
|
|
@@ -2056,6 +2150,7 @@ function getBaseStyles() {
|
|
|
2056
2150
|
--shadow-sm:0 1px 2px rgba(0,0,0,0.05);
|
|
2057
2151
|
--shadow-md:0 1px 3px rgba(0,0,0,0.08),0 1px 2px rgba(0,0,0,0.04);
|
|
2058
2152
|
--shadow-lg:0 4px 12px rgba(0,0,0,0.08),0 1px 4px rgba(0,0,0,0.04);
|
|
2153
|
+
--breakdown-db:#6366f1;--breakdown-fetch:#f59e0b;--breakdown-app:#94a3b8;
|
|
2059
2154
|
--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
|
|
2060
2155
|
--sans:Inter,system-ui,-apple-system,sans-serif;
|
|
2061
2156
|
}
|
|
@@ -2384,6 +2479,30 @@ function getPerformanceStyles() {
|
|
|
2384
2479
|
.perf-metric-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-family:var(--sans);font-weight:600}
|
|
2385
2480
|
.perf-metric-value{font-size:21px;font-weight:700;font-family:var(--mono)}
|
|
2386
2481
|
|
|
2482
|
+
/* Time breakdown */
|
|
2483
|
+
.perf-breakdown{padding:16px 28px;border-bottom:1px solid var(--border-subtle)}
|
|
2484
|
+
.perf-breakdown-bar{display:flex;height:10px;border-radius:5px;overflow:hidden;background:var(--bg-muted);border:1px solid var(--border)}
|
|
2485
|
+
.perf-breakdown-bar-sm{height:6px;border-radius:3px;flex:1}
|
|
2486
|
+
.perf-breakdown-seg{min-width:2px;transition:width .3s}
|
|
2487
|
+
.perf-breakdown-db{background:var(--breakdown-db)}
|
|
2488
|
+
.perf-breakdown-fetch{background:var(--breakdown-fetch)}
|
|
2489
|
+
.perf-breakdown-app{background:var(--breakdown-app)}
|
|
2490
|
+
.perf-breakdown-legend{display:flex;gap:16px;margin-top:8px;font-size:11px;font-family:var(--mono);color:var(--text-muted)}
|
|
2491
|
+
.perf-breakdown-item{display:flex;align-items:center;gap:5px}
|
|
2492
|
+
.perf-breakdown-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
2493
|
+
span.perf-breakdown-dot.perf-breakdown-db{background:var(--breakdown-db)}
|
|
2494
|
+
span.perf-breakdown-dot.perf-breakdown-fetch{background:var(--breakdown-fetch)}
|
|
2495
|
+
span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
|
|
2496
|
+
.perf-breakdown-inline{margin:0 0 8px;display:flex;align-items:center;gap:10px}
|
|
2497
|
+
.perf-breakdown-labels{display:flex;gap:8px;font-size:10px;font-family:var(--mono);color:var(--text-muted);flex-shrink:0}
|
|
2498
|
+
.perf-breakdown-lbl{display:flex;align-items:center;gap:3px}
|
|
2499
|
+
.perf-col-breakdown{flex:1;min-width:140px;display:flex;align-items:center;gap:4px;flex-wrap:wrap}
|
|
2500
|
+
.perf-bd-tag{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:3px;font-size:10px;font-family:var(--mono);font-weight:500;white-space:nowrap}
|
|
2501
|
+
.perf-bd-tag-db{color:#818cf8;background:rgba(99,102,241,0.1)}
|
|
2502
|
+
.perf-bd-tag-fetch{color:#fbbf24;background:rgba(245,158,11,0.1)}
|
|
2503
|
+
.perf-bd-tag-app{color:var(--breakdown-app);background:rgba(148,163,184,0.1)}
|
|
2504
|
+
.perf-col-muted{color:var(--text-dim)}
|
|
2505
|
+
|
|
2387
2506
|
/* Chart */
|
|
2388
2507
|
.perf-chart-wrap{padding:16px 28px}
|
|
2389
2508
|
.perf-canvas{border-radius:var(--radius);background:var(--bg-muted);border:1px solid var(--border)}
|
|
@@ -2579,7 +2698,7 @@ var init_project = __esm({
|
|
|
2579
2698
|
});
|
|
2580
2699
|
|
|
2581
2700
|
// src/analysis/rules/patterns.ts
|
|
2582
|
-
var SECRET_KEYS, TOKEN_PARAMS, SAFE_PARAMS, STACK_TRACE_RE, DB_CONN_RE, SQL_FRAGMENT_RE, SECRET_VAL_RE, LOG_SECRET_RE, MASKED_RE, EMAIL_RE, INTERNAL_ID_KEYS, INTERNAL_ID_SUFFIX, RULE_HINTS;
|
|
2701
|
+
var SECRET_KEYS, TOKEN_PARAMS, SAFE_PARAMS, STACK_TRACE_RE, DB_CONN_RE, SQL_FRAGMENT_RE, SECRET_VAL_RE, LOG_SECRET_RE, MASKED_RE, EMAIL_RE, INTERNAL_ID_KEYS, INTERNAL_ID_SUFFIX, SELECT_STAR_RE, SELECT_DOT_STAR_RE, RULE_HINTS;
|
|
2583
2702
|
var init_patterns = __esm({
|
|
2584
2703
|
"src/analysis/rules/patterns.ts"() {
|
|
2585
2704
|
"use strict";
|
|
@@ -2595,6 +2714,8 @@ var init_patterns = __esm({
|
|
|
2595
2714
|
EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
2596
2715
|
INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
2597
2716
|
INTERNAL_ID_SUFFIX = /Id$|_id$/;
|
|
2717
|
+
SELECT_STAR_RE = /^SELECT\s+\*/i;
|
|
2718
|
+
SELECT_DOT_STAR_RE = /\.\*\s+FROM/i;
|
|
2598
2719
|
RULE_HINTS = {
|
|
2599
2720
|
"exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
|
|
2600
2721
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
@@ -2955,6 +3076,36 @@ var init_cors_credentials = __esm({
|
|
|
2955
3076
|
}
|
|
2956
3077
|
});
|
|
2957
3078
|
|
|
3079
|
+
// src/utils/response.ts
|
|
3080
|
+
function unwrapResponse(parsed) {
|
|
3081
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
3082
|
+
const obj = parsed;
|
|
3083
|
+
const keys = Object.keys(obj);
|
|
3084
|
+
if (keys.length > 3) return parsed;
|
|
3085
|
+
let best = null;
|
|
3086
|
+
let bestSize = 0;
|
|
3087
|
+
for (const key of keys) {
|
|
3088
|
+
const val = obj[key];
|
|
3089
|
+
if (Array.isArray(val) && val.length > bestSize) {
|
|
3090
|
+
best = val;
|
|
3091
|
+
bestSize = val.length;
|
|
3092
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
3093
|
+
const size = Object.keys(val).length;
|
|
3094
|
+
if (size > bestSize) {
|
|
3095
|
+
best = val;
|
|
3096
|
+
bestSize = size;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
3101
|
+
}
|
|
3102
|
+
var init_response = __esm({
|
|
3103
|
+
"src/utils/response.ts"() {
|
|
3104
|
+
"use strict";
|
|
3105
|
+
init_thresholds();
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
|
|
2958
3109
|
// src/analysis/rules/response-pii-leak.ts
|
|
2959
3110
|
function tryParseJson2(body) {
|
|
2960
3111
|
if (!body) return null;
|
|
@@ -2996,28 +3147,6 @@ function hasInternalIds(obj) {
|
|
|
2996
3147
|
}
|
|
2997
3148
|
return false;
|
|
2998
3149
|
}
|
|
2999
|
-
function unwrapResponse(parsed) {
|
|
3000
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
3001
|
-
const obj = parsed;
|
|
3002
|
-
const keys = Object.keys(obj);
|
|
3003
|
-
if (keys.length > 3) return parsed;
|
|
3004
|
-
let best = null;
|
|
3005
|
-
let bestSize = 0;
|
|
3006
|
-
for (const key of keys) {
|
|
3007
|
-
const val = obj[key];
|
|
3008
|
-
if (Array.isArray(val) && val.length > bestSize) {
|
|
3009
|
-
best = val;
|
|
3010
|
-
bestSize = val.length;
|
|
3011
|
-
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
3012
|
-
const size = Object.keys(val).length;
|
|
3013
|
-
if (size > bestSize) {
|
|
3014
|
-
best = val;
|
|
3015
|
-
bestSize = size;
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
return best && bestSize >= 3 ? best : parsed;
|
|
3020
|
-
}
|
|
3021
3150
|
function detectPII(method, reqBody, resBody) {
|
|
3022
3151
|
const target = unwrapResponse(resBody);
|
|
3023
3152
|
if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
|
|
@@ -3065,6 +3194,7 @@ var init_response_pii_leak = __esm({
|
|
|
3065
3194
|
"src/analysis/rules/response-pii-leak.ts"() {
|
|
3066
3195
|
"use strict";
|
|
3067
3196
|
init_patterns();
|
|
3197
|
+
init_response();
|
|
3068
3198
|
WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
3069
3199
|
FULL_RECORD_MIN_FIELDS = 5;
|
|
3070
3200
|
LIST_PII_MIN_ITEMS = 2;
|
|
@@ -3176,7 +3306,28 @@ var init_rules = __esm({
|
|
|
3176
3306
|
}
|
|
3177
3307
|
});
|
|
3178
3308
|
|
|
3179
|
-
// src/
|
|
3309
|
+
// src/utils/collections.ts
|
|
3310
|
+
function groupBy(items, keyFn) {
|
|
3311
|
+
const map = /* @__PURE__ */ new Map();
|
|
3312
|
+
for (const item of items) {
|
|
3313
|
+
const key = keyFn(item);
|
|
3314
|
+
if (key == null) continue;
|
|
3315
|
+
let arr = map.get(key);
|
|
3316
|
+
if (!arr) {
|
|
3317
|
+
arr = [];
|
|
3318
|
+
map.set(key, arr);
|
|
3319
|
+
}
|
|
3320
|
+
arr.push(item);
|
|
3321
|
+
}
|
|
3322
|
+
return map;
|
|
3323
|
+
}
|
|
3324
|
+
var init_collections = __esm({
|
|
3325
|
+
"src/utils/collections.ts"() {
|
|
3326
|
+
"use strict";
|
|
3327
|
+
}
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
// src/analysis/insights/query-helpers.ts
|
|
3180
3331
|
function getQueryShape(q) {
|
|
3181
3332
|
if (q.sql) return normalizeQueryParams(q.sql) ?? "";
|
|
3182
3333
|
return `${q.operation ?? q.normalizedOp ?? "?"}:${q.model ?? q.table ?? ""}`;
|
|
@@ -3188,400 +3339,781 @@ function getQueryInfo(q) {
|
|
|
3188
3339
|
table: q.table ?? q.model ?? ""
|
|
3189
3340
|
};
|
|
3190
3341
|
}
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3342
|
+
var init_query_helpers = __esm({
|
|
3343
|
+
"src/analysis/insights/query-helpers.ts"() {
|
|
3344
|
+
"use strict";
|
|
3345
|
+
init_normalize();
|
|
3346
|
+
}
|
|
3347
|
+
});
|
|
3348
|
+
|
|
3349
|
+
// src/analysis/insights/prepare.ts
|
|
3350
|
+
function createEndpointGroup() {
|
|
3351
|
+
return {
|
|
3352
|
+
total: 0,
|
|
3353
|
+
errors: 0,
|
|
3354
|
+
totalDuration: 0,
|
|
3355
|
+
queryCount: 0,
|
|
3356
|
+
totalSize: 0,
|
|
3357
|
+
totalQueryTimeMs: 0,
|
|
3358
|
+
totalFetchTimeMs: 0,
|
|
3359
|
+
queryShapeDurations: /* @__PURE__ */ new Map()
|
|
3360
|
+
};
|
|
3199
3361
|
}
|
|
3200
|
-
function
|
|
3201
|
-
const insights = [];
|
|
3362
|
+
function prepareContext(ctx) {
|
|
3202
3363
|
const nonStatic = ctx.requests.filter(
|
|
3203
3364
|
(r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
|
|
3204
3365
|
);
|
|
3205
|
-
const queriesByReq =
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
let arr = queriesByReq.get(q.parentRequestId);
|
|
3209
|
-
if (!arr) {
|
|
3210
|
-
arr = [];
|
|
3211
|
-
queriesByReq.set(q.parentRequestId, arr);
|
|
3212
|
-
}
|
|
3213
|
-
arr.push(q);
|
|
3214
|
-
}
|
|
3215
|
-
const reqById = /* @__PURE__ */ new Map();
|
|
3216
|
-
for (const r of nonStatic) reqById.set(r.id, r);
|
|
3217
|
-
const n1Seen = /* @__PURE__ */ new Set();
|
|
3218
|
-
for (const [reqId, reqQueries] of queriesByReq) {
|
|
3219
|
-
const req = reqById.get(reqId);
|
|
3220
|
-
if (!req) continue;
|
|
3221
|
-
const endpoint = `${req.method} ${req.path}`;
|
|
3222
|
-
const shapeGroups = /* @__PURE__ */ new Map();
|
|
3223
|
-
for (const q of reqQueries) {
|
|
3224
|
-
const shape = getQueryShape(q);
|
|
3225
|
-
let group = shapeGroups.get(shape);
|
|
3226
|
-
if (!group) {
|
|
3227
|
-
group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: q };
|
|
3228
|
-
shapeGroups.set(shape, group);
|
|
3229
|
-
}
|
|
3230
|
-
group.count++;
|
|
3231
|
-
group.distinctSql.add(q.sql ?? shape);
|
|
3232
|
-
}
|
|
3233
|
-
for (const [, sg] of shapeGroups) {
|
|
3234
|
-
if (sg.count <= N1_QUERY_THRESHOLD || sg.distinctSql.size <= 1) continue;
|
|
3235
|
-
const info = getQueryInfo(sg.first);
|
|
3236
|
-
const key = `${endpoint}:${info.op}:${info.table || "unknown"}`;
|
|
3237
|
-
if (n1Seen.has(key)) continue;
|
|
3238
|
-
n1Seen.add(key);
|
|
3239
|
-
insights.push({
|
|
3240
|
-
severity: "critical",
|
|
3241
|
-
type: "n1",
|
|
3242
|
-
title: "N+1 Query Pattern",
|
|
3243
|
-
desc: `${endpoint} runs ${sg.count}x ${info.op} ${info.table} with different params in a single request`,
|
|
3244
|
-
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.",
|
|
3245
|
-
nav: "queries"
|
|
3246
|
-
});
|
|
3247
|
-
}
|
|
3248
|
-
}
|
|
3249
|
-
const ceQueryMap = /* @__PURE__ */ new Map();
|
|
3250
|
-
const ceAllEndpoints = /* @__PURE__ */ new Set();
|
|
3251
|
-
for (const [reqId, reqQueries] of queriesByReq) {
|
|
3252
|
-
const req = reqById.get(reqId);
|
|
3253
|
-
if (!req) continue;
|
|
3254
|
-
const endpoint = `${req.method} ${req.path}`;
|
|
3255
|
-
ceAllEndpoints.add(endpoint);
|
|
3256
|
-
const seenInReq = /* @__PURE__ */ new Set();
|
|
3257
|
-
for (const q of reqQueries) {
|
|
3258
|
-
const shape = getQueryShape(q);
|
|
3259
|
-
let entry = ceQueryMap.get(shape);
|
|
3260
|
-
if (!entry) {
|
|
3261
|
-
entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: q };
|
|
3262
|
-
ceQueryMap.set(shape, entry);
|
|
3263
|
-
}
|
|
3264
|
-
entry.count++;
|
|
3265
|
-
if (!seenInReq.has(shape)) {
|
|
3266
|
-
seenInReq.add(shape);
|
|
3267
|
-
entry.endpoints.add(endpoint);
|
|
3268
|
-
}
|
|
3269
|
-
}
|
|
3270
|
-
}
|
|
3271
|
-
if (ceAllEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
|
|
3272
|
-
for (const [, cem] of ceQueryMap) {
|
|
3273
|
-
if (cem.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
|
|
3274
|
-
if (cem.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
|
|
3275
|
-
const pct = Math.round(cem.endpoints.size / ceAllEndpoints.size * 100);
|
|
3276
|
-
if (pct < CROSS_ENDPOINT_PCT) continue;
|
|
3277
|
-
const info = getQueryInfo(cem.first);
|
|
3278
|
-
const label = info.op + (info.table ? ` ${info.table}` : "");
|
|
3279
|
-
insights.push({
|
|
3280
|
-
severity: "warning",
|
|
3281
|
-
type: "cross-endpoint",
|
|
3282
|
-
title: "Repeated Query Across Endpoints",
|
|
3283
|
-
desc: `${label} runs on ${cem.endpoints.size} of ${ceAllEndpoints.size} endpoints (${pct}%).`,
|
|
3284
|
-
hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
|
|
3285
|
-
nav: "queries"
|
|
3286
|
-
});
|
|
3287
|
-
}
|
|
3288
|
-
}
|
|
3289
|
-
const rqSeen = /* @__PURE__ */ new Set();
|
|
3290
|
-
for (const [reqId, reqQueries] of queriesByReq) {
|
|
3291
|
-
const req = reqById.get(reqId);
|
|
3292
|
-
if (!req) continue;
|
|
3293
|
-
const endpoint = `${req.method} ${req.path}`;
|
|
3294
|
-
const exact = /* @__PURE__ */ new Map();
|
|
3295
|
-
for (const q of reqQueries) {
|
|
3296
|
-
if (!q.sql) continue;
|
|
3297
|
-
let entry = exact.get(q.sql);
|
|
3298
|
-
if (!entry) {
|
|
3299
|
-
entry = { count: 0, first: q };
|
|
3300
|
-
exact.set(q.sql, entry);
|
|
3301
|
-
}
|
|
3302
|
-
entry.count++;
|
|
3303
|
-
}
|
|
3304
|
-
for (const [, e] of exact) {
|
|
3305
|
-
if (e.count < REDUNDANT_QUERY_MIN_COUNT) continue;
|
|
3306
|
-
const info = getQueryInfo(e.first);
|
|
3307
|
-
const label = info.op + (info.table ? ` ${info.table}` : "");
|
|
3308
|
-
const dedupKey = `${endpoint}:${label}`;
|
|
3309
|
-
if (rqSeen.has(dedupKey)) continue;
|
|
3310
|
-
rqSeen.add(dedupKey);
|
|
3311
|
-
insights.push({
|
|
3312
|
-
severity: "warning",
|
|
3313
|
-
type: "redundant-query",
|
|
3314
|
-
title: "Redundant Query",
|
|
3315
|
-
desc: `${label} runs ${e.count}x with identical params in ${endpoint}.`,
|
|
3316
|
-
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.",
|
|
3317
|
-
nav: "queries"
|
|
3318
|
-
});
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
if (ctx.errors.length > 0) {
|
|
3322
|
-
const errGroups = /* @__PURE__ */ new Map();
|
|
3323
|
-
for (const e of ctx.errors) {
|
|
3324
|
-
const name = e.name || "Error";
|
|
3325
|
-
errGroups.set(name, (errGroups.get(name) ?? 0) + 1);
|
|
3326
|
-
}
|
|
3327
|
-
for (const [name, cnt] of errGroups) {
|
|
3328
|
-
insights.push({
|
|
3329
|
-
severity: "critical",
|
|
3330
|
-
type: "error",
|
|
3331
|
-
title: "Unhandled Error",
|
|
3332
|
-
desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
|
|
3333
|
-
hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
|
|
3334
|
-
nav: "errors"
|
|
3335
|
-
});
|
|
3336
|
-
}
|
|
3337
|
-
}
|
|
3366
|
+
const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
|
|
3367
|
+
const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
|
|
3368
|
+
const reqById = new Map(nonStatic.map((r) => [r.id, r]));
|
|
3338
3369
|
const endpointGroups = /* @__PURE__ */ new Map();
|
|
3339
3370
|
for (const r of nonStatic) {
|
|
3340
|
-
const ep =
|
|
3371
|
+
const ep = getEndpointKey(r.method, r.path);
|
|
3341
3372
|
let g = endpointGroups.get(ep);
|
|
3342
3373
|
if (!g) {
|
|
3343
|
-
g =
|
|
3374
|
+
g = createEndpointGroup();
|
|
3344
3375
|
endpointGroups.set(ep, g);
|
|
3345
3376
|
}
|
|
3346
3377
|
g.total++;
|
|
3347
3378
|
if (r.statusCode >= 400) g.errors++;
|
|
3348
3379
|
g.totalDuration += r.durationMs;
|
|
3349
|
-
g.queryCount += (queriesByReq.get(r.id) ?? []).length;
|
|
3350
3380
|
g.totalSize += r.responseSize ?? 0;
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3354
|
-
const errorRate = Math.round(g.errors / g.total * 100);
|
|
3355
|
-
if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
|
|
3356
|
-
insights.push({
|
|
3357
|
-
severity: "critical",
|
|
3358
|
-
type: "error-hotspot",
|
|
3359
|
-
title: "Error Hotspot",
|
|
3360
|
-
desc: `${ep} \u2014 ${errorRate}% error rate (${g.errors}/${g.total} requests)`,
|
|
3361
|
-
hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces.",
|
|
3362
|
-
nav: "requests"
|
|
3363
|
-
});
|
|
3364
|
-
}
|
|
3365
|
-
}
|
|
3366
|
-
const dupCounts = /* @__PURE__ */ new Map();
|
|
3367
|
-
const flowCount = /* @__PURE__ */ new Map();
|
|
3368
|
-
for (const flow of ctx.flows) {
|
|
3369
|
-
if (!flow.requests) continue;
|
|
3370
|
-
const seenInFlow = /* @__PURE__ */ new Set();
|
|
3371
|
-
for (const fr of flow.requests) {
|
|
3372
|
-
if (!fr.isDuplicate) continue;
|
|
3373
|
-
const dupKey = `${fr.method} ${fr.label ?? fr.path ?? fr.url}`;
|
|
3374
|
-
dupCounts.set(dupKey, (dupCounts.get(dupKey) ?? 0) + 1);
|
|
3375
|
-
if (!seenInFlow.has(dupKey)) {
|
|
3376
|
-
seenInFlow.add(dupKey);
|
|
3377
|
-
flowCount.set(dupKey, (flowCount.get(dupKey) ?? 0) + 1);
|
|
3378
|
-
}
|
|
3379
|
-
}
|
|
3380
|
-
}
|
|
3381
|
-
const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
3382
|
-
for (let i = 0; i < Math.min(dupEntries.length, 3); i++) {
|
|
3383
|
-
const d = dupEntries[i];
|
|
3384
|
-
insights.push({
|
|
3385
|
-
severity: "warning",
|
|
3386
|
-
type: "duplicate",
|
|
3387
|
-
title: "Duplicate API Call",
|
|
3388
|
-
desc: `${d.key} loaded ${d.count}x as duplicate across ${d.flows} action${d.flows !== 1 ? "s" : ""}`,
|
|
3389
|
-
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.",
|
|
3390
|
-
nav: "actions"
|
|
3391
|
-
});
|
|
3392
|
-
}
|
|
3393
|
-
for (const [ep, g] of endpointGroups) {
|
|
3394
|
-
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3395
|
-
const avgMs = Math.round(g.totalDuration / g.total);
|
|
3396
|
-
if (avgMs >= SLOW_ENDPOINT_THRESHOLD_MS) {
|
|
3397
|
-
insights.push({
|
|
3398
|
-
severity: "warning",
|
|
3399
|
-
type: "slow",
|
|
3400
|
-
title: "Slow Endpoint",
|
|
3401
|
-
desc: `${ep} \u2014 avg ${formatDuration(avgMs)} across ${g.total} request${g.total !== 1 ? "s" : ""}`,
|
|
3402
|
-
hint: "Consistently slow responses hurt user experience. Check the Queries tab to see if database queries are the bottleneck.",
|
|
3403
|
-
nav: "requests"
|
|
3404
|
-
});
|
|
3405
|
-
}
|
|
3406
|
-
}
|
|
3407
|
-
for (const [ep, g] of endpointGroups) {
|
|
3408
|
-
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3409
|
-
const avgQueries = Math.round(g.queryCount / g.total);
|
|
3410
|
-
if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
|
|
3411
|
-
insights.push({
|
|
3412
|
-
severity: "warning",
|
|
3413
|
-
type: "query-heavy",
|
|
3414
|
-
title: "Query-Heavy Endpoint",
|
|
3415
|
-
desc: `${ep} \u2014 avg ${avgQueries} queries/request`,
|
|
3416
|
-
hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches.",
|
|
3417
|
-
nav: "queries"
|
|
3418
|
-
});
|
|
3419
|
-
}
|
|
3420
|
-
}
|
|
3421
|
-
const selectStarSeen = /* @__PURE__ */ new Map();
|
|
3422
|
-
for (const [, reqQueries] of queriesByReq) {
|
|
3381
|
+
const reqQueries = queriesByReq.get(r.id) ?? [];
|
|
3382
|
+
g.queryCount += reqQueries.length;
|
|
3423
3383
|
for (const q of reqQueries) {
|
|
3424
|
-
|
|
3425
|
-
const
|
|
3426
|
-
if (!isSelectStar) continue;
|
|
3384
|
+
g.totalQueryTimeMs += q.durationMs;
|
|
3385
|
+
const shape = getQueryShape(q);
|
|
3427
3386
|
const info = getQueryInfo(q);
|
|
3428
|
-
|
|
3429
|
-
|
|
3387
|
+
let sd = g.queryShapeDurations.get(shape);
|
|
3388
|
+
if (!sd) {
|
|
3389
|
+
sd = { totalMs: 0, count: 0, label: info.op + (info.table ? ` ${info.table}` : "") };
|
|
3390
|
+
g.queryShapeDurations.set(shape, sd);
|
|
3391
|
+
}
|
|
3392
|
+
sd.totalMs += q.durationMs;
|
|
3393
|
+
sd.count++;
|
|
3430
3394
|
}
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
insights.push({
|
|
3435
|
-
severity: "warning",
|
|
3436
|
-
type: "select-star",
|
|
3437
|
-
title: "SELECT * Query",
|
|
3438
|
-
desc: `SELECT * on ${table} \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
|
|
3439
|
-
hint: "SELECT * fetches all columns including ones you don\u2019t need. Specify only required columns to reduce data transfer and memory usage.",
|
|
3440
|
-
nav: "queries"
|
|
3441
|
-
});
|
|
3442
|
-
}
|
|
3443
|
-
const highRowSeen = /* @__PURE__ */ new Map();
|
|
3444
|
-
for (const [, reqQueries] of queriesByReq) {
|
|
3445
|
-
for (const q of reqQueries) {
|
|
3446
|
-
if (!q.rowCount || q.rowCount <= HIGH_ROW_COUNT) continue;
|
|
3447
|
-
const info = getQueryInfo(q);
|
|
3448
|
-
const key = `${info.op} ${info.table || "unknown"}`;
|
|
3449
|
-
let entry = highRowSeen.get(key);
|
|
3450
|
-
if (!entry) {
|
|
3451
|
-
entry = { max: 0, count: 0 };
|
|
3452
|
-
highRowSeen.set(key, entry);
|
|
3453
|
-
}
|
|
3454
|
-
entry.count++;
|
|
3455
|
-
if (q.rowCount > entry.max) entry.max = q.rowCount;
|
|
3395
|
+
const reqFetches = fetchesByReq.get(r.id) ?? [];
|
|
3396
|
+
for (const f of reqFetches) {
|
|
3397
|
+
g.totalFetchTimeMs += f.durationMs;
|
|
3456
3398
|
}
|
|
3457
3399
|
}
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3400
|
+
return {
|
|
3401
|
+
...ctx,
|
|
3402
|
+
nonStatic,
|
|
3403
|
+
queriesByReq,
|
|
3404
|
+
fetchesByReq,
|
|
3405
|
+
reqById,
|
|
3406
|
+
endpointGroups
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
var init_prepare = __esm({
|
|
3410
|
+
"src/analysis/insights/prepare.ts"() {
|
|
3411
|
+
"use strict";
|
|
3412
|
+
init_collections();
|
|
3413
|
+
init_endpoint();
|
|
3414
|
+
init_constants();
|
|
3415
|
+
init_query_helpers();
|
|
3468
3416
|
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
for (const
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
bestSize = val.length;
|
|
3491
|
-
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
3492
|
-
const size = Object.keys(val).length;
|
|
3493
|
-
if (size > bestSize) {
|
|
3494
|
-
best = val;
|
|
3495
|
-
bestSize = size;
|
|
3496
|
-
}
|
|
3417
|
+
});
|
|
3418
|
+
|
|
3419
|
+
// src/analysis/insights/runner.ts
|
|
3420
|
+
var SEVERITY_ORDER, InsightRunner;
|
|
3421
|
+
var init_runner = __esm({
|
|
3422
|
+
"src/analysis/insights/runner.ts"() {
|
|
3423
|
+
"use strict";
|
|
3424
|
+
init_prepare();
|
|
3425
|
+
SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
3426
|
+
InsightRunner = class {
|
|
3427
|
+
rules = [];
|
|
3428
|
+
register(rule) {
|
|
3429
|
+
this.rules.push(rule);
|
|
3430
|
+
}
|
|
3431
|
+
run(ctx) {
|
|
3432
|
+
const prepared = prepareContext(ctx);
|
|
3433
|
+
const insights = [];
|
|
3434
|
+
for (const rule of this.rules) {
|
|
3435
|
+
try {
|
|
3436
|
+
insights.push(...rule.check(prepared));
|
|
3437
|
+
} catch {
|
|
3497
3438
|
}
|
|
3498
3439
|
}
|
|
3499
|
-
|
|
3440
|
+
insights.sort(
|
|
3441
|
+
(a, b) => (SEVERITY_ORDER[a.severity] ?? 2) - (SEVERITY_ORDER[b.severity] ?? 2)
|
|
3442
|
+
);
|
|
3443
|
+
return insights;
|
|
3500
3444
|
}
|
|
3501
|
-
}
|
|
3502
|
-
const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
3503
|
-
if (!inspectObj || typeof inspectObj !== "object" || Array.isArray(inspectObj)) continue;
|
|
3504
|
-
const fields = Object.keys(inspectObj);
|
|
3505
|
-
if (fields.length < OVERFETCH_MIN_FIELDS) continue;
|
|
3506
|
-
let internalIdCount = 0;
|
|
3507
|
-
let nullCount = 0;
|
|
3508
|
-
for (const key of fields) {
|
|
3509
|
-
if (INTERNAL_ID_SUFFIX.test(key) || key === "id" || key === "_id") internalIdCount++;
|
|
3510
|
-
const val = inspectObj[key];
|
|
3511
|
-
if (val === null || val === void 0) nullCount++;
|
|
3512
|
-
}
|
|
3513
|
-
const nullRatio = nullCount / fields.length;
|
|
3514
|
-
const reasons = [];
|
|
3515
|
-
if (internalIdCount >= OVERFETCH_MIN_INTERNAL_IDS) reasons.push(`${internalIdCount} internal ID fields`);
|
|
3516
|
-
if (nullRatio >= OVERFETCH_NULL_RATIO) reasons.push(`${Math.round(nullRatio * 100)}% null fields`);
|
|
3517
|
-
if (fields.length >= OVERFETCH_MIN_FIELDS && reasons.length === 0 && fields.length >= 12) {
|
|
3518
|
-
reasons.push(`${fields.length} fields returned`);
|
|
3519
|
-
}
|
|
3520
|
-
if (reasons.length > 0) {
|
|
3521
|
-
overfetchSeen.add(ep);
|
|
3522
|
-
insights.push({
|
|
3523
|
-
severity: "info",
|
|
3524
|
-
type: "response-overfetch",
|
|
3525
|
-
title: "Response Overfetch",
|
|
3526
|
-
desc: `${ep} \u2014 ${reasons.join(", ")}`,
|
|
3527
|
-
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.",
|
|
3528
|
-
nav: "requests"
|
|
3529
|
-
});
|
|
3530
|
-
}
|
|
3531
|
-
}
|
|
3532
|
-
for (const [ep, g] of endpointGroups) {
|
|
3533
|
-
if (g.total < OVERFETCH_MIN_REQUESTS) continue;
|
|
3534
|
-
const avgSize = Math.round(g.totalSize / g.total);
|
|
3535
|
-
if (avgSize > LARGE_RESPONSE_BYTES) {
|
|
3536
|
-
insights.push({
|
|
3537
|
-
severity: "info",
|
|
3538
|
-
type: "large-response",
|
|
3539
|
-
title: "Large Response",
|
|
3540
|
-
desc: `${ep} \u2014 avg ${formatSize(avgSize)} response`,
|
|
3541
|
-
hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression.",
|
|
3542
|
-
nav: "requests"
|
|
3543
|
-
});
|
|
3544
|
-
}
|
|
3545
|
-
}
|
|
3546
|
-
if (ctx.securityFindings) {
|
|
3547
|
-
for (const f of ctx.securityFindings) {
|
|
3548
|
-
insights.push({
|
|
3549
|
-
severity: f.severity,
|
|
3550
|
-
type: "security",
|
|
3551
|
-
title: f.title,
|
|
3552
|
-
desc: f.desc,
|
|
3553
|
-
hint: f.hint,
|
|
3554
|
-
nav: "security"
|
|
3555
|
-
});
|
|
3556
|
-
}
|
|
3445
|
+
};
|
|
3557
3446
|
}
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
var
|
|
3563
|
-
"src/analysis/insights.ts"() {
|
|
3447
|
+
});
|
|
3448
|
+
|
|
3449
|
+
// src/analysis/insights/rules/n1.ts
|
|
3450
|
+
var n1Rule;
|
|
3451
|
+
var init_n1 = __esm({
|
|
3452
|
+
"src/analysis/insights/rules/n1.ts"() {
|
|
3564
3453
|
"use strict";
|
|
3565
|
-
|
|
3454
|
+
init_query_helpers();
|
|
3455
|
+
init_endpoint();
|
|
3566
3456
|
init_thresholds();
|
|
3567
|
-
|
|
3568
|
-
|
|
3457
|
+
n1Rule = {
|
|
3458
|
+
id: "n1",
|
|
3459
|
+
check(ctx) {
|
|
3460
|
+
const insights = [];
|
|
3461
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3462
|
+
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3463
|
+
const req = ctx.reqById.get(reqId);
|
|
3464
|
+
if (!req) continue;
|
|
3465
|
+
const endpoint = getEndpointKey(req.method, req.path);
|
|
3466
|
+
const shapeGroups = /* @__PURE__ */ new Map();
|
|
3467
|
+
for (const q of reqQueries) {
|
|
3468
|
+
const shape = getQueryShape(q);
|
|
3469
|
+
let group = shapeGroups.get(shape);
|
|
3470
|
+
if (!group) {
|
|
3471
|
+
group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: q };
|
|
3472
|
+
shapeGroups.set(shape, group);
|
|
3473
|
+
}
|
|
3474
|
+
group.count++;
|
|
3475
|
+
group.distinctSql.add(q.sql ?? shape);
|
|
3476
|
+
}
|
|
3477
|
+
for (const [, sg] of shapeGroups) {
|
|
3478
|
+
if (sg.count <= N1_QUERY_THRESHOLD || sg.distinctSql.size <= 1) continue;
|
|
3479
|
+
const info = getQueryInfo(sg.first);
|
|
3480
|
+
const key = `${endpoint}:${info.op}:${info.table || "unknown"}`;
|
|
3481
|
+
if (seen.has(key)) continue;
|
|
3482
|
+
seen.add(key);
|
|
3483
|
+
insights.push({
|
|
3484
|
+
severity: "critical",
|
|
3485
|
+
type: "n1",
|
|
3486
|
+
title: "N+1 Query Pattern",
|
|
3487
|
+
desc: `${endpoint} runs ${sg.count}x ${info.op} ${info.table} with different params in a single request`,
|
|
3488
|
+
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.",
|
|
3489
|
+
nav: "queries"
|
|
3490
|
+
});
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
return insights;
|
|
3494
|
+
}
|
|
3495
|
+
};
|
|
3569
3496
|
}
|
|
3570
3497
|
});
|
|
3571
3498
|
|
|
3572
|
-
// src/analysis/
|
|
3573
|
-
var
|
|
3574
|
-
var
|
|
3575
|
-
"src/analysis/
|
|
3499
|
+
// src/analysis/insights/rules/cross-endpoint.ts
|
|
3500
|
+
var crossEndpointRule;
|
|
3501
|
+
var init_cross_endpoint = __esm({
|
|
3502
|
+
"src/analysis/insights/rules/cross-endpoint.ts"() {
|
|
3503
|
+
"use strict";
|
|
3504
|
+
init_query_helpers();
|
|
3505
|
+
init_endpoint();
|
|
3506
|
+
init_thresholds();
|
|
3507
|
+
crossEndpointRule = {
|
|
3508
|
+
id: "cross-endpoint",
|
|
3509
|
+
check(ctx) {
|
|
3510
|
+
const insights = [];
|
|
3511
|
+
const queryMap = /* @__PURE__ */ new Map();
|
|
3512
|
+
const allEndpoints = /* @__PURE__ */ new Set();
|
|
3513
|
+
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3514
|
+
const req = ctx.reqById.get(reqId);
|
|
3515
|
+
if (!req) continue;
|
|
3516
|
+
const endpoint = getEndpointKey(req.method, req.path);
|
|
3517
|
+
allEndpoints.add(endpoint);
|
|
3518
|
+
const seenInReq = /* @__PURE__ */ new Set();
|
|
3519
|
+
for (const q of reqQueries) {
|
|
3520
|
+
const shape = getQueryShape(q);
|
|
3521
|
+
let entry = queryMap.get(shape);
|
|
3522
|
+
if (!entry) {
|
|
3523
|
+
entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: q };
|
|
3524
|
+
queryMap.set(shape, entry);
|
|
3525
|
+
}
|
|
3526
|
+
entry.count++;
|
|
3527
|
+
if (!seenInReq.has(shape)) {
|
|
3528
|
+
seenInReq.add(shape);
|
|
3529
|
+
entry.endpoints.add(endpoint);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
|
|
3534
|
+
for (const [, cem] of queryMap) {
|
|
3535
|
+
if (cem.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
|
|
3536
|
+
if (cem.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
|
|
3537
|
+
const p = Math.round(cem.endpoints.size / allEndpoints.size * 100);
|
|
3538
|
+
if (p < CROSS_ENDPOINT_PCT) continue;
|
|
3539
|
+
const info = getQueryInfo(cem.first);
|
|
3540
|
+
const label = info.op + (info.table ? ` ${info.table}` : "");
|
|
3541
|
+
insights.push({
|
|
3542
|
+
severity: "warning",
|
|
3543
|
+
type: "cross-endpoint",
|
|
3544
|
+
title: "Repeated Query Across Endpoints",
|
|
3545
|
+
desc: `${label} runs on ${cem.endpoints.size} of ${allEndpoints.size} endpoints (${p}%).`,
|
|
3546
|
+
hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
|
|
3547
|
+
nav: "queries"
|
|
3548
|
+
});
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
return insights;
|
|
3552
|
+
}
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
});
|
|
3556
|
+
|
|
3557
|
+
// src/analysis/insights/rules/redundant-query.ts
|
|
3558
|
+
var redundantQueryRule;
|
|
3559
|
+
var init_redundant_query = __esm({
|
|
3560
|
+
"src/analysis/insights/rules/redundant-query.ts"() {
|
|
3561
|
+
"use strict";
|
|
3562
|
+
init_query_helpers();
|
|
3563
|
+
init_endpoint();
|
|
3564
|
+
init_thresholds();
|
|
3565
|
+
redundantQueryRule = {
|
|
3566
|
+
id: "redundant-query",
|
|
3567
|
+
check(ctx) {
|
|
3568
|
+
const insights = [];
|
|
3569
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3570
|
+
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3571
|
+
const req = ctx.reqById.get(reqId);
|
|
3572
|
+
if (!req) continue;
|
|
3573
|
+
const endpoint = getEndpointKey(req.method, req.path);
|
|
3574
|
+
const exact = /* @__PURE__ */ new Map();
|
|
3575
|
+
for (const q of reqQueries) {
|
|
3576
|
+
if (!q.sql) continue;
|
|
3577
|
+
let entry = exact.get(q.sql);
|
|
3578
|
+
if (!entry) {
|
|
3579
|
+
entry = { count: 0, first: q };
|
|
3580
|
+
exact.set(q.sql, entry);
|
|
3581
|
+
}
|
|
3582
|
+
entry.count++;
|
|
3583
|
+
}
|
|
3584
|
+
for (const [, e] of exact) {
|
|
3585
|
+
if (e.count < REDUNDANT_QUERY_MIN_COUNT) continue;
|
|
3586
|
+
const info = getQueryInfo(e.first);
|
|
3587
|
+
const label = info.op + (info.table ? ` ${info.table}` : "");
|
|
3588
|
+
const dedupKey = `${endpoint}:${label}`;
|
|
3589
|
+
if (seen.has(dedupKey)) continue;
|
|
3590
|
+
seen.add(dedupKey);
|
|
3591
|
+
insights.push({
|
|
3592
|
+
severity: "warning",
|
|
3593
|
+
type: "redundant-query",
|
|
3594
|
+
title: "Redundant Query",
|
|
3595
|
+
desc: `${label} runs ${e.count}x with identical params in ${endpoint}.`,
|
|
3596
|
+
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.",
|
|
3597
|
+
nav: "queries"
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
return insights;
|
|
3602
|
+
}
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3605
|
+
});
|
|
3606
|
+
|
|
3607
|
+
// src/analysis/insights/rules/error.ts
|
|
3608
|
+
var errorRule;
|
|
3609
|
+
var init_error = __esm({
|
|
3610
|
+
"src/analysis/insights/rules/error.ts"() {
|
|
3611
|
+
"use strict";
|
|
3612
|
+
errorRule = {
|
|
3613
|
+
id: "error",
|
|
3614
|
+
check(ctx) {
|
|
3615
|
+
if (ctx.errors.length === 0) return [];
|
|
3616
|
+
const insights = [];
|
|
3617
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3618
|
+
for (const e of ctx.errors) {
|
|
3619
|
+
const name = e.name || "Error";
|
|
3620
|
+
groups.set(name, (groups.get(name) ?? 0) + 1);
|
|
3621
|
+
}
|
|
3622
|
+
for (const [name, cnt] of groups) {
|
|
3623
|
+
insights.push({
|
|
3624
|
+
severity: "critical",
|
|
3625
|
+
type: "error",
|
|
3626
|
+
title: "Unhandled Error",
|
|
3627
|
+
desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
|
|
3628
|
+
hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
|
|
3629
|
+
nav: "errors"
|
|
3630
|
+
});
|
|
3631
|
+
}
|
|
3632
|
+
return insights;
|
|
3633
|
+
}
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
});
|
|
3637
|
+
|
|
3638
|
+
// src/analysis/insights/rules/error-hotspot.ts
|
|
3639
|
+
var errorHotspotRule;
|
|
3640
|
+
var init_error_hotspot = __esm({
|
|
3641
|
+
"src/analysis/insights/rules/error-hotspot.ts"() {
|
|
3642
|
+
"use strict";
|
|
3643
|
+
init_thresholds();
|
|
3644
|
+
errorHotspotRule = {
|
|
3645
|
+
id: "error-hotspot",
|
|
3646
|
+
check(ctx) {
|
|
3647
|
+
const insights = [];
|
|
3648
|
+
for (const [ep, g] of ctx.endpointGroups) {
|
|
3649
|
+
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3650
|
+
const errorRate = Math.round(g.errors / g.total * 100);
|
|
3651
|
+
if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
|
|
3652
|
+
insights.push({
|
|
3653
|
+
severity: "critical",
|
|
3654
|
+
type: "error-hotspot",
|
|
3655
|
+
title: "Error Hotspot",
|
|
3656
|
+
desc: `${ep} \u2014 ${errorRate}% error rate (${g.errors}/${g.total} requests)`,
|
|
3657
|
+
hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces.",
|
|
3658
|
+
nav: "requests"
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
return insights;
|
|
3663
|
+
}
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
3666
|
+
});
|
|
3667
|
+
|
|
3668
|
+
// src/analysis/insights/rules/duplicate.ts
|
|
3669
|
+
var duplicateRule;
|
|
3670
|
+
var init_duplicate = __esm({
|
|
3671
|
+
"src/analysis/insights/rules/duplicate.ts"() {
|
|
3672
|
+
"use strict";
|
|
3673
|
+
init_thresholds();
|
|
3674
|
+
duplicateRule = {
|
|
3675
|
+
id: "duplicate",
|
|
3676
|
+
check(ctx) {
|
|
3677
|
+
const dupCounts = /* @__PURE__ */ new Map();
|
|
3678
|
+
const flowCount = /* @__PURE__ */ new Map();
|
|
3679
|
+
for (const flow of ctx.flows) {
|
|
3680
|
+
if (!flow.requests) continue;
|
|
3681
|
+
const seenInFlow = /* @__PURE__ */ new Set();
|
|
3682
|
+
for (const fr of flow.requests) {
|
|
3683
|
+
if (!fr.isDuplicate) continue;
|
|
3684
|
+
const dupKey = `${fr.method} ${fr.label ?? fr.path ?? fr.url}`;
|
|
3685
|
+
dupCounts.set(dupKey, (dupCounts.get(dupKey) ?? 0) + 1);
|
|
3686
|
+
if (!seenInFlow.has(dupKey)) {
|
|
3687
|
+
seenInFlow.add(dupKey);
|
|
3688
|
+
flowCount.set(dupKey, (flowCount.get(dupKey) ?? 0) + 1);
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
3693
|
+
const insights = [];
|
|
3694
|
+
for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
|
|
3695
|
+
const d = dupEntries[i];
|
|
3696
|
+
insights.push({
|
|
3697
|
+
severity: "warning",
|
|
3698
|
+
type: "duplicate",
|
|
3699
|
+
title: "Duplicate API Call",
|
|
3700
|
+
desc: `${d.key} loaded ${d.count}x as duplicate across ${d.flows} action${d.flows !== 1 ? "s" : ""}`,
|
|
3701
|
+
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.",
|
|
3702
|
+
nav: "actions"
|
|
3703
|
+
});
|
|
3704
|
+
}
|
|
3705
|
+
return insights;
|
|
3706
|
+
}
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3709
|
+
});
|
|
3710
|
+
|
|
3711
|
+
// src/utils/format.ts
|
|
3712
|
+
function formatDuration(ms) {
|
|
3713
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
3714
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
3715
|
+
}
|
|
3716
|
+
function formatSize(bytes) {
|
|
3717
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
3718
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
3719
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
3720
|
+
}
|
|
3721
|
+
function pct(part, total) {
|
|
3722
|
+
return total > 0 ? Math.round(part / total * 100) : 0;
|
|
3723
|
+
}
|
|
3724
|
+
var init_format = __esm({
|
|
3725
|
+
"src/utils/format.ts"() {
|
|
3726
|
+
"use strict";
|
|
3727
|
+
}
|
|
3728
|
+
});
|
|
3729
|
+
|
|
3730
|
+
// src/analysis/insights/rules/slow.ts
|
|
3731
|
+
var slowRule;
|
|
3732
|
+
var init_slow = __esm({
|
|
3733
|
+
"src/analysis/insights/rules/slow.ts"() {
|
|
3734
|
+
"use strict";
|
|
3735
|
+
init_format();
|
|
3736
|
+
init_thresholds();
|
|
3737
|
+
slowRule = {
|
|
3738
|
+
id: "slow",
|
|
3739
|
+
check(ctx) {
|
|
3740
|
+
const insights = [];
|
|
3741
|
+
for (const [ep, g] of ctx.endpointGroups) {
|
|
3742
|
+
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3743
|
+
const avgMs = Math.round(g.totalDuration / g.total);
|
|
3744
|
+
if (avgMs < SLOW_ENDPOINT_THRESHOLD_MS) continue;
|
|
3745
|
+
const avgQueryMs = Math.round(g.totalQueryTimeMs / g.total);
|
|
3746
|
+
const avgFetchMs = Math.round(g.totalFetchTimeMs / g.total);
|
|
3747
|
+
const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
|
|
3748
|
+
const parts = [];
|
|
3749
|
+
if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
|
|
3750
|
+
if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
|
|
3751
|
+
if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
|
|
3752
|
+
const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
|
|
3753
|
+
let detail;
|
|
3754
|
+
let slowestMs = 0;
|
|
3755
|
+
for (const [, sd] of g.queryShapeDurations) {
|
|
3756
|
+
const avgShapeMs = sd.totalMs / sd.count;
|
|
3757
|
+
if (avgShapeMs > slowestMs) {
|
|
3758
|
+
slowestMs = avgShapeMs;
|
|
3759
|
+
detail = `Slowest query: ${sd.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${sd.count}x)`;
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
insights.push({
|
|
3763
|
+
severity: "warning",
|
|
3764
|
+
type: "slow",
|
|
3765
|
+
title: "Slow Endpoint",
|
|
3766
|
+
desc: `${ep} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
|
|
3767
|
+
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.",
|
|
3768
|
+
detail,
|
|
3769
|
+
nav: "requests"
|
|
3770
|
+
});
|
|
3771
|
+
}
|
|
3772
|
+
return insights;
|
|
3773
|
+
}
|
|
3774
|
+
};
|
|
3775
|
+
}
|
|
3776
|
+
});
|
|
3777
|
+
|
|
3778
|
+
// src/analysis/insights/rules/query-heavy.ts
|
|
3779
|
+
var queryHeavyRule;
|
|
3780
|
+
var init_query_heavy = __esm({
|
|
3781
|
+
"src/analysis/insights/rules/query-heavy.ts"() {
|
|
3782
|
+
"use strict";
|
|
3783
|
+
init_thresholds();
|
|
3784
|
+
queryHeavyRule = {
|
|
3785
|
+
id: "query-heavy",
|
|
3786
|
+
check(ctx) {
|
|
3787
|
+
const insights = [];
|
|
3788
|
+
for (const [ep, g] of ctx.endpointGroups) {
|
|
3789
|
+
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3790
|
+
const avgQueries = Math.round(g.queryCount / g.total);
|
|
3791
|
+
if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
|
|
3792
|
+
insights.push({
|
|
3793
|
+
severity: "warning",
|
|
3794
|
+
type: "query-heavy",
|
|
3795
|
+
title: "Query-Heavy Endpoint",
|
|
3796
|
+
desc: `${ep} \u2014 avg ${avgQueries} queries/request`,
|
|
3797
|
+
hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches.",
|
|
3798
|
+
nav: "queries"
|
|
3799
|
+
});
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
return insights;
|
|
3803
|
+
}
|
|
3804
|
+
};
|
|
3805
|
+
}
|
|
3806
|
+
});
|
|
3807
|
+
|
|
3808
|
+
// src/analysis/insights/rules/select-star.ts
|
|
3809
|
+
var selectStarRule;
|
|
3810
|
+
var init_select_star = __esm({
|
|
3811
|
+
"src/analysis/insights/rules/select-star.ts"() {
|
|
3812
|
+
"use strict";
|
|
3813
|
+
init_query_helpers();
|
|
3814
|
+
init_thresholds();
|
|
3815
|
+
init_patterns();
|
|
3816
|
+
selectStarRule = {
|
|
3817
|
+
id: "select-star",
|
|
3818
|
+
check(ctx) {
|
|
3819
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3820
|
+
for (const [, reqQueries] of ctx.queriesByReq) {
|
|
3821
|
+
for (const q of reqQueries) {
|
|
3822
|
+
if (!q.sql) continue;
|
|
3823
|
+
const isSelectStar = SELECT_STAR_RE.test(q.sql.trim()) || SELECT_DOT_STAR_RE.test(q.sql);
|
|
3824
|
+
if (!isSelectStar) continue;
|
|
3825
|
+
const info = getQueryInfo(q);
|
|
3826
|
+
const key = info.table || "unknown";
|
|
3827
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
const insights = [];
|
|
3831
|
+
for (const [table, count] of seen) {
|
|
3832
|
+
if (count < OVERFETCH_MIN_REQUESTS) continue;
|
|
3833
|
+
insights.push({
|
|
3834
|
+
severity: "warning",
|
|
3835
|
+
type: "select-star",
|
|
3836
|
+
title: "SELECT * Query",
|
|
3837
|
+
desc: `SELECT * on ${table} \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
|
|
3838
|
+
hint: "SELECT * fetches all columns including ones you don\u2019t need. Specify only required columns to reduce data transfer and memory usage.",
|
|
3839
|
+
nav: "queries"
|
|
3840
|
+
});
|
|
3841
|
+
}
|
|
3842
|
+
return insights;
|
|
3843
|
+
}
|
|
3844
|
+
};
|
|
3845
|
+
}
|
|
3846
|
+
});
|
|
3847
|
+
|
|
3848
|
+
// src/analysis/insights/rules/high-rows.ts
|
|
3849
|
+
var highRowsRule;
|
|
3850
|
+
var init_high_rows = __esm({
|
|
3851
|
+
"src/analysis/insights/rules/high-rows.ts"() {
|
|
3852
|
+
"use strict";
|
|
3853
|
+
init_query_helpers();
|
|
3854
|
+
init_thresholds();
|
|
3855
|
+
highRowsRule = {
|
|
3856
|
+
id: "high-rows",
|
|
3857
|
+
check(ctx) {
|
|
3858
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3859
|
+
for (const [, reqQueries] of ctx.queriesByReq) {
|
|
3860
|
+
for (const q of reqQueries) {
|
|
3861
|
+
if (!q.rowCount || q.rowCount <= HIGH_ROW_COUNT) continue;
|
|
3862
|
+
const info = getQueryInfo(q);
|
|
3863
|
+
const key = `${info.op} ${info.table || "unknown"}`;
|
|
3864
|
+
let entry = seen.get(key);
|
|
3865
|
+
if (!entry) {
|
|
3866
|
+
entry = { max: 0, count: 0 };
|
|
3867
|
+
seen.set(key, entry);
|
|
3868
|
+
}
|
|
3869
|
+
entry.count++;
|
|
3870
|
+
if (q.rowCount > entry.max) entry.max = q.rowCount;
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
const insights = [];
|
|
3874
|
+
for (const [key, hrs] of seen) {
|
|
3875
|
+
if (hrs.count < OVERFETCH_MIN_REQUESTS) continue;
|
|
3876
|
+
insights.push({
|
|
3877
|
+
severity: "warning",
|
|
3878
|
+
type: "high-rows",
|
|
3879
|
+
title: "Large Result Set",
|
|
3880
|
+
desc: `${key} returns ${hrs.max}+ rows (${hrs.count}x)`,
|
|
3881
|
+
hint: "Fetching many rows slows responses and wastes memory. Add a LIMIT clause, implement pagination, or filter with a WHERE condition.",
|
|
3882
|
+
nav: "queries"
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
return insights;
|
|
3886
|
+
}
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3889
|
+
});
|
|
3890
|
+
|
|
3891
|
+
// src/analysis/insights/rules/response-overfetch.ts
|
|
3892
|
+
var responseOverfetchRule;
|
|
3893
|
+
var init_response_overfetch = __esm({
|
|
3894
|
+
"src/analysis/insights/rules/response-overfetch.ts"() {
|
|
3895
|
+
"use strict";
|
|
3896
|
+
init_endpoint();
|
|
3897
|
+
init_response();
|
|
3898
|
+
init_patterns();
|
|
3899
|
+
init_thresholds();
|
|
3900
|
+
responseOverfetchRule = {
|
|
3901
|
+
id: "response-overfetch",
|
|
3902
|
+
check(ctx) {
|
|
3903
|
+
const insights = [];
|
|
3904
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3905
|
+
for (const r of ctx.nonStatic) {
|
|
3906
|
+
if (r.statusCode >= 400 || !r.responseBody) continue;
|
|
3907
|
+
const ep = getEndpointKey(r.method, r.path);
|
|
3908
|
+
if (seen.has(ep)) continue;
|
|
3909
|
+
let parsed;
|
|
3910
|
+
try {
|
|
3911
|
+
parsed = JSON.parse(r.responseBody);
|
|
3912
|
+
} catch {
|
|
3913
|
+
continue;
|
|
3914
|
+
}
|
|
3915
|
+
const target = unwrapResponse(parsed);
|
|
3916
|
+
const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
3917
|
+
if (!inspectObj || typeof inspectObj !== "object" || Array.isArray(inspectObj)) continue;
|
|
3918
|
+
const fields = Object.keys(inspectObj);
|
|
3919
|
+
if (fields.length < OVERFETCH_MIN_FIELDS) continue;
|
|
3920
|
+
let internalIdCount = 0;
|
|
3921
|
+
let nullCount = 0;
|
|
3922
|
+
for (const key of fields) {
|
|
3923
|
+
if (INTERNAL_ID_SUFFIX.test(key) || key === "id" || key === "_id") internalIdCount++;
|
|
3924
|
+
const val = inspectObj[key];
|
|
3925
|
+
if (val === null || val === void 0) nullCount++;
|
|
3926
|
+
}
|
|
3927
|
+
const nullRatio = nullCount / fields.length;
|
|
3928
|
+
const reasons = [];
|
|
3929
|
+
if (internalIdCount >= OVERFETCH_MIN_INTERNAL_IDS) reasons.push(`${internalIdCount} internal ID fields`);
|
|
3930
|
+
if (nullRatio >= OVERFETCH_NULL_RATIO) reasons.push(`${Math.round(nullRatio * 100)}% null fields`);
|
|
3931
|
+
if (reasons.length === 0 && fields.length >= OVERFETCH_MANY_FIELDS) {
|
|
3932
|
+
reasons.push(`${fields.length} fields returned`);
|
|
3933
|
+
}
|
|
3934
|
+
if (reasons.length > 0) {
|
|
3935
|
+
seen.add(ep);
|
|
3936
|
+
insights.push({
|
|
3937
|
+
severity: "info",
|
|
3938
|
+
type: "response-overfetch",
|
|
3939
|
+
title: "Response Overfetch",
|
|
3940
|
+
desc: `${ep} \u2014 ${reasons.join(", ")}`,
|
|
3941
|
+
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.",
|
|
3942
|
+
nav: "requests"
|
|
3943
|
+
});
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
return insights;
|
|
3947
|
+
}
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
});
|
|
3951
|
+
|
|
3952
|
+
// src/analysis/insights/rules/large-response.ts
|
|
3953
|
+
var largeResponseRule;
|
|
3954
|
+
var init_large_response = __esm({
|
|
3955
|
+
"src/analysis/insights/rules/large-response.ts"() {
|
|
3956
|
+
"use strict";
|
|
3957
|
+
init_format();
|
|
3958
|
+
init_thresholds();
|
|
3959
|
+
largeResponseRule = {
|
|
3960
|
+
id: "large-response",
|
|
3961
|
+
check(ctx) {
|
|
3962
|
+
const insights = [];
|
|
3963
|
+
for (const [ep, g] of ctx.endpointGroups) {
|
|
3964
|
+
if (g.total < OVERFETCH_MIN_REQUESTS) continue;
|
|
3965
|
+
const avgSize = Math.round(g.totalSize / g.total);
|
|
3966
|
+
if (avgSize > LARGE_RESPONSE_BYTES) {
|
|
3967
|
+
insights.push({
|
|
3968
|
+
severity: "info",
|
|
3969
|
+
type: "large-response",
|
|
3970
|
+
title: "Large Response",
|
|
3971
|
+
desc: `${ep} \u2014 avg ${formatSize(avgSize)} response`,
|
|
3972
|
+
hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression.",
|
|
3973
|
+
nav: "requests"
|
|
3974
|
+
});
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
return insights;
|
|
3978
|
+
}
|
|
3979
|
+
};
|
|
3980
|
+
}
|
|
3981
|
+
});
|
|
3982
|
+
|
|
3983
|
+
// src/analysis/insights/rules/regression.ts
|
|
3984
|
+
var regressionRule;
|
|
3985
|
+
var init_regression = __esm({
|
|
3986
|
+
"src/analysis/insights/rules/regression.ts"() {
|
|
3987
|
+
"use strict";
|
|
3988
|
+
init_format();
|
|
3989
|
+
init_thresholds();
|
|
3990
|
+
regressionRule = {
|
|
3991
|
+
id: "regression",
|
|
3992
|
+
check(ctx) {
|
|
3993
|
+
if (!ctx.previousMetrics || ctx.previousMetrics.length === 0) return [];
|
|
3994
|
+
const insights = [];
|
|
3995
|
+
for (const epMetrics of ctx.previousMetrics) {
|
|
3996
|
+
if (epMetrics.sessions.length < 2) continue;
|
|
3997
|
+
const prev = epMetrics.sessions[epMetrics.sessions.length - 2];
|
|
3998
|
+
const current = epMetrics.sessions[epMetrics.sessions.length - 1];
|
|
3999
|
+
if (prev.requestCount < REGRESSION_MIN_REQUESTS || current.requestCount < REGRESSION_MIN_REQUESTS) continue;
|
|
4000
|
+
const p95Increase = current.p95DurationMs - prev.p95DurationMs;
|
|
4001
|
+
const p95PctChange = prev.p95DurationMs > 0 ? Math.round(p95Increase / prev.p95DurationMs * 100) : 0;
|
|
4002
|
+
if (p95Increase >= REGRESSION_MIN_INCREASE_MS && p95PctChange >= REGRESSION_PCT_THRESHOLD) {
|
|
4003
|
+
insights.push({
|
|
4004
|
+
severity: "warning",
|
|
4005
|
+
type: "regression",
|
|
4006
|
+
title: "Performance Regression",
|
|
4007
|
+
desc: `${epMetrics.endpoint} p95 degraded ${formatDuration(prev.p95DurationMs)} \u2192 ${formatDuration(current.p95DurationMs)} (+${p95PctChange}%)`,
|
|
4008
|
+
hint: "This endpoint is slower than the previous session. Check if recent code changes added queries or processing.",
|
|
4009
|
+
nav: "graph"
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
4012
|
+
if (prev.avgQueryCount > 0 && current.avgQueryCount > prev.avgQueryCount * QUERY_COUNT_REGRESSION_RATIO) {
|
|
4013
|
+
insights.push({
|
|
4014
|
+
severity: "warning",
|
|
4015
|
+
type: "regression",
|
|
4016
|
+
title: "Query Count Regression",
|
|
4017
|
+
desc: `${epMetrics.endpoint} queries/request increased ${prev.avgQueryCount} \u2192 ${current.avgQueryCount}`,
|
|
4018
|
+
hint: "This endpoint is making more database queries than before. Check for new N+1 patterns or removed query optimizations.",
|
|
4019
|
+
nav: "queries"
|
|
4020
|
+
});
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
return insights;
|
|
4024
|
+
}
|
|
4025
|
+
};
|
|
4026
|
+
}
|
|
4027
|
+
});
|
|
4028
|
+
|
|
4029
|
+
// src/analysis/insights/rules/security.ts
|
|
4030
|
+
var securityRule;
|
|
4031
|
+
var init_security2 = __esm({
|
|
4032
|
+
"src/analysis/insights/rules/security.ts"() {
|
|
4033
|
+
"use strict";
|
|
4034
|
+
securityRule = {
|
|
4035
|
+
id: "security",
|
|
4036
|
+
check(ctx) {
|
|
4037
|
+
if (!ctx.securityFindings) return [];
|
|
4038
|
+
return ctx.securityFindings.map((f) => ({
|
|
4039
|
+
severity: f.severity,
|
|
4040
|
+
type: "security",
|
|
4041
|
+
title: f.title,
|
|
4042
|
+
desc: f.desc,
|
|
4043
|
+
hint: f.hint,
|
|
4044
|
+
nav: "security"
|
|
4045
|
+
}));
|
|
4046
|
+
}
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
});
|
|
4050
|
+
|
|
4051
|
+
// src/analysis/insights/index.ts
|
|
4052
|
+
function createDefaultInsightRunner() {
|
|
4053
|
+
const runner = new InsightRunner();
|
|
4054
|
+
runner.register(n1Rule);
|
|
4055
|
+
runner.register(crossEndpointRule);
|
|
4056
|
+
runner.register(redundantQueryRule);
|
|
4057
|
+
runner.register(errorRule);
|
|
4058
|
+
runner.register(errorHotspotRule);
|
|
4059
|
+
runner.register(duplicateRule);
|
|
4060
|
+
runner.register(slowRule);
|
|
4061
|
+
runner.register(queryHeavyRule);
|
|
4062
|
+
runner.register(selectStarRule);
|
|
4063
|
+
runner.register(highRowsRule);
|
|
4064
|
+
runner.register(responseOverfetchRule);
|
|
4065
|
+
runner.register(largeResponseRule);
|
|
4066
|
+
runner.register(regressionRule);
|
|
4067
|
+
runner.register(securityRule);
|
|
4068
|
+
return runner;
|
|
4069
|
+
}
|
|
4070
|
+
function computeInsights(ctx) {
|
|
4071
|
+
return createDefaultInsightRunner().run(ctx);
|
|
4072
|
+
}
|
|
4073
|
+
var init_insights2 = __esm({
|
|
4074
|
+
"src/analysis/insights/index.ts"() {
|
|
4075
|
+
"use strict";
|
|
4076
|
+
init_runner();
|
|
4077
|
+
init_runner();
|
|
4078
|
+
init_n1();
|
|
4079
|
+
init_cross_endpoint();
|
|
4080
|
+
init_redundant_query();
|
|
4081
|
+
init_error();
|
|
4082
|
+
init_error_hotspot();
|
|
4083
|
+
init_duplicate();
|
|
4084
|
+
init_slow();
|
|
4085
|
+
init_query_heavy();
|
|
4086
|
+
init_select_star();
|
|
4087
|
+
init_high_rows();
|
|
4088
|
+
init_response_overfetch();
|
|
4089
|
+
init_large_response();
|
|
4090
|
+
init_regression();
|
|
4091
|
+
init_security2();
|
|
4092
|
+
}
|
|
4093
|
+
});
|
|
4094
|
+
|
|
4095
|
+
// src/analysis/insights.ts
|
|
4096
|
+
var init_insights3 = __esm({
|
|
4097
|
+
"src/analysis/insights.ts"() {
|
|
3576
4098
|
"use strict";
|
|
3577
|
-
init_request_log();
|
|
3578
|
-
init_store();
|
|
3579
|
-
init_request_log();
|
|
3580
|
-
init_group();
|
|
3581
|
-
init_rules();
|
|
3582
4099
|
init_insights2();
|
|
4100
|
+
}
|
|
4101
|
+
});
|
|
4102
|
+
|
|
4103
|
+
// src/analysis/engine.ts
|
|
4104
|
+
var AnalysisEngine;
|
|
4105
|
+
var init_engine = __esm({
|
|
4106
|
+
"src/analysis/engine.ts"() {
|
|
4107
|
+
"use strict";
|
|
4108
|
+
init_request_log();
|
|
4109
|
+
init_store();
|
|
4110
|
+
init_request_log();
|
|
4111
|
+
init_group();
|
|
4112
|
+
init_rules();
|
|
4113
|
+
init_insights3();
|
|
3583
4114
|
AnalysisEngine = class {
|
|
3584
|
-
constructor(debounceMs = 300) {
|
|
4115
|
+
constructor(metricsStore, debounceMs = 300) {
|
|
4116
|
+
this.metricsStore = metricsStore;
|
|
3585
4117
|
this.debounceMs = debounceMs;
|
|
3586
4118
|
this.scanner = createDefaultScanner();
|
|
3587
4119
|
this.boundRequestListener = () => this.scheduleRecompute();
|
|
@@ -3639,6 +4171,7 @@ var init_engine = __esm({
|
|
|
3639
4171
|
const queries = defaultQueryStore.getAll();
|
|
3640
4172
|
const errors = defaultErrorStore.getAll();
|
|
3641
4173
|
const logs = defaultLogStore.getAll();
|
|
4174
|
+
const fetches = defaultFetchStore.getAll();
|
|
3642
4175
|
const flows = groupRequestsIntoFlows(requests);
|
|
3643
4176
|
this.cachedFindings = this.scanner.scan({ requests, logs });
|
|
3644
4177
|
this.cachedInsights = computeInsights({
|
|
@@ -3646,6 +4179,8 @@ var init_engine = __esm({
|
|
|
3646
4179
|
queries,
|
|
3647
4180
|
errors,
|
|
3648
4181
|
flows,
|
|
4182
|
+
fetches,
|
|
4183
|
+
previousMetrics: this.metricsStore.getAll(),
|
|
3649
4184
|
securityFindings: this.cachedFindings
|
|
3650
4185
|
});
|
|
3651
4186
|
for (const fn of this.listeners) {
|
|
@@ -3668,8 +4203,9 @@ var init_src = __esm({
|
|
|
3668
4203
|
init_adapter_registry();
|
|
3669
4204
|
init_rules();
|
|
3670
4205
|
init_engine();
|
|
4206
|
+
init_insights3();
|
|
3671
4207
|
init_insights2();
|
|
3672
|
-
VERSION = "0.7.
|
|
4208
|
+
VERSION = "0.7.6";
|
|
3673
4209
|
}
|
|
3674
4210
|
});
|
|
3675
4211
|
|
|
@@ -4282,7 +4818,7 @@ function getFlowInsights() {
|
|
|
4282
4818
|
}
|
|
4283
4819
|
`;
|
|
4284
4820
|
}
|
|
4285
|
-
var
|
|
4821
|
+
var init_insights4 = __esm({
|
|
4286
4822
|
"src/dashboard/client/views/flows/insights.ts"() {
|
|
4287
4823
|
"use strict";
|
|
4288
4824
|
init_constants();
|
|
@@ -4474,7 +5010,7 @@ function getFlowsView() {
|
|
|
4474
5010
|
var init_flows2 = __esm({
|
|
4475
5011
|
"src/dashboard/client/views/flows.ts"() {
|
|
4476
5012
|
"use strict";
|
|
4477
|
-
|
|
5013
|
+
init_insights4();
|
|
4478
5014
|
init_detail();
|
|
4479
5015
|
}
|
|
4480
5016
|
});
|
|
@@ -5090,6 +5626,27 @@ function getGraphOverview() {
|
|
|
5090
5626
|
(s.avgQueryCount > 0 ? '<span class="perf-ep-stat' + (s.avgQueryCount > HIGH_QUERY_THRESHOLD ? ' perf-ep-stat-warn' : '') + '">' + s.avgQueryCount + ' q/req</span>' : '') +
|
|
5091
5627
|
'<span class="perf-ep-stat perf-ep-stat-muted">' + s.totalRequests + ' req' + (s.totalRequests !== 1 ? 's' : '') + '</span>';
|
|
5092
5628
|
|
|
5629
|
+
var ovTotal = (s.avgQueryTimeMs || 0) + (s.avgFetchTimeMs || 0) + (s.avgAppTimeMs || 0);
|
|
5630
|
+
var ovBarHtml = '';
|
|
5631
|
+
if (ovTotal > 0) {
|
|
5632
|
+
var ovDbPct = Math.round((s.avgQueryTimeMs || 0) / ovTotal * 100);
|
|
5633
|
+
var ovFetchPct = Math.round((s.avgFetchTimeMs || 0) / ovTotal * 100);
|
|
5634
|
+
var ovAppPct = Math.max(0, 100 - ovDbPct - ovFetchPct);
|
|
5635
|
+
ovBarHtml =
|
|
5636
|
+
'<div class="perf-breakdown-inline">' +
|
|
5637
|
+
'<div class="perf-breakdown-bar perf-breakdown-bar-sm">' +
|
|
5638
|
+
(ovDbPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-db" style="width:' + ovDbPct + '%"></div>' : '') +
|
|
5639
|
+
(ovFetchPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:' + ovFetchPct + '%"></div>' : '') +
|
|
5640
|
+
(ovAppPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-app" style="width:' + ovAppPct + '%"></div>' : '') +
|
|
5641
|
+
'</div>' +
|
|
5642
|
+
'<span class="perf-breakdown-labels">' +
|
|
5643
|
+
(ovDbPct > 0 ? '<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-db"></span>' + fmtMs(s.avgQueryTimeMs || 0) + '</span>' : '') +
|
|
5644
|
+
(ovFetchPct > 0 ? '<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>' + fmtMs(s.avgFetchTimeMs || 0) + '</span>' : '') +
|
|
5645
|
+
'<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-app"></span>' + fmtMs(s.avgAppTimeMs || 0) + '</span>' +
|
|
5646
|
+
'</span>' +
|
|
5647
|
+
'</div>';
|
|
5648
|
+
}
|
|
5649
|
+
|
|
5093
5650
|
var chartId = 'inline-scatter-' + idx;
|
|
5094
5651
|
|
|
5095
5652
|
card.innerHTML =
|
|
@@ -5097,6 +5654,7 @@ function getGraphOverview() {
|
|
|
5097
5654
|
'<span class="perf-ep-name">' + escHtml(ep.endpoint) + '</span>' +
|
|
5098
5655
|
'<span class="perf-ep-stats">' + statsHtml + '</span>' +
|
|
5099
5656
|
'</div>' +
|
|
5657
|
+
ovBarHtml +
|
|
5100
5658
|
'<canvas id="' + chartId + '" class="perf-inline-canvas"></canvas>';
|
|
5101
5659
|
|
|
5102
5660
|
list.appendChild(card);
|
|
@@ -5132,7 +5690,6 @@ function getGraphDetail() {
|
|
|
5132
5690
|
var g = healthGrade(s.p95Ms);
|
|
5133
5691
|
var errors = Math.round(s.errorRate * s.totalRequests);
|
|
5134
5692
|
|
|
5135
|
-
// header
|
|
5136
5693
|
var header = document.createElement('div');
|
|
5137
5694
|
header.className = 'perf-detail-header';
|
|
5138
5695
|
header.innerHTML =
|
|
@@ -5142,7 +5699,6 @@ function getGraphDetail() {
|
|
|
5142
5699
|
'</div>';
|
|
5143
5700
|
container.appendChild(header);
|
|
5144
5701
|
|
|
5145
|
-
// metric cards
|
|
5146
5702
|
var metrics = document.createElement('div');
|
|
5147
5703
|
metrics.className = 'perf-metric-row';
|
|
5148
5704
|
metrics.innerHTML =
|
|
@@ -5151,7 +5707,38 @@ function getGraphDetail() {
|
|
|
5151
5707
|
buildMetricCard('Queries/req', String(s.avgQueryCount), s.avgQueryCount > ${HIGH_QUERY_COUNT_PER_REQ} ? 'var(--amber)' : 'var(--text)');
|
|
5152
5708
|
container.appendChild(metrics);
|
|
5153
5709
|
|
|
5154
|
-
|
|
5710
|
+
var totalAvg = (s.avgQueryTimeMs || 0) + (s.avgFetchTimeMs || 0) + (s.avgAppTimeMs || 0);
|
|
5711
|
+
if (totalAvg > 0) {
|
|
5712
|
+
var dbPct = Math.round((s.avgQueryTimeMs || 0) / totalAvg * 100);
|
|
5713
|
+
var fetchPct = Math.round((s.avgFetchTimeMs || 0) / totalAvg * 100);
|
|
5714
|
+
var appPct = Math.max(0, 100 - dbPct - fetchPct);
|
|
5715
|
+
|
|
5716
|
+
var breakdown = document.createElement('div');
|
|
5717
|
+
breakdown.className = 'perf-breakdown';
|
|
5718
|
+
|
|
5719
|
+
var breakdownLabel = document.createElement('div');
|
|
5720
|
+
breakdownLabel.className = 'perf-section-title';
|
|
5721
|
+
breakdownLabel.textContent = 'Time Breakdown';
|
|
5722
|
+
breakdown.appendChild(breakdownLabel);
|
|
5723
|
+
|
|
5724
|
+
var bar = document.createElement('div');
|
|
5725
|
+
bar.className = 'perf-breakdown-bar';
|
|
5726
|
+
if (dbPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-db" style="width:' + dbPct + '%"></div>';
|
|
5727
|
+
if (fetchPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:' + fetchPct + '%"></div>';
|
|
5728
|
+
if (appPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-app" style="width:' + appPct + '%"></div>';
|
|
5729
|
+
breakdown.appendChild(bar);
|
|
5730
|
+
|
|
5731
|
+
var legend = document.createElement('div');
|
|
5732
|
+
legend.className = 'perf-breakdown-legend';
|
|
5733
|
+
legend.innerHTML =
|
|
5734
|
+
'<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-db"></span>DB ' + fmtMs(s.avgQueryTimeMs || 0) + ' (' + dbPct + '%)</span>' +
|
|
5735
|
+
'<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>Fetch ' + fmtMs(s.avgFetchTimeMs || 0) + ' (' + fetchPct + '%)</span>' +
|
|
5736
|
+
'<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-app"></span>App ' + fmtMs(s.avgAppTimeMs || 0) + ' (' + appPct + '%)</span>';
|
|
5737
|
+
breakdown.appendChild(legend);
|
|
5738
|
+
|
|
5739
|
+
container.appendChild(breakdown);
|
|
5740
|
+
}
|
|
5741
|
+
|
|
5155
5742
|
var chartWrap = document.createElement('div');
|
|
5156
5743
|
chartWrap.className = 'perf-chart-wrap';
|
|
5157
5744
|
var chartLabel = document.createElement('div');
|
|
@@ -5169,7 +5756,6 @@ function getGraphDetail() {
|
|
|
5169
5756
|
|
|
5170
5757
|
drawScatterChart(canvas, ep.requests);
|
|
5171
5758
|
|
|
5172
|
-
// recent requests table
|
|
5173
5759
|
if (ep.requests.length > 0) {
|
|
5174
5760
|
var tableWrap = document.createElement('div');
|
|
5175
5761
|
tableWrap.className = 'perf-history-wrap';
|
|
@@ -5180,6 +5766,7 @@ function getGraphDetail() {
|
|
|
5180
5766
|
'<span class="perf-col perf-col-date">Time</span>' +
|
|
5181
5767
|
'<span class="perf-col perf-col-health">Health</span>' +
|
|
5182
5768
|
'<span class="perf-col perf-col-avg">Duration</span>' +
|
|
5769
|
+
'<span class="perf-col perf-col-breakdown">Breakdown</span>' +
|
|
5183
5770
|
'<span class="perf-col perf-col-status">Status</span>' +
|
|
5184
5771
|
'<span class="perf-col perf-col-qpr">Queries</span>';
|
|
5185
5772
|
tableWrap.appendChild(colHeader);
|
|
@@ -5198,10 +5785,20 @@ function getGraphDetail() {
|
|
|
5198
5785
|
var row = document.createElement('div');
|
|
5199
5786
|
row.className = 'perf-hist-row' + (isError ? ' perf-hist-row-err' : '');
|
|
5200
5787
|
row.setAttribute('data-req-idx', item.origIdx);
|
|
5788
|
+
var rDbMs = r.queryTimeMs || 0;
|
|
5789
|
+
var rFetchMs = r.fetchTimeMs || 0;
|
|
5790
|
+
var rAppMs = Math.max(0, r.durationMs - rDbMs - rFetchMs);
|
|
5791
|
+
var breakdownParts = [];
|
|
5792
|
+
if (rDbMs > 0) breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-db">DB ' + fmtMs(rDbMs) + '</span>');
|
|
5793
|
+
if (rFetchMs > 0) breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-fetch">Fetch ' + fmtMs(rFetchMs) + '</span>');
|
|
5794
|
+
breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-app">App ' + fmtMs(rAppMs) + '</span>');
|
|
5795
|
+
var breakdownHtml = breakdownParts.join('');
|
|
5796
|
+
|
|
5201
5797
|
row.innerHTML =
|
|
5202
5798
|
'<span class="perf-col perf-col-date">' + timeStr + '</span>' +
|
|
5203
5799
|
'<span class="perf-col perf-col-health"><span class="perf-badge perf-badge-sm" style="color:' + rg.color + ';background:' + rg.bg + ';border-color:' + rg.border + '">' + rg.label + '</span></span>' +
|
|
5204
5800
|
'<span class="perf-col perf-col-avg">' + fmtMs(r.durationMs) + '</span>' +
|
|
5801
|
+
'<span class="perf-col perf-col-breakdown">' + breakdownHtml + '</span>' +
|
|
5205
5802
|
'<span class="perf-col perf-col-status" style="color:' + (isError ? 'var(--red)' : 'var(--text-muted)') + '">' + r.statusCode + '</span>' +
|
|
5206
5803
|
'<span class="perf-col perf-col-qpr">' + r.queryCount + '</span>';
|
|
5207
5804
|
tableWrap.appendChild(row);
|
|
@@ -5744,7 +6341,7 @@ function getSecurityView() {
|
|
|
5744
6341
|
}
|
|
5745
6342
|
`;
|
|
5746
6343
|
}
|
|
5747
|
-
var
|
|
6344
|
+
var init_security3 = __esm({
|
|
5748
6345
|
"src/dashboard/client/views/security.ts"() {
|
|
5749
6346
|
"use strict";
|
|
5750
6347
|
}
|
|
@@ -6009,7 +6606,7 @@ var init_client = __esm({
|
|
|
6009
6606
|
init_timeline2();
|
|
6010
6607
|
init_graph2();
|
|
6011
6608
|
init_overview3();
|
|
6012
|
-
|
|
6609
|
+
init_security3();
|
|
6013
6610
|
init_app2();
|
|
6014
6611
|
}
|
|
6015
6612
|
});
|
|
@@ -6113,7 +6710,7 @@ function createDashboardHandler(deps) {
|
|
|
6113
6710
|
routes[DASHBOARD_API_TAB] = (req, res) => {
|
|
6114
6711
|
const raw = (req.url ?? "").split("tab=")[1];
|
|
6115
6712
|
if (raw) {
|
|
6116
|
-
const tab = decodeURIComponent(raw).slice(0,
|
|
6713
|
+
const tab = decodeURIComponent(raw).slice(0, MAX_TAB_NAME_LENGTH);
|
|
6117
6714
|
if (VALID_TABS.has(tab) && isTelemetryEnabled()) recordTabViewed(tab);
|
|
6118
6715
|
}
|
|
6119
6716
|
res.writeHead(204);
|
|
@@ -6166,6 +6763,9 @@ var init_router = __esm({
|
|
|
6166
6763
|
|
|
6167
6764
|
// src/output/terminal.ts
|
|
6168
6765
|
import pc from "picocolors";
|
|
6766
|
+
function print(line) {
|
|
6767
|
+
process.stdout.write(line + "\n");
|
|
6768
|
+
}
|
|
6169
6769
|
function severityIcon(severity) {
|
|
6170
6770
|
if (severity === "critical") return pc.red("\u2717");
|
|
6171
6771
|
if (severity === "warning") return pc.yellow("\u26A0");
|
|
@@ -6184,7 +6784,12 @@ function formatConsoleLine(insight, dashboardUrl, suffix) {
|
|
|
6184
6784
|
const title = colorTitle(insight.severity, insight.title);
|
|
6185
6785
|
const desc = pc.dim(truncate(insight.desc) + (suffix ?? ""));
|
|
6186
6786
|
const link = pc.dim(`\u2192 ${dashboardUrl}`);
|
|
6187
|
-
|
|
6787
|
+
let line = ` ${icon} ${title} \u2014 ${desc} ${link}`;
|
|
6788
|
+
if (insight.detail) {
|
|
6789
|
+
line += `
|
|
6790
|
+
${pc.dim("\u2514 " + insight.detail)}`;
|
|
6791
|
+
}
|
|
6792
|
+
return line;
|
|
6188
6793
|
}
|
|
6189
6794
|
function createConsoleInsightListener(proxyPort, metricsStore) {
|
|
6190
6795
|
const printedKeys = /* @__PURE__ */ new Set();
|
|
@@ -6199,7 +6804,7 @@ function createConsoleInsightListener(proxyPort, metricsStore) {
|
|
|
6199
6804
|
printedKeys.add(key);
|
|
6200
6805
|
let suffix;
|
|
6201
6806
|
if (insight.type === "slow") {
|
|
6202
|
-
const ep = metricsStore.
|
|
6807
|
+
const ep = metricsStore.getEndpoint(endpoint);
|
|
6203
6808
|
if (ep && ep.sessions.length > 1) {
|
|
6204
6809
|
const prev = ep.sessions[ep.sessions.length - 2];
|
|
6205
6810
|
suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
|
|
@@ -6208,8 +6813,8 @@ function createConsoleInsightListener(proxyPort, metricsStore) {
|
|
|
6208
6813
|
lines.push(formatConsoleLine(insight, dashUrl, suffix));
|
|
6209
6814
|
}
|
|
6210
6815
|
if (lines.length > 0) {
|
|
6211
|
-
|
|
6212
|
-
for (const line of lines)
|
|
6816
|
+
print("");
|
|
6817
|
+
for (const line of lines) print(line);
|
|
6213
6818
|
}
|
|
6214
6819
|
};
|
|
6215
6820
|
}
|
|
@@ -6342,6 +6947,7 @@ function captureInProcess(req, res, requestId) {
|
|
|
6342
6947
|
} catch {
|
|
6343
6948
|
}
|
|
6344
6949
|
const result = originalEnd.apply(this, args);
|
|
6950
|
+
const endTime = performance.now();
|
|
6345
6951
|
try {
|
|
6346
6952
|
const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
|
|
6347
6953
|
let body = resChunks.length > 0 ? Buffer.concat(resChunks) : null;
|
|
@@ -6359,6 +6965,7 @@ function captureInProcess(req, res, requestId) {
|
|
|
6359
6965
|
responseBody: body,
|
|
6360
6966
|
responseContentType: String(res.getHeader("content-type") ?? ""),
|
|
6361
6967
|
startTime,
|
|
6968
|
+
endTime,
|
|
6362
6969
|
config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
|
|
6363
6970
|
});
|
|
6364
6971
|
} catch {
|
|
@@ -6374,42 +6981,15 @@ var init_capture = __esm({
|
|
|
6374
6981
|
}
|
|
6375
6982
|
});
|
|
6376
6983
|
|
|
6377
|
-
// src/runtime/
|
|
6378
|
-
var setup_exports = {};
|
|
6379
|
-
__export(setup_exports, {
|
|
6380
|
-
setup: () => setup
|
|
6381
|
-
});
|
|
6984
|
+
// src/runtime/interceptor.ts
|
|
6382
6985
|
import http from "http";
|
|
6383
6986
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
6384
|
-
function
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
setEmitter(routeEvent2);
|
|
6388
|
-
setupFetchHook();
|
|
6389
|
-
setupConsoleHook();
|
|
6390
|
-
setupErrorHook();
|
|
6391
|
-
const registry = createDefaultRegistry();
|
|
6392
|
-
registry.patchAll(routeEvent2);
|
|
6393
|
-
const cwd = process.cwd();
|
|
6394
|
-
const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
|
|
6395
|
-
metricsStore.start();
|
|
6396
|
-
const analysisEngine = new AnalysisEngine();
|
|
6397
|
-
analysisEngine.start();
|
|
6398
|
-
const config = {
|
|
6399
|
-
proxyPort: 0,
|
|
6400
|
-
targetPort: 0,
|
|
6401
|
-
showStatic: false,
|
|
6402
|
-
maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
|
|
6403
|
-
};
|
|
6404
|
-
const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine });
|
|
6405
|
-
onRequest((req) => {
|
|
6406
|
-
const queryCount = defaultQueryStore.getByRequest(req.id).length;
|
|
6407
|
-
metricsStore.recordRequest(req, queryCount);
|
|
6408
|
-
});
|
|
6409
|
-
const originalEmit = http.Server.prototype.emit;
|
|
6987
|
+
function installInterceptor(deps) {
|
|
6988
|
+
originalEmit = http.Server.prototype.emit;
|
|
6989
|
+
const saved = originalEmit;
|
|
6410
6990
|
let bannerPrinted = false;
|
|
6411
6991
|
http.Server.prototype.emit = safeWrap(
|
|
6412
|
-
|
|
6992
|
+
saved,
|
|
6413
6993
|
function(original, event, ...args) {
|
|
6414
6994
|
if (event !== "request") return original.apply(this, [event, ...args]);
|
|
6415
6995
|
const req = args[0];
|
|
@@ -6419,9 +6999,8 @@ function setup() {
|
|
|
6419
6999
|
const port = req.socket.localPort;
|
|
6420
7000
|
if (port) {
|
|
6421
7001
|
bannerPrinted = true;
|
|
6422
|
-
config.proxyPort = port;
|
|
6423
|
-
|
|
6424
|
-
console.log(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}`);
|
|
7002
|
+
deps.config.proxyPort = port;
|
|
7003
|
+
deps.onFirstRequest(port);
|
|
6425
7004
|
}
|
|
6426
7005
|
}
|
|
6427
7006
|
if (isDashboardRequest(url)) {
|
|
@@ -6430,7 +7009,7 @@ function setup() {
|
|
|
6430
7009
|
res.end("Not Found");
|
|
6431
7010
|
return true;
|
|
6432
7011
|
}
|
|
6433
|
-
handleDashboard(req, res, config);
|
|
7012
|
+
deps.handleDashboard(req, res, deps.config);
|
|
6434
7013
|
return true;
|
|
6435
7014
|
}
|
|
6436
7015
|
const requestId = randomUUID6();
|
|
@@ -6446,8 +7025,72 @@ function setup() {
|
|
|
6446
7025
|
);
|
|
6447
7026
|
}
|
|
6448
7027
|
);
|
|
6449
|
-
|
|
7028
|
+
}
|
|
7029
|
+
function uninstallInterceptor() {
|
|
7030
|
+
if (originalEmit) {
|
|
6450
7031
|
http.Server.prototype.emit = originalEmit;
|
|
7032
|
+
originalEmit = null;
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
var originalEmit;
|
|
7036
|
+
var init_interceptor = __esm({
|
|
7037
|
+
"src/runtime/interceptor.ts"() {
|
|
7038
|
+
"use strict";
|
|
7039
|
+
init_context();
|
|
7040
|
+
init_router();
|
|
7041
|
+
init_safe_wrap();
|
|
7042
|
+
init_guard();
|
|
7043
|
+
init_capture();
|
|
7044
|
+
originalEmit = null;
|
|
7045
|
+
}
|
|
7046
|
+
});
|
|
7047
|
+
|
|
7048
|
+
// src/runtime/setup.ts
|
|
7049
|
+
var setup_exports = {};
|
|
7050
|
+
__export(setup_exports, {
|
|
7051
|
+
setup: () => setup
|
|
7052
|
+
});
|
|
7053
|
+
function setup() {
|
|
7054
|
+
if (initialized) return;
|
|
7055
|
+
initialized = true;
|
|
7056
|
+
setEmitter(routeEvent2);
|
|
7057
|
+
setupFetchHook();
|
|
7058
|
+
setupConsoleHook();
|
|
7059
|
+
setupErrorHook();
|
|
7060
|
+
const registry = createDefaultRegistry();
|
|
7061
|
+
registry.patchAll(routeEvent2);
|
|
7062
|
+
const cwd = process.cwd();
|
|
7063
|
+
const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
|
|
7064
|
+
metricsStore.start();
|
|
7065
|
+
const analysisEngine = new AnalysisEngine(metricsStore);
|
|
7066
|
+
analysisEngine.start();
|
|
7067
|
+
const config = {
|
|
7068
|
+
proxyPort: 0,
|
|
7069
|
+
targetPort: 0,
|
|
7070
|
+
showStatic: false,
|
|
7071
|
+
maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
|
|
7072
|
+
};
|
|
7073
|
+
const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine });
|
|
7074
|
+
onRequest((req) => {
|
|
7075
|
+
const queries = defaultQueryStore.getByRequest(req.id);
|
|
7076
|
+
const fetches = defaultFetchStore.getByRequest(req.id);
|
|
7077
|
+
metricsStore.recordRequest(req, {
|
|
7078
|
+
queryCount: queries.length,
|
|
7079
|
+
queryTimeMs: queries.reduce((s, q) => s + q.durationMs, 0),
|
|
7080
|
+
fetchTimeMs: fetches.reduce((s, f) => s + f.durationMs, 0)
|
|
7081
|
+
});
|
|
7082
|
+
});
|
|
7083
|
+
installInterceptor({
|
|
7084
|
+
handleDashboard,
|
|
7085
|
+
config,
|
|
7086
|
+
onFirstRequest(port) {
|
|
7087
|
+
analysisEngine.onUpdate(createConsoleInsightListener(port, metricsStore));
|
|
7088
|
+
process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
|
|
7089
|
+
`);
|
|
7090
|
+
}
|
|
7091
|
+
});
|
|
7092
|
+
health.setTeardown(() => {
|
|
7093
|
+
uninstallInterceptor();
|
|
6451
7094
|
analysisEngine.stop();
|
|
6452
7095
|
metricsStore.stop();
|
|
6453
7096
|
});
|
|
@@ -6472,7 +7115,6 @@ var initialized;
|
|
|
6472
7115
|
var init_setup = __esm({
|
|
6473
7116
|
"src/runtime/setup.ts"() {
|
|
6474
7117
|
"use strict";
|
|
6475
|
-
init_context();
|
|
6476
7118
|
init_transport2();
|
|
6477
7119
|
init_fetch();
|
|
6478
7120
|
init_console();
|
|
@@ -6485,10 +7127,8 @@ var init_setup = __esm({
|
|
|
6485
7127
|
init_terminal();
|
|
6486
7128
|
init_src();
|
|
6487
7129
|
init_constants();
|
|
6488
|
-
init_safe_wrap();
|
|
6489
7130
|
init_health2();
|
|
6490
|
-
|
|
6491
|
-
init_capture();
|
|
7131
|
+
init_interceptor();
|
|
6492
7132
|
initialized = false;
|
|
6493
7133
|
}
|
|
6494
7134
|
});
|