Skip to content

Commit 20f0186

Browse files
committed
perf: break @sentry/bun import chain for fast completions
Shell completions were loading @sentry/bun (~280ms) via the import chain: complete.ts → db/index.ts → telemetry.ts → @sentry/bun. Completions only read cached SQLite data and never need telemetry. Changes: - db/index.ts: Remove top-level telemetry import. Add disableDbTracing() flag (same pattern as disableResponseCache/disableOrgCache). Lazy- require telemetry.ts inside getDatabase() only when tracing is enabled. - bin.ts: Move __complete dispatch before all heavy imports. Restructure as lightweight dispatcher: runCompletion() loads only db + complete modules; runCli() loads the full CLI with telemetry, Stricli, etc. Results (compiled binary): - Completion: 320ms → 190ms (40% faster) - Dev mode: 530ms → 60ms (89% faster) - Normal commands: unchanged (~510ms)
1 parent 5d272c9 commit 20f0186

File tree

2 files changed

+181
-128
lines changed

2 files changed

+181
-128
lines changed

src/bin.ts

Lines changed: 153 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,10 @@
1-
import { isatty } from "node:tty";
2-
import { run } from "@stricli/core";
3-
import { app } from "./app.js";
4-
import { buildContext } from "./context.js";
5-
import { AuthError, formatError, getExitCode } from "./lib/errors.js";
6-
import { error } from "./lib/formatters/colors.js";
7-
import { runInteractiveLogin } from "./lib/interactive-login.js";
8-
import { getEnvLogLevel, setLogLevel } from "./lib/logger.js";
9-
import { isTrialEligible, promptAndStartTrial } from "./lib/seer-trial.js";
10-
import { withTelemetry } from "./lib/telemetry.js";
11-
import { startCleanupOldBinary } from "./lib/upgrade.js";
12-
import {
13-
abortPendingVersionCheck,
14-
getUpdateNotification,
15-
maybeCheckForUpdateInBackground,
16-
shouldSuppressNotification,
17-
} from "./lib/version-check.js";
1+
/**
2+
* CLI entry point with fast-path dispatch.
3+
*
4+
* Shell completion (`__complete`) is dispatched before any heavy imports
5+
* to avoid loading `@sentry/bun` (~280ms). All other commands go through
6+
* the full CLI with telemetry, middleware, and error recovery.
7+
*/
188

199
// Exit cleanly when downstream pipe consumer closes (e.g., `sentry issue list | head`).
2010
// EPIPE (errno -32) is normal Unix behavior — not an error. Node.js/Bun ignore SIGPIPE
@@ -30,6 +20,20 @@ function handleStreamError(err: NodeJS.ErrnoException): void {
3020
process.stdout.on("error", handleStreamError);
3121
process.stderr.on("error", handleStreamError);
3222

23+
/**
24+
* Fast-path: shell completion.
25+
*
26+
* Dispatched before importing the full CLI to avoid loading @sentry/bun,
27+
* @stricli/core, and other heavy dependencies. Only loads the lightweight
28+
* completion engine and SQLite cache modules.
29+
*/
30+
async function runCompletion(completionArgs: string[]): Promise<void> {
31+
const { disableDbTracing } = await import("./lib/db/index.js");
32+
disableDbTracing();
33+
const { handleComplete } = await import("./lib/complete.js");
34+
await handleComplete(completionArgs);
35+
}
36+
3337
/**
3438
* Error-recovery middleware for the CLI.
3539
*
@@ -46,128 +50,142 @@ process.stderr.on("error", handleStreamError);
4650
* @returns A function with the same signature, with error recovery added
4751
*/
4852
type ErrorMiddleware = (
49-
next: (argv: string[]) => Promise<void>,
50-
args: string[]
53+
proceed: (cmdInput: string[]) => Promise<void>,
54+
retryArgs: string[]
5155
) => Promise<void>;
5256

5357
/**
54-
* Seer trial prompt middleware.
58+
* Full CLI execution with telemetry, middleware, and error recovery.
5559
*
56-
* Catches trial-eligible SeerErrors and offers to start a free trial.
57-
* On success, retries the original command. On failure/decline, re-throws
58-
* so the outer error handler displays the full error with upgrade URL.
60+
* All heavy imports are loaded here (not at module top level) so the
61+
* `__complete` fast-path can skip them entirely.
5962
*/
60-
const seerTrialMiddleware: ErrorMiddleware = async (next, args) => {
61-
try {
62-
await next(args);
63-
} catch (err) {
64-
if (isTrialEligible(err)) {
65-
const started = await promptAndStartTrial(
66-
// biome-ignore lint/style/noNonNullAssertion: isTrialEligible guarantees orgSlug is defined
67-
err.orgSlug!,
68-
err.reason
69-
);
70-
71-
if (started) {
72-
process.stderr.write("\nRetrying command...\n\n");
73-
await next(args);
74-
return;
63+
async function runCli(cliArgs: string[]): Promise<void> {
64+
const { isatty } = await import("node:tty");
65+
const { run } = await import("@stricli/core");
66+
const { app } = await import("./app.js");
67+
const { buildContext } = await import("./context.js");
68+
const { AuthError, formatError, getExitCode } = await import(
69+
"./lib/errors.js"
70+
);
71+
const { error } = await import("./lib/formatters/colors.js");
72+
const { runInteractiveLogin } = await import("./lib/interactive-login.js");
73+
const { getEnvLogLevel, setLogLevel } = await import("./lib/logger.js");
74+
const { isTrialEligible, promptAndStartTrial } = await import(
75+
"./lib/seer-trial.js"
76+
);
77+
const { withTelemetry } = await import("./lib/telemetry.js");
78+
const { startCleanupOldBinary } = await import("./lib/upgrade.js");
79+
const {
80+
abortPendingVersionCheck,
81+
getUpdateNotification,
82+
maybeCheckForUpdateInBackground,
83+
shouldSuppressNotification,
84+
} = await import("./lib/version-check.js");
85+
86+
// ---------------------------------------------------------------------------
87+
// Error-recovery middleware
88+
// ---------------------------------------------------------------------------
89+
90+
/**
91+
* Seer trial prompt middleware.
92+
*
93+
* Catches trial-eligible SeerErrors and offers to start a free trial.
94+
* On success, retries the original command. On failure/decline, re-throws
95+
* so the outer error handler displays the full error with upgrade URL.
96+
*/
97+
const seerTrialMiddleware: ErrorMiddleware = async (next, argv) => {
98+
try {
99+
await next(argv);
100+
} catch (err) {
101+
if (isTrialEligible(err)) {
102+
const started = await promptAndStartTrial(
103+
// biome-ignore lint/style/noNonNullAssertion: isTrialEligible guarantees orgSlug is defined
104+
err.orgSlug!,
105+
err.reason
106+
);
107+
108+
if (started) {
109+
process.stderr.write("\nRetrying command...\n\n");
110+
await next(argv);
111+
return;
112+
}
75113
}
114+
throw err;
76115
}
77-
throw err;
78-
}
79-
};
80-
81-
/**
82-
* Auto-authentication middleware.
83-
*
84-
* Catches auth errors (not_authenticated, expired) in interactive TTYs
85-
* and runs the login flow. On success, retries through the full middleware
86-
* chain so inner middlewares (e.g., trial prompt) also apply to the retry.
87-
*/
88-
const autoAuthMiddleware: ErrorMiddleware = async (next, args) => {
89-
try {
90-
await next(args);
91-
} catch (err) {
92-
// Use isatty(0) for reliable stdin TTY detection (process.stdin.isTTY can be undefined in Bun)
93-
// Errors can opt-out via skipAutoAuth (e.g., auth status command)
94-
if (
95-
err instanceof AuthError &&
96-
(err.reason === "not_authenticated" || err.reason === "expired") &&
97-
!err.skipAutoAuth &&
98-
isatty(0)
99-
) {
100-
process.stderr.write(
101-
err.reason === "expired"
102-
? "Authentication expired. Starting login flow...\n\n"
103-
: "Authentication required. Starting login flow...\n\n"
104-
);
105-
106-
const loginSuccess = await runInteractiveLogin();
107-
108-
if (loginSuccess) {
109-
process.stderr.write("\nRetrying command...\n\n");
110-
await next(args);
116+
};
117+
118+
/**
119+
* Auto-authentication middleware.
120+
*
121+
* Catches auth errors (not_authenticated, expired) in interactive TTYs
122+
* and runs the login flow. On success, retries through the full middleware
123+
* chain so inner middlewares (e.g., trial prompt) also apply to the retry.
124+
*/
125+
const autoAuthMiddleware: ErrorMiddleware = async (next, argv) => {
126+
try {
127+
await next(argv);
128+
} catch (err) {
129+
// Use isatty(0) for reliable stdin TTY detection (process.stdin.isTTY can be undefined in Bun)
130+
// Errors can opt-out via skipAutoAuth (e.g., auth status command)
131+
if (
132+
err instanceof AuthError &&
133+
(err.reason === "not_authenticated" || err.reason === "expired") &&
134+
!err.skipAutoAuth &&
135+
isatty(0)
136+
) {
137+
process.stderr.write(
138+
err.reason === "expired"
139+
? "Authentication expired. Starting login flow...\n\n"
140+
: "Authentication required. Starting login flow...\n\n"
141+
);
142+
143+
const loginSuccess = await runInteractiveLogin();
144+
145+
if (loginSuccess) {
146+
process.stderr.write("\nRetrying command...\n\n");
147+
await next(argv);
148+
return;
149+
}
150+
151+
// Login failed or was cancelled
152+
process.exitCode = 1;
111153
return;
112154
}
113155

114-
// Login failed or was cancelled
115-
process.exitCode = 1;
116-
return;
156+
throw err;
117157
}
118-
119-
throw err;
158+
};
159+
160+
/**
161+
* Error-recovery middlewares applied around command execution.
162+
*
163+
* Order matters: applied innermost-first, so the last entry wraps the
164+
* outermost layer. Auth middleware is outermost so it catches errors
165+
* from both the command and any inner middleware retries.
166+
*/
167+
const errorMiddlewares: ErrorMiddleware[] = [
168+
seerTrialMiddleware,
169+
autoAuthMiddleware,
170+
];
171+
172+
/** Run CLI command with telemetry wrapper */
173+
async function runCommand(argv: string[]): Promise<void> {
174+
await withTelemetry(async (span) =>
175+
run(app, argv, buildContext(process, span))
176+
);
120177
}
121-
};
122-
123-
/**
124-
* Error-recovery middlewares applied around command execution.
125-
*
126-
* Order matters: applied innermost-first, so the last entry wraps the
127-
* outermost layer. Auth middleware is outermost so it catches errors
128-
* from both the command and any inner middleware retries.
129-
*
130-
* To add a new middleware, append it to this array.
131-
*/
132-
const errorMiddlewares: ErrorMiddleware[] = [
133-
seerTrialMiddleware,
134-
autoAuthMiddleware,
135-
];
136-
137-
/** Run CLI command with telemetry wrapper */
138-
async function runCommand(args: string[]): Promise<void> {
139-
await withTelemetry(async (span) =>
140-
run(app, args, buildContext(process, span))
141-
);
142-
}
143178

144-
/**
145-
* Build the command executor by composing error-recovery middlewares.
146-
*
147-
* Wraps `runCommand` with each middleware in order (innermost-first),
148-
* producing a single function that handles all error recovery.
149-
*/
150-
function buildExecutor(): (args: string[]) => Promise<void> {
179+
/** Build the command executor by composing error-recovery middlewares. */
151180
let executor = runCommand;
152181
for (const mw of errorMiddlewares) {
153-
const next = executor;
154-
executor = (args) => mw(next, args);
182+
const inner = executor;
183+
executor = (argv) => mw(inner, argv);
155184
}
156-
return executor;
157-
}
158-
159-
/** Command executor with all error-recovery middlewares applied */
160-
const executeCommand = buildExecutor();
161185

162-
async function main(): Promise<void> {
163-
const args = process.argv.slice(2);
164-
165-
// Fast-path: shell completion (no telemetry, no middleware, no upgrade check)
166-
if (args[0] === "__complete") {
167-
const { handleComplete } = await import("./lib/complete.js");
168-
await handleComplete(args.slice(1));
169-
return;
170-
}
186+
// ---------------------------------------------------------------------------
187+
// Main execution
188+
// ---------------------------------------------------------------------------
171189

172190
// Clean up old binary from previous Windows upgrade (no-op if file doesn't exist)
173191
startCleanupOldBinary();
@@ -180,15 +198,15 @@ async function main(): Promise<void> {
180198
setLogLevel(envLogLevel);
181199
}
182200

183-
const suppressNotification = shouldSuppressNotification(args);
201+
const suppressNotification = shouldSuppressNotification(cliArgs);
184202

185203
// Start background update check (non-blocking)
186204
if (!suppressNotification) {
187205
maybeCheckForUpdateInBackground();
188206
}
189207

190208
try {
191-
await executeCommand(args);
209+
await executor(cliArgs);
192210
} catch (err) {
193211
process.stderr.write(`${error("Error:")} ${formatError(err)}\n`);
194212
process.exitCode = getExitCode(err);
@@ -207,4 +225,14 @@ async function main(): Promise<void> {
207225
}
208226
}
209227

210-
main();
228+
// ---------------------------------------------------------------------------
229+
// Dispatch: check argv before any heavy imports
230+
// ---------------------------------------------------------------------------
231+
232+
const args = process.argv.slice(2);
233+
234+
if (args[0] === "__complete") {
235+
runCompletion(args.slice(1));
236+
} else {
237+
runCli(args);
238+
}

src/lib/db/index.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import { Database } from "bun:sqlite";
77
import { chmodSync, mkdirSync } from "node:fs";
88
import { join } from "node:path";
9-
import { createTracedDatabase } from "../telemetry.js";
109
import { migrateFromJson } from "./migration.js";
1110
import { initSchema, runMigrations } from "./schema.js";
1211

@@ -22,6 +21,23 @@ const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
2221
/** Probability of running cleanup on write operations */
2322
const CLEANUP_PROBABILITY = 0.1;
2423

24+
/** When true, skip the Sentry tracing wrapper on the database connection. */
25+
let tracingDisabled = false;
26+
27+
/**
28+
* Disable database query tracing for this process.
29+
*
30+
* Call before the first `getDatabase()` invocation to avoid loading
31+
* `@sentry/bun` (~280ms). Used by the `__complete` fast-path where
32+
* only cached reads are needed and telemetry adds unacceptable latency.
33+
*
34+
* Follows the same pattern as {@link disableResponseCache} and
35+
* {@link disableOrgCache}.
36+
*/
37+
export function disableDbTracing(): void {
38+
tracingDisabled = true;
39+
}
40+
2541
/** Traced database wrapper (returned by getDatabase) */
2642
let db: Database | null = null;
2743
/** Raw database without tracing (used for repair operations) */
@@ -100,8 +116,17 @@ export function getDatabase(): Database {
100116
runMigrations(rawDb);
101117
migrateFromJson(rawDb);
102118

103-
// Wrap with tracing proxy for automatic query instrumentation
104-
db = createTracedDatabase(rawDb);
119+
// Wrap with tracing proxy for automatic query instrumentation.
120+
// Lazy-require telemetry to avoid top-level import of @sentry/bun (~280ms).
121+
// Shell completions disable tracing entirely via disableDbTracing().
122+
if (tracingDisabled) {
123+
db = rawDb;
124+
} else {
125+
const { createTracedDatabase } = require("../telemetry.js") as {
126+
createTracedDatabase: (d: Database) => Database;
127+
};
128+
db = createTracedDatabase(rawDb);
129+
}
105130
dbOpenedPath = dbPath;
106131

107132
return db;

0 commit comments

Comments
 (0)