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,79 @@ 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+ // Track whether exit handlers have been set up to prevent duplicate registration.
93+ let exitHandlersRegistered = false
94+
95+ /**
96+ * Set up exit handlers for telemetry finalization.
97+ * This registers handlers for both normal exits (beforeExit) and common fatal signals.
98+ *
99+ * Flushing strategy:
100+ * - Batch-based: Auto-flush when queue reaches 10 events.
101+ * - beforeExit: Async handler for clean shutdowns (when event loop empties).
102+ * - Fatal signals (SIGINT, SIGTERM, SIGHUP): Best-effort sync flush.
103+ * - Accepts that forced exits (SIGKILL, process.exit()) may lose final events.
104+ *
105+ * Call this once during CLI initialization to ensure telemetry is flushed on exit.
106+ * Safe to call multiple times - only registers handlers once.
107+ *
108+ * @example
109+ * ```typescript
110+ * // In src/cli.mts
111+ * setupTelemetryExitHandlers()
112+ * ```
113+ */
114+ export function setupTelemetryExitHandlers ( ) : void {
115+ // Prevent duplicate handler registration.
116+ if ( exitHandlersRegistered ) {
117+ debug ( 'Telemetry exit handlers already registered, skipping' )
118+ return
119+ }
120+
121+ exitHandlersRegistered = true
122+
123+ // Use beforeExit for async finalization during clean shutdowns.
124+ // This fires when the event loop empties but before process actually exits.
125+ process . on ( 'beforeExit' , ( ) => {
126+ debug ( 'beforeExit handler triggered' )
127+ void finalizeTelemetry ( )
128+ } )
129+
130+ // Register handlers for common fatal signals as best-effort fallback.
131+ // These are synchronous contexts, so we can only trigger flush without awaiting.
132+ const fatalSignals : NodeJS . Signals [ ] = [ 'SIGINT' , 'SIGTERM' , 'SIGHUP' ]
133+
134+ for ( const signal of fatalSignals ) {
135+ try {
136+ process . on ( signal , ( ) => {
137+ debug ( `Signal ${ signal } received, attempting sync flush` )
138+ finalizeTelemetrySync ( )
139+ } )
140+ } catch ( e ) {
141+ // Some signals may not be available on all platforms.
142+ debug ( `Failed to register handler for signal ${ signal } : ${ e } ` )
143+ }
144+ }
145+
146+ debug ( 'Telemetry exit handlers registered (beforeExit + common signals)' )
147+ }
148+
64149/**
65150 * Track subprocess exit and finalize telemetry.
66151 * This is a convenience function that tracks completion/error based on exit code
@@ -200,7 +285,7 @@ function sanitizeArgv(argv: string[]): string[] {
200285 // Remove user home directory from file paths.
201286 const homeDir = homedir ( )
202287 if ( homeDir ) {
203- return arg . replace ( new RegExp ( homeDir , 'g' ) , '~' )
288+ return arg . replace ( new RegExp ( escapeRegExp ( homeDir ) , 'g' ) , '~' )
204289 }
205290
206291 return arg
@@ -222,15 +307,18 @@ function sanitizeErrorAttribute(input: string | undefined): string | undefined {
222307 // Remove user home directory.
223308 const homeDir = homedir ( )
224309 if ( homeDir ) {
225- return input . replace ( new RegExp ( homeDir , 'g' ) , '~' )
310+ return input . replace ( new RegExp ( escapeRegExp ( homeDir ) , 'g' ) , '~' )
226311 }
227312
228313 return input
229314}
230315
231316/**
232317 * Generic event tracking function.
233- * Tracks any telemetry event with optional error details and flush.
318+ * Tracks any telemetry event with optional error details and explicit flush.
319+ *
320+ * Events are automatically flushed via batch size or exit handlers.
321+ * Use the flush option only when immediate submission is required.
234322 *
235323 * @param eventType Type of event to track.
236324 * @param context Event context.
@@ -324,6 +412,7 @@ export async function trackCliEvent(
324412/**
325413 * Track CLI completion event.
326414 * Should be called on successful CLI exit.
415+ * Flushes immediately since this is typically the last event before process exit.
327416 *
328417 * @param argv
329418 * @param startTime Start timestamp from trackCliStart.
@@ -352,6 +441,7 @@ export async function trackCliComplete(
352441/**
353442 * Track CLI error event.
354443 * Should be called when CLI exits with an error.
444+ * Flushes immediately since this is typically the last event before process exit.
355445 *
356446 * @param argv
357447 * @param startTime Start timestamp from trackCliStart.
0 commit comments