Skip to content

Commit 6da79cd

Browse files
authored
fix: restore beforeExit handler for version commands and add comprehensive telemetry debug logging (#3657)
* fix: restore beforeExit handler for version commands and add comprehensive telemetry debug logging * Clean up linting
1 parent 3297c5e commit 6da79cd

9 files changed

Lines changed: 88 additions & 19 deletions

File tree

bin/run.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ process.env.HEROKU_UPDATE_INSTRUCTIONS = process.env.HEROKU_UPDATE_INSTRUCTIONS
1212
const now = new Date()
1313
const cliStartTime = now.getTime()
1414

15-
const {isTelemetryEnabled} = await import('../dist/lib/analytics-telemetry/telemetry-utils.js')
15+
const {isTelemetryEnabled, getTelemetryDisabledReason, telemetryDebug} = await import('../dist/lib/analytics-telemetry/telemetry-utils.js')
16+
const enableTelemetry = isTelemetryEnabled()
1617

17-
if (isTelemetryEnabled()) {
18+
if (enableTelemetry) {
19+
telemetryDebug('Telemetry enabled: setting up handlers (beforeExit, SIGINT, SIGTERM)')
1820
// Dynamically import telemetry modules
1921
const {setupTelemetryHandlers} = await import('../dist/lib/analytics-telemetry/worker-client.js')
2022
const {computeDuration} = await import('../dist/lib/analytics-telemetry/telemetry-utils.js')
@@ -23,8 +25,11 @@ if (isTelemetryEnabled()) {
2325
setupTelemetryHandlers({
2426
cliStartTime,
2527
computeDuration,
26-
enableTelemetry: isTelemetryEnabled(),
28+
enableTelemetry,
2729
})
30+
} else {
31+
const reason = getTelemetryDisabledReason()
32+
telemetryDebug('Telemetry disabled (%s): skipping telemetry handler setup', reason)
2833
}
2934

3035
await execute({dir: import.meta.url})

src/hooks/command_not_found/setup-otel-telemetry.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import {Hook} from '@oclif/core/hooks'
22

33
const performance_analytics: Hook<'command_not_found'> = async function () {
4-
const {isTelemetryEnabled} = await import('../../lib/analytics-telemetry/telemetry-utils.js')
4+
const {isTelemetryEnabled, getTelemetryDisabledReason, telemetryDebug} = await import('../../lib/analytics-telemetry/telemetry-utils.js')
55

66
// Use the consolidated telemetry check
77
if (!isTelemetryEnabled()) {
8+
const reason = getTelemetryDisabledReason()
9+
telemetryDebug('Telemetry disabled (%s): skipping command_not_found hook, not setting up telemetry for invalid command', reason)
810
return
911
}
1012

13+
telemetryDebug('Telemetry enabled: command_not_found hook setting up telemetry for invalid command')
1114
const {telemetryManager} = await import('../../lib/analytics-telemetry/telemetry-manager.js')
1215
const globalAny = global as any
1316
globalAny.cliTelemetry = telemetryManager.reportCmdNotFound(this.config)

src/hooks/finally/send-otel-and-sentry-errors.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,16 @@ const finallyHook: Hook<'finally'> = async function (options) {
4040
return
4141
}
4242

43-
const {isTelemetryEnabled, spawnTelemetryWorker} = await import('../../lib/analytics-telemetry/telemetry-utils.js')
43+
const {isTelemetryEnabled, getTelemetryDisabledReason, spawnTelemetryWorker, telemetryDebug} = await import('../../lib/analytics-telemetry/telemetry-utils.js')
4444

4545
// Use the consolidated telemetry check
4646
if (!isTelemetryEnabled()) {
47+
const reason = getTelemetryDisabledReason()
48+
telemetryDebug('Telemetry disabled (%s): skipping finally hook, not sending error: %s', reason, options.error.message)
4749
return
4850
}
4951

52+
telemetryDebug('Telemetry enabled: finally hook spawning worker to send error: %s', options.error.message)
5053
// Spawn background process to send error without blocking
5154
spawnTelemetryWorker(options.error)
5255
}

src/hooks/init/setup-otel-telemetry.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import {Hook} from '@oclif/core/hooks'
22

33
const performance_analytics: Hook<'init'> = async function (options) {
4-
const {isTelemetryEnabled} = await import('../../lib/analytics-telemetry/telemetry-utils.js')
4+
const {isTelemetryEnabled, getTelemetryDisabledReason, telemetryDebug} = await import('../../lib/analytics-telemetry/telemetry-utils.js')
55

66
// Use the consolidated telemetry check
77
if (!isTelemetryEnabled()) {
8+
const reason = getTelemetryDisabledReason()
9+
telemetryDebug('Telemetry disabled (%s): skipping init hook, not setting up telemetry object', reason)
810
return
911
}
1012

13+
telemetryDebug('Telemetry enabled: init hook setting up telemetry object for command')
1114
const {telemetryManager} = await import('../../lib/analytics-telemetry/telemetry-manager.js')
1215
const globalAny = global as any
1316
globalAny.cliTelemetry = telemetryManager.setupTelemetry(this.config, options)

src/hooks/postrun/send-otel-telemetry.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,25 @@ const performance_analytics: Hook<'postrun'> = async function () {
77
return
88
}
99

10-
const {computeDuration, isTelemetryEnabled, spawnTelemetryWorker} = await import('../../lib/analytics-telemetry/telemetry-utils.js')
10+
const {computeDuration, isTelemetryEnabled, getTelemetryDisabledReason, spawnTelemetryWorker, telemetryDebug} = await import('../../lib/analytics-telemetry/telemetry-utils.js')
1111

1212
// Use the consolidated telemetry check
1313
if (!isTelemetryEnabled()) {
14+
const reason = getTelemetryDisabledReason()
15+
telemetryDebug('Telemetry disabled (%s): skipping postrun hook, not sending telemetry for command: %s', reason, globalAny.cliTelemetry.command)
1416
return
1517
}
1618

19+
telemetryDebug('Telemetry enabled: postrun hook spawning worker to send telemetry for command: %s', globalAny.cliTelemetry.command)
1720
const cmdStartTime = globalAny.cliTelemetry.commandRunDuration
1821
globalAny.cliTelemetry.commandRunDuration = computeDuration(cmdStartTime)
1922
globalAny.cliTelemetry.lifecycleHookCompletion.postrun = true
2023

2124
// Spawn background process to send telemetry without blocking
2225
spawnTelemetryWorker(globalAny.cliTelemetry)
26+
27+
// Mark telemetry as sent to prevent duplicate sends from beforeExit handler
28+
globalAny.telemetrySent = true
2329
}
2430

2531
export default performance_analytics

src/hooks/prerun/collect-and-send-herokulytics.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import {Hook} from '@oclif/core/hooks'
22

33
const analytics: Hook<'prerun'> = async function (options) {
4-
const {isTelemetryEnabled, spawnTelemetryWorker} = await import('../../lib/analytics-telemetry/telemetry-utils.js')
4+
const {isTelemetryEnabled, getTelemetryDisabledReason, spawnTelemetryWorker, telemetryDebug} = await import('../../lib/analytics-telemetry/telemetry-utils.js')
55

66
// Use the consolidated telemetry check
77
if (!isTelemetryEnabled()) {
8+
const reason = getTelemetryDisabledReason()
9+
telemetryDebug('Telemetry disabled (%s): skipping prerun hook, not sending Herokulytics for command: %s', reason, options.Command.id)
810
return
911
}
1012

13+
telemetryDebug('Telemetry enabled: prerun hook spawning worker to send Herokulytics for command: %s', options.Command.id)
1114
const {telemetryManager} = await import('../../lib/analytics-telemetry/telemetry-manager.js')
1215
const globalAny = global as any
1316

src/lib/analytics-telemetry/telemetry-manager.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ class TelemetryManager {
6565
*/
6666
async sendTelemetry(currentTelemetry: TelemetryData): Promise<void> {
6767
if (!isTelemetryEnabled()) {
68-
telemetryDebug('Telemetry disabled, skipping send')
6968
return
7069
}
7170

src/lib/analytics-telemetry/telemetry-utils.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export type TelemetryData = CLIError | Telemetry
7171

7272
export interface TelemetryGlobal {
7373
cliTelemetry?: Telemetry
74+
telemetrySent?: boolean
7475
}
7576

7677
// All data types that can be sent via worker
@@ -85,6 +86,25 @@ export function computeDuration(cmdStartTime: number): number {
8586
return cmdFinishTime - cmdStartTime
8687
}
8788

89+
/**
90+
* Get the reason why telemetry is disabled (for logging purposes)
91+
*/
92+
export function getTelemetryDisabledReason(): null | string {
93+
if (process.env.DISABLE_TELEMETRY === 'true') {
94+
return 'DISABLE_TELEMETRY=true'
95+
}
96+
97+
if (process.platform === 'win32' && process.env.ENABLE_WINDOWS_TELEMETRY !== 'true') {
98+
return 'Windows platform requires ENABLE_WINDOWS_TELEMETRY=true'
99+
}
100+
101+
if (process.env.IS_HEROKU_TEST_ENV === 'true') {
102+
return 'IS_HEROKU_TEST_ENV=true'
103+
}
104+
105+
return null
106+
}
107+
88108
/**
89109
* Get authentication token, cached to avoid recreating Config/APIClient
90110
* Lazy-loads @heroku-cli/command and @oclif/core/config to avoid loading them during CLI init
@@ -119,11 +139,21 @@ export function getVersion(): string {
119139

120140
/**
121141
* Check if telemetry is enabled based on environment variables
142+
* Returns both the enabled status and a reason string for logging
122143
*/
123144
export function isTelemetryEnabled(): boolean {
124-
if (process.env.DISABLE_TELEMETRY === 'true') return false
125-
if (process.platform === 'win32' && process.env.ENABLE_WINDOWS_TELEMETRY !== 'true') return false
126-
if (process.env.IS_HEROKU_TEST_ENV === 'true') return false
145+
if (process.env.DISABLE_TELEMETRY === 'true') {
146+
return false
147+
}
148+
149+
if (process.platform === 'win32' && process.env.ENABLE_WINDOWS_TELEMETRY !== 'true') {
150+
return false
151+
}
152+
153+
if (process.env.IS_HEROKU_TEST_ENV === 'true') {
154+
return false
155+
}
156+
127157
return true
128158
}
129159

src/lib/analytics-telemetry/worker-client.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
/* eslint-disable n/no-process-exit */
2-
import {CLIError, spawnTelemetryWorker, TelemetryGlobal} from './telemetry-utils.js'
2+
/* eslint-disable no-var */
3+
import {
4+
CLIError, spawnTelemetryWorker, telemetryDebug, TelemetryGlobal,
5+
} from './telemetry-utils.js'
36

47
// Extend global with telemetry property
58
declare global {
6-
// eslint-disable-next-line no-var
79
var cliTelemetry: TelemetryGlobal['cliTelemetry']
10+
var telemetrySent: TelemetryGlobal['telemetrySent']
811
}
912

1013
interface SetupTelemetryOptions {
@@ -14,17 +17,31 @@ interface SetupTelemetryOptions {
1417
}
1518

1619
/**
17-
* Setup telemetry handlers for signal handlers
18-
* Note: Normal command completion telemetry is handled by the postrun hook.
19-
* This only handles SIGINT/SIGTERM cases where hooks don't run.
20+
* Setup telemetry handlers for beforeExit and signal handlers
21+
* - beforeExit: Fallback for commands where postrun hook doesn't run (e.g., version, --help flags)
22+
* - postrun hook: Handles regular commands (sets telemetrySent flag to prevent duplicates)
23+
* - SIGINT/SIGTERM: Handles interrupted commands
2024
*/
2125
export function setupTelemetryHandlers(options: SetupTelemetryOptions): void {
2226
const {cliStartTime, computeDuration, enableTelemetry} = options
2327

2428
if (!enableTelemetry) return
2529

26-
// Note: beforeExit handler removed to avoid duplicate telemetry sends.
27-
// The postrun hook now handles normal command completion telemetry.
30+
// Fallback handler for commands that don't trigger postrun hook
31+
// (e.g., version, --version, --help flags handled by oclif)
32+
process.once('beforeExit', code => {
33+
// Only send if telemetry wasn't already sent by postrun hook
34+
if (global.cliTelemetry && !global.telemetrySent) {
35+
telemetryDebug('Telemetry enabled: beforeExit spawning worker to send telemetry for command: %s (postrun did not run)', global.cliTelemetry.command)
36+
const cmdStartTime = global.cliTelemetry.commandRunDuration
37+
global.cliTelemetry.commandRunDuration = computeDuration(cmdStartTime)
38+
global.cliTelemetry.exitCode = code
39+
global.cliTelemetry.cliRunDuration = computeDuration(cliStartTime)
40+
41+
spawnTelemetryWorker(global.cliTelemetry)
42+
global.telemetrySent = true
43+
}
44+
})
2845

2946
process.on('SIGINT', () => {
3047
// Spawn background process to send telemetry

0 commit comments

Comments
 (0)