Skip to content

feat(cli): add crash reporting with trace ids#2880

Open
matthewvolk wants to merge 2 commits intoalphafrom
CATALYST-1725-tracing
Open

feat(cli): add crash reporting with trace ids#2880
matthewvolk wants to merge 2 commits intoalphafrom
CATALYST-1725-tracing

Conversation

@matthewvolk
Copy link
Contributor

What/Why?

When the CLI crashes, customers send screenshots to support which aren't actionable. This PR instruments the CLI so that on any error, a trace ID (UUID) is displayed. The customer shares it with support, who can look up the session across telemetry systems.

This works because every outgoing BigCommerce API request now includes an X-Correlation-Id header set to the trace ID.

Changes

Centralized error handling

  • New withErrorHandler() HOF that wraps Commander .action() callbacks
  • On error: tracks via telemetry, displays trace ID, exits with code 1
  • Removes duplicated try/catch + consola.error + process.exit(1) from all 6 command actions (deploy, build, dev, start, project list/create/link)

Telemetry singleton

  • getTelemetry() / resetTelemetry() singleton pattern replaces per-module new Telemetry() instances
  • sessionId changed from randomBytes(32).toString('hex') to randomUUID() for a human-readable, support-friendly format
  • New traceId(), durationMs(), and trackError() methods
  • All track() calls enriched with traceId, nodeVersion, platform, arch, cliVersion

API correlation

  • X-Correlation-Id header added to all 6 BigCommerce API fetch calls (4 in deploy.ts, 2 in project.ts)

Global safety net

  • uncaughtException and unhandledRejection handlers in index.ts

User-facing output on crash:

Error: Failed to fetch upload signature: 401 Unauthorized

Trace ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Share this Trace ID with BigCommerce support.

Testing

  • 67 tests passing across 13 test files
  • New test suites for error-handler.spec.ts and telemetry.spec.ts
  • Updated assertions in deploy.spec.ts and project.spec.ts
  • Lint passes with 0 errors/warnings

Migration

N/A — no breaking changes to public API or file structure.

Centralized error handling via withErrorHandler HOF, singleton telemetry
with UUID trace ids, X-Correlation-Id headers on all API calls, and
global uncaught exception handlers. On any CLI error, a trace ID is
displayed for support debugging.
@matthewvolk matthewvolk requested a review from a team as a code owner February 13, 2026 00:48
@changeset-bot
Copy link

changeset-bot bot commented Feb 13, 2026

⚠️ No Changeset found

Latest commit: d9d52a4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Feb 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
catalyst Ready Ready Preview, Comment Feb 13, 2026 4:41pm

Request Review

Copy link
Contributor

@chanceaclark chanceaclark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like a lot of this PR can be simplified. Let's work together tomorrow to trim it down.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can omit the changeset here

properties: {
...payload,
sessionId: this.sessionId,
traceId: this.traceId(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trace id and session id above are the same thing. Can we just normalize this all to use session id internally but show it as trace id externally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup!

@@ -46,5 +45,8 @@ export const telemetryPreHook = async (command: Command) => {
};

export const telemetryPostHook = async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there arguments on this that we can determine if the command failed with an error? It would be cleaner than having to add a closure around each command and is more scalable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I was actually just playing around with this in a sandbox and here's what I found:

const { Command } = require('commander');

const p = new Command();

p.command('test').action(async () => { throw new Error('boom'); });
p.hook('postAction', () => { console.log('POST_ACTION_FIRED'); });
p.parseAsync(['node', 'test', 'test']).catch(e => console.log('CAUGHT:', e.message));

If you run that, you don't see POST_ACTION_FIRED. I guess hooks in Commander work like .then()?

TL;DR... to your original question, I don't think so... but open to your thoughts

});
}

async trackError(commandName: string, error: unknown) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add a whole new function if we are only calling it once? Can we just use the existing track function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll combine 👍

}
}

let telemetryInstance: Telemetry | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why we need a singleton here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now it's because the pre-hook, post-hook, error handler, and command action callback (for X-Correlation-Id) all call getTelemetry() independently — without the singleton they'd each get a fresh sessionId... though after I incorporate your feedback to leverage the postHook more, maybe this changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, see #2880 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a fresh session per command call is expected? As in you call the CLI once, you get a new session, then call it again it's a separate session.

@matthewvolk
Copy link
Contributor Author

@chanceaclark just updated, let me know what you think 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants