brakit 0.6.1 → 0.6.2
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/bin/brakit.js +281 -56
- package/dist/index.d.ts +1 -1
- package/dist/index.js +20 -34
- package/package.json +1 -1
package/dist/bin/brakit.js
CHANGED
|
@@ -32,6 +32,7 @@ var DASHBOARD_API_ACTIVITY = "/__brakit/api/activity";
|
|
|
32
32
|
var DASHBOARD_API_METRICS_LIVE = "/__brakit/api/metrics/live";
|
|
33
33
|
var DASHBOARD_API_INSIGHTS = "/__brakit/api/insights";
|
|
34
34
|
var DASHBOARD_API_SECURITY = "/__brakit/api/security";
|
|
35
|
+
var DASHBOARD_API_TAB = "/__brakit/api/tab";
|
|
35
36
|
|
|
36
37
|
// src/constants/limits.ts
|
|
37
38
|
var MAX_REQUEST_ENTRIES = 1e3;
|
|
@@ -50,7 +51,6 @@ var ERROR_RATE_THRESHOLD_PCT = 20;
|
|
|
50
51
|
var SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
|
|
51
52
|
var MIN_REQUESTS_FOR_INSIGHT = 2;
|
|
52
53
|
var HIGH_QUERY_COUNT_PER_REQ = 5;
|
|
53
|
-
var AUTH_OVERHEAD_PCT = 30;
|
|
54
54
|
var CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
|
|
55
55
|
var CROSS_ENDPOINT_PCT = 50;
|
|
56
56
|
var CROSS_ENDPOINT_MIN_OCCURRENCES = 5;
|
|
@@ -164,7 +164,7 @@ var offRequest = (fn) => defaultStore.offRequest(fn);
|
|
|
164
164
|
|
|
165
165
|
// src/proxy/handler.ts
|
|
166
166
|
function proxyRequest(clientReq, clientRes, config) {
|
|
167
|
-
const
|
|
167
|
+
const startTime2 = performance.now();
|
|
168
168
|
const method = clientReq.method ?? "GET";
|
|
169
169
|
const requestId = randomUUID();
|
|
170
170
|
const shouldCaptureBody = method !== "GET" && method !== "HEAD";
|
|
@@ -194,7 +194,7 @@ function proxyRequest(clientReq, clientRes, config) {
|
|
|
194
194
|
clientReq,
|
|
195
195
|
clientRes,
|
|
196
196
|
proxyRes,
|
|
197
|
-
|
|
197
|
+
startTime2,
|
|
198
198
|
shouldCaptureBody ? bodyChunks : [],
|
|
199
199
|
config,
|
|
200
200
|
requestId
|
|
@@ -217,7 +217,7 @@ function proxyRequest(clientReq, clientRes, config) {
|
|
|
217
217
|
});
|
|
218
218
|
clientReq.pipe(proxyReq);
|
|
219
219
|
}
|
|
220
|
-
function handleProxyResponse(clientReq, clientRes, proxyRes,
|
|
220
|
+
function handleProxyResponse(clientReq, clientRes, proxyRes, startTime2, bodyChunks, config, requestId) {
|
|
221
221
|
const responseChunks = [];
|
|
222
222
|
let responseSize = 0;
|
|
223
223
|
clientRes.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
|
@@ -242,7 +242,7 @@ function handleProxyResponse(clientReq, clientRes, proxyRes, startTime, bodyChun
|
|
|
242
242
|
responseHeaders: proxyRes.headers,
|
|
243
243
|
responseBody,
|
|
244
244
|
responseContentType: proxyRes.headers["content-type"] ?? "",
|
|
245
|
-
startTime,
|
|
245
|
+
startTime: startTime2,
|
|
246
246
|
config
|
|
247
247
|
});
|
|
248
248
|
});
|
|
@@ -578,7 +578,7 @@ function buildFlow(rawRequests) {
|
|
|
578
578
|
markDuplicates(rawRequests);
|
|
579
579
|
const requests = collapsePolling(rawRequests);
|
|
580
580
|
const first = requests[0];
|
|
581
|
-
const
|
|
581
|
+
const startTime2 = first.startedAt;
|
|
582
582
|
const endTime = Math.max(
|
|
583
583
|
...requests.map(
|
|
584
584
|
(r) => r.pollingDurationMs ? r.startedAt + r.pollingDurationMs : r.startedAt + r.durationMs
|
|
@@ -592,8 +592,8 @@ function buildFlow(rawRequests) {
|
|
|
592
592
|
id: randomUUID2(),
|
|
593
593
|
label: deriveFlowLabel(requests, sourcePage),
|
|
594
594
|
requests,
|
|
595
|
-
startTime,
|
|
596
|
-
totalDurationMs: Math.round(endTime -
|
|
595
|
+
startTime: startTime2,
|
|
596
|
+
totalDurationMs: Math.round(endTime - startTime2),
|
|
597
597
|
hasErrors: requests.some((r) => r.statusCode >= 400),
|
|
598
598
|
warnings: detectWarnings(rawRequests),
|
|
599
599
|
sourcePage,
|
|
@@ -1927,7 +1927,6 @@ var HEALTH_GOOD_MS = 300;
|
|
|
1927
1927
|
var HEALTH_OK_MS = 800;
|
|
1928
1928
|
var HEALTH_SLOW_MS = 2e3;
|
|
1929
1929
|
var SLOW_QUERY_THRESHOLD_MS = 100;
|
|
1930
|
-
var AUTH_SLOW_MS = 500;
|
|
1931
1930
|
var AUTH_SKIP_CATEGORIES = `{ 'auth-handshake': 1, 'auth-check': 1, 'middleware': 1 }`;
|
|
1932
1931
|
var TIMELINE_CACHE_MAX = 50;
|
|
1933
1932
|
var TIMELINE_ROOT_MARGIN = "'200px'";
|
|
@@ -2276,7 +2275,6 @@ function getFlowInsights() {
|
|
|
2276
2275
|
var warnings = [];
|
|
2277
2276
|
var duplicates = [];
|
|
2278
2277
|
var seen = new Map();
|
|
2279
|
-
var authMs = 0;
|
|
2280
2278
|
var totalMs = 0;
|
|
2281
2279
|
for (var i = 0; i < reqs.length; i++) {
|
|
2282
2280
|
var req = reqs[i];
|
|
@@ -2285,10 +2283,6 @@ function getFlowInsights() {
|
|
|
2285
2283
|
totalMs += dur;
|
|
2286
2284
|
|
|
2287
2285
|
if (skipCats[req.category]) {
|
|
2288
|
-
authMs += dur;
|
|
2289
|
-
if (dur > ${AUTH_SLOW_MS}) {
|
|
2290
|
-
warnings.push('Slow auth: ' + label + ' took ' + formatDuration(dur));
|
|
2291
|
-
}
|
|
2292
2286
|
continue;
|
|
2293
2287
|
}
|
|
2294
2288
|
|
|
@@ -2310,13 +2304,6 @@ function getFlowInsights() {
|
|
|
2310
2304
|
successes.push(label);
|
|
2311
2305
|
}
|
|
2312
2306
|
|
|
2313
|
-
if (totalMs > 0 && authMs > 0) {
|
|
2314
|
-
var authPct = Math.round((authMs / totalMs) * 100);
|
|
2315
|
-
if (authPct >= ${AUTH_OVERHEAD_PCT}) {
|
|
2316
|
-
warnings.unshift('Auth overhead: ' + authPct + '% of this action (' + formatDuration(authMs) + ') is spent in auth/middleware');
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
|
|
2320
2307
|
for (var d of seen.values()) duplicates.push(d);
|
|
2321
2308
|
var tip = '';
|
|
2322
2309
|
if (duplicates.length > 0) {
|
|
@@ -3820,6 +3807,7 @@ function getApp() {
|
|
|
3820
3807
|
sidebarItems.forEach(function(i) { i.classList.remove('active'); });
|
|
3821
3808
|
item.classList.add('active');
|
|
3822
3809
|
state.activeView = view;
|
|
3810
|
+
fetch('${DASHBOARD_API_TAB}?tab=' + encodeURIComponent(view)).catch(function(){});
|
|
3823
3811
|
document.getElementById('header-title').textContent = VIEW_TITLES[view] || view;
|
|
3824
3812
|
document.getElementById('header-sub').textContent = VIEW_SUBTITLES[view] || '';
|
|
3825
3813
|
document.getElementById('mode-toggle').style.display = view === 'actions' ? 'flex' : 'none';
|
|
@@ -3943,6 +3931,155 @@ ${getLayoutHtml(config)}
|
|
|
3943
3931
|
</html>`;
|
|
3944
3932
|
}
|
|
3945
3933
|
|
|
3934
|
+
// src/telemetry/index.ts
|
|
3935
|
+
import { platform, release, arch } from "os";
|
|
3936
|
+
|
|
3937
|
+
// src/telemetry/config.ts
|
|
3938
|
+
import { homedir } from "os";
|
|
3939
|
+
import { join } from "path";
|
|
3940
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
3941
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3942
|
+
var CONFIG_DIR = join(homedir(), ".brakit");
|
|
3943
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
3944
|
+
function readConfig() {
|
|
3945
|
+
try {
|
|
3946
|
+
if (!existsSync3(CONFIG_PATH)) return null;
|
|
3947
|
+
return JSON.parse(readFileSync3(CONFIG_PATH, "utf-8"));
|
|
3948
|
+
} catch {
|
|
3949
|
+
return null;
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
function writeConfig(config) {
|
|
3953
|
+
try {
|
|
3954
|
+
if (!existsSync3(CONFIG_DIR)) mkdirSync3(CONFIG_DIR, { recursive: true });
|
|
3955
|
+
writeFileSync3(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
3956
|
+
} catch {
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
function getOrCreateConfig() {
|
|
3960
|
+
const existing = readConfig();
|
|
3961
|
+
if (existing && typeof existing.telemetry === "boolean" && existing.anonymousId) {
|
|
3962
|
+
return existing;
|
|
3963
|
+
}
|
|
3964
|
+
const config = { telemetry: true, anonymousId: randomUUID5() };
|
|
3965
|
+
writeConfig(config);
|
|
3966
|
+
return config;
|
|
3967
|
+
}
|
|
3968
|
+
function isTelemetryEnabled() {
|
|
3969
|
+
const env = process.env.BRAKIT_TELEMETRY;
|
|
3970
|
+
if (env !== void 0) return env !== "false" && env !== "0" && env !== "off";
|
|
3971
|
+
return readConfig()?.telemetry ?? true;
|
|
3972
|
+
}
|
|
3973
|
+
function setTelemetryEnabled(enabled) {
|
|
3974
|
+
const config = getOrCreateConfig();
|
|
3975
|
+
config.telemetry = enabled;
|
|
3976
|
+
writeConfig(config);
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
// src/telemetry/index.ts
|
|
3980
|
+
var POSTHOG_HOST = "https://app.posthog.com";
|
|
3981
|
+
var POSTHOG_KEY = "phc_gH8aQFZ2Fn8db9LEdgomOvymLiP6mm6FPTYXffQceR8";
|
|
3982
|
+
var startTime = 0;
|
|
3983
|
+
var sessionFramework = "";
|
|
3984
|
+
var sessionPackageManager = "";
|
|
3985
|
+
var sessionIsCustomCommand = false;
|
|
3986
|
+
var sessionAdapters = [];
|
|
3987
|
+
var requestCount = 0;
|
|
3988
|
+
var insightTypes = /* @__PURE__ */ new Set();
|
|
3989
|
+
var rulesTriggered = /* @__PURE__ */ new Set();
|
|
3990
|
+
var tabsViewed = /* @__PURE__ */ new Set();
|
|
3991
|
+
var dashboardOpened = false;
|
|
3992
|
+
var explainUsed = false;
|
|
3993
|
+
function initSession(framework, packageManager, isCustomCommand, adapters) {
|
|
3994
|
+
startTime = Date.now();
|
|
3995
|
+
sessionFramework = framework;
|
|
3996
|
+
sessionPackageManager = packageManager;
|
|
3997
|
+
sessionIsCustomCommand = isCustomCommand;
|
|
3998
|
+
sessionAdapters = adapters;
|
|
3999
|
+
}
|
|
4000
|
+
function recordRequestCount(count) {
|
|
4001
|
+
requestCount = count;
|
|
4002
|
+
}
|
|
4003
|
+
function recordInsightTypes(types) {
|
|
4004
|
+
for (const t of types) insightTypes.add(t);
|
|
4005
|
+
}
|
|
4006
|
+
function recordRulesTriggered(rules) {
|
|
4007
|
+
for (const r of rules) rulesTriggered.add(r);
|
|
4008
|
+
}
|
|
4009
|
+
function recordTabViewed(tab) {
|
|
4010
|
+
tabsViewed.add(tab);
|
|
4011
|
+
}
|
|
4012
|
+
function recordDashboardOpened() {
|
|
4013
|
+
dashboardOpened = true;
|
|
4014
|
+
}
|
|
4015
|
+
function speedBucket(ms) {
|
|
4016
|
+
if (ms === 0) return "none";
|
|
4017
|
+
if (ms < 200) return "<200ms";
|
|
4018
|
+
if (ms < 500) return "200-500ms";
|
|
4019
|
+
if (ms < 1e3) return "500-1000ms";
|
|
4020
|
+
if (ms < 2e3) return "1000-2000ms";
|
|
4021
|
+
if (ms < 5e3) return "2000-5000ms";
|
|
4022
|
+
return ">5000ms";
|
|
4023
|
+
}
|
|
4024
|
+
function trackSession(metricsStore, analysisEngine) {
|
|
4025
|
+
if (!isTelemetryEnabled()) return;
|
|
4026
|
+
const isFirstSession = readConfig() === null;
|
|
4027
|
+
const config = getOrCreateConfig();
|
|
4028
|
+
const live = metricsStore.getLiveEndpoints();
|
|
4029
|
+
const insights = analysisEngine.getInsights();
|
|
4030
|
+
const findings = analysisEngine.getFindings();
|
|
4031
|
+
let totalRequests = 0;
|
|
4032
|
+
let totalDuration = 0;
|
|
4033
|
+
let slowestP95 = 0;
|
|
4034
|
+
for (const ep of live) {
|
|
4035
|
+
totalRequests += ep.summary.totalRequests;
|
|
4036
|
+
totalDuration += ep.summary.p95Ms * ep.summary.totalRequests;
|
|
4037
|
+
if (ep.summary.p95Ms > slowestP95) slowestP95 = ep.summary.p95Ms;
|
|
4038
|
+
}
|
|
4039
|
+
const payload = {
|
|
4040
|
+
api_key: POSTHOG_KEY,
|
|
4041
|
+
event: "session",
|
|
4042
|
+
distinct_id: config.anonymousId,
|
|
4043
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4044
|
+
properties: {
|
|
4045
|
+
brakit_version: VERSION,
|
|
4046
|
+
node_version: process.version,
|
|
4047
|
+
os: `${platform()}-${release()}`,
|
|
4048
|
+
arch: arch(),
|
|
4049
|
+
framework: sessionFramework,
|
|
4050
|
+
package_manager: sessionPackageManager,
|
|
4051
|
+
is_custom_command: sessionIsCustomCommand,
|
|
4052
|
+
first_session: isFirstSession,
|
|
4053
|
+
adapters_detected: sessionAdapters,
|
|
4054
|
+
request_count: requestCount,
|
|
4055
|
+
error_count: defaultErrorStore.getAll().length,
|
|
4056
|
+
query_count: defaultQueryStore.getAll().length,
|
|
4057
|
+
fetch_count: defaultFetchStore.getAll().length,
|
|
4058
|
+
insight_count: insights.length,
|
|
4059
|
+
finding_count: findings.length,
|
|
4060
|
+
insight_types: [...insightTypes],
|
|
4061
|
+
rules_triggered: [...rulesTriggered],
|
|
4062
|
+
endpoint_count: live.length,
|
|
4063
|
+
avg_duration_ms: totalRequests > 0 ? Math.round(totalDuration / totalRequests) : 0,
|
|
4064
|
+
slowest_endpoint_bucket: speedBucket(slowestP95),
|
|
4065
|
+
tabs_viewed: [...tabsViewed],
|
|
4066
|
+
dashboard_opened: dashboardOpened,
|
|
4067
|
+
explain_used: explainUsed,
|
|
4068
|
+
session_duration_s: Math.round((Date.now() - startTime) / 1e3),
|
|
4069
|
+
$lib: "brakit",
|
|
4070
|
+
$ip: null,
|
|
4071
|
+
$geoip_disable: true
|
|
4072
|
+
}
|
|
4073
|
+
};
|
|
4074
|
+
fetch(`${POSTHOG_HOST}/capture`, {
|
|
4075
|
+
method: "POST",
|
|
4076
|
+
headers: { "content-type": "application/json" },
|
|
4077
|
+
body: JSON.stringify(payload),
|
|
4078
|
+
signal: AbortSignal.timeout(5e3)
|
|
4079
|
+
}).catch(() => {
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
|
|
3946
4083
|
// src/dashboard/router.ts
|
|
3947
4084
|
function isDashboardRequest(url) {
|
|
3948
4085
|
return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
|
|
@@ -3966,6 +4103,12 @@ function createDashboardHandler(deps) {
|
|
|
3966
4103
|
routes[DASHBOARD_API_INSIGHTS] = createInsightsHandler(deps.analysisEngine);
|
|
3967
4104
|
routes[DASHBOARD_API_SECURITY] = createSecurityHandler(deps.analysisEngine);
|
|
3968
4105
|
}
|
|
4106
|
+
routes[DASHBOARD_API_TAB] = (req, res) => {
|
|
4107
|
+
const tab = (req.url ?? "").split("tab=")[1];
|
|
4108
|
+
if (tab && isTelemetryEnabled()) recordTabViewed(decodeURIComponent(tab));
|
|
4109
|
+
res.writeHead(204);
|
|
4110
|
+
res.end();
|
|
4111
|
+
};
|
|
3969
4112
|
return (req, res, config) => {
|
|
3970
4113
|
const path = (req.url ?? "/").split("?")[0];
|
|
3971
4114
|
const handler = routes[path];
|
|
@@ -3973,6 +4116,7 @@ function createDashboardHandler(deps) {
|
|
|
3973
4116
|
handler(req, res);
|
|
3974
4117
|
return;
|
|
3975
4118
|
}
|
|
4119
|
+
if (isTelemetryEnabled()) recordDashboardOpened();
|
|
3976
4120
|
res.writeHead(200, {
|
|
3977
4121
|
"content-type": "text/html; charset=utf-8",
|
|
3978
4122
|
"cache-control": "no-cache"
|
|
@@ -3998,7 +4142,7 @@ function createProxyServer(config, handleDashboard) {
|
|
|
3998
4142
|
|
|
3999
4143
|
// src/detect/project.ts
|
|
4000
4144
|
import { readFile as readFile2 } from "fs/promises";
|
|
4001
|
-
import { join } from "path";
|
|
4145
|
+
import { join as join2 } from "path";
|
|
4002
4146
|
var FRAMEWORKS = [
|
|
4003
4147
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
4004
4148
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -4007,7 +4151,7 @@ var FRAMEWORKS = [
|
|
|
4007
4151
|
{ name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
|
|
4008
4152
|
];
|
|
4009
4153
|
async function detectProject(rootDir) {
|
|
4010
|
-
const pkgPath =
|
|
4154
|
+
const pkgPath = join2(rootDir, "package.json");
|
|
4011
4155
|
const raw = await readFile2(pkgPath, "utf-8");
|
|
4012
4156
|
const pkg = JSON.parse(raw);
|
|
4013
4157
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
@@ -4019,7 +4163,7 @@ async function detectProject(rootDir) {
|
|
|
4019
4163
|
if (allDeps[f.dep]) {
|
|
4020
4164
|
framework = f.name;
|
|
4021
4165
|
devCommand = f.devCmd;
|
|
4022
|
-
devBin =
|
|
4166
|
+
devBin = join2(rootDir, "node_modules", ".bin", f.bin);
|
|
4023
4167
|
defaultPort = f.defaultPort;
|
|
4024
4168
|
break;
|
|
4025
4169
|
}
|
|
@@ -4028,11 +4172,11 @@ async function detectProject(rootDir) {
|
|
|
4028
4172
|
return { framework, devCommand, devBin, defaultPort, packageManager };
|
|
4029
4173
|
}
|
|
4030
4174
|
async function detectPackageManager(rootDir) {
|
|
4031
|
-
if (await fileExists(
|
|
4032
|
-
if (await fileExists(
|
|
4033
|
-
if (await fileExists(
|
|
4034
|
-
if (await fileExists(
|
|
4035
|
-
if (await fileExists(
|
|
4175
|
+
if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
|
|
4176
|
+
if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
|
|
4177
|
+
if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
4178
|
+
if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
|
|
4179
|
+
if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
|
|
4036
4180
|
return "unknown";
|
|
4037
4181
|
}
|
|
4038
4182
|
|
|
@@ -4575,7 +4719,6 @@ function normalizeQueryParams(sql) {
|
|
|
4575
4719
|
}
|
|
4576
4720
|
|
|
4577
4721
|
// src/analysis/insights.ts
|
|
4578
|
-
var AUTH_CATEGORIES = /* @__PURE__ */ new Set(["auth-handshake", "auth-check", "middleware"]);
|
|
4579
4722
|
function getQueryShape(q) {
|
|
4580
4723
|
if (q.sql) return normalizeQueryParams(q.sql) ?? "";
|
|
4581
4724
|
return `${q.operation ?? q.normalizedOp ?? "?"}:${q.model ?? q.table ?? ""}`;
|
|
@@ -4817,29 +4960,6 @@ function computeInsights(ctx) {
|
|
|
4817
4960
|
});
|
|
4818
4961
|
}
|
|
4819
4962
|
}
|
|
4820
|
-
for (const flow of ctx.flows) {
|
|
4821
|
-
if (!flow.requests || flow.requests.length < 2) continue;
|
|
4822
|
-
let authMs = 0;
|
|
4823
|
-
let totalMs = 0;
|
|
4824
|
-
for (const r of flow.requests) {
|
|
4825
|
-
const dur = r.pollingDurationMs ?? r.durationMs;
|
|
4826
|
-
totalMs += dur;
|
|
4827
|
-
if (AUTH_CATEGORIES.has(r.category ?? "")) authMs += dur;
|
|
4828
|
-
}
|
|
4829
|
-
if (totalMs > 0 && authMs > 0) {
|
|
4830
|
-
const pct = Math.round(authMs / totalMs * 100);
|
|
4831
|
-
if (pct >= AUTH_OVERHEAD_PCT) {
|
|
4832
|
-
insights.push({
|
|
4833
|
-
severity: "warning",
|
|
4834
|
-
type: "auth-overhead",
|
|
4835
|
-
title: "Auth Overhead",
|
|
4836
|
-
desc: `${flow.label} \u2014 ${pct}% of time (${formatDuration(authMs)}) spent in auth/middleware`,
|
|
4837
|
-
hint: "Auth checks consume a significant portion of this action. If using a third-party auth provider, check if session caching can reduce roundtrips.",
|
|
4838
|
-
nav: "actions"
|
|
4839
|
-
});
|
|
4840
|
-
}
|
|
4841
|
-
}
|
|
4842
|
-
}
|
|
4843
4963
|
const selectStarSeen = /* @__PURE__ */ new Map();
|
|
4844
4964
|
for (const [, reqQueries] of queriesByReq) {
|
|
4845
4965
|
for (const q of reqQueries) {
|
|
@@ -5061,7 +5181,7 @@ var AnalysisEngine = class {
|
|
|
5061
5181
|
};
|
|
5062
5182
|
|
|
5063
5183
|
// src/index.ts
|
|
5064
|
-
var VERSION = "0.6.
|
|
5184
|
+
var VERSION = "0.6.2";
|
|
5065
5185
|
|
|
5066
5186
|
// src/lifecycle/startup.ts
|
|
5067
5187
|
import pc2 from "picocolors";
|
|
@@ -5083,6 +5203,53 @@ function printBanner(proxyPort, targetPort) {
|
|
|
5083
5203
|
);
|
|
5084
5204
|
console.log();
|
|
5085
5205
|
}
|
|
5206
|
+
function severityIcon(severity) {
|
|
5207
|
+
if (severity === "critical") return pc.red("\u2717");
|
|
5208
|
+
if (severity === "warning") return pc.yellow("\u26A0");
|
|
5209
|
+
return pc.dim("\u25CB");
|
|
5210
|
+
}
|
|
5211
|
+
function colorTitle(severity, text) {
|
|
5212
|
+
if (severity === "critical") return pc.red(pc.bold(text));
|
|
5213
|
+
if (severity === "warning") return pc.yellow(pc.bold(text));
|
|
5214
|
+
return pc.dim(text);
|
|
5215
|
+
}
|
|
5216
|
+
function truncate(s, max = 80) {
|
|
5217
|
+
return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
|
|
5218
|
+
}
|
|
5219
|
+
function formatConsoleLine(insight, dashboardUrl, suffix) {
|
|
5220
|
+
const icon = severityIcon(insight.severity);
|
|
5221
|
+
const title = colorTitle(insight.severity, insight.title);
|
|
5222
|
+
const desc = pc.dim(truncate(insight.desc) + (suffix ?? ""));
|
|
5223
|
+
const link = pc.dim(`\u2192 ${dashboardUrl}`);
|
|
5224
|
+
return ` ${icon} ${title} \u2014 ${desc} ${link}`;
|
|
5225
|
+
}
|
|
5226
|
+
function createConsoleInsightListener(proxyPort, metricsStore) {
|
|
5227
|
+
const printedKeys = /* @__PURE__ */ new Set();
|
|
5228
|
+
const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
|
|
5229
|
+
return (insights) => {
|
|
5230
|
+
const lines = [];
|
|
5231
|
+
for (const insight of insights) {
|
|
5232
|
+
if (insight.severity === "info") continue;
|
|
5233
|
+
const endpoint = insight.desc.match(/^(\S+\s+\S+)/)?.[1] ?? insight.desc;
|
|
5234
|
+
const key = `${insight.type}:${endpoint}`;
|
|
5235
|
+
if (printedKeys.has(key)) continue;
|
|
5236
|
+
printedKeys.add(key);
|
|
5237
|
+
let suffix;
|
|
5238
|
+
if (insight.type === "slow") {
|
|
5239
|
+
const ep = metricsStore.getAll().find((e) => e.endpoint === endpoint);
|
|
5240
|
+
if (ep && ep.sessions.length > 1) {
|
|
5241
|
+
const prev = ep.sessions[ep.sessions.length - 2];
|
|
5242
|
+
suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
5245
|
+
lines.push(formatConsoleLine(insight, dashUrl, suffix));
|
|
5246
|
+
}
|
|
5247
|
+
if (lines.length > 0) {
|
|
5248
|
+
console.log();
|
|
5249
|
+
for (const line of lines) console.log(line);
|
|
5250
|
+
}
|
|
5251
|
+
};
|
|
5252
|
+
}
|
|
5086
5253
|
|
|
5087
5254
|
// src/process/spawn.ts
|
|
5088
5255
|
import { spawn } from "child_process";
|
|
@@ -5202,6 +5369,14 @@ async function startBrakit(opts) {
|
|
|
5202
5369
|
metricsStore.start();
|
|
5203
5370
|
const analysisEngine = new AnalysisEngine();
|
|
5204
5371
|
analysisEngine.start();
|
|
5372
|
+
analysisEngine.onUpdate(createConsoleInsightListener(proxyPort, metricsStore));
|
|
5373
|
+
if (isTelemetryEnabled()) {
|
|
5374
|
+
initSession(project.framework, project.packageManager, !!customCommand, []);
|
|
5375
|
+
analysisEngine.onUpdate((insights, findings) => {
|
|
5376
|
+
recordInsightTypes(insights.map((i) => i.type));
|
|
5377
|
+
recordRulesTriggered(findings.map((f) => f.rule));
|
|
5378
|
+
});
|
|
5379
|
+
}
|
|
5205
5380
|
const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine });
|
|
5206
5381
|
console.log(pc2.dim(` Starting ${project.devCommand} on port ${targetPort}...`));
|
|
5207
5382
|
const devProcess = customCommand ? spawnCustomCommand(customCommand, targetPort, proxyPort, rootDir) : spawnDevServer(project.devBin, targetPort, proxyPort, rootDir);
|
|
@@ -5215,9 +5390,12 @@ async function startBrakit(opts) {
|
|
|
5215
5390
|
proxy.listen(proxyPort, () => {
|
|
5216
5391
|
printBanner(proxyPort, targetPort);
|
|
5217
5392
|
});
|
|
5393
|
+
let reqCount = 0;
|
|
5218
5394
|
onRequest((req) => {
|
|
5219
5395
|
const queryCount = defaultQueryStore.getByRequest(req.id).length;
|
|
5220
5396
|
metricsStore.recordRequest(req, queryCount);
|
|
5397
|
+
reqCount++;
|
|
5398
|
+
recordRequestCount(reqCount);
|
|
5221
5399
|
});
|
|
5222
5400
|
return { proxy, devProcess, metricsStore, analysisEngine, config, project };
|
|
5223
5401
|
}
|
|
@@ -5230,6 +5408,7 @@ function createShutdownHandler(instance) {
|
|
|
5230
5408
|
if (shuttingDown) return;
|
|
5231
5409
|
shuttingDown = true;
|
|
5232
5410
|
console.log(pc3.dim("\n Shutting down..."));
|
|
5411
|
+
trackSession(instance.metricsStore, instance.analysisEngine);
|
|
5233
5412
|
instance.analysisEngine.stop();
|
|
5234
5413
|
instance.metricsStore.stop();
|
|
5235
5414
|
instance.proxy.close();
|
|
@@ -5290,8 +5469,54 @@ var dev_default = defineCommand({
|
|
|
5290
5469
|
}
|
|
5291
5470
|
});
|
|
5292
5471
|
|
|
5472
|
+
// src/cli/commands/telemetry.ts
|
|
5473
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
5474
|
+
import pc5 from "picocolors";
|
|
5475
|
+
var telemetry_default = defineCommand2({
|
|
5476
|
+
meta: {
|
|
5477
|
+
name: "telemetry",
|
|
5478
|
+
description: "Manage anonymous telemetry settings"
|
|
5479
|
+
},
|
|
5480
|
+
args: {
|
|
5481
|
+
action: {
|
|
5482
|
+
type: "positional",
|
|
5483
|
+
description: "on | off | status",
|
|
5484
|
+
required: false
|
|
5485
|
+
}
|
|
5486
|
+
},
|
|
5487
|
+
run({ args }) {
|
|
5488
|
+
const action = args.action?.toLowerCase();
|
|
5489
|
+
if (action === "on") {
|
|
5490
|
+
setTelemetryEnabled(true);
|
|
5491
|
+
console.log(pc5.green(" Telemetry enabled."));
|
|
5492
|
+
return;
|
|
5493
|
+
}
|
|
5494
|
+
if (action === "off") {
|
|
5495
|
+
setTelemetryEnabled(false);
|
|
5496
|
+
console.log(pc5.yellow(" Telemetry disabled. No data will be collected."));
|
|
5497
|
+
return;
|
|
5498
|
+
}
|
|
5499
|
+
const enabled = isTelemetryEnabled();
|
|
5500
|
+
console.log();
|
|
5501
|
+
console.log(` ${pc5.bold("Telemetry")}: ${enabled ? pc5.green("enabled") : pc5.yellow("disabled")}`);
|
|
5502
|
+
console.log();
|
|
5503
|
+
console.log(pc5.dim(" brakit collects anonymous usage data to improve the tool."));
|
|
5504
|
+
console.log(pc5.dim(" No URLs, queries, bodies, or source code are ever sent."));
|
|
5505
|
+
console.log();
|
|
5506
|
+
console.log(pc5.dim(" Opt out: ") + pc5.bold("brakit telemetry off"));
|
|
5507
|
+
console.log(pc5.dim(" Opt in: ") + pc5.bold("brakit telemetry on"));
|
|
5508
|
+
console.log(pc5.dim(" Env override: BRAKIT_TELEMETRY=false"));
|
|
5509
|
+
console.log();
|
|
5510
|
+
}
|
|
5511
|
+
});
|
|
5512
|
+
|
|
5293
5513
|
// bin/brakit.ts
|
|
5294
|
-
if (process.argv[2] === "
|
|
5514
|
+
if (process.argv[2] === "telemetry") {
|
|
5295
5515
|
process.argv.splice(2, 1);
|
|
5516
|
+
runMain(telemetry_default);
|
|
5517
|
+
} else {
|
|
5518
|
+
if (process.argv[2] === "dev") {
|
|
5519
|
+
process.argv.splice(2, 1);
|
|
5520
|
+
}
|
|
5521
|
+
runMain(dev_default);
|
|
5296
5522
|
}
|
|
5297
|
-
runMain(dev_default);
|
package/dist/index.d.ts
CHANGED
|
@@ -142,7 +142,7 @@ declare class SecurityScanner {
|
|
|
142
142
|
declare function createDefaultScanner(): SecurityScanner;
|
|
143
143
|
|
|
144
144
|
type InsightSeverity = "critical" | "warning" | "info";
|
|
145
|
-
type InsightType = "n1" | "cross-endpoint" | "redundant-query" | "error" | "error-hotspot" | "duplicate" | "slow" | "query-heavy" | "
|
|
145
|
+
type InsightType = "n1" | "cross-endpoint" | "redundant-query" | "error" | "error-hotspot" | "duplicate" | "slow" | "query-heavy" | "select-star" | "high-rows" | "large-response" | "response-overfetch" | "security";
|
|
146
146
|
interface Insight {
|
|
147
147
|
severity: InsightSeverity;
|
|
148
148
|
type: InsightType;
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,6 @@ var ERROR_RATE_THRESHOLD_PCT = 20;
|
|
|
24
24
|
var SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
|
|
25
25
|
var MIN_REQUESTS_FOR_INSIGHT = 2;
|
|
26
26
|
var HIGH_QUERY_COUNT_PER_REQ = 5;
|
|
27
|
-
var AUTH_OVERHEAD_PCT = 30;
|
|
28
27
|
var CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
|
|
29
28
|
var CROSS_ENDPOINT_PCT = 50;
|
|
30
29
|
var CROSS_ENDPOINT_MIN_OCCURRENCES = 5;
|
|
@@ -702,6 +701,17 @@ var HEALTH_GRADES = `[
|
|
|
702
701
|
{ max: Infinity, label: 'Critical', color: 'var(--red)', bg: 'rgba(220,38,38,0.08)', border: 'rgba(220,38,38,0.2)' }
|
|
703
702
|
]`;
|
|
704
703
|
|
|
704
|
+
// src/telemetry/index.ts
|
|
705
|
+
import { platform, release, arch } from "os";
|
|
706
|
+
|
|
707
|
+
// src/telemetry/config.ts
|
|
708
|
+
import { homedir } from "os";
|
|
709
|
+
import { join } from "path";
|
|
710
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
711
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
712
|
+
var CONFIG_DIR = join(homedir(), ".brakit");
|
|
713
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
714
|
+
|
|
705
715
|
// src/dashboard/router.ts
|
|
706
716
|
function isDashboardRequest(url) {
|
|
707
717
|
return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
|
|
@@ -724,7 +734,7 @@ function createProxyServer(config, handleDashboard) {
|
|
|
724
734
|
|
|
725
735
|
// src/detect/project.ts
|
|
726
736
|
import { readFile as readFile2 } from "fs/promises";
|
|
727
|
-
import { join } from "path";
|
|
737
|
+
import { join as join2 } from "path";
|
|
728
738
|
var FRAMEWORKS = [
|
|
729
739
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
730
740
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -733,7 +743,7 @@ var FRAMEWORKS = [
|
|
|
733
743
|
{ name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
|
|
734
744
|
];
|
|
735
745
|
async function detectProject(rootDir) {
|
|
736
|
-
const pkgPath =
|
|
746
|
+
const pkgPath = join2(rootDir, "package.json");
|
|
737
747
|
const raw = await readFile2(pkgPath, "utf-8");
|
|
738
748
|
const pkg = JSON.parse(raw);
|
|
739
749
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
@@ -745,7 +755,7 @@ async function detectProject(rootDir) {
|
|
|
745
755
|
if (allDeps[f.dep]) {
|
|
746
756
|
framework = f.name;
|
|
747
757
|
devCommand = f.devCmd;
|
|
748
|
-
devBin =
|
|
758
|
+
devBin = join2(rootDir, "node_modules", ".bin", f.bin);
|
|
749
759
|
defaultPort = f.defaultPort;
|
|
750
760
|
break;
|
|
751
761
|
}
|
|
@@ -754,11 +764,11 @@ async function detectProject(rootDir) {
|
|
|
754
764
|
return { framework, devCommand, devBin, defaultPort, packageManager };
|
|
755
765
|
}
|
|
756
766
|
async function detectPackageManager(rootDir) {
|
|
757
|
-
if (await fileExists(
|
|
758
|
-
if (await fileExists(
|
|
759
|
-
if (await fileExists(
|
|
760
|
-
if (await fileExists(
|
|
761
|
-
if (await fileExists(
|
|
767
|
+
if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
|
|
768
|
+
if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
|
|
769
|
+
if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
770
|
+
if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
|
|
771
|
+
if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
|
|
762
772
|
return "unknown";
|
|
763
773
|
}
|
|
764
774
|
|
|
@@ -1333,7 +1343,6 @@ function normalizeQueryParams(sql) {
|
|
|
1333
1343
|
}
|
|
1334
1344
|
|
|
1335
1345
|
// src/analysis/insights.ts
|
|
1336
|
-
var AUTH_CATEGORIES = /* @__PURE__ */ new Set(["auth-handshake", "auth-check", "middleware"]);
|
|
1337
1346
|
function getQueryShape(q) {
|
|
1338
1347
|
if (q.sql) return normalizeQueryParams(q.sql) ?? "";
|
|
1339
1348
|
return `${q.operation ?? q.normalizedOp ?? "?"}:${q.model ?? q.table ?? ""}`;
|
|
@@ -1575,29 +1584,6 @@ function computeInsights(ctx) {
|
|
|
1575
1584
|
});
|
|
1576
1585
|
}
|
|
1577
1586
|
}
|
|
1578
|
-
for (const flow of ctx.flows) {
|
|
1579
|
-
if (!flow.requests || flow.requests.length < 2) continue;
|
|
1580
|
-
let authMs = 0;
|
|
1581
|
-
let totalMs = 0;
|
|
1582
|
-
for (const r of flow.requests) {
|
|
1583
|
-
const dur = r.pollingDurationMs ?? r.durationMs;
|
|
1584
|
-
totalMs += dur;
|
|
1585
|
-
if (AUTH_CATEGORIES.has(r.category ?? "")) authMs += dur;
|
|
1586
|
-
}
|
|
1587
|
-
if (totalMs > 0 && authMs > 0) {
|
|
1588
|
-
const pct = Math.round(authMs / totalMs * 100);
|
|
1589
|
-
if (pct >= AUTH_OVERHEAD_PCT) {
|
|
1590
|
-
insights.push({
|
|
1591
|
-
severity: "warning",
|
|
1592
|
-
type: "auth-overhead",
|
|
1593
|
-
title: "Auth Overhead",
|
|
1594
|
-
desc: `${flow.label} \u2014 ${pct}% of time (${formatDuration(authMs)}) spent in auth/middleware`,
|
|
1595
|
-
hint: "Auth checks consume a significant portion of this action. If using a third-party auth provider, check if session caching can reduce roundtrips.",
|
|
1596
|
-
nav: "actions"
|
|
1597
|
-
});
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
1587
|
const selectStarSeen = /* @__PURE__ */ new Map();
|
|
1602
1588
|
for (const [, reqQueries] of queriesByReq) {
|
|
1603
1589
|
for (const q of reqQueries) {
|
|
@@ -1819,7 +1805,7 @@ var AnalysisEngine = class {
|
|
|
1819
1805
|
};
|
|
1820
1806
|
|
|
1821
1807
|
// src/index.ts
|
|
1822
|
-
var VERSION = "0.6.
|
|
1808
|
+
var VERSION = "0.6.2";
|
|
1823
1809
|
export {
|
|
1824
1810
|
AdapterRegistry,
|
|
1825
1811
|
AnalysisEngine,
|
package/package.json
CHANGED