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.
Files changed (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +14 -16
  3. package/bin/run.js +13 -49
  4. package/dist/commands/domains/add.d.ts +1 -1
  5. package/dist/commands/pg/backups/schedule.d.ts +1 -1
  6. package/dist/commands/spaces/index.d.ts +1 -1
  7. package/dist/commands/spaces/vpn/connections.d.ts +1 -1
  8. package/dist/hooks/command_not_found/performance_analytics.js +5 -4
  9. package/dist/hooks/finally/sentry.d.ts +3 -0
  10. package/dist/hooks/finally/sentry.js +42 -0
  11. package/dist/hooks/init/performance_analytics.js +5 -4
  12. package/dist/hooks/postrun/performance_analytics.js +6 -6
  13. package/dist/hooks/prerun/analytics.js +14 -7
  14. package/dist/lib/analytics-telemetry/global-telemetry.d.ts +30 -0
  15. package/dist/lib/analytics-telemetry/global-telemetry.js +103 -0
  16. package/dist/lib/analytics-telemetry/honeycomb-client.d.ts +15 -0
  17. package/dist/lib/analytics-telemetry/honeycomb-client.js +135 -0
  18. package/dist/lib/analytics-telemetry/sentry-client.d.ts +9 -0
  19. package/dist/lib/analytics-telemetry/sentry-client.js +58 -0
  20. package/dist/lib/analytics-telemetry/telemetry-utils.d.ts +68 -0
  21. package/dist/lib/analytics-telemetry/telemetry-utils.js +115 -0
  22. package/dist/lib/analytics-telemetry/telemetry-worker.d.ts +5 -0
  23. package/dist/lib/analytics-telemetry/telemetry-worker.js +37 -0
  24. package/dist/lib/analytics-telemetry/worker-client.d.ts +15 -0
  25. package/dist/lib/analytics-telemetry/worker-client.js +44 -0
  26. package/dist/lib/api.d.ts +1 -1
  27. package/dist/lib/apps/app-transfer.d.ts +1 -1
  28. package/dist/lib/apps/error_info.d.ts +1 -1
  29. package/dist/lib/apps/generation.d.ts +3 -3
  30. package/dist/lib/buildpacks/buildpacks.d.ts +1 -1
  31. package/dist/lib/container/docker_helper.d.ts +4 -4
  32. package/dist/lib/data/types.d.ts +37 -37
  33. package/dist/lib/domains/domains.js +15 -8
  34. package/dist/lib/pg/download.d.ts +1 -1
  35. package/dist/lib/pg/push_pull.d.ts +1 -1
  36. package/dist/lib/pg/setter.d.ts +1 -1
  37. package/dist/lib/pg/types.d.ts +31 -31
  38. package/dist/lib/pipelines/setup/validate.d.ts +1 -1
  39. package/dist/lib/redis/api.d.ts +7 -7
  40. package/dist/lib/spaces/hosts.d.ts +1 -1
  41. package/dist/lib/types/app_errors.d.ts +1 -1
  42. package/dist/lib/types/completion.d.ts +1 -1
  43. package/dist/lib/types/favorites.d.ts +2 -2
  44. package/dist/lib/types/notifications.d.ts +3 -3
  45. package/dist/lib/utils/multisort.d.ts +1 -1
  46. package/npm-shrinkwrap.json +837 -842
  47. package/oclif.manifest.json +1018 -1018
  48. package/package.json +7 -5
  49. package/dist/global_telemetry.d.ts +0 -62
  50. 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
- Heroku CLI
2
- ==========
3
-
4
- ![Heroku logo](https://d4yt8xl9b7in.cloudfront.net/assets/home/logotype-heroku.png)
5
- [![Node CI Suite](https://github.com/heroku/cli/actions/workflows/ci.yml/badge.svg)](https://github.com/heroku/cli/actions/workflows/ci.yml)
6
- [![npm](https://img.shields.io/npm/v/heroku.svg)](https://www.npmjs.com/package/heroku)
7
- [![ISC License](https://img.shields.io/github/license/heroku/cli.svg)](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>&nbsp;</p>
12
4
 
13
- To get started see <https://devcenter.heroku.com/start>
5
+ [![Node CI Suite](https://github.com/heroku/cli/actions/workflows/ci.yml/badge.svg)](https://github.com/heroku/cli/actions/workflows/ci.yml)
6
+ [![npm](https://img.shields.io/npm/v/heroku.svg)](https://www.npmjs.com/package/heroku)
7
+ [![ISC License](https://img.shields.io/github/license/heroku/cli.svg)](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
- Key features include:
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
- Releasing
118
+ Contributing
120
119
  =========
121
- See the [Heroku CLI Release Steps](https://salesforce.quip.com/aPLDA1ZwjNlW).
122
120
 
123
- Review our [PR guidelines](./.github/PULL_REQUEST_TEMPLATE.md).
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
- /* eslint-disable n/no-process-exit */
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 DEBUG=oclif:perf or DEBUG=* is set
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
- globalTelemetry.initializeInstrumentation()
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 * as Heroku from '@heroku-cli/schema';
3
- declare type CertChoice = {
3
+ type CertChoice = {
4
4
  name: string;
5
5
  value: null | string | undefined;
6
6
  };
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@heroku-cli/command';
2
- declare type BackupSchedule = {
2
+ type BackupSchedule = {
3
3
  hour: string;
4
4
  timezone: string;
5
5
  schedule_name?: string;
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@heroku-cli/command';
2
2
  import { Space } from '../../lib/types/fir.js';
3
- declare type SpaceArray = Array<Required<Space>>;
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
- declare type VpnConnectionTunnels = Required<Heroku.PrivateSpacesVpn>['tunnels'];
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
- // Skip telemetry on Windows for performance (unless explicitly enabled)
3
- if (process.platform === 'win32' && process.env.ENABLE_WINDOWS_TELEMETRY !== 'true') {
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
- const telemetry = await import('../../global_telemetry.js');
7
- global.cliTelemetry = telemetry.reportCmdNotFound(this.config);
8
+ global.cliTelemetry = reportCmdNotFound(this.config);
8
9
  };
9
10
  export default performance_analytics;
@@ -0,0 +1,3 @@
1
+ import { Hook } from '@oclif/core/hooks';
2
+ declare const finallyHook: Hook<'finally'>;
3
+ export default finallyHook;
@@ -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
- // Skip telemetry on Windows for performance (unless explicitly enabled)
3
- if (process.platform === 'win32' && process.env.ENABLE_WINDOWS_TELEMETRY !== 'true') {
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
- const telemetry = await import('../../global_telemetry.js');
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
- if (process.env.IS_HEROKU_TEST_ENV === 'true' || !global.cliTelemetry) {
2
+ const globalAny = global;
3
+ if (!globalAny.cliTelemetry) {
3
4
  return;
4
5
  }
5
- // Skip analytics on Windows for performance (unless explicitly enabled)
6
- if (process.platform === 'win32' && process.env.ENABLE_WINDOWS_TELEMETRY !== 'true') {
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 = telemetry.computeDuration(cmdStartTime);
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
- if (process.env.IS_HEROKU_TEST_ENV === 'true') {
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
- globalAny.cliTelemetry = telemetry.setupTelemetry(this.config, options);
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>;