55 * Usage:
66 * ```typescript
77 * import {
8+ * setupTelemetryExitHandlers,
9+ * finalizeTelemetry,
10+ * finalizeTelemetrySync,
811 * trackCliStart,
912 * trackCliEvent,
1013 * trackCliComplete,
1417 * trackSubprocessError
1518 * } from './utils/telemetry/integration.mts'
1619 *
20+ * // Set up exit handlers once during CLI initialization.
21+ * setupTelemetryExitHandlers()
22+ *
1723 * // Track main CLI execution.
1824 * const startTime = await trackCliStart(process.argv)
1925 * await trackCliComplete(process.argv, startTime, 0)
2733 *
2834 * // On subprocess error.
2935 * await trackSubprocessError('npm', subStart, error, 1)
36+ *
37+ * // Manual finalization (usually not needed if exit handlers are set up).
38+ * await finalizeTelemetry() // Async version.
39+ * finalizeTelemetrySync() // Sync version (best-effort).
3040 * ```
3141 */
3242import { homedir } from 'node:os'
3343import process from 'node:process'
3444
3545import { debugFn } from '@socketsecurity/registry/lib/debug'
46+ import { escapeRegExp } from '@socketsecurity/registry/lib/regexps'
3647
3748import { TelemetryService } from './service.mts'
3849import constants , { CONFIG_KEY_DEFAULT_ORG } from '../../constants.mts'
@@ -48,8 +59,9 @@ const debug = (message: string): void => {
4859}
4960
5061/**
51- * Finalize telemetry and clean up resources.
62+ * Finalize telemetry and clean up resources (async version) .
5263 * This should be called before process.exit to ensure telemetry is sent and resources are cleaned up.
64+ * Use this in async contexts like beforeExit handlers.
5365 *
5466 * @returns Promise that resolves when finalization completes.
5567 */
@@ -61,6 +73,76 @@ export async function finalizeTelemetry(): Promise<void> {
6173 }
6274}
6375
76+ /**
77+ * Finalize telemetry synchronously (best-effort).
78+ * This triggers a flush without awaiting it.
79+ * Use this in synchronous contexts like signal handlers where async operations are not possible.
80+ *
81+ * Note: This is best-effort only. Events may be lost if the process exits before flush completes.
82+ * Prefer finalizeTelemetry() (async version) when possible.
83+ */
84+ export function finalizeTelemetrySync ( ) : void {
85+ const instance = TelemetryService . getCurrentInstance ( )
86+ if ( instance ) {
87+ debug ( 'Triggering sync flush (best-effort)' )
88+ void instance . flush ( )
89+ }
90+ }
91+
92+ /**
93+ * Set up exit handlers for telemetry finalization.
94+ * This registers handlers for both normal exits (beforeExit) and common fatal signals.
95+ *
96+ * Flushing strategy:
97+ * - Batch-based: Auto-flush when queue reaches 10 events.
98+ * - beforeExit: Async handler for clean shutdowns (when event loop empties).
99+ * - Fatal signals (SIGINT, SIGTERM, SIGHUP): Best-effort sync flush.
100+ * - Accepts that forced exits (SIGKILL, process.exit()) may lose final events.
101+ *
102+ * Call this once during CLI initialization to ensure telemetry is flushed on exit.
103+ *
104+ * @example
105+ * ```typescript
106+ * // In src/cli.mts
107+ * setupTelemetryExitHandlers()
108+ * ```
109+ */
110+ export function setupTelemetryExitHandlers ( ) : void {
111+ let handlersRegistered = false
112+
113+ // Use beforeExit for async finalization during clean shutdowns.
114+ // This fires when the event loop empties but before process actually exits.
115+ process . on ( 'beforeExit' , ( ) => {
116+ if ( ! handlersRegistered ) {
117+ return
118+ }
119+ debug ( 'beforeExit handler triggered' )
120+ void finalizeTelemetry ( )
121+ } )
122+
123+ // Register handlers for common fatal signals as best-effort fallback.
124+ // These are synchronous contexts, so we can only trigger flush without awaiting.
125+ const fatalSignals : NodeJS . Signals [ ] = [ 'SIGINT' , 'SIGTERM' , 'SIGHUP' ]
126+
127+ for ( const signal of fatalSignals ) {
128+ try {
129+ process . on ( signal , ( ) => {
130+ if ( ! handlersRegistered ) {
131+ return
132+ }
133+ debug ( `Signal ${ signal } received, attempting sync flush` )
134+ finalizeTelemetrySync ( )
135+ } )
136+ } catch ( e ) {
137+ // Some signals may not be available on all platforms.
138+ debug ( `Failed to register handler for signal ${ signal } : ${ e } ` )
139+ }
140+ }
141+
142+ handlersRegistered = true
143+ debug ( 'Telemetry exit handlers registered (beforeExit + common signals)' )
144+ }
145+
64146/**
65147 * Track subprocess exit and finalize telemetry.
66148 * This is a convenience function that tracks completion/error based on exit code
@@ -200,7 +282,7 @@ function sanitizeArgv(argv: string[]): string[] {
200282 // Remove user home directory from file paths.
201283 const homeDir = homedir ( )
202284 if ( homeDir ) {
203- return arg . replace ( new RegExp ( homeDir , 'g' ) , '~' )
285+ return arg . replace ( new RegExp ( escapeRegExp ( homeDir ) , 'g' ) , '~' )
204286 }
205287
206288 return arg
@@ -222,7 +304,7 @@ function sanitizeErrorAttribute(input: string | undefined): string | undefined {
222304 // Remove user home directory.
223305 const homeDir = homedir ( )
224306 if ( homeDir ) {
225- return input . replace ( new RegExp ( homeDir , 'g' ) , '~' )
307+ return input . replace ( new RegExp ( escapeRegExp ( homeDir ) , 'g' ) , '~' )
226308 }
227309
228310 return input
@@ -324,6 +406,7 @@ export async function trackCliEvent(
324406/**
325407 * Track CLI completion event.
326408 * Should be called on successful CLI exit.
409+ * Events are flushed automatically by exit handlers.
327410 *
328411 * @param argv
329412 * @param startTime Start timestamp from trackCliStart.
@@ -336,22 +419,16 @@ export async function trackCliComplete(
336419) : Promise < void > {
337420 debug ( 'Capture end of command' )
338421
339- await trackEvent (
340- 'cli_complete' ,
341- buildContext ( argv ) ,
342- {
343- duration : calculateDuration ( startTime ) ,
344- exit_code : normalizeExitCode ( exitCode , 0 ) ,
345- } ,
346- {
347- flush : true ,
348- } ,
349- )
422+ await trackEvent ( 'cli_complete' , buildContext ( argv ) , {
423+ duration : calculateDuration ( startTime ) ,
424+ exit_code : normalizeExitCode ( exitCode , 0 ) ,
425+ } )
350426}
351427
352428/**
353429 * Track CLI error event.
354430 * Should be called when CLI exits with an error.
431+ * Events are flushed automatically by exit handlers.
355432 *
356433 * @param argv
357434 * @param startTime Start timestamp from trackCliStart.
@@ -375,7 +452,6 @@ export async function trackCliError(
375452 } ,
376453 {
377454 error : normalizeError ( error ) ,
378- flush : true ,
379455 } ,
380456 )
381457}
0 commit comments