-
Notifications
You must be signed in to change notification settings - Fork 235
Expand file tree
/
Copy pathtelemetry-utils.ts
More file actions
165 lines (146 loc) · 4.27 KB
/
telemetry-utils.ts
File metadata and controls
165 lines (146 loc) · 4.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import {APIClient} from '@heroku-cli/command'
import {Config} from '@oclif/core/config'
import debug from 'debug'
import {spawn} from 'node:child_process'
import path from 'path'
import {fileURLToPath} from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const root = path.resolve(__dirname, '../../../package.json')
// Debug instance for telemetry operations
export const telemetryDebug = debug('analytics-telemetry')
// Environment flags
export const isDev = process.env.IS_DEV_ENVIRONMENT === 'true'
export const isTelemetryDisabled = process.env.DISABLE_TELEMETRY === 'true'
// Cached values
let version: string | undefined
let cachedToken: string | undefined
export interface CLIError extends Error {
cliRunDuration?: number | string
code?: string
context?: {
isTTY?: boolean
}
http?: {
statusCode?: number
}
oclif?: {
exit?: number
}
statusCode?: number
}
export interface Telemetry {
cliRunDuration: number
command: string
commandRunDuration: number
exitCode: number
exitState: string
isTTY: boolean | undefined
isVersionOrHelp: boolean
lifecycleHookCompletion: {
command_not_found: boolean
init: boolean
postrun: boolean
prerun: boolean
}
os: string
version: string
}
// Union type for data that can be sent to telemetry
export type TelemetryData = CLIError | Telemetry
export interface TelemetryGlobal {
cliTelemetry?: Telemetry
}
/**
* Compute duration from a start time to now
*/
export function computeDuration(cmdStartTime: number): number {
const now = new Date()
const cmdFinishTime = now.getTime()
return cmdFinishTime - cmdStartTime
}
/**
* Get authentication token, cached to avoid recreating Config/APIClient
*/
export function getToken(): string | undefined {
if (cachedToken !== undefined) {
return cachedToken
}
try {
const config = new Config({root})
const heroku = new APIClient(config)
cachedToken = heroku.auth
return cachedToken
} catch {
// If config initialization fails, return empty string
cachedToken = ''
return cachedToken
}
}
/**
* Get CLI version
*/
export function getVersion(): string {
return version || 'unknown'
}
/**
* Check if telemetry is enabled based on environment variables
*/
export function isTelemetryEnabled(): boolean {
if (process.env.DISABLE_TELEMETRY === 'true') return false
if (process.platform === 'win32' && process.env.ENABLE_WINDOWS_TELEMETRY !== 'true') return false
if (process.env.IS_HEROKU_TEST_ENV === 'true') return false
return true
}
/**
* Serialize data for telemetry worker, handling Error objects specially
*/
export function serializeTelemetryData(data: TelemetryData): string {
// If it's an Error object, convert to plain object with all properties
if (data instanceof Error) {
const errorData = data as CLIError
return JSON.stringify({
// Include any other enumerable properties first
...data,
// Then override with important properties to ensure they're captured
cliRunDuration: errorData.cliRunDuration,
code: errorData.code,
http: errorData.http,
message: errorData.message,
name: errorData.name,
oclif: errorData.oclif,
stack: errorData.stack,
statusCode: errorData.statusCode,
})
}
return JSON.stringify(data)
}
/**
* Set CLI version (called once during setup)
*/
export function setVersion(v: string): void {
version = v
}
/**
* Spawn telemetry worker process in background
* This avoids blocking the main CLI process with telemetry overhead
*/
export function spawnTelemetryWorker(data: TelemetryData): void {
try {
const workerPath = path.join(__dirname, '..', '..', '..', 'dist', 'lib', 'analytics-telemetry', 'telemetry-worker.js')
const child = spawn(process.execPath, [workerPath], {
detached: true,
// Keep stderr attached to see DEBUG output, but ignore stdout
stdio: ['pipe', 'ignore', 'inherit'],
// On Windows, prevent console window from appearing
windowsHide: true,
})
// Send data via stdin
child.stdin.write(serializeTelemetryData(data))
child.stdin.end()
// Detach from parent so it can exit immediately
child.unref()
} catch {
// Silently fail - don't let telemetry errors affect user experience
}
}