@@ -49,11 +49,36 @@ async function initTelemetryContext(): Promise<void> {
4949 }
5050}
5151
52+ /**
53+ * Mark the active session as crashed.
54+ *
55+ * Checks both current scope and isolation scope since processSessionIntegration
56+ * stores the session on the isolation scope. Called when a command error
57+ * propagates through withTelemetry — the SDK auto-marks crashes for truly
58+ * uncaught exceptions (mechanism.handled === false), but command errors need
59+ * explicit marking.
60+ *
61+ * @internal Exported for testing
62+ */
63+ export function markSessionCrashed ( ) : void {
64+ const session =
65+ Sentry . getCurrentScope ( ) . getSession ( ) ??
66+ Sentry . getIsolationScope ( ) . getSession ( ) ;
67+ if ( session ) {
68+ session . status = "crashed" ;
69+ }
70+ }
71+
5272/**
5373 * Wrap CLI execution with telemetry tracking.
5474 *
55- * Creates a Sentry session and span for the command execution.
56- * Captures any unhandled exceptions and reports them.
75+ * Creates a Sentry span for the command execution and captures exceptions.
76+ * Session lifecycle is managed by the SDK's processSessionIntegration
77+ * (started during Sentry.init) and a beforeExit handler (registered in
78+ * initSentry) that ends healthy sessions and flushes events. This ensures
79+ * sessions are reliably tracked even for unhandled rejections and other
80+ * paths that bypass this function's try/catch.
81+ *
5782 * Telemetry can be disabled via SENTRY_CLI_NO_TELEMETRY=1 env var.
5883 *
5984 * @param callback - The CLI execution function to wrap, receives the span for naming
@@ -71,9 +96,6 @@ export async function withTelemetry<T>(
7196 // Initialize user and instance context
7297 await initTelemetryContext ( ) ;
7398
74- Sentry . startSession ( ) ;
75- Sentry . captureSession ( ) ;
76-
7799 try {
78100 return await Sentry . startSpanManual (
79101 { name : "cli.command" , op : "cli.command" , forceTransaction : true } ,
@@ -92,31 +114,47 @@ export async function withTelemetry<T>(
92114 e instanceof AuthError && e . reason === "not_authenticated" ;
93115 if ( ! isExpectedAuthState ) {
94116 Sentry . captureException ( e ) ;
95- const session = Sentry . getCurrentScope ( ) . getSession ( ) ;
96- if ( session ) {
97- session . status = "crashed" ;
98- }
117+ markSessionCrashed ( ) ;
99118 }
100119 throw e ;
101- } finally {
102- Sentry . endSession ( ) ;
103- // Flush with a timeout to ensure events are sent before process exits
104- try {
105- await client . flush ( 3000 ) ;
106- } catch {
107- // Ignore flush errors - telemetry should never block CLI
108- }
109120 }
110121}
111122
112123/**
113- * Initialize Sentry for telemetry .
124+ * Create a beforeExit handler that ends healthy sessions and flushes events .
114125 *
115- * @param enabled - Whether telemetry is enabled
116- * @returns The Sentry client, or undefined if initialization failed
126+ * The SDK's processSessionIntegration only ends non-OK sessions (crashed/errored).
127+ * This handler complements it by ending OK sessions (clean exit → 'exited')
128+ * and flushing pending events. Includes a re-entry guard since flush is async
129+ * and causes beforeExit to re-fire when complete.
130+ *
131+ * @param client - The Sentry client to flush
132+ * @returns A handler function for process.on("beforeExit")
117133 *
118134 * @internal Exported for testing
119135 */
136+ export function createBeforeExitHandler ( client : Sentry . BunClient ) : ( ) => void {
137+ let isFlushing = false ;
138+ return ( ) => {
139+ if ( isFlushing ) {
140+ return ;
141+ }
142+ isFlushing = true ;
143+
144+ const session = Sentry . getIsolationScope ( ) . getSession ( ) ;
145+ if ( session ?. status === "ok" ) {
146+ Sentry . endSession ( ) ;
147+ }
148+
149+ // Flush pending events before exit. Convert PromiseLike to Promise
150+ // for proper error handling. The async work causes beforeExit to
151+ // re-fire when complete, which the isFlushing guard handles.
152+ Promise . resolve ( client . flush ( 3000 ) ) . catch ( ( ) => {
153+ // Ignore flush errors — telemetry should never block CLI exit
154+ } ) ;
155+ } ;
156+ }
157+
120158/**
121159 * Integrations to exclude for CLI.
122160 * These add overhead without benefit for short-lived CLI processes.
@@ -128,6 +166,17 @@ const EXCLUDED_INTEGRATIONS = new Set([
128166 "Modules" , // Lists all loaded modules - unnecessary for CLI telemetry
129167] ) ;
130168
169+ /** Current beforeExit handler, tracked so it can be replaced on re-init */
170+ let currentBeforeExitHandler : ( ( ) => void ) | null = null ;
171+
172+ /**
173+ * Initialize Sentry for telemetry.
174+ *
175+ * @param enabled - Whether telemetry is enabled
176+ * @returns The Sentry client, or undefined if initialization failed
177+ *
178+ * @internal Exported for testing
179+ */
131180export function initSentry ( enabled : boolean ) : Sentry . BunClient | undefined {
132181 const environment = process . env . NODE_ENV ?? "development" ;
133182
@@ -187,6 +236,23 @@ export function initSentry(enabled: boolean): Sentry.BunClient | undefined {
187236
188237 // Tag whether targeting self-hosted Sentry (not SaaS)
189238 Sentry . setTag ( "is_self_hosted" , ! isSentrySaasUrl ( getSentryBaseUrl ( ) ) ) ;
239+
240+ // End healthy sessions and flush events when the event loop drains.
241+ // The SDK's processSessionIntegration starts a session during init and
242+ // registers its own beforeExit handler that ends non-OK (crashed/errored)
243+ // sessions. We complement it by ending OK sessions (clean exit → 'exited')
244+ // and flushing pending events. This covers unhandled rejections and other
245+ // paths that bypass withTelemetry's try/catch.
246+ // Ref: https://nodejs.org/api/process.html#event-beforeexit
247+ //
248+ // Replace previous handler on re-init (e.g., auto-login retry calls
249+ // withTelemetry → initSentry twice) to avoid duplicate handlers with
250+ // independent re-entry guards and stale client references.
251+ if ( currentBeforeExitHandler ) {
252+ process . removeListener ( "beforeExit" , currentBeforeExitHandler ) ;
253+ }
254+ currentBeforeExitHandler = createBeforeExitHandler ( client ) ;
255+ process . on ( "beforeExit" , currentBeforeExitHandler ) ;
190256 }
191257
192258 return client ;
0 commit comments