brakit 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +116 -3
- package/dist/api.js +593 -309
- package/dist/bin/brakit.js +11 -9
- package/dist/runtime/index.js +1142 -471
- 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.",
|
|
@@ -3176,7 +3297,28 @@ var init_rules = __esm({
|
|
|
3176
3297
|
}
|
|
3177
3298
|
});
|
|
3178
3299
|
|
|
3179
|
-
// src/
|
|
3300
|
+
// src/utils/collections.ts
|
|
3301
|
+
function groupBy(items, keyFn) {
|
|
3302
|
+
const map = /* @__PURE__ */ new Map();
|
|
3303
|
+
for (const item of items) {
|
|
3304
|
+
const key = keyFn(item);
|
|
3305
|
+
if (key == null) continue;
|
|
3306
|
+
let arr = map.get(key);
|
|
3307
|
+
if (!arr) {
|
|
3308
|
+
arr = [];
|
|
3309
|
+
map.set(key, arr);
|
|
3310
|
+
}
|
|
3311
|
+
arr.push(item);
|
|
3312
|
+
}
|
|
3313
|
+
return map;
|
|
3314
|
+
}
|
|
3315
|
+
var init_collections = __esm({
|
|
3316
|
+
"src/utils/collections.ts"() {
|
|
3317
|
+
"use strict";
|
|
3318
|
+
}
|
|
3319
|
+
});
|
|
3320
|
+
|
|
3321
|
+
// src/analysis/insights/query-helpers.ts
|
|
3180
3322
|
function getQueryShape(q) {
|
|
3181
3323
|
if (q.sql) return normalizeQueryParams(q.sql) ?? "";
|
|
3182
3324
|
return `${q.operation ?? q.normalizedOp ?? "?"}:${q.model ?? q.table ?? ""}`;
|
|
@@ -3188,384 +3330,794 @@ function getQueryInfo(q) {
|
|
|
3188
3330
|
table: q.table ?? q.model ?? ""
|
|
3189
3331
|
};
|
|
3190
3332
|
}
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3333
|
+
var init_query_helpers = __esm({
|
|
3334
|
+
"src/analysis/insights/query-helpers.ts"() {
|
|
3335
|
+
"use strict";
|
|
3336
|
+
init_normalize();
|
|
3337
|
+
}
|
|
3338
|
+
});
|
|
3339
|
+
|
|
3340
|
+
// src/analysis/insights/prepare.ts
|
|
3341
|
+
function createEndpointGroup() {
|
|
3342
|
+
return {
|
|
3343
|
+
total: 0,
|
|
3344
|
+
errors: 0,
|
|
3345
|
+
totalDuration: 0,
|
|
3346
|
+
queryCount: 0,
|
|
3347
|
+
totalSize: 0,
|
|
3348
|
+
totalQueryTimeMs: 0,
|
|
3349
|
+
totalFetchTimeMs: 0,
|
|
3350
|
+
queryShapeDurations: /* @__PURE__ */ new Map()
|
|
3351
|
+
};
|
|
3199
3352
|
}
|
|
3200
|
-
function
|
|
3201
|
-
const insights = [];
|
|
3353
|
+
function prepareContext(ctx) {
|
|
3202
3354
|
const nonStatic = ctx.requests.filter(
|
|
3203
3355
|
(r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
|
|
3204
3356
|
);
|
|
3205
|
-
const queriesByReq =
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3357
|
+
const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
|
|
3358
|
+
const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
|
|
3359
|
+
const reqById = new Map(nonStatic.map((r) => [r.id, r]));
|
|
3360
|
+
const endpointGroups = /* @__PURE__ */ new Map();
|
|
3361
|
+
for (const r of nonStatic) {
|
|
3362
|
+
const ep = getEndpointKey(r.method, r.path);
|
|
3363
|
+
let g = endpointGroups.get(ep);
|
|
3364
|
+
if (!g) {
|
|
3365
|
+
g = createEndpointGroup();
|
|
3366
|
+
endpointGroups.set(ep, g);
|
|
3212
3367
|
}
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
const req = reqById.get(reqId);
|
|
3220
|
-
if (!req) continue;
|
|
3221
|
-
const endpoint = `${req.method} ${req.path}`;
|
|
3222
|
-
const shapeGroups = /* @__PURE__ */ new Map();
|
|
3368
|
+
g.total++;
|
|
3369
|
+
if (r.statusCode >= 400) g.errors++;
|
|
3370
|
+
g.totalDuration += r.durationMs;
|
|
3371
|
+
g.totalSize += r.responseSize ?? 0;
|
|
3372
|
+
const reqQueries = queriesByReq.get(r.id) ?? [];
|
|
3373
|
+
g.queryCount += reqQueries.length;
|
|
3223
3374
|
for (const q of reqQueries) {
|
|
3375
|
+
g.totalQueryTimeMs += q.durationMs;
|
|
3224
3376
|
const shape = getQueryShape(q);
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3377
|
+
const info = getQueryInfo(q);
|
|
3378
|
+
let sd = g.queryShapeDurations.get(shape);
|
|
3379
|
+
if (!sd) {
|
|
3380
|
+
sd = { totalMs: 0, count: 0, label: info.op + (info.table ? ` ${info.table}` : "") };
|
|
3381
|
+
g.queryShapeDurations.set(shape, sd);
|
|
3229
3382
|
}
|
|
3230
|
-
|
|
3231
|
-
|
|
3383
|
+
sd.totalMs += q.durationMs;
|
|
3384
|
+
sd.count++;
|
|
3232
3385
|
}
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
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
|
-
});
|
|
3386
|
+
const reqFetches = fetchesByReq.get(r.id) ?? [];
|
|
3387
|
+
for (const f of reqFetches) {
|
|
3388
|
+
g.totalFetchTimeMs += f.durationMs;
|
|
3247
3389
|
}
|
|
3248
3390
|
}
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3391
|
+
return {
|
|
3392
|
+
...ctx,
|
|
3393
|
+
nonStatic,
|
|
3394
|
+
queriesByReq,
|
|
3395
|
+
fetchesByReq,
|
|
3396
|
+
reqById,
|
|
3397
|
+
endpointGroups
|
|
3398
|
+
};
|
|
3399
|
+
}
|
|
3400
|
+
var init_prepare = __esm({
|
|
3401
|
+
"src/analysis/insights/prepare.ts"() {
|
|
3402
|
+
"use strict";
|
|
3403
|
+
init_collections();
|
|
3404
|
+
init_endpoint();
|
|
3405
|
+
init_constants();
|
|
3406
|
+
init_query_helpers();
|
|
3407
|
+
}
|
|
3408
|
+
});
|
|
3409
|
+
|
|
3410
|
+
// src/analysis/insights/runner.ts
|
|
3411
|
+
var SEVERITY_ORDER, InsightRunner;
|
|
3412
|
+
var init_runner = __esm({
|
|
3413
|
+
"src/analysis/insights/runner.ts"() {
|
|
3414
|
+
"use strict";
|
|
3415
|
+
init_prepare();
|
|
3416
|
+
SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
3417
|
+
InsightRunner = class {
|
|
3418
|
+
rules = [];
|
|
3419
|
+
register(rule) {
|
|
3420
|
+
this.rules.push(rule);
|
|
3263
3421
|
}
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3422
|
+
run(ctx) {
|
|
3423
|
+
const prepared = prepareContext(ctx);
|
|
3424
|
+
const insights = [];
|
|
3425
|
+
for (const rule of this.rules) {
|
|
3426
|
+
try {
|
|
3427
|
+
insights.push(...rule.check(prepared));
|
|
3428
|
+
} catch {
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
insights.sort(
|
|
3432
|
+
(a, b) => (SEVERITY_ORDER[a.severity] ?? 2) - (SEVERITY_ORDER[b.severity] ?? 2)
|
|
3433
|
+
);
|
|
3434
|
+
return insights;
|
|
3268
3435
|
}
|
|
3269
|
-
}
|
|
3436
|
+
};
|
|
3270
3437
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3438
|
+
});
|
|
3439
|
+
|
|
3440
|
+
// src/analysis/insights/rules/n1.ts
|
|
3441
|
+
var n1Rule;
|
|
3442
|
+
var init_n1 = __esm({
|
|
3443
|
+
"src/analysis/insights/rules/n1.ts"() {
|
|
3444
|
+
"use strict";
|
|
3445
|
+
init_query_helpers();
|
|
3446
|
+
init_endpoint();
|
|
3447
|
+
init_thresholds();
|
|
3448
|
+
n1Rule = {
|
|
3449
|
+
id: "n1",
|
|
3450
|
+
check(ctx) {
|
|
3451
|
+
const insights = [];
|
|
3452
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3453
|
+
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3454
|
+
const req = ctx.reqById.get(reqId);
|
|
3455
|
+
if (!req) continue;
|
|
3456
|
+
const endpoint = getEndpointKey(req.method, req.path);
|
|
3457
|
+
const shapeGroups = /* @__PURE__ */ new Map();
|
|
3458
|
+
for (const q of reqQueries) {
|
|
3459
|
+
const shape = getQueryShape(q);
|
|
3460
|
+
let group = shapeGroups.get(shape);
|
|
3461
|
+
if (!group) {
|
|
3462
|
+
group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: q };
|
|
3463
|
+
shapeGroups.set(shape, group);
|
|
3464
|
+
}
|
|
3465
|
+
group.count++;
|
|
3466
|
+
group.distinctSql.add(q.sql ?? shape);
|
|
3467
|
+
}
|
|
3468
|
+
for (const [, sg] of shapeGroups) {
|
|
3469
|
+
if (sg.count <= N1_QUERY_THRESHOLD || sg.distinctSql.size <= 1) continue;
|
|
3470
|
+
const info = getQueryInfo(sg.first);
|
|
3471
|
+
const key = `${endpoint}:${info.op}:${info.table || "unknown"}`;
|
|
3472
|
+
if (seen.has(key)) continue;
|
|
3473
|
+
seen.add(key);
|
|
3474
|
+
insights.push({
|
|
3475
|
+
severity: "critical",
|
|
3476
|
+
type: "n1",
|
|
3477
|
+
title: "N+1 Query Pattern",
|
|
3478
|
+
desc: `${endpoint} runs ${sg.count}x ${info.op} ${info.table} with different params in a single request`,
|
|
3479
|
+
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.",
|
|
3480
|
+
nav: "queries"
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
return insights;
|
|
3485
|
+
}
|
|
3486
|
+
};
|
|
3288
3487
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3488
|
+
});
|
|
3489
|
+
|
|
3490
|
+
// src/analysis/insights/rules/cross-endpoint.ts
|
|
3491
|
+
var crossEndpointRule;
|
|
3492
|
+
var init_cross_endpoint = __esm({
|
|
3493
|
+
"src/analysis/insights/rules/cross-endpoint.ts"() {
|
|
3494
|
+
"use strict";
|
|
3495
|
+
init_query_helpers();
|
|
3496
|
+
init_endpoint();
|
|
3497
|
+
init_thresholds();
|
|
3498
|
+
crossEndpointRule = {
|
|
3499
|
+
id: "cross-endpoint",
|
|
3500
|
+
check(ctx) {
|
|
3501
|
+
const insights = [];
|
|
3502
|
+
const queryMap = /* @__PURE__ */ new Map();
|
|
3503
|
+
const allEndpoints = /* @__PURE__ */ new Set();
|
|
3504
|
+
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3505
|
+
const req = ctx.reqById.get(reqId);
|
|
3506
|
+
if (!req) continue;
|
|
3507
|
+
const endpoint = getEndpointKey(req.method, req.path);
|
|
3508
|
+
allEndpoints.add(endpoint);
|
|
3509
|
+
const seenInReq = /* @__PURE__ */ new Set();
|
|
3510
|
+
for (const q of reqQueries) {
|
|
3511
|
+
const shape = getQueryShape(q);
|
|
3512
|
+
let entry = queryMap.get(shape);
|
|
3513
|
+
if (!entry) {
|
|
3514
|
+
entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: q };
|
|
3515
|
+
queryMap.set(shape, entry);
|
|
3516
|
+
}
|
|
3517
|
+
entry.count++;
|
|
3518
|
+
if (!seenInReq.has(shape)) {
|
|
3519
|
+
seenInReq.add(shape);
|
|
3520
|
+
entry.endpoints.add(endpoint);
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
|
|
3525
|
+
for (const [, cem] of queryMap) {
|
|
3526
|
+
if (cem.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
|
|
3527
|
+
if (cem.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
|
|
3528
|
+
const p = Math.round(cem.endpoints.size / allEndpoints.size * 100);
|
|
3529
|
+
if (p < CROSS_ENDPOINT_PCT) continue;
|
|
3530
|
+
const info = getQueryInfo(cem.first);
|
|
3531
|
+
const label = info.op + (info.table ? ` ${info.table}` : "");
|
|
3532
|
+
insights.push({
|
|
3533
|
+
severity: "warning",
|
|
3534
|
+
type: "cross-endpoint",
|
|
3535
|
+
title: "Repeated Query Across Endpoints",
|
|
3536
|
+
desc: `${label} runs on ${cem.endpoints.size} of ${allEndpoints.size} endpoints (${p}%).`,
|
|
3537
|
+
hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
|
|
3538
|
+
nav: "queries"
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
return insights;
|
|
3301
3543
|
}
|
|
3302
|
-
|
|
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
|
-
}
|
|
3544
|
+
};
|
|
3320
3545
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3546
|
+
});
|
|
3547
|
+
|
|
3548
|
+
// src/analysis/insights/rules/redundant-query.ts
|
|
3549
|
+
var redundantQueryRule;
|
|
3550
|
+
var init_redundant_query = __esm({
|
|
3551
|
+
"src/analysis/insights/rules/redundant-query.ts"() {
|
|
3552
|
+
"use strict";
|
|
3553
|
+
init_query_helpers();
|
|
3554
|
+
init_endpoint();
|
|
3555
|
+
init_thresholds();
|
|
3556
|
+
redundantQueryRule = {
|
|
3557
|
+
id: "redundant-query",
|
|
3558
|
+
check(ctx) {
|
|
3559
|
+
const insights = [];
|
|
3560
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3561
|
+
for (const [reqId, reqQueries] of ctx.queriesByReq) {
|
|
3562
|
+
const req = ctx.reqById.get(reqId);
|
|
3563
|
+
if (!req) continue;
|
|
3564
|
+
const endpoint = getEndpointKey(req.method, req.path);
|
|
3565
|
+
const exact = /* @__PURE__ */ new Map();
|
|
3566
|
+
for (const q of reqQueries) {
|
|
3567
|
+
if (!q.sql) continue;
|
|
3568
|
+
let entry = exact.get(q.sql);
|
|
3569
|
+
if (!entry) {
|
|
3570
|
+
entry = { count: 0, first: q };
|
|
3571
|
+
exact.set(q.sql, entry);
|
|
3572
|
+
}
|
|
3573
|
+
entry.count++;
|
|
3574
|
+
}
|
|
3575
|
+
for (const [, e] of exact) {
|
|
3576
|
+
if (e.count < REDUNDANT_QUERY_MIN_COUNT) continue;
|
|
3577
|
+
const info = getQueryInfo(e.first);
|
|
3578
|
+
const label = info.op + (info.table ? ` ${info.table}` : "");
|
|
3579
|
+
const dedupKey = `${endpoint}:${label}`;
|
|
3580
|
+
if (seen.has(dedupKey)) continue;
|
|
3581
|
+
seen.add(dedupKey);
|
|
3582
|
+
insights.push({
|
|
3583
|
+
severity: "warning",
|
|
3584
|
+
type: "redundant-query",
|
|
3585
|
+
title: "Redundant Query",
|
|
3586
|
+
desc: `${label} runs ${e.count}x with identical params in ${endpoint}.`,
|
|
3587
|
+
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.",
|
|
3588
|
+
nav: "queries"
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
return insights;
|
|
3593
|
+
}
|
|
3594
|
+
};
|
|
3337
3595
|
}
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3596
|
+
});
|
|
3597
|
+
|
|
3598
|
+
// src/analysis/insights/rules/error.ts
|
|
3599
|
+
var errorRule;
|
|
3600
|
+
var init_error = __esm({
|
|
3601
|
+
"src/analysis/insights/rules/error.ts"() {
|
|
3602
|
+
"use strict";
|
|
3603
|
+
errorRule = {
|
|
3604
|
+
id: "error",
|
|
3605
|
+
check(ctx) {
|
|
3606
|
+
if (ctx.errors.length === 0) return [];
|
|
3607
|
+
const insights = [];
|
|
3608
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3609
|
+
for (const e of ctx.errors) {
|
|
3610
|
+
const name = e.name || "Error";
|
|
3611
|
+
groups.set(name, (groups.get(name) ?? 0) + 1);
|
|
3612
|
+
}
|
|
3613
|
+
for (const [name, cnt] of groups) {
|
|
3614
|
+
insights.push({
|
|
3615
|
+
severity: "critical",
|
|
3616
|
+
type: "error",
|
|
3617
|
+
title: "Unhandled Error",
|
|
3618
|
+
desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
|
|
3619
|
+
hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
|
|
3620
|
+
nav: "errors"
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
return insights;
|
|
3624
|
+
}
|
|
3625
|
+
};
|
|
3351
3626
|
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
// src/analysis/insights/rules/error-hotspot.ts
|
|
3630
|
+
var errorHotspotRule;
|
|
3631
|
+
var init_error_hotspot = __esm({
|
|
3632
|
+
"src/analysis/insights/rules/error-hotspot.ts"() {
|
|
3633
|
+
"use strict";
|
|
3634
|
+
init_thresholds();
|
|
3635
|
+
errorHotspotRule = {
|
|
3636
|
+
id: "error-hotspot",
|
|
3637
|
+
check(ctx) {
|
|
3638
|
+
const insights = [];
|
|
3639
|
+
for (const [ep, g] of ctx.endpointGroups) {
|
|
3640
|
+
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3641
|
+
const errorRate = Math.round(g.errors / g.total * 100);
|
|
3642
|
+
if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
|
|
3643
|
+
insights.push({
|
|
3644
|
+
severity: "critical",
|
|
3645
|
+
type: "error-hotspot",
|
|
3646
|
+
title: "Error Hotspot",
|
|
3647
|
+
desc: `${ep} \u2014 ${errorRate}% error rate (${g.errors}/${g.total} requests)`,
|
|
3648
|
+
hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces.",
|
|
3649
|
+
nav: "requests"
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
return insights;
|
|
3654
|
+
}
|
|
3655
|
+
};
|
|
3365
3656
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3657
|
+
});
|
|
3658
|
+
|
|
3659
|
+
// src/analysis/insights/rules/duplicate.ts
|
|
3660
|
+
var duplicateRule;
|
|
3661
|
+
var init_duplicate = __esm({
|
|
3662
|
+
"src/analysis/insights/rules/duplicate.ts"() {
|
|
3663
|
+
"use strict";
|
|
3664
|
+
init_thresholds();
|
|
3665
|
+
duplicateRule = {
|
|
3666
|
+
id: "duplicate",
|
|
3667
|
+
check(ctx) {
|
|
3668
|
+
const dupCounts = /* @__PURE__ */ new Map();
|
|
3669
|
+
const flowCount = /* @__PURE__ */ new Map();
|
|
3670
|
+
for (const flow of ctx.flows) {
|
|
3671
|
+
if (!flow.requests) continue;
|
|
3672
|
+
const seenInFlow = /* @__PURE__ */ new Set();
|
|
3673
|
+
for (const fr of flow.requests) {
|
|
3674
|
+
if (!fr.isDuplicate) continue;
|
|
3675
|
+
const dupKey = `${fr.method} ${fr.label ?? fr.path ?? fr.url}`;
|
|
3676
|
+
dupCounts.set(dupKey, (dupCounts.get(dupKey) ?? 0) + 1);
|
|
3677
|
+
if (!seenInFlow.has(dupKey)) {
|
|
3678
|
+
seenInFlow.add(dupKey);
|
|
3679
|
+
flowCount.set(dupKey, (flowCount.get(dupKey) ?? 0) + 1);
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
3684
|
+
const insights = [];
|
|
3685
|
+
for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
|
|
3686
|
+
const d = dupEntries[i];
|
|
3687
|
+
insights.push({
|
|
3688
|
+
severity: "warning",
|
|
3689
|
+
type: "duplicate",
|
|
3690
|
+
title: "Duplicate API Call",
|
|
3691
|
+
desc: `${d.key} loaded ${d.count}x as duplicate across ${d.flows} action${d.flows !== 1 ? "s" : ""}`,
|
|
3692
|
+
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.",
|
|
3693
|
+
nav: "actions"
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
return insights;
|
|
3378
3697
|
}
|
|
3379
|
-
}
|
|
3698
|
+
};
|
|
3380
3699
|
}
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3700
|
+
});
|
|
3701
|
+
|
|
3702
|
+
// src/utils/format.ts
|
|
3703
|
+
function formatDuration(ms) {
|
|
3704
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
3705
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
3706
|
+
}
|
|
3707
|
+
function formatSize(bytes) {
|
|
3708
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
3709
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
3710
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
3711
|
+
}
|
|
3712
|
+
function pct(part, total) {
|
|
3713
|
+
return total > 0 ? Math.round(part / total * 100) : 0;
|
|
3714
|
+
}
|
|
3715
|
+
var init_format = __esm({
|
|
3716
|
+
"src/utils/format.ts"() {
|
|
3717
|
+
"use strict";
|
|
3392
3718
|
}
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3719
|
+
});
|
|
3720
|
+
|
|
3721
|
+
// src/analysis/insights/rules/slow.ts
|
|
3722
|
+
var slowRule;
|
|
3723
|
+
var init_slow = __esm({
|
|
3724
|
+
"src/analysis/insights/rules/slow.ts"() {
|
|
3725
|
+
"use strict";
|
|
3726
|
+
init_format();
|
|
3727
|
+
init_thresholds();
|
|
3728
|
+
slowRule = {
|
|
3729
|
+
id: "slow",
|
|
3730
|
+
check(ctx) {
|
|
3731
|
+
const insights = [];
|
|
3732
|
+
for (const [ep, g] of ctx.endpointGroups) {
|
|
3733
|
+
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3734
|
+
const avgMs = Math.round(g.totalDuration / g.total);
|
|
3735
|
+
if (avgMs < SLOW_ENDPOINT_THRESHOLD_MS) continue;
|
|
3736
|
+
const avgQueryMs = Math.round(g.totalQueryTimeMs / g.total);
|
|
3737
|
+
const avgFetchMs = Math.round(g.totalFetchTimeMs / g.total);
|
|
3738
|
+
const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
|
|
3739
|
+
const parts = [];
|
|
3740
|
+
if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
|
|
3741
|
+
if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
|
|
3742
|
+
if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
|
|
3743
|
+
const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
|
|
3744
|
+
let detail;
|
|
3745
|
+
let slowestMs = 0;
|
|
3746
|
+
for (const [, sd] of g.queryShapeDurations) {
|
|
3747
|
+
const avgShapeMs = sd.totalMs / sd.count;
|
|
3748
|
+
if (avgShapeMs > slowestMs) {
|
|
3749
|
+
slowestMs = avgShapeMs;
|
|
3750
|
+
detail = `Slowest query: ${sd.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${sd.count}x)`;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
insights.push({
|
|
3754
|
+
severity: "warning",
|
|
3755
|
+
type: "slow",
|
|
3756
|
+
title: "Slow Endpoint",
|
|
3757
|
+
desc: `${ep} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
|
|
3758
|
+
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.",
|
|
3759
|
+
detail,
|
|
3760
|
+
nav: "requests"
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
return insights;
|
|
3764
|
+
}
|
|
3765
|
+
};
|
|
3406
3766
|
}
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3767
|
+
});
|
|
3768
|
+
|
|
3769
|
+
// src/analysis/insights/rules/query-heavy.ts
|
|
3770
|
+
var queryHeavyRule;
|
|
3771
|
+
var init_query_heavy = __esm({
|
|
3772
|
+
"src/analysis/insights/rules/query-heavy.ts"() {
|
|
3773
|
+
"use strict";
|
|
3774
|
+
init_thresholds();
|
|
3775
|
+
queryHeavyRule = {
|
|
3776
|
+
id: "query-heavy",
|
|
3777
|
+
check(ctx) {
|
|
3778
|
+
const insights = [];
|
|
3779
|
+
for (const [ep, g] of ctx.endpointGroups) {
|
|
3780
|
+
if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
|
|
3781
|
+
const avgQueries = Math.round(g.queryCount / g.total);
|
|
3782
|
+
if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
|
|
3783
|
+
insights.push({
|
|
3784
|
+
severity: "warning",
|
|
3785
|
+
type: "query-heavy",
|
|
3786
|
+
title: "Query-Heavy Endpoint",
|
|
3787
|
+
desc: `${ep} \u2014 avg ${avgQueries} queries/request`,
|
|
3788
|
+
hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches.",
|
|
3789
|
+
nav: "queries"
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
return insights;
|
|
3794
|
+
}
|
|
3795
|
+
};
|
|
3420
3796
|
}
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3797
|
+
});
|
|
3798
|
+
|
|
3799
|
+
// src/analysis/insights/rules/select-star.ts
|
|
3800
|
+
var selectStarRule;
|
|
3801
|
+
var init_select_star = __esm({
|
|
3802
|
+
"src/analysis/insights/rules/select-star.ts"() {
|
|
3803
|
+
"use strict";
|
|
3804
|
+
init_query_helpers();
|
|
3805
|
+
init_thresholds();
|
|
3806
|
+
init_patterns();
|
|
3807
|
+
selectStarRule = {
|
|
3808
|
+
id: "select-star",
|
|
3809
|
+
check(ctx) {
|
|
3810
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3811
|
+
for (const [, reqQueries] of ctx.queriesByReq) {
|
|
3812
|
+
for (const q of reqQueries) {
|
|
3813
|
+
if (!q.sql) continue;
|
|
3814
|
+
const isSelectStar = SELECT_STAR_RE.test(q.sql.trim()) || SELECT_DOT_STAR_RE.test(q.sql);
|
|
3815
|
+
if (!isSelectStar) continue;
|
|
3816
|
+
const info = getQueryInfo(q);
|
|
3817
|
+
const key = info.table || "unknown";
|
|
3818
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
const insights = [];
|
|
3822
|
+
for (const [table, count] of seen) {
|
|
3823
|
+
if (count < OVERFETCH_MIN_REQUESTS) continue;
|
|
3824
|
+
insights.push({
|
|
3825
|
+
severity: "warning",
|
|
3826
|
+
type: "select-star",
|
|
3827
|
+
title: "SELECT * Query",
|
|
3828
|
+
desc: `SELECT * on ${table} \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
|
|
3829
|
+
hint: "SELECT * fetches all columns including ones you don\u2019t need. Specify only required columns to reduce data transfer and memory usage.",
|
|
3830
|
+
nav: "queries"
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
return insights;
|
|
3834
|
+
}
|
|
3835
|
+
};
|
|
3431
3836
|
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3837
|
+
});
|
|
3838
|
+
|
|
3839
|
+
// src/analysis/insights/rules/high-rows.ts
|
|
3840
|
+
var highRowsRule;
|
|
3841
|
+
var init_high_rows = __esm({
|
|
3842
|
+
"src/analysis/insights/rules/high-rows.ts"() {
|
|
3843
|
+
"use strict";
|
|
3844
|
+
init_query_helpers();
|
|
3845
|
+
init_thresholds();
|
|
3846
|
+
highRowsRule = {
|
|
3847
|
+
id: "high-rows",
|
|
3848
|
+
check(ctx) {
|
|
3849
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3850
|
+
for (const [, reqQueries] of ctx.queriesByReq) {
|
|
3851
|
+
for (const q of reqQueries) {
|
|
3852
|
+
if (!q.rowCount || q.rowCount <= HIGH_ROW_COUNT) continue;
|
|
3853
|
+
const info = getQueryInfo(q);
|
|
3854
|
+
const key = `${info.op} ${info.table || "unknown"}`;
|
|
3855
|
+
let entry = seen.get(key);
|
|
3856
|
+
if (!entry) {
|
|
3857
|
+
entry = { max: 0, count: 0 };
|
|
3858
|
+
seen.set(key, entry);
|
|
3859
|
+
}
|
|
3860
|
+
entry.count++;
|
|
3861
|
+
if (q.rowCount > entry.max) entry.max = q.rowCount;
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
const insights = [];
|
|
3865
|
+
for (const [key, hrs] of seen) {
|
|
3866
|
+
if (hrs.count < OVERFETCH_MIN_REQUESTS) continue;
|
|
3867
|
+
insights.push({
|
|
3868
|
+
severity: "warning",
|
|
3869
|
+
type: "high-rows",
|
|
3870
|
+
title: "Large Result Set",
|
|
3871
|
+
desc: `${key} returns ${hrs.max}+ rows (${hrs.count}x)`,
|
|
3872
|
+
hint: "Fetching many rows slows responses and wastes memory. Add a LIMIT clause, implement pagination, or filter with a WHERE condition.",
|
|
3873
|
+
nav: "queries"
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3876
|
+
return insights;
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3442
3879
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3880
|
+
});
|
|
3881
|
+
|
|
3882
|
+
// src/utils/response.ts
|
|
3883
|
+
function unwrapResponse2(parsed) {
|
|
3884
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
3885
|
+
const obj = parsed;
|
|
3886
|
+
const keys = Object.keys(obj);
|
|
3887
|
+
if (keys.length > 3) return parsed;
|
|
3888
|
+
let best = null;
|
|
3889
|
+
let bestSize = 0;
|
|
3890
|
+
for (const key of keys) {
|
|
3891
|
+
const val = obj[key];
|
|
3892
|
+
if (Array.isArray(val) && val.length > bestSize) {
|
|
3893
|
+
best = val;
|
|
3894
|
+
bestSize = val.length;
|
|
3895
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
3896
|
+
const size = Object.keys(val).length;
|
|
3897
|
+
if (size > bestSize) {
|
|
3898
|
+
best = val;
|
|
3899
|
+
bestSize = size;
|
|
3900
|
+
}
|
|
3456
3901
|
}
|
|
3457
3902
|
}
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
desc: `${key} returns ${hrs.max}+ rows (${hrs.count}x)`,
|
|
3465
|
-
hint: "Fetching many rows slows responses and wastes memory. Add a LIMIT clause, implement pagination, or filter with a WHERE condition.",
|
|
3466
|
-
nav: "queries"
|
|
3467
|
-
});
|
|
3903
|
+
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
3904
|
+
}
|
|
3905
|
+
var init_response = __esm({
|
|
3906
|
+
"src/utils/response.ts"() {
|
|
3907
|
+
"use strict";
|
|
3908
|
+
init_thresholds();
|
|
3468
3909
|
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
const
|
|
3488
|
-
if (
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3910
|
+
});
|
|
3911
|
+
|
|
3912
|
+
// src/analysis/insights/rules/response-overfetch.ts
|
|
3913
|
+
var responseOverfetchRule;
|
|
3914
|
+
var init_response_overfetch = __esm({
|
|
3915
|
+
"src/analysis/insights/rules/response-overfetch.ts"() {
|
|
3916
|
+
"use strict";
|
|
3917
|
+
init_endpoint();
|
|
3918
|
+
init_response();
|
|
3919
|
+
init_patterns();
|
|
3920
|
+
init_thresholds();
|
|
3921
|
+
responseOverfetchRule = {
|
|
3922
|
+
id: "response-overfetch",
|
|
3923
|
+
check(ctx) {
|
|
3924
|
+
const insights = [];
|
|
3925
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3926
|
+
for (const r of ctx.nonStatic) {
|
|
3927
|
+
if (r.statusCode >= 400 || !r.responseBody) continue;
|
|
3928
|
+
const ep = getEndpointKey(r.method, r.path);
|
|
3929
|
+
if (seen.has(ep)) continue;
|
|
3930
|
+
let parsed;
|
|
3931
|
+
try {
|
|
3932
|
+
parsed = JSON.parse(r.responseBody);
|
|
3933
|
+
} catch {
|
|
3934
|
+
continue;
|
|
3935
|
+
}
|
|
3936
|
+
const target = unwrapResponse2(parsed);
|
|
3937
|
+
const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
3938
|
+
if (!inspectObj || typeof inspectObj !== "object" || Array.isArray(inspectObj)) continue;
|
|
3939
|
+
const fields = Object.keys(inspectObj);
|
|
3940
|
+
if (fields.length < OVERFETCH_MIN_FIELDS) continue;
|
|
3941
|
+
let internalIdCount = 0;
|
|
3942
|
+
let nullCount = 0;
|
|
3943
|
+
for (const key of fields) {
|
|
3944
|
+
if (INTERNAL_ID_SUFFIX.test(key) || key === "id" || key === "_id") internalIdCount++;
|
|
3945
|
+
const val = inspectObj[key];
|
|
3946
|
+
if (val === null || val === void 0) nullCount++;
|
|
3947
|
+
}
|
|
3948
|
+
const nullRatio = nullCount / fields.length;
|
|
3949
|
+
const reasons = [];
|
|
3950
|
+
if (internalIdCount >= OVERFETCH_MIN_INTERNAL_IDS) reasons.push(`${internalIdCount} internal ID fields`);
|
|
3951
|
+
if (nullRatio >= OVERFETCH_NULL_RATIO) reasons.push(`${Math.round(nullRatio * 100)}% null fields`);
|
|
3952
|
+
if (reasons.length === 0 && fields.length >= OVERFETCH_MANY_FIELDS) {
|
|
3953
|
+
reasons.push(`${fields.length} fields returned`);
|
|
3954
|
+
}
|
|
3955
|
+
if (reasons.length > 0) {
|
|
3956
|
+
seen.add(ep);
|
|
3957
|
+
insights.push({
|
|
3958
|
+
severity: "info",
|
|
3959
|
+
type: "response-overfetch",
|
|
3960
|
+
title: "Response Overfetch",
|
|
3961
|
+
desc: `${ep} \u2014 ${reasons.join(", ")}`,
|
|
3962
|
+
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.",
|
|
3963
|
+
nav: "requests"
|
|
3964
|
+
});
|
|
3497
3965
|
}
|
|
3498
3966
|
}
|
|
3499
|
-
|
|
3967
|
+
return insights;
|
|
3500
3968
|
}
|
|
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
|
-
}
|
|
3969
|
+
};
|
|
3531
3970
|
}
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3971
|
+
});
|
|
3972
|
+
|
|
3973
|
+
// src/analysis/insights/rules/large-response.ts
|
|
3974
|
+
var largeResponseRule;
|
|
3975
|
+
var init_large_response = __esm({
|
|
3976
|
+
"src/analysis/insights/rules/large-response.ts"() {
|
|
3977
|
+
"use strict";
|
|
3978
|
+
init_format();
|
|
3979
|
+
init_thresholds();
|
|
3980
|
+
largeResponseRule = {
|
|
3981
|
+
id: "large-response",
|
|
3982
|
+
check(ctx) {
|
|
3983
|
+
const insights = [];
|
|
3984
|
+
for (const [ep, g] of ctx.endpointGroups) {
|
|
3985
|
+
if (g.total < OVERFETCH_MIN_REQUESTS) continue;
|
|
3986
|
+
const avgSize = Math.round(g.totalSize / g.total);
|
|
3987
|
+
if (avgSize > LARGE_RESPONSE_BYTES) {
|
|
3988
|
+
insights.push({
|
|
3989
|
+
severity: "info",
|
|
3990
|
+
type: "large-response",
|
|
3991
|
+
title: "Large Response",
|
|
3992
|
+
desc: `${ep} \u2014 avg ${formatSize(avgSize)} response`,
|
|
3993
|
+
hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression.",
|
|
3994
|
+
nav: "requests"
|
|
3995
|
+
});
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
return insights;
|
|
3999
|
+
}
|
|
4000
|
+
};
|
|
3545
4001
|
}
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
4002
|
+
});
|
|
4003
|
+
|
|
4004
|
+
// src/analysis/insights/rules/regression.ts
|
|
4005
|
+
var regressionRule;
|
|
4006
|
+
var init_regression = __esm({
|
|
4007
|
+
"src/analysis/insights/rules/regression.ts"() {
|
|
4008
|
+
"use strict";
|
|
4009
|
+
init_format();
|
|
4010
|
+
init_thresholds();
|
|
4011
|
+
regressionRule = {
|
|
4012
|
+
id: "regression",
|
|
4013
|
+
check(ctx) {
|
|
4014
|
+
if (!ctx.previousMetrics || ctx.previousMetrics.length === 0) return [];
|
|
4015
|
+
const insights = [];
|
|
4016
|
+
for (const epMetrics of ctx.previousMetrics) {
|
|
4017
|
+
if (epMetrics.sessions.length < 2) continue;
|
|
4018
|
+
const prev = epMetrics.sessions[epMetrics.sessions.length - 2];
|
|
4019
|
+
const current = epMetrics.sessions[epMetrics.sessions.length - 1];
|
|
4020
|
+
if (prev.requestCount < REGRESSION_MIN_REQUESTS || current.requestCount < REGRESSION_MIN_REQUESTS) continue;
|
|
4021
|
+
const p95Increase = current.p95DurationMs - prev.p95DurationMs;
|
|
4022
|
+
const p95PctChange = prev.p95DurationMs > 0 ? Math.round(p95Increase / prev.p95DurationMs * 100) : 0;
|
|
4023
|
+
if (p95Increase >= REGRESSION_MIN_INCREASE_MS && p95PctChange >= REGRESSION_PCT_THRESHOLD) {
|
|
4024
|
+
insights.push({
|
|
4025
|
+
severity: "warning",
|
|
4026
|
+
type: "regression",
|
|
4027
|
+
title: "Performance Regression",
|
|
4028
|
+
desc: `${epMetrics.endpoint} p95 degraded ${formatDuration(prev.p95DurationMs)} \u2192 ${formatDuration(current.p95DurationMs)} (+${p95PctChange}%)`,
|
|
4029
|
+
hint: "This endpoint is slower than the previous session. Check if recent code changes added queries or processing.",
|
|
4030
|
+
nav: "graph"
|
|
4031
|
+
});
|
|
4032
|
+
}
|
|
4033
|
+
if (prev.avgQueryCount > 0 && current.avgQueryCount > prev.avgQueryCount * QUERY_COUNT_REGRESSION_RATIO) {
|
|
4034
|
+
insights.push({
|
|
4035
|
+
severity: "warning",
|
|
4036
|
+
type: "regression",
|
|
4037
|
+
title: "Query Count Regression",
|
|
4038
|
+
desc: `${epMetrics.endpoint} queries/request increased ${prev.avgQueryCount} \u2192 ${current.avgQueryCount}`,
|
|
4039
|
+
hint: "This endpoint is making more database queries than before. Check for new N+1 patterns or removed query optimizations.",
|
|
4040
|
+
nav: "queries"
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
return insights;
|
|
4045
|
+
}
|
|
4046
|
+
};
|
|
4047
|
+
}
|
|
4048
|
+
});
|
|
4049
|
+
|
|
4050
|
+
// src/analysis/insights/rules/security.ts
|
|
4051
|
+
var securityRule;
|
|
4052
|
+
var init_security2 = __esm({
|
|
4053
|
+
"src/analysis/insights/rules/security.ts"() {
|
|
4054
|
+
"use strict";
|
|
4055
|
+
securityRule = {
|
|
4056
|
+
id: "security",
|
|
4057
|
+
check(ctx) {
|
|
4058
|
+
if (!ctx.securityFindings) return [];
|
|
4059
|
+
return ctx.securityFindings.map((f) => ({
|
|
4060
|
+
severity: f.severity,
|
|
4061
|
+
type: "security",
|
|
4062
|
+
title: f.title,
|
|
4063
|
+
desc: f.desc,
|
|
4064
|
+
hint: f.hint,
|
|
4065
|
+
nav: "security"
|
|
4066
|
+
}));
|
|
4067
|
+
}
|
|
4068
|
+
};
|
|
3557
4069
|
}
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
4070
|
+
});
|
|
4071
|
+
|
|
4072
|
+
// src/analysis/insights/index.ts
|
|
4073
|
+
function createDefaultInsightRunner() {
|
|
4074
|
+
const runner = new InsightRunner();
|
|
4075
|
+
runner.register(n1Rule);
|
|
4076
|
+
runner.register(crossEndpointRule);
|
|
4077
|
+
runner.register(redundantQueryRule);
|
|
4078
|
+
runner.register(errorRule);
|
|
4079
|
+
runner.register(errorHotspotRule);
|
|
4080
|
+
runner.register(duplicateRule);
|
|
4081
|
+
runner.register(slowRule);
|
|
4082
|
+
runner.register(queryHeavyRule);
|
|
4083
|
+
runner.register(selectStarRule);
|
|
4084
|
+
runner.register(highRowsRule);
|
|
4085
|
+
runner.register(responseOverfetchRule);
|
|
4086
|
+
runner.register(largeResponseRule);
|
|
4087
|
+
runner.register(regressionRule);
|
|
4088
|
+
runner.register(securityRule);
|
|
4089
|
+
return runner;
|
|
4090
|
+
}
|
|
4091
|
+
function computeInsights(ctx) {
|
|
4092
|
+
return createDefaultInsightRunner().run(ctx);
|
|
3561
4093
|
}
|
|
3562
4094
|
var init_insights2 = __esm({
|
|
4095
|
+
"src/analysis/insights/index.ts"() {
|
|
4096
|
+
"use strict";
|
|
4097
|
+
init_runner();
|
|
4098
|
+
init_runner();
|
|
4099
|
+
init_n1();
|
|
4100
|
+
init_cross_endpoint();
|
|
4101
|
+
init_redundant_query();
|
|
4102
|
+
init_error();
|
|
4103
|
+
init_error_hotspot();
|
|
4104
|
+
init_duplicate();
|
|
4105
|
+
init_slow();
|
|
4106
|
+
init_query_heavy();
|
|
4107
|
+
init_select_star();
|
|
4108
|
+
init_high_rows();
|
|
4109
|
+
init_response_overfetch();
|
|
4110
|
+
init_large_response();
|
|
4111
|
+
init_regression();
|
|
4112
|
+
init_security2();
|
|
4113
|
+
}
|
|
4114
|
+
});
|
|
4115
|
+
|
|
4116
|
+
// src/analysis/insights.ts
|
|
4117
|
+
var init_insights3 = __esm({
|
|
3563
4118
|
"src/analysis/insights.ts"() {
|
|
3564
4119
|
"use strict";
|
|
3565
|
-
|
|
3566
|
-
init_thresholds();
|
|
3567
|
-
init_normalize();
|
|
3568
|
-
init_patterns();
|
|
4120
|
+
init_insights2();
|
|
3569
4121
|
}
|
|
3570
4122
|
});
|
|
3571
4123
|
|
|
@@ -3579,9 +4131,10 @@ var init_engine = __esm({
|
|
|
3579
4131
|
init_request_log();
|
|
3580
4132
|
init_group();
|
|
3581
4133
|
init_rules();
|
|
3582
|
-
|
|
4134
|
+
init_insights3();
|
|
3583
4135
|
AnalysisEngine = class {
|
|
3584
|
-
constructor(debounceMs = 300) {
|
|
4136
|
+
constructor(metricsStore, debounceMs = 300) {
|
|
4137
|
+
this.metricsStore = metricsStore;
|
|
3585
4138
|
this.debounceMs = debounceMs;
|
|
3586
4139
|
this.scanner = createDefaultScanner();
|
|
3587
4140
|
this.boundRequestListener = () => this.scheduleRecompute();
|
|
@@ -3639,6 +4192,7 @@ var init_engine = __esm({
|
|
|
3639
4192
|
const queries = defaultQueryStore.getAll();
|
|
3640
4193
|
const errors = defaultErrorStore.getAll();
|
|
3641
4194
|
const logs = defaultLogStore.getAll();
|
|
4195
|
+
const fetches = defaultFetchStore.getAll();
|
|
3642
4196
|
const flows = groupRequestsIntoFlows(requests);
|
|
3643
4197
|
this.cachedFindings = this.scanner.scan({ requests, logs });
|
|
3644
4198
|
this.cachedInsights = computeInsights({
|
|
@@ -3646,6 +4200,8 @@ var init_engine = __esm({
|
|
|
3646
4200
|
queries,
|
|
3647
4201
|
errors,
|
|
3648
4202
|
flows,
|
|
4203
|
+
fetches,
|
|
4204
|
+
previousMetrics: this.metricsStore.getAll(),
|
|
3649
4205
|
securityFindings: this.cachedFindings
|
|
3650
4206
|
});
|
|
3651
4207
|
for (const fn of this.listeners) {
|
|
@@ -3668,8 +4224,9 @@ var init_src = __esm({
|
|
|
3668
4224
|
init_adapter_registry();
|
|
3669
4225
|
init_rules();
|
|
3670
4226
|
init_engine();
|
|
4227
|
+
init_insights3();
|
|
3671
4228
|
init_insights2();
|
|
3672
|
-
VERSION = "0.7.
|
|
4229
|
+
VERSION = "0.7.5";
|
|
3673
4230
|
}
|
|
3674
4231
|
});
|
|
3675
4232
|
|
|
@@ -4282,7 +4839,7 @@ function getFlowInsights() {
|
|
|
4282
4839
|
}
|
|
4283
4840
|
`;
|
|
4284
4841
|
}
|
|
4285
|
-
var
|
|
4842
|
+
var init_insights4 = __esm({
|
|
4286
4843
|
"src/dashboard/client/views/flows/insights.ts"() {
|
|
4287
4844
|
"use strict";
|
|
4288
4845
|
init_constants();
|
|
@@ -4474,7 +5031,7 @@ function getFlowsView() {
|
|
|
4474
5031
|
var init_flows2 = __esm({
|
|
4475
5032
|
"src/dashboard/client/views/flows.ts"() {
|
|
4476
5033
|
"use strict";
|
|
4477
|
-
|
|
5034
|
+
init_insights4();
|
|
4478
5035
|
init_detail();
|
|
4479
5036
|
}
|
|
4480
5037
|
});
|
|
@@ -5090,6 +5647,27 @@ function getGraphOverview() {
|
|
|
5090
5647
|
(s.avgQueryCount > 0 ? '<span class="perf-ep-stat' + (s.avgQueryCount > HIGH_QUERY_THRESHOLD ? ' perf-ep-stat-warn' : '') + '">' + s.avgQueryCount + ' q/req</span>' : '') +
|
|
5091
5648
|
'<span class="perf-ep-stat perf-ep-stat-muted">' + s.totalRequests + ' req' + (s.totalRequests !== 1 ? 's' : '') + '</span>';
|
|
5092
5649
|
|
|
5650
|
+
var ovTotal = (s.avgQueryTimeMs || 0) + (s.avgFetchTimeMs || 0) + (s.avgAppTimeMs || 0);
|
|
5651
|
+
var ovBarHtml = '';
|
|
5652
|
+
if (ovTotal > 0) {
|
|
5653
|
+
var ovDbPct = Math.round((s.avgQueryTimeMs || 0) / ovTotal * 100);
|
|
5654
|
+
var ovFetchPct = Math.round((s.avgFetchTimeMs || 0) / ovTotal * 100);
|
|
5655
|
+
var ovAppPct = Math.max(0, 100 - ovDbPct - ovFetchPct);
|
|
5656
|
+
ovBarHtml =
|
|
5657
|
+
'<div class="perf-breakdown-inline">' +
|
|
5658
|
+
'<div class="perf-breakdown-bar perf-breakdown-bar-sm">' +
|
|
5659
|
+
(ovDbPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-db" style="width:' + ovDbPct + '%"></div>' : '') +
|
|
5660
|
+
(ovFetchPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:' + ovFetchPct + '%"></div>' : '') +
|
|
5661
|
+
(ovAppPct > 0 ? '<div class="perf-breakdown-seg perf-breakdown-app" style="width:' + ovAppPct + '%"></div>' : '') +
|
|
5662
|
+
'</div>' +
|
|
5663
|
+
'<span class="perf-breakdown-labels">' +
|
|
5664
|
+
(ovDbPct > 0 ? '<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-db"></span>' + fmtMs(s.avgQueryTimeMs || 0) + '</span>' : '') +
|
|
5665
|
+
(ovFetchPct > 0 ? '<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>' + fmtMs(s.avgFetchTimeMs || 0) + '</span>' : '') +
|
|
5666
|
+
'<span class="perf-breakdown-lbl"><span class="perf-breakdown-dot perf-breakdown-app"></span>' + fmtMs(s.avgAppTimeMs || 0) + '</span>' +
|
|
5667
|
+
'</span>' +
|
|
5668
|
+
'</div>';
|
|
5669
|
+
}
|
|
5670
|
+
|
|
5093
5671
|
var chartId = 'inline-scatter-' + idx;
|
|
5094
5672
|
|
|
5095
5673
|
card.innerHTML =
|
|
@@ -5097,6 +5675,7 @@ function getGraphOverview() {
|
|
|
5097
5675
|
'<span class="perf-ep-name">' + escHtml(ep.endpoint) + '</span>' +
|
|
5098
5676
|
'<span class="perf-ep-stats">' + statsHtml + '</span>' +
|
|
5099
5677
|
'</div>' +
|
|
5678
|
+
ovBarHtml +
|
|
5100
5679
|
'<canvas id="' + chartId + '" class="perf-inline-canvas"></canvas>';
|
|
5101
5680
|
|
|
5102
5681
|
list.appendChild(card);
|
|
@@ -5132,7 +5711,6 @@ function getGraphDetail() {
|
|
|
5132
5711
|
var g = healthGrade(s.p95Ms);
|
|
5133
5712
|
var errors = Math.round(s.errorRate * s.totalRequests);
|
|
5134
5713
|
|
|
5135
|
-
// header
|
|
5136
5714
|
var header = document.createElement('div');
|
|
5137
5715
|
header.className = 'perf-detail-header';
|
|
5138
5716
|
header.innerHTML =
|
|
@@ -5142,7 +5720,6 @@ function getGraphDetail() {
|
|
|
5142
5720
|
'</div>';
|
|
5143
5721
|
container.appendChild(header);
|
|
5144
5722
|
|
|
5145
|
-
// metric cards
|
|
5146
5723
|
var metrics = document.createElement('div');
|
|
5147
5724
|
metrics.className = 'perf-metric-row';
|
|
5148
5725
|
metrics.innerHTML =
|
|
@@ -5151,7 +5728,38 @@ function getGraphDetail() {
|
|
|
5151
5728
|
buildMetricCard('Queries/req', String(s.avgQueryCount), s.avgQueryCount > ${HIGH_QUERY_COUNT_PER_REQ} ? 'var(--amber)' : 'var(--text)');
|
|
5152
5729
|
container.appendChild(metrics);
|
|
5153
5730
|
|
|
5154
|
-
|
|
5731
|
+
var totalAvg = (s.avgQueryTimeMs || 0) + (s.avgFetchTimeMs || 0) + (s.avgAppTimeMs || 0);
|
|
5732
|
+
if (totalAvg > 0) {
|
|
5733
|
+
var dbPct = Math.round((s.avgQueryTimeMs || 0) / totalAvg * 100);
|
|
5734
|
+
var fetchPct = Math.round((s.avgFetchTimeMs || 0) / totalAvg * 100);
|
|
5735
|
+
var appPct = Math.max(0, 100 - dbPct - fetchPct);
|
|
5736
|
+
|
|
5737
|
+
var breakdown = document.createElement('div');
|
|
5738
|
+
breakdown.className = 'perf-breakdown';
|
|
5739
|
+
|
|
5740
|
+
var breakdownLabel = document.createElement('div');
|
|
5741
|
+
breakdownLabel.className = 'perf-section-title';
|
|
5742
|
+
breakdownLabel.textContent = 'Time Breakdown';
|
|
5743
|
+
breakdown.appendChild(breakdownLabel);
|
|
5744
|
+
|
|
5745
|
+
var bar = document.createElement('div');
|
|
5746
|
+
bar.className = 'perf-breakdown-bar';
|
|
5747
|
+
if (dbPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-db" style="width:' + dbPct + '%"></div>';
|
|
5748
|
+
if (fetchPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-fetch" style="width:' + fetchPct + '%"></div>';
|
|
5749
|
+
if (appPct > 0) bar.innerHTML += '<div class="perf-breakdown-seg perf-breakdown-app" style="width:' + appPct + '%"></div>';
|
|
5750
|
+
breakdown.appendChild(bar);
|
|
5751
|
+
|
|
5752
|
+
var legend = document.createElement('div');
|
|
5753
|
+
legend.className = 'perf-breakdown-legend';
|
|
5754
|
+
legend.innerHTML =
|
|
5755
|
+
'<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-db"></span>DB ' + fmtMs(s.avgQueryTimeMs || 0) + ' (' + dbPct + '%)</span>' +
|
|
5756
|
+
'<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-fetch"></span>Fetch ' + fmtMs(s.avgFetchTimeMs || 0) + ' (' + fetchPct + '%)</span>' +
|
|
5757
|
+
'<span class="perf-breakdown-item"><span class="perf-breakdown-dot perf-breakdown-app"></span>App ' + fmtMs(s.avgAppTimeMs || 0) + ' (' + appPct + '%)</span>';
|
|
5758
|
+
breakdown.appendChild(legend);
|
|
5759
|
+
|
|
5760
|
+
container.appendChild(breakdown);
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5155
5763
|
var chartWrap = document.createElement('div');
|
|
5156
5764
|
chartWrap.className = 'perf-chart-wrap';
|
|
5157
5765
|
var chartLabel = document.createElement('div');
|
|
@@ -5169,7 +5777,6 @@ function getGraphDetail() {
|
|
|
5169
5777
|
|
|
5170
5778
|
drawScatterChart(canvas, ep.requests);
|
|
5171
5779
|
|
|
5172
|
-
// recent requests table
|
|
5173
5780
|
if (ep.requests.length > 0) {
|
|
5174
5781
|
var tableWrap = document.createElement('div');
|
|
5175
5782
|
tableWrap.className = 'perf-history-wrap';
|
|
@@ -5180,6 +5787,7 @@ function getGraphDetail() {
|
|
|
5180
5787
|
'<span class="perf-col perf-col-date">Time</span>' +
|
|
5181
5788
|
'<span class="perf-col perf-col-health">Health</span>' +
|
|
5182
5789
|
'<span class="perf-col perf-col-avg">Duration</span>' +
|
|
5790
|
+
'<span class="perf-col perf-col-breakdown">Breakdown</span>' +
|
|
5183
5791
|
'<span class="perf-col perf-col-status">Status</span>' +
|
|
5184
5792
|
'<span class="perf-col perf-col-qpr">Queries</span>';
|
|
5185
5793
|
tableWrap.appendChild(colHeader);
|
|
@@ -5198,10 +5806,20 @@ function getGraphDetail() {
|
|
|
5198
5806
|
var row = document.createElement('div');
|
|
5199
5807
|
row.className = 'perf-hist-row' + (isError ? ' perf-hist-row-err' : '');
|
|
5200
5808
|
row.setAttribute('data-req-idx', item.origIdx);
|
|
5809
|
+
var rDbMs = r.queryTimeMs || 0;
|
|
5810
|
+
var rFetchMs = r.fetchTimeMs || 0;
|
|
5811
|
+
var rAppMs = Math.max(0, r.durationMs - rDbMs - rFetchMs);
|
|
5812
|
+
var breakdownParts = [];
|
|
5813
|
+
if (rDbMs > 0) breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-db">DB ' + fmtMs(rDbMs) + '</span>');
|
|
5814
|
+
if (rFetchMs > 0) breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-fetch">Fetch ' + fmtMs(rFetchMs) + '</span>');
|
|
5815
|
+
breakdownParts.push('<span class="perf-bd-tag perf-bd-tag-app">App ' + fmtMs(rAppMs) + '</span>');
|
|
5816
|
+
var breakdownHtml = breakdownParts.join('');
|
|
5817
|
+
|
|
5201
5818
|
row.innerHTML =
|
|
5202
5819
|
'<span class="perf-col perf-col-date">' + timeStr + '</span>' +
|
|
5203
5820
|
'<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
5821
|
'<span class="perf-col perf-col-avg">' + fmtMs(r.durationMs) + '</span>' +
|
|
5822
|
+
'<span class="perf-col perf-col-breakdown">' + breakdownHtml + '</span>' +
|
|
5205
5823
|
'<span class="perf-col perf-col-status" style="color:' + (isError ? 'var(--red)' : 'var(--text-muted)') + '">' + r.statusCode + '</span>' +
|
|
5206
5824
|
'<span class="perf-col perf-col-qpr">' + r.queryCount + '</span>';
|
|
5207
5825
|
tableWrap.appendChild(row);
|
|
@@ -5744,7 +6362,7 @@ function getSecurityView() {
|
|
|
5744
6362
|
}
|
|
5745
6363
|
`;
|
|
5746
6364
|
}
|
|
5747
|
-
var
|
|
6365
|
+
var init_security3 = __esm({
|
|
5748
6366
|
"src/dashboard/client/views/security.ts"() {
|
|
5749
6367
|
"use strict";
|
|
5750
6368
|
}
|
|
@@ -6009,7 +6627,7 @@ var init_client = __esm({
|
|
|
6009
6627
|
init_timeline2();
|
|
6010
6628
|
init_graph2();
|
|
6011
6629
|
init_overview3();
|
|
6012
|
-
|
|
6630
|
+
init_security3();
|
|
6013
6631
|
init_app2();
|
|
6014
6632
|
}
|
|
6015
6633
|
});
|
|
@@ -6113,7 +6731,7 @@ function createDashboardHandler(deps) {
|
|
|
6113
6731
|
routes[DASHBOARD_API_TAB] = (req, res) => {
|
|
6114
6732
|
const raw = (req.url ?? "").split("tab=")[1];
|
|
6115
6733
|
if (raw) {
|
|
6116
|
-
const tab = decodeURIComponent(raw).slice(0,
|
|
6734
|
+
const tab = decodeURIComponent(raw).slice(0, MAX_TAB_NAME_LENGTH);
|
|
6117
6735
|
if (VALID_TABS.has(tab) && isTelemetryEnabled()) recordTabViewed(tab);
|
|
6118
6736
|
}
|
|
6119
6737
|
res.writeHead(204);
|
|
@@ -6166,6 +6784,9 @@ var init_router = __esm({
|
|
|
6166
6784
|
|
|
6167
6785
|
// src/output/terminal.ts
|
|
6168
6786
|
import pc from "picocolors";
|
|
6787
|
+
function print(line) {
|
|
6788
|
+
process.stdout.write(line + "\n");
|
|
6789
|
+
}
|
|
6169
6790
|
function severityIcon(severity) {
|
|
6170
6791
|
if (severity === "critical") return pc.red("\u2717");
|
|
6171
6792
|
if (severity === "warning") return pc.yellow("\u26A0");
|
|
@@ -6184,7 +6805,12 @@ function formatConsoleLine(insight, dashboardUrl, suffix) {
|
|
|
6184
6805
|
const title = colorTitle(insight.severity, insight.title);
|
|
6185
6806
|
const desc = pc.dim(truncate(insight.desc) + (suffix ?? ""));
|
|
6186
6807
|
const link = pc.dim(`\u2192 ${dashboardUrl}`);
|
|
6187
|
-
|
|
6808
|
+
let line = ` ${icon} ${title} \u2014 ${desc} ${link}`;
|
|
6809
|
+
if (insight.detail) {
|
|
6810
|
+
line += `
|
|
6811
|
+
${pc.dim("\u2514 " + insight.detail)}`;
|
|
6812
|
+
}
|
|
6813
|
+
return line;
|
|
6188
6814
|
}
|
|
6189
6815
|
function createConsoleInsightListener(proxyPort, metricsStore) {
|
|
6190
6816
|
const printedKeys = /* @__PURE__ */ new Set();
|
|
@@ -6199,7 +6825,7 @@ function createConsoleInsightListener(proxyPort, metricsStore) {
|
|
|
6199
6825
|
printedKeys.add(key);
|
|
6200
6826
|
let suffix;
|
|
6201
6827
|
if (insight.type === "slow") {
|
|
6202
|
-
const ep = metricsStore.
|
|
6828
|
+
const ep = metricsStore.getEndpoint(endpoint);
|
|
6203
6829
|
if (ep && ep.sessions.length > 1) {
|
|
6204
6830
|
const prev = ep.sessions[ep.sessions.length - 2];
|
|
6205
6831
|
suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
|
|
@@ -6208,8 +6834,8 @@ function createConsoleInsightListener(proxyPort, metricsStore) {
|
|
|
6208
6834
|
lines.push(formatConsoleLine(insight, dashUrl, suffix));
|
|
6209
6835
|
}
|
|
6210
6836
|
if (lines.length > 0) {
|
|
6211
|
-
|
|
6212
|
-
for (const line of lines)
|
|
6837
|
+
print("");
|
|
6838
|
+
for (const line of lines) print(line);
|
|
6213
6839
|
}
|
|
6214
6840
|
};
|
|
6215
6841
|
}
|
|
@@ -6303,6 +6929,12 @@ function decompress(body, encoding) {
|
|
|
6303
6929
|
}
|
|
6304
6930
|
return body;
|
|
6305
6931
|
}
|
|
6932
|
+
function toBuffer(chunk) {
|
|
6933
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
6934
|
+
if (chunk instanceof Uint8Array) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
6935
|
+
if (typeof chunk === "string") return Buffer.from(chunk);
|
|
6936
|
+
return null;
|
|
6937
|
+
}
|
|
6306
6938
|
function captureInProcess(req, res, requestId) {
|
|
6307
6939
|
const startTime = performance.now();
|
|
6308
6940
|
const method = req.method ?? "GET";
|
|
@@ -6314,9 +6946,11 @@ function captureInProcess(req, res, requestId) {
|
|
|
6314
6946
|
try {
|
|
6315
6947
|
const chunk = args[0];
|
|
6316
6948
|
if (chunk != null && typeof chunk !== "function" && resSize < DEFAULT_MAX_BODY_CAPTURE) {
|
|
6317
|
-
const buf =
|
|
6318
|
-
|
|
6319
|
-
|
|
6949
|
+
const buf = toBuffer(chunk);
|
|
6950
|
+
if (buf) {
|
|
6951
|
+
resChunks.push(buf);
|
|
6952
|
+
resSize += buf.length;
|
|
6953
|
+
}
|
|
6320
6954
|
}
|
|
6321
6955
|
} catch {
|
|
6322
6956
|
}
|
|
@@ -6326,12 +6960,15 @@ function captureInProcess(req, res, requestId) {
|
|
|
6326
6960
|
try {
|
|
6327
6961
|
const chunk = typeof args[0] !== "function" ? args[0] : void 0;
|
|
6328
6962
|
if (chunk != null && resSize < DEFAULT_MAX_BODY_CAPTURE) {
|
|
6329
|
-
const buf =
|
|
6330
|
-
|
|
6963
|
+
const buf = toBuffer(chunk);
|
|
6964
|
+
if (buf) {
|
|
6965
|
+
resChunks.push(buf);
|
|
6966
|
+
}
|
|
6331
6967
|
}
|
|
6332
6968
|
} catch {
|
|
6333
6969
|
}
|
|
6334
6970
|
const result = originalEnd.apply(this, args);
|
|
6971
|
+
const endTime = performance.now();
|
|
6335
6972
|
try {
|
|
6336
6973
|
const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
|
|
6337
6974
|
let body = resChunks.length > 0 ? Buffer.concat(resChunks) : null;
|
|
@@ -6349,6 +6986,7 @@ function captureInProcess(req, res, requestId) {
|
|
|
6349
6986
|
responseBody: body,
|
|
6350
6987
|
responseContentType: String(res.getHeader("content-type") ?? ""),
|
|
6351
6988
|
startTime,
|
|
6989
|
+
endTime,
|
|
6352
6990
|
config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
|
|
6353
6991
|
});
|
|
6354
6992
|
} catch {
|
|
@@ -6364,42 +7002,15 @@ var init_capture = __esm({
|
|
|
6364
7002
|
}
|
|
6365
7003
|
});
|
|
6366
7004
|
|
|
6367
|
-
// src/runtime/
|
|
6368
|
-
var setup_exports = {};
|
|
6369
|
-
__export(setup_exports, {
|
|
6370
|
-
setup: () => setup
|
|
6371
|
-
});
|
|
7005
|
+
// src/runtime/interceptor.ts
|
|
6372
7006
|
import http from "http";
|
|
6373
7007
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
6374
|
-
function
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
setEmitter(routeEvent2);
|
|
6378
|
-
setupFetchHook();
|
|
6379
|
-
setupConsoleHook();
|
|
6380
|
-
setupErrorHook();
|
|
6381
|
-
const registry = createDefaultRegistry();
|
|
6382
|
-
registry.patchAll(routeEvent2);
|
|
6383
|
-
const cwd = process.cwd();
|
|
6384
|
-
const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
|
|
6385
|
-
metricsStore.start();
|
|
6386
|
-
const analysisEngine = new AnalysisEngine();
|
|
6387
|
-
analysisEngine.start();
|
|
6388
|
-
const config = {
|
|
6389
|
-
proxyPort: 0,
|
|
6390
|
-
targetPort: 0,
|
|
6391
|
-
showStatic: false,
|
|
6392
|
-
maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
|
|
6393
|
-
};
|
|
6394
|
-
const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine });
|
|
6395
|
-
onRequest((req) => {
|
|
6396
|
-
const queryCount = defaultQueryStore.getByRequest(req.id).length;
|
|
6397
|
-
metricsStore.recordRequest(req, queryCount);
|
|
6398
|
-
});
|
|
6399
|
-
const originalEmit = http.Server.prototype.emit;
|
|
7008
|
+
function installInterceptor(deps) {
|
|
7009
|
+
originalEmit = http.Server.prototype.emit;
|
|
7010
|
+
const saved = originalEmit;
|
|
6400
7011
|
let bannerPrinted = false;
|
|
6401
7012
|
http.Server.prototype.emit = safeWrap(
|
|
6402
|
-
|
|
7013
|
+
saved,
|
|
6403
7014
|
function(original, event, ...args) {
|
|
6404
7015
|
if (event !== "request") return original.apply(this, [event, ...args]);
|
|
6405
7016
|
const req = args[0];
|
|
@@ -6409,9 +7020,8 @@ function setup() {
|
|
|
6409
7020
|
const port = req.socket.localPort;
|
|
6410
7021
|
if (port) {
|
|
6411
7022
|
bannerPrinted = true;
|
|
6412
|
-
config.proxyPort = port;
|
|
6413
|
-
|
|
6414
|
-
console.log(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}`);
|
|
7023
|
+
deps.config.proxyPort = port;
|
|
7024
|
+
deps.onFirstRequest(port);
|
|
6415
7025
|
}
|
|
6416
7026
|
}
|
|
6417
7027
|
if (isDashboardRequest(url)) {
|
|
@@ -6420,7 +7030,7 @@ function setup() {
|
|
|
6420
7030
|
res.end("Not Found");
|
|
6421
7031
|
return true;
|
|
6422
7032
|
}
|
|
6423
|
-
handleDashboard(req, res, config);
|
|
7033
|
+
deps.handleDashboard(req, res, deps.config);
|
|
6424
7034
|
return true;
|
|
6425
7035
|
}
|
|
6426
7036
|
const requestId = randomUUID6();
|
|
@@ -6436,8 +7046,72 @@ function setup() {
|
|
|
6436
7046
|
);
|
|
6437
7047
|
}
|
|
6438
7048
|
);
|
|
6439
|
-
|
|
7049
|
+
}
|
|
7050
|
+
function uninstallInterceptor() {
|
|
7051
|
+
if (originalEmit) {
|
|
6440
7052
|
http.Server.prototype.emit = originalEmit;
|
|
7053
|
+
originalEmit = null;
|
|
7054
|
+
}
|
|
7055
|
+
}
|
|
7056
|
+
var originalEmit;
|
|
7057
|
+
var init_interceptor = __esm({
|
|
7058
|
+
"src/runtime/interceptor.ts"() {
|
|
7059
|
+
"use strict";
|
|
7060
|
+
init_context();
|
|
7061
|
+
init_router();
|
|
7062
|
+
init_safe_wrap();
|
|
7063
|
+
init_guard();
|
|
7064
|
+
init_capture();
|
|
7065
|
+
originalEmit = null;
|
|
7066
|
+
}
|
|
7067
|
+
});
|
|
7068
|
+
|
|
7069
|
+
// src/runtime/setup.ts
|
|
7070
|
+
var setup_exports = {};
|
|
7071
|
+
__export(setup_exports, {
|
|
7072
|
+
setup: () => setup
|
|
7073
|
+
});
|
|
7074
|
+
function setup() {
|
|
7075
|
+
if (initialized) return;
|
|
7076
|
+
initialized = true;
|
|
7077
|
+
setEmitter(routeEvent2);
|
|
7078
|
+
setupFetchHook();
|
|
7079
|
+
setupConsoleHook();
|
|
7080
|
+
setupErrorHook();
|
|
7081
|
+
const registry = createDefaultRegistry();
|
|
7082
|
+
registry.patchAll(routeEvent2);
|
|
7083
|
+
const cwd = process.cwd();
|
|
7084
|
+
const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
|
|
7085
|
+
metricsStore.start();
|
|
7086
|
+
const analysisEngine = new AnalysisEngine(metricsStore);
|
|
7087
|
+
analysisEngine.start();
|
|
7088
|
+
const config = {
|
|
7089
|
+
proxyPort: 0,
|
|
7090
|
+
targetPort: 0,
|
|
7091
|
+
showStatic: false,
|
|
7092
|
+
maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
|
|
7093
|
+
};
|
|
7094
|
+
const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine });
|
|
7095
|
+
onRequest((req) => {
|
|
7096
|
+
const queries = defaultQueryStore.getByRequest(req.id);
|
|
7097
|
+
const fetches = defaultFetchStore.getByRequest(req.id);
|
|
7098
|
+
metricsStore.recordRequest(req, {
|
|
7099
|
+
queryCount: queries.length,
|
|
7100
|
+
queryTimeMs: queries.reduce((s, q) => s + q.durationMs, 0),
|
|
7101
|
+
fetchTimeMs: fetches.reduce((s, f) => s + f.durationMs, 0)
|
|
7102
|
+
});
|
|
7103
|
+
});
|
|
7104
|
+
installInterceptor({
|
|
7105
|
+
handleDashboard,
|
|
7106
|
+
config,
|
|
7107
|
+
onFirstRequest(port) {
|
|
7108
|
+
analysisEngine.onUpdate(createConsoleInsightListener(port, metricsStore));
|
|
7109
|
+
process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
|
|
7110
|
+
`);
|
|
7111
|
+
}
|
|
7112
|
+
});
|
|
7113
|
+
health.setTeardown(() => {
|
|
7114
|
+
uninstallInterceptor();
|
|
6441
7115
|
analysisEngine.stop();
|
|
6442
7116
|
metricsStore.stop();
|
|
6443
7117
|
});
|
|
@@ -6462,7 +7136,6 @@ var initialized;
|
|
|
6462
7136
|
var init_setup = __esm({
|
|
6463
7137
|
"src/runtime/setup.ts"() {
|
|
6464
7138
|
"use strict";
|
|
6465
|
-
init_context();
|
|
6466
7139
|
init_transport2();
|
|
6467
7140
|
init_fetch();
|
|
6468
7141
|
init_console();
|
|
@@ -6475,10 +7148,8 @@ var init_setup = __esm({
|
|
|
6475
7148
|
init_terminal();
|
|
6476
7149
|
init_src();
|
|
6477
7150
|
init_constants();
|
|
6478
|
-
init_safe_wrap();
|
|
6479
7151
|
init_health2();
|
|
6480
|
-
|
|
6481
|
-
init_capture();
|
|
7152
|
+
init_interceptor();
|
|
6482
7153
|
initialized = false;
|
|
6483
7154
|
}
|
|
6484
7155
|
});
|