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.
@@ -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 startTime = performance.now();
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
- startTime,
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, startTime, bodyChunks, config, requestId) {
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 startTime = first.startedAt;
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 - startTime),
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 = join(rootDir, "package.json");
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 = join(rootDir, "node_modules", ".bin", f.bin);
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(join(rootDir, "bun.lockb"))) return "bun";
4032
- if (await fileExists(join(rootDir, "bun.lock"))) return "bun";
4033
- if (await fileExists(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
4034
- if (await fileExists(join(rootDir, "yarn.lock"))) return "yarn";
4035
- if (await fileExists(join(rootDir, "package-lock.json"))) return "npm";
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.1";
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] === "dev") {
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" | "auth-overhead" | "select-star" | "high-rows" | "large-response" | "response-overfetch" | "security";
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 = join(rootDir, "package.json");
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 = join(rootDir, "node_modules", ".bin", f.bin);
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(join(rootDir, "bun.lockb"))) return "bun";
758
- if (await fileExists(join(rootDir, "bun.lock"))) return "bun";
759
- if (await fileExists(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
760
- if (await fileExists(join(rootDir, "yarn.lock"))) return "yarn";
761
- if (await fileExists(join(rootDir, "package-lock.json"))) return "npm";
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.1";
1808
+ var VERSION = "0.6.2";
1823
1809
  export {
1824
1810
  AdapterRegistry,
1825
1811
  AnalysisEngine,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brakit",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "See what your API is really doing. Security scanning, N+1 detection, duplicate calls, DB queries — one command, zero config.",
5
5
  "type": "module",
6
6
  "bin": {