heroku 11.0.2 → 11.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +14 -16
- package/bin/run.js +13 -49
- package/dist/commands/domains/add.d.ts +1 -1
- package/dist/commands/pg/backups/schedule.d.ts +1 -1
- package/dist/commands/spaces/index.d.ts +1 -1
- package/dist/commands/spaces/vpn/connections.d.ts +1 -1
- package/dist/hooks/command_not_found/performance_analytics.js +5 -4
- package/dist/hooks/finally/sentry.d.ts +3 -0
- package/dist/hooks/finally/sentry.js +42 -0
- package/dist/hooks/init/performance_analytics.js +5 -4
- package/dist/hooks/postrun/performance_analytics.js +6 -6
- package/dist/hooks/prerun/analytics.js +14 -7
- package/dist/lib/analytics-telemetry/global-telemetry.d.ts +30 -0
- package/dist/lib/analytics-telemetry/global-telemetry.js +103 -0
- package/dist/lib/analytics-telemetry/honeycomb-client.d.ts +15 -0
- package/dist/lib/analytics-telemetry/honeycomb-client.js +135 -0
- package/dist/lib/analytics-telemetry/sentry-client.d.ts +9 -0
- package/dist/lib/analytics-telemetry/sentry-client.js +58 -0
- package/dist/lib/analytics-telemetry/telemetry-utils.d.ts +68 -0
- package/dist/lib/analytics-telemetry/telemetry-utils.js +115 -0
- package/dist/lib/analytics-telemetry/telemetry-worker.d.ts +5 -0
- package/dist/lib/analytics-telemetry/telemetry-worker.js +37 -0
- package/dist/lib/analytics-telemetry/worker-client.d.ts +15 -0
- package/dist/lib/analytics-telemetry/worker-client.js +44 -0
- package/dist/lib/api.d.ts +1 -1
- package/dist/lib/apps/app-transfer.d.ts +1 -1
- package/dist/lib/apps/error_info.d.ts +1 -1
- package/dist/lib/apps/generation.d.ts +3 -3
- package/dist/lib/buildpacks/buildpacks.d.ts +1 -1
- package/dist/lib/container/docker_helper.d.ts +4 -4
- package/dist/lib/data/types.d.ts +37 -37
- package/dist/lib/domains/domains.js +15 -8
- package/dist/lib/pg/download.d.ts +1 -1
- package/dist/lib/pg/push_pull.d.ts +1 -1
- package/dist/lib/pg/setter.d.ts +1 -1
- package/dist/lib/pg/types.d.ts +31 -31
- package/dist/lib/pipelines/setup/validate.d.ts +1 -1
- package/dist/lib/redis/api.d.ts +7 -7
- package/dist/lib/spaces/hosts.d.ts +1 -1
- package/dist/lib/types/app_errors.d.ts +1 -1
- package/dist/lib/types/completion.d.ts +1 -1
- package/dist/lib/types/favorites.d.ts +2 -2
- package/dist/lib/types/notifications.d.ts +3 -3
- package/dist/lib/utils/multisort.d.ts +1 -1
- package/npm-shrinkwrap.json +837 -842
- package/oclif.manifest.json +1018 -1018
- package/package.json +7 -5
- package/dist/global_telemetry.d.ts +0 -62
- package/dist/global_telemetry.js +0 -216
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
## [11.1.0](https://github.com/heroku/cli/compare/v11.0.2...v11.1.0) (2026-03-31)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* add isTTY context to telemetry and filter SIGINT from Sentry ([#3632](https://github.com/heroku/cli/issues/3632)) ([74bbdfb](https://github.com/heroku/cli/commit/74bbdfbe45eef40b7232c9159e3cc4a541290296))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Performance Improvements
|
|
16
|
+
|
|
17
|
+
* optimize telemetry initialization and reduce startup overhead ([#3620](https://github.com/heroku/cli/issues/3620)) ([65575a0](https://github.com/heroku/cli/commit/65575a01f7da69d602af52d1991f880be23bd3df))
|
|
18
|
+
|
|
7
19
|
## [11.0.2](https://github.com/heroku/cli/compare/v11.0.1...v11.0.2) (2026-03-27)
|
|
8
20
|
|
|
9
21
|
|
package/README.md
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-

|
|
5
|
-
[](https://github.com/heroku/cli/actions/workflows/ci.yml)
|
|
6
|
-
[](https://www.npmjs.com/package/heroku)
|
|
7
|
-
[](https://github.com/heroku/cli/blob/main/LICENSE)
|
|
8
|
-
|
|
9
|
-
The Heroku CLI is used to manage Heroku apps from the command line. It is built using [oclif](https://oclif.io).
|
|
10
|
-
|
|
11
|
-
For more about Heroku see <https://www.heroku.com/home>
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="assets/Heroku-Logo-Mark-Light-RGB.svg" alt="Heroku logo" width="100">
|
|
3
|
+
<p> </p>
|
|
12
4
|
|
|
13
|
-
|
|
5
|
+
[](https://github.com/heroku/cli/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/heroku)
|
|
7
|
+
[](https://github.com/heroku/cli/blob/main/LICENSE)
|
|
8
|
+
</div>
|
|
14
9
|
|
|
15
10
|
Overview
|
|
16
11
|
========
|
|
17
12
|
|
|
18
13
|
The Heroku CLI is a command-line interface for managing Heroku applications and services. Built with Node.js and [oclif](https://oclif.io), it provides an extensible architecture for interacting with the Heroku platform.
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
For more about Heroku see <https://www.heroku.com/home>
|
|
16
|
+
|
|
17
|
+
To get started see <https://devcenter.heroku.com/start>
|
|
18
|
+
|
|
19
|
+
Key features of the CLI include:
|
|
21
20
|
|
|
22
21
|
- **App management** - Deploy, scale, and monitor your applications
|
|
23
22
|
- **Heroku Postgres database management** - Backup, restore, and manage Heroku Postgres databases
|
|
@@ -116,8 +115,7 @@ Using WebStorm (from JetBrains / IntelliJ), you can run/debug an individual test
|
|
|
116
115
|
- Create a new run/debug configuration
|
|
117
116
|
- Select the 'Mocha' type
|
|
118
117
|
|
|
119
|
-
|
|
118
|
+
Contributing
|
|
120
119
|
=========
|
|
121
|
-
See the [Heroku CLI Release Steps](https://salesforce.quip.com/aPLDA1ZwjNlW).
|
|
122
120
|
|
|
123
|
-
|
|
121
|
+
Please review our [Contributing guidelines](./CONTRIBUTING.md) as well as our [PR template](./.github/PULL_REQUEST_TEMPLATE.md).
|
package/bin/run.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --no-deprecation
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
/* eslint-disable n/no-unpublished-bin */
|
|
4
4
|
|
|
5
5
|
import {execute, settings} from '@oclif/core'
|
|
6
6
|
|
|
7
|
-
// Enable performance tracking when
|
|
8
|
-
if (process.env.DEBUG?.includes('oclif:perf') || process.env.DEBUG === '*') {
|
|
7
|
+
// Enable performance tracking when oclif:perf is specified in DEBUG
|
|
8
|
+
if (process.env.DEBUG?.includes('oclif:perf') || process.env.DEBUG === 'oclif:*' || process.env.DEBUG === '*') {
|
|
9
9
|
settings.performanceEnabled = true
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -16,54 +16,18 @@ const cliStartTime = now.getTime()
|
|
|
16
16
|
|
|
17
17
|
// Skip telemetry entirely on Windows for performance (unless explicitly enabled)
|
|
18
18
|
const enableTelemetry = process.platform !== 'win32' || process.env.ENABLE_WINDOWS_TELEMETRY === 'true'
|
|
19
|
-
let globalTelemetry
|
|
20
|
-
|
|
21
|
-
if (enableTelemetry) {
|
|
22
|
-
// Dynamically import telemetry only when needed
|
|
23
|
-
globalTelemetry = await import('../dist/global_telemetry.js')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
process.once('beforeExit', async code => {
|
|
27
|
-
if (!enableTelemetry) return
|
|
28
|
-
|
|
29
|
-
// capture as successful exit
|
|
30
|
-
if (global.cliTelemetry) {
|
|
31
|
-
if (global.cliTelemetry.isVersionOrHelp) {
|
|
32
|
-
const cmdStartTime = global.cliTelemetry.commandRunDuration
|
|
33
|
-
global.cliTelemetry.commandRunDuration = globalTelemetry.computeDuration(cmdStartTime)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
global.cliTelemetry.exitCode = code
|
|
37
|
-
global.cliTelemetry.cliRunDuration = globalTelemetry.computeDuration(cliStartTime)
|
|
38
|
-
const telemetryData = global.cliTelemetry
|
|
39
|
-
await globalTelemetry.sendTelemetry(telemetryData)
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
process.on('SIGINT', () => {
|
|
44
|
-
if (enableTelemetry) {
|
|
45
|
-
// Fire-and-forget: attempt to send telemetry but don't block exit
|
|
46
|
-
const error = new Error('Received SIGINT')
|
|
47
|
-
error.cliRunDuration = globalTelemetry.computeDuration(cliStartTime)
|
|
48
|
-
globalTelemetry.sendTelemetry(error).catch(() => {})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
process.exit(1)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
process.on('SIGTERM', () => {
|
|
55
|
-
if (enableTelemetry) {
|
|
56
|
-
// Fire-and-forget: attempt to send telemetry but don't block exit
|
|
57
|
-
const error = new Error('Received SIGTERM')
|
|
58
|
-
error.cliRunDuration = globalTelemetry.computeDuration(cliStartTime)
|
|
59
|
-
globalTelemetry.sendTelemetry(error).catch(() => {})
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
process.exit(1)
|
|
63
|
-
})
|
|
64
19
|
|
|
65
20
|
if (enableTelemetry) {
|
|
66
|
-
|
|
21
|
+
// Dynamically import telemetry modules
|
|
22
|
+
const {setupTelemetryHandlers} = await import('../dist/lib/analytics-telemetry/worker-client.js')
|
|
23
|
+
const {computeDuration} = await import('../dist/lib/analytics-telemetry/telemetry-utils.js')
|
|
24
|
+
|
|
25
|
+
// Setup all telemetry handlers (beforeExit, SIGINT, SIGTERM)
|
|
26
|
+
setupTelemetryHandlers({
|
|
27
|
+
cliStartTime,
|
|
28
|
+
computeDuration,
|
|
29
|
+
enableTelemetry,
|
|
30
|
+
})
|
|
67
31
|
}
|
|
68
32
|
|
|
69
33
|
await execute({dir: import.meta.url})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from '@heroku-cli/command';
|
|
2
2
|
import { Space } from '../../lib/types/fir.js';
|
|
3
|
-
|
|
3
|
+
type SpaceArray = Array<Required<Space>>;
|
|
4
4
|
export default class Index extends Command {
|
|
5
5
|
static description: string;
|
|
6
6
|
static flags: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from '@heroku-cli/command';
|
|
2
2
|
import * as Heroku from '@heroku-cli/schema';
|
|
3
|
-
|
|
3
|
+
type VpnConnectionTunnels = Required<Heroku.PrivateSpacesVpn>['tunnels'];
|
|
4
4
|
export default class Connections extends Command {
|
|
5
5
|
static description: string;
|
|
6
6
|
static example: string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const performance_analytics = async function () {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
const { isTelemetryEnabled } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
|
|
3
|
+
const { reportCmdNotFound } = await import('../../lib/analytics-telemetry/global-telemetry.js');
|
|
4
|
+
// Use the consolidated telemetry check
|
|
5
|
+
if (!isTelemetryEnabled()) {
|
|
4
6
|
return;
|
|
5
7
|
}
|
|
6
|
-
|
|
7
|
-
global.cliTelemetry = telemetry.reportCmdNotFound(this.config);
|
|
8
|
+
global.cliTelemetry = reportCmdNotFound(this.config);
|
|
8
9
|
};
|
|
9
10
|
export default performance_analytics;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if an error is a user error (not a bug) that should be filtered out
|
|
3
|
+
* Returns true if the error should NOT be sent to Sentry
|
|
4
|
+
*/
|
|
5
|
+
function isUserError(error) {
|
|
6
|
+
// Filter out 4xx HTTP errors (client errors)
|
|
7
|
+
if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
// Check for http.statusCode (e.g., HerokuAPIError)
|
|
11
|
+
if (error.http?.statusCode && error.http.statusCode >= 400 && error.http.statusCode < 500) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
// Filter out "command not found" errors (user typos, not bugs)
|
|
15
|
+
// Message format: "Run <bin> help for a list of available commands."
|
|
16
|
+
if (error.message && error.message.includes('Run') && error.message.includes('help') && error.message.includes('for a list of available commands')) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
// Also check for exit code 127 (command not found)
|
|
20
|
+
if (error.oclif?.exit === 127) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const finallyHook = async function (options) {
|
|
26
|
+
// Only process if there was an error
|
|
27
|
+
if (!options.error) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Filter out user errors (not bugs)
|
|
31
|
+
if (isUserError(options.error)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const { isTelemetryEnabled, spawnTelemetryWorker } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
|
|
35
|
+
// Use the consolidated telemetry check
|
|
36
|
+
if (!isTelemetryEnabled()) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Spawn background process to send error without blocking
|
|
40
|
+
spawnTelemetryWorker(options.error);
|
|
41
|
+
};
|
|
42
|
+
export default finallyHook;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const performance_analytics = async function (options) {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
const { isTelemetryEnabled } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
|
|
3
|
+
const { setupTelemetry } = await import('../../lib/analytics-telemetry/global-telemetry.js');
|
|
4
|
+
// Use the consolidated telemetry check
|
|
5
|
+
if (!isTelemetryEnabled()) {
|
|
4
6
|
return;
|
|
5
7
|
}
|
|
6
|
-
|
|
7
|
-
global.cliTelemetry = telemetry.setupTelemetry(this.config, options);
|
|
8
|
+
global.cliTelemetry = setupTelemetry(this.config, options);
|
|
8
9
|
};
|
|
9
10
|
export default performance_analytics;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
const performance_analytics = async function () {
|
|
2
|
-
|
|
2
|
+
const globalAny = global;
|
|
3
|
+
if (!globalAny.cliTelemetry) {
|
|
3
4
|
return;
|
|
4
5
|
}
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
const { isTelemetryEnabled, computeDuration } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
|
|
7
|
+
// Use the consolidated telemetry check
|
|
8
|
+
if (!isTelemetryEnabled()) {
|
|
7
9
|
return;
|
|
8
10
|
}
|
|
9
|
-
const telemetry = await import('../../global_telemetry.js');
|
|
10
|
-
const globalAny = global;
|
|
11
11
|
const cmdStartTime = globalAny.cliTelemetry.commandRunDuration;
|
|
12
|
-
globalAny.cliTelemetry.commandRunDuration =
|
|
12
|
+
globalAny.cliTelemetry.commandRunDuration = computeDuration(cmdStartTime);
|
|
13
13
|
globalAny.cliTelemetry.lifecycleHookCompletion.postrun = true;
|
|
14
14
|
await Reflect.get(globalThis, 'recordPromise');
|
|
15
15
|
};
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import Analytics from '../../analytics.js';
|
|
2
2
|
const analytics = async function (options) {
|
|
3
|
-
|
|
3
|
+
const { isTelemetryEnabled } = await import('../../lib/analytics-telemetry/telemetry-utils.js');
|
|
4
|
+
const { setupTelemetry } = await import('../../lib/analytics-telemetry/global-telemetry.js');
|
|
5
|
+
// Use the consolidated telemetry check
|
|
6
|
+
if (!isTelemetryEnabled()) {
|
|
4
7
|
return;
|
|
5
8
|
}
|
|
6
|
-
// Skip analytics on Windows for performance (unless explicitly enabled)
|
|
7
|
-
if (process.platform === 'win32' && process.env.ENABLE_WINDOWS_TELEMETRY !== 'true') {
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
|
-
const telemetry = await import('../../global_telemetry.js');
|
|
11
9
|
const globalAny = global;
|
|
12
|
-
|
|
10
|
+
// Only setup telemetry if not already initialized (avoid overwriting init hook data)
|
|
11
|
+
if (globalAny.cliTelemetry) {
|
|
12
|
+
// Update existing telemetry for regular commands
|
|
13
|
+
globalAny.cliTelemetry.command = options.Command.id;
|
|
14
|
+
globalAny.cliTelemetry.isVersionOrHelp = false;
|
|
15
|
+
globalAny.cliTelemetry.lifecycleHookCompletion.prerun = true;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
globalAny.cliTelemetry = setupTelemetry(this.config, options);
|
|
19
|
+
}
|
|
13
20
|
const analyticsInstance = new Analytics(this.config);
|
|
14
21
|
Reflect.set(globalThis, 'recordPromise', analyticsInstance.record(options));
|
|
15
22
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global telemetry orchestrator
|
|
3
|
+
* This module provides the main public API for telemetry and delegates to specialized modules
|
|
4
|
+
*/
|
|
5
|
+
import type { Config } from '@oclif/core/interfaces';
|
|
6
|
+
import { Telemetry, TelemetryData } from './telemetry-utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Options passed to telemetry setup (from oclif hooks)
|
|
9
|
+
*/
|
|
10
|
+
interface TelemetryOptions {
|
|
11
|
+
Command?: {
|
|
12
|
+
id: string;
|
|
13
|
+
};
|
|
14
|
+
id?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create telemetry object for command_not_found errors
|
|
18
|
+
*/
|
|
19
|
+
export declare function reportCmdNotFound(config: Config): Telemetry;
|
|
20
|
+
/**
|
|
21
|
+
* Main orchestrator: Send telemetry data to appropriate destinations
|
|
22
|
+
* - Errors go to both Honeycomb and Sentry
|
|
23
|
+
* - Regular telemetry goes to Honeycomb only
|
|
24
|
+
*/
|
|
25
|
+
export declare function sendTelemetry(currentTelemetry: TelemetryData): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Create telemetry object for regular commands or version/help
|
|
28
|
+
*/
|
|
29
|
+
export declare function setupTelemetry(config: Config, opts: TelemetryOptions): Telemetry;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global telemetry orchestrator
|
|
3
|
+
* This module provides the main public API for telemetry and delegates to specialized modules
|
|
4
|
+
*/
|
|
5
|
+
// Import internal dependencies
|
|
6
|
+
import { sendToHoneycomb } from './honeycomb-client.js';
|
|
7
|
+
import { sendToSentry } from './sentry-client.js';
|
|
8
|
+
import { isTelemetryDisabled, setVersion, telemetryDebug, } from './telemetry-utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* Create telemetry object for command_not_found errors
|
|
11
|
+
*/
|
|
12
|
+
export function reportCmdNotFound(config) {
|
|
13
|
+
return {
|
|
14
|
+
cliRunDuration: 0,
|
|
15
|
+
command: 'invalid_command',
|
|
16
|
+
commandRunDuration: 0,
|
|
17
|
+
exitCode: 0,
|
|
18
|
+
exitState: 'command_not_found',
|
|
19
|
+
isTTY: process.stdin.isTTY,
|
|
20
|
+
isVersionOrHelp: false,
|
|
21
|
+
lifecycleHookCompletion: {
|
|
22
|
+
command_not_found: true,
|
|
23
|
+
init: true,
|
|
24
|
+
postrun: false,
|
|
25
|
+
prerun: false,
|
|
26
|
+
},
|
|
27
|
+
os: config.platform,
|
|
28
|
+
version: config.version,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Main orchestrator: Send telemetry data to appropriate destinations
|
|
33
|
+
* - Errors go to both Honeycomb and Sentry
|
|
34
|
+
* - Regular telemetry goes to Honeycomb only
|
|
35
|
+
*/
|
|
36
|
+
export async function sendTelemetry(currentTelemetry) {
|
|
37
|
+
if (isTelemetryDisabled) {
|
|
38
|
+
telemetryDebug('Telemetry disabled, skipping send');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const telemetry = currentTelemetry;
|
|
42
|
+
if (telemetry instanceof Error) {
|
|
43
|
+
// Filter SIGINT errors from Sentry (user Ctrl+C is not an error to report)
|
|
44
|
+
// But still send to Honeycomb for analytics
|
|
45
|
+
const isSIGINT = telemetry.message === 'Received SIGINT';
|
|
46
|
+
if (isSIGINT) {
|
|
47
|
+
telemetryDebug('Sending error to Honeycomb: %s', telemetry.message);
|
|
48
|
+
await sendToHoneycomb(telemetry);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
telemetryDebug('Sending error to Honeycomb and Sentry: %s', telemetry.message);
|
|
52
|
+
await Promise.all([
|
|
53
|
+
sendToHoneycomb(telemetry),
|
|
54
|
+
sendToSentry(telemetry),
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
telemetryDebug('Sending telemetry for command: %s', telemetry.command);
|
|
60
|
+
await sendToHoneycomb(telemetry);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create telemetry object for regular commands or version/help
|
|
65
|
+
*/
|
|
66
|
+
export function setupTelemetry(config, opts) {
|
|
67
|
+
// Store version from config (eliminates need to read package.json)
|
|
68
|
+
setVersion(config.version);
|
|
69
|
+
const now = new Date();
|
|
70
|
+
const cmdStartTime = now.getTime();
|
|
71
|
+
const isRegularCmd = Boolean(opts.Command);
|
|
72
|
+
const mcpMode = process.env.HEROKU_MCP_MODE === 'true';
|
|
73
|
+
const mcpServerVersion = process.env.HEROKU_MCP_SERVER_VERSION || 'unknown';
|
|
74
|
+
const irregularTelemetryObject = {
|
|
75
|
+
cliRunDuration: 0,
|
|
76
|
+
command: opts.id || 'unknown',
|
|
77
|
+
commandRunDuration: cmdStartTime,
|
|
78
|
+
exitCode: 0,
|
|
79
|
+
exitState: 'successful',
|
|
80
|
+
isTTY: process.stdin.isTTY,
|
|
81
|
+
isVersionOrHelp: true,
|
|
82
|
+
lifecycleHookCompletion: {
|
|
83
|
+
command_not_found: false,
|
|
84
|
+
init: true,
|
|
85
|
+
postrun: false,
|
|
86
|
+
prerun: false,
|
|
87
|
+
},
|
|
88
|
+
os: config.platform,
|
|
89
|
+
version: `${config.version}${mcpMode ? ` (MCP ${mcpServerVersion})` : ''}`,
|
|
90
|
+
};
|
|
91
|
+
if (isRegularCmd && opts.Command) {
|
|
92
|
+
return {
|
|
93
|
+
...irregularTelemetryObject,
|
|
94
|
+
command: opts.Command.id,
|
|
95
|
+
isVersionOrHelp: false,
|
|
96
|
+
lifecycleHookCompletion: {
|
|
97
|
+
...irregularTelemetryObject.lifecycleHookCompletion,
|
|
98
|
+
prerun: true,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return irregularTelemetryObject;
|
|
103
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
2
|
+
import { TelemetryData } from './telemetry-utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Get the BatchSpanProcessor (for backward compatibility)
|
|
5
|
+
*/
|
|
6
|
+
export declare function getProcessor(): BatchSpanProcessor;
|
|
7
|
+
/**
|
|
8
|
+
* Initialize OpenTelemetry instrumentation with Sentry context
|
|
9
|
+
* This should only be called when both OpenTelemetry and Sentry are needed together
|
|
10
|
+
*/
|
|
11
|
+
export declare function initializeInstrumentation(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Send telemetry data to Honeycomb via OpenTelemetry
|
|
14
|
+
*/
|
|
15
|
+
export declare function sendToHoneycomb(data: TelemetryData): Promise<void>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import opentelemetry, { SpanStatusCode } from '@opentelemetry/api';
|
|
2
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
3
|
+
import { Resource } from '@opentelemetry/resources';
|
|
4
|
+
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
5
|
+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
6
|
+
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
|
|
7
|
+
import * as Sentry from '@sentry/node';
|
|
8
|
+
import { SentryPropagator } from '@sentry/opentelemetry';
|
|
9
|
+
import debug from 'debug';
|
|
10
|
+
import { getToken, getVersion, isDev, isTelemetryDisabled, telemetryDebug, } from './telemetry-utils.js';
|
|
11
|
+
// Lazy-loaded state
|
|
12
|
+
let isInitialized = false;
|
|
13
|
+
let provider;
|
|
14
|
+
let processor;
|
|
15
|
+
/**
|
|
16
|
+
* Get the BatchSpanProcessor (for backward compatibility)
|
|
17
|
+
*/
|
|
18
|
+
export function getProcessor() {
|
|
19
|
+
ensureInitialized();
|
|
20
|
+
return processor;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Initialize OpenTelemetry instrumentation with Sentry context
|
|
24
|
+
* This should only be called when both OpenTelemetry and Sentry are needed together
|
|
25
|
+
*/
|
|
26
|
+
export function initializeInstrumentation() {
|
|
27
|
+
if (isTelemetryDisabled) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
telemetryDebug('initializeInstrumentation() called - registering provider with Sentry context');
|
|
31
|
+
ensureInitialized();
|
|
32
|
+
// Note: Sentry initialization happens in sentry-client.ts
|
|
33
|
+
// This function assumes Sentry is already initialized
|
|
34
|
+
provider.register({
|
|
35
|
+
contextManager: new Sentry.SentryContextManager(),
|
|
36
|
+
propagator: new SentryPropagator(),
|
|
37
|
+
});
|
|
38
|
+
telemetryDebug('Provider registered with Sentry context manager');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Send telemetry data to Honeycomb via OpenTelemetry
|
|
42
|
+
*/
|
|
43
|
+
export async function sendToHoneycomb(data) {
|
|
44
|
+
ensureInitialized();
|
|
45
|
+
try {
|
|
46
|
+
const tracer = opentelemetry.trace.getTracer('heroku-cli', getVersion());
|
|
47
|
+
const span = tracer.startSpan('node_app_execution');
|
|
48
|
+
telemetryDebug('Created span: node_app_execution');
|
|
49
|
+
if (data instanceof Error) {
|
|
50
|
+
const errorData = data;
|
|
51
|
+
telemetryDebug('Honeycomb payload (error): %O', {
|
|
52
|
+
cliRunDuration: errorData.cliRunDuration,
|
|
53
|
+
code: errorData.code,
|
|
54
|
+
message: errorData.message,
|
|
55
|
+
name: errorData.name,
|
|
56
|
+
stack: errorData.stack,
|
|
57
|
+
});
|
|
58
|
+
span.recordException(data);
|
|
59
|
+
span.setStatus({
|
|
60
|
+
code: SpanStatusCode.ERROR,
|
|
61
|
+
message: data.message,
|
|
62
|
+
});
|
|
63
|
+
telemetryDebug('Recorded exception in span: %s', data.message);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
telemetryDebug('Honeycomb payload (telemetry): %O', {
|
|
67
|
+
cliRunDuration: data.cliRunDuration,
|
|
68
|
+
command: data.command,
|
|
69
|
+
commandRunDuration: data.commandRunDuration,
|
|
70
|
+
exitCode: data.exitCode,
|
|
71
|
+
exitState: data.exitState,
|
|
72
|
+
lifecycleHookCompletion: data.lifecycleHookCompletion,
|
|
73
|
+
os: data.os,
|
|
74
|
+
version: data.version,
|
|
75
|
+
});
|
|
76
|
+
span.setAttribute('heroku_client.command', data.command);
|
|
77
|
+
span.setAttribute('heroku_client.os', data.os);
|
|
78
|
+
span.setAttribute('heroku_client.version', data.version);
|
|
79
|
+
span.setAttribute('heroku_client.exit_code', data.exitCode);
|
|
80
|
+
span.setAttribute('heroku_client.exit_state', data.exitState);
|
|
81
|
+
span.setAttribute('heroku_client.cli_run_duration', data.cliRunDuration);
|
|
82
|
+
span.setAttribute('heroku_client.command_run_duration', data.commandRunDuration);
|
|
83
|
+
span.setAttribute('heroku_client.lifecycle_hook.init', data.lifecycleHookCompletion.init);
|
|
84
|
+
span.setAttribute('heroku_client.lifecycle_hook.prerun', data.lifecycleHookCompletion.prerun);
|
|
85
|
+
span.setAttribute('heroku_client.lifecycle_hook.postrun', data.lifecycleHookCompletion.postrun);
|
|
86
|
+
span.setAttribute('heroku_client.lifecycle_hook.command_not_found', data.lifecycleHookCompletion.command_not_found);
|
|
87
|
+
telemetryDebug('Set span attributes for command: %s (duration: %dms)', data.command, data.cliRunDuration);
|
|
88
|
+
}
|
|
89
|
+
span.end();
|
|
90
|
+
telemetryDebug('Span ended, flushing to exporter...');
|
|
91
|
+
await getProcessor().forceFlush();
|
|
92
|
+
telemetryDebug('Successfully flushed telemetry to Honeycomb');
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
telemetryDebug('Error sending telemetry to Honeycomb: %O', error);
|
|
96
|
+
debug('could not send telemetry');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Ensure OpenTelemetry is initialized (lazy initialization)
|
|
101
|
+
*/
|
|
102
|
+
function ensureInitialized() {
|
|
103
|
+
if (isInitialized || isTelemetryDisabled) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
telemetryDebug('Initializing OpenTelemetry...');
|
|
107
|
+
isInitialized = true;
|
|
108
|
+
const resource = Resource
|
|
109
|
+
.default()
|
|
110
|
+
.merge(new Resource({
|
|
111
|
+
[SemanticResourceAttributes.SERVICE_NAME]: 'heroku-cli',
|
|
112
|
+
[SemanticResourceAttributes.SERVICE_VERSION]: undefined, // will be set later
|
|
113
|
+
}));
|
|
114
|
+
// Initialize without Sentry sampler initially (Sentry loaded lazily)
|
|
115
|
+
provider = new NodeTracerProvider({
|
|
116
|
+
resource,
|
|
117
|
+
});
|
|
118
|
+
telemetryDebug('NodeTracerProvider created');
|
|
119
|
+
// eslint-disable-next-line no-negated-condition, unicorn/no-negated-condition
|
|
120
|
+
const headers = { Authorization: `Bearer ${process.env.IS_HEROKU_TEST_ENV !== 'true' ? getToken() : ''}` };
|
|
121
|
+
const url = isDev ? 'https://backboard.staging.herokudev.com/otel/v1/traces' : 'https://backboard.heroku.com/otel/v1/traces';
|
|
122
|
+
telemetryDebug('OTLP exporter endpoint: %s', url);
|
|
123
|
+
const exporter = new OTLPTraceExporter({
|
|
124
|
+
compression: undefined,
|
|
125
|
+
headers,
|
|
126
|
+
url,
|
|
127
|
+
});
|
|
128
|
+
processor = new BatchSpanProcessor(exporter);
|
|
129
|
+
provider.addSpanProcessor(processor);
|
|
130
|
+
telemetryDebug('BatchSpanProcessor added to provider');
|
|
131
|
+
// Register the provider to make it the global tracer provider
|
|
132
|
+
// We don't use Sentry context manager here to avoid loading Sentry upfront
|
|
133
|
+
provider.register();
|
|
134
|
+
telemetryDebug('OpenTelemetry provider registered globally');
|
|
135
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CLIError } from './telemetry-utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Ensure Sentry is initialized (lazy initialization)
|
|
4
|
+
*/
|
|
5
|
+
export declare function ensureSentryInitialized(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Send error data to Sentry
|
|
8
|
+
*/
|
|
9
|
+
export declare function sendToSentry(data: CLIError): Promise<void>;
|