-
Notifications
You must be signed in to change notification settings - Fork 41
fix: emit telemetry eagerly in dev --logs to avoid SIGINT race #1372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -381,51 +381,57 @@ export const registerDev = (program: Command) => { | |
| console.log(`Log: ${logger.getRelativeLogPath()}`); | ||
| console.log(`Press Ctrl+C to stop\n`); | ||
|
|
||
| const devResult = await withCommandRunTelemetry( | ||
| 'dev', | ||
| { | ||
| dev_action: 'server' as const, | ||
| ui_mode: 'terminal' as const, | ||
| has_stream: false, | ||
| agent_protocol: standardize(AgentProtocol, (config.protocol ?? 'http').toLowerCase()), | ||
| invoke_count: 0, | ||
| }, | ||
| async (): Promise<Result> => { | ||
| await new Promise<void>((resolve, reject) => { | ||
| const devCallbacks = { | ||
| onLog: (level: string, msg: string) => { | ||
| const prefix = level === 'error' ? '❌' : level === 'warn' ? '⚠️' : '→'; | ||
| console.log(`${prefix} ${msg}`); | ||
| logger.log(msg, level === 'error' ? 'error' : 'info'); | ||
| }, | ||
| onExit: (code: number | null) => { | ||
| console.log(`\nServer exited with code ${code ?? 0}`); | ||
| logger.finalize(code === 0); | ||
| if (code !== 0 && code !== null) { | ||
| reject(new Error(`Server exited with code ${code}`)); | ||
| } else { | ||
| resolve(); | ||
| } | ||
| }, | ||
| }; | ||
|
|
||
| const server = createDevServer(config, { | ||
| port: actualPort, | ||
| envVars: mergedEnvVars, | ||
| callbacks: devCallbacks, | ||
| }); | ||
| server.start().catch(reject); | ||
|
|
||
| process.once('SIGINT', () => { | ||
| console.log('\nStopping server...'); | ||
| collector?.stop(); | ||
| server.kill(); | ||
| }); | ||
| // Emit telemetry eagerly before the blocking server loop. | ||
| // The global SIGINT handler calls process.exit(0) synchronously, | ||
| // which would prevent withCommandRunTelemetry from ever flushing. | ||
| { | ||
| const client = await TelemetryClientAccessor.get().catch(() => undefined); | ||
| if (client) { | ||
| client.emit('cli.command_run', 0, { | ||
| command_group: 'dev', | ||
| command: 'dev', | ||
| exit_reason: 'success', | ||
| dev_action: 'server', | ||
| ui_mode: 'terminal', | ||
| has_stream: false, | ||
| agent_protocol: standardize(AgentProtocol, (config.protocol ?? 'http').toLowerCase()), | ||
| invoke_count: 0, | ||
| }); | ||
| return { success: true as const }; | ||
| await client.flush(); | ||
| } | ||
| ); | ||
| if (!devResult.success) throw devResult.error; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No test coverage for this fix. |
||
|
|
||
| await new Promise<void>((resolve, reject) => { | ||
| const devCallbacks = { | ||
| onLog: (level: string, msg: string) => { | ||
| const prefix = level === 'error' ? '❌' : level === 'warn' ? '⚠️' : '→'; | ||
| console.log(`${prefix} ${msg}`); | ||
| logger.log(msg, level === 'error' ? 'error' : 'info'); | ||
| }, | ||
| onExit: (code: number | null) => { | ||
| console.log(`\nServer exited with code ${code ?? 0}`); | ||
| logger.finalize(code === 0); | ||
| if (code !== 0 && code !== null) { | ||
| reject(new Error(`Server exited with code ${code}`)); | ||
| } else { | ||
| resolve(); | ||
| } | ||
| }, | ||
| }; | ||
|
|
||
| const server = createDevServer(config, { | ||
| port: actualPort, | ||
| envVars: mergedEnvVars, | ||
| callbacks: devCallbacks, | ||
| }); | ||
| server.start().catch(reject); | ||
|
|
||
| process.once('SIGINT', () => { | ||
| console.log('\nStopping server...'); | ||
| collector?.stop(); | ||
| server.kill(); | ||
| }); | ||
| }); | ||
| process.exit(0); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoding
exit_reason: 'success'here loses the failure-telemetry behavior the previouswithCommandRunTelemetrywrapper provided. Ifserver.start()rejects (e.g. binary not found, port grabbed betweenfindAvailablePortandserver.start), oronExitfires with a non-zero code, theawait new Promise(...)below throws, the outercatch (error)at line 513 prints the error and exits 1 — but telemetry has already recorded this as a successful run. PreviouslytrackCommandRun/classifyErrorwould have emittedexit_reason: 'failure'witherror_nameanderror_source.A few ways to fix:
await new Promiseand wrapserver.start()/ startup in a try-block such that failures emit a separate failure event (and skip the success emit).setupGlobalCleanupto await registered async cleanups beforeprocess.exit), then keepwithCommandRunTelemetryand get correct success/failure telemetry for free. Bigger change but fixes the root cause and would also help browser-mode.runBrowserMode. Least work but means we lose error classification on dev-server startup failures.My preference would be (a) since it's localized and preserves the existing failure-telemetry contract.