Add trace ID extraction and appending to error messages - #1664
Conversation
When a failed API or envd response carries a trace header (X-Trace-ID, or the GCP X-Cloud-Trace-Context / AWS X-Amzn-Trace-Id edge headers), the JS SDK, Python SDK (sync and async), and CLI now append '(trace ID: ...)' to the error message so users can report the ID and it can be correlated with server-side traces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147b6bTi2gq4Yvm7VrLrxLD
🦋 Changeset detectedLatest commit: 0adc234 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
PR SummaryLow Risk Overview Header parsing is centralized in Wiring: Reviewed by Cursor Bugbot for commit 0adc234. Bugbot is set up for automated code reviews on this repo. Configure here. |
Package ArtifactsBuilt from d76c350. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-2.38.4-claude-trace-id-error-messages-cbzqwl.0.tgzCLI ( npm install ./e2b-cli-2.16.2-claude-trace-id-error-messages-cbzqwl.0.tgzPython SDK ( pip install ./e2b-2.38.0+claude.trace.id.error.messages.cbzqwl-py3-none-any.whl |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147b6bTi2gq4Yvm7VrLrxLD
| if (key?.toLowerCase() !== 'root' || !value) { | ||
| continue | ||
| } | ||
|
|
||
| const match = value.match(/^1-([0-9a-f]{8})-([0-9a-f]{24})$/i) | ||
| return match ? match[1] + match[2] : value | ||
| } | ||
| } | ||
|
|
||
| return undefined | ||
| } | ||
|
|
||
| function throwE2BRequestError( | ||
| error: E2BResponseError, | ||
| errMsg?: string, | ||
| traceId?: string | ||
| ): never { | ||
| let message: string | ||
| const code = error.code ?? 0 | ||
| switch (code) { |
There was a problem hiding this comment.
🟡 CLAUDE.md requires 'Create or update tests covering affected codepaths,' but this PR leaves packages/cli/tests/utils/errors.test.ts untouched even though errors.ts gains a new extractTraceId() function and trace-ID appending in throwE2BRequestError/handleE2BRequestError. No case exercises X-Trace-ID, GCP X-Cloud-Trace-Context, or AWS X-Amzn-Trace-Id extraction/normalization for the CLI, unlike the JS SDK (new traceId.test.ts + updated handleApiError.test.ts/handleEnvdApiError.test.ts) and Python SDK (test_trace_id.py), which both got full coverage for the identical logic.
Extended reasoning...
This PR introduces a new, non-trivial codepath in packages/cli/src/utils/errors.ts: extractTraceId() parses three different header formats (a direct X-Trace-ID, the GCP X-Cloud-Trace-Context edge header which requires splitting on /, and the AWS X-Amzn-Trace-Id edge header which requires parsing a Root=1-<8 hex>-<24 hex> field and normalizing it into a 32-hex trace ID), and it wires the extracted value into throwE2BRequestError/handleE2BRequestError so it gets appended to the thrown error message as (trace ID: ...).
None of this is covered by tests in the CLI package. packages/cli/tests/utils/errors.test.ts already exists and already tests handleE2BRequestError for various status codes, but the PR leaves it completely untouched — a grep for trace/Trace across packages/cli/tests returns nothing. There is no test that passes a response/headers-bearing object into handleE2BRequestError, so none of the three extraction branches, the AWS regex normalization, the priority ordering (direct > GCP > AWS), or the final message-appending behavior is exercised at all for the CLI.
This is a direct violation of the repository's own CLAUDE.md instructions, which explicitly state: 'Create or update tests covering affected codepaths and run them using pnpm run test.' The PR's own diff demonstrates the intended standard: the JS SDK got a full new traceId.test.ts (extraction, normalization, priority ordering, edge cases) plus updated handleApiError.test.ts and handleEnvdApiError.test.ts with trace-ID-specific cases, and the Python SDK got a full new test_trace_id.py covering the same logic end to end. The CLI implementation — which, per the related duplication finding, re-implements this parsing logic independently rather than sharing it with the SDK's traceId.ts — is the only one of the three places this feature landed that received zero test coverage.
Concrete proof of the gap: take the AWS-header path, since it's the most failure-prone (regex parsing + normalization). In extractTraceId, if X-Amzn-Trace-Id is Root=1-5759e988-bd862e3fe1be46a994272793;Sampled=1, the code should return 5759e988bd862e3fe1be46a994272793 (the two hex groups joined), and this exact case is verified in both packages/js-sdk/tests/traceId.test.ts and packages/python-sdk/tests/test_trace_id.py. But if the CLI's regex or splitting logic were subtly wrong — e.g. an off-by-one in the hex-group lengths, or the ; split failing to find Root= first — nothing in pnpm run test for the CLI package would catch it, since handleE2BRequestError is only ever invoked in the test suite without a response argument. The same blind spot applies to the GCP-header branch and to the case-insensitivity of headers.get('x-trace-id').
Fix: add test cases to packages/cli/tests/utils/errors.test.ts mirroring the ones already written for the JS SDK — pass a response: { headers: new Headers({...}) } into handleE2BRequestError and assert the thrown E2BRequestError's message ends with (trace ID: ...) for each of: direct X-Trace-ID, GCP X-Cloud-Trace-Context, AWS X-Amzn-Trace-Id (including the raw-fallback case for a malformed Root= value), priority ordering when multiple headers are present, and the no-headers case where the message is left unchanged.
This does not block merging on its own — missing tests don't cause a runtime failure, and the trace-ID feature works correctly as shipped. But it's an explicit, unambiguous violation of a stated project convention, and the parity gap with the JS/Python SDKs (both of which got thorough coverage for the identical logic) makes it worth flagging as a nit for the author to close before merge.
| } | ||
|
|
||
| /** | ||
| * Extract a trace ID from HTTP response headers of a failed request, so the | ||
| * error message can carry an ID that E2B support can correlate with | ||
| * server-side traces. Checks `X-Trace-ID`, then the GCP | ||
| * (`X-Cloud-Trace-Context`) and AWS (`X-Amzn-Trace-Id`) edge trace headers. | ||
| */ | ||
| function extractTraceId(response?: ResponseHeadersLike): string | undefined { | ||
| const headers = response?.headers | ||
| if (!headers || typeof headers.get !== 'function') { | ||
| return undefined | ||
| } | ||
|
|
||
| const direct = headers.get('x-trace-id')?.trim() | ||
| if (direct) { | ||
| return direct | ||
| } | ||
|
|
||
| // GCP edge: "TRACE_ID/SPAN_ID;o=OPTIONS" | ||
| const gcp = headers.get('x-cloud-trace-context')?.split('/')[0]?.trim() | ||
| if (gcp) { | ||
| return gcp | ||
| } | ||
|
|
||
| // AWS edge: "Root=1-<8 hex>-<24 hex>;..." — the two hex parts joined are | ||
| // the 32-hex trace ID the server logs | ||
| const aws = headers.get('x-amzn-trace-id') | ||
| if (aws) { | ||
| for (const field of aws.split(';')) { |
There was a problem hiding this comment.
🟡 packages/cli/src/utils/errors.ts:37-70 duplicates the exact trace-ID header parsing logic (X-Trace-ID / X-Cloud-Trace-Context / X-Amzn-Trace-Id, including the AWS regex and GCP split) already implemented in packages/js-sdk/src/traceId.ts's extractTraceId. Since the CLI already depends on and imports from 'e2b', consider exporting extractTraceId from js-sdk's public index and importing it here instead of copy-pasting, to avoid the two copies silently drifting on future changes.
Extended reasoning...
What the bug is: packages/cli/src/utils/errors.ts (lines 29-58/37-70) reimplements a byte-for-byte copy of the trace-ID extraction logic that this same PR adds to packages/js-sdk/src/traceId.ts. Both versions check headers in the same priority order — X-Trace-ID (direct, trimmed), X-Cloud-Trace-Context (GCP edge, split('/')[0].trim()), and X-Amzn-Trace-Id (AWS edge, splitting on ;, matching Root=1-<8 hex>-<24 hex> with the identical regex /^1-([0-9a-f]{8})-([0-9a-f]{24})$/i, and joining the two hex groups) — with only the wrapper signature differing (CLI takes a {headers}-shaped object, js-sdk's version takes the Headers object directly).\n\nWhy it happens / code path: This PR introduces extractTraceId/appendTraceId in packages/js-sdk/src/traceId.ts and wires them into packages/js-sdk/src/api/index.ts and packages/js-sdk/src/envd/api.ts. Rather than reusing that helper for the CLI's error handling in packages/cli/src/utils/errors.ts, the PR hand-writes an equivalent extractTraceId function locally. The CLI package already depends on and imports from e2b (see packages/cli/src/api.ts: import * as e2b from 'e2b', and package.json lists e2b: workspace:^), so nothing structurally prevents sharing the implementation — it's purely that extractTraceId is not currently re-exported from js-sdk's public index.ts, which would need to be added for the CLI to import it.\n\nWhy existing code doesn't prevent it: There's no lint rule or test that would catch two independently-written implementations of the same parsing logic living in separate packages; both pass their own test suites (traceId.test.ts for js-sdk, presumably covered indirectly for the CLI) without any cross-check that they stay in sync.\n\nImpact: This doesn't cause incorrect behavior today — both copies are currently identical and produce the same trace IDs. The risk is purely maintenance: any future change to trace-ID parsing (e.g. supporting a new header format, fixing an edge case in the AWS regex, adjusting GCP parsing) has to be applied in two places, and it would be easy to update one copy and forget the other, causing the CLI and SDK to silently disagree on trace IDs for the same failure.\n\nProof (concrete example): Suppose in a follow-up PR someone fixes a bug in the AWS regex — say widening it to accept a 16-hex span ID variant, changing the pattern in packages/js-sdk/src/traceId.ts only (since that's the 'canonical' looking implementation reused by the JS SDK's API/envd handlers). packages/cli/src/utils/errors.ts's local copy still has the old regex. Now: a request fails with an AWS trace header in the new format; the JS SDK correctly extracts and reports the trace ID; the CLI (which wraps the same underlying e2b client and hits the same API) falls through the regex match and falls back to the raw Root=... string (or drops the ID depending on which branch), producing an inconsistent/wrong trace ID for the exact same server-side failure. A user following CLI-reported error output to file a support ticket would give E2B support a malformed or missing trace ID, right where this feature is supposed to help.\n\nHow to fix: Export extractTraceId (and optionally appendTraceId) from js-sdk's public src/index.ts, then in packages/cli/src/utils/errors.ts import it and adapt the call site to pass res.response?.headers directly (a trivial signature bridge, since the CLI's ResponseHeadersLike.headers is already the same {get(name): string|null} shape as js-sdk's HeadersLike), deleting the ~45-line duplicated block entirely.
mishushakov
left a comment
There was a problem hiding this comment.
reviewed, please remove duplicate method in JS SDK and you can modify the default Error classes to accept a trace field in the constructor that will append it to the message, which is cleaner than relying on helpers side-effects
|
|
||
| function throwE2BRequestError(error: E2BResponseError, errMsg?: string): never { | ||
| type ResponseHeadersLike = { | ||
| headers?: { get(name: string): string | null } |
There was a problem hiding this comment.
you can use built-in Headers type from undici?
| errorClass, | ||
| stackTrace | ||
| return appendTraceId( | ||
| apiErrorFromCode( |
There was a problem hiding this comment.
maybe just change apiErrorFromCode instead
| * 3. `X-Amzn-Trace-Id` (AWS edge) — `Root=1-<8 hex>-<24 hex>;...`, the two | ||
| * hex parts joined are the 32-hex trace ID the server logs. | ||
| */ | ||
| export function extractTraceId( |
There was a problem hiding this comment.
this is same code as in packages/cli/src/utils/errors.ts
| * Append the trace ID of the failed request to the error message, so users | ||
| * can include it when reporting the failure to E2B. | ||
| */ | ||
| export function appendTraceId<T extends Error | undefined>( |
There was a problem hiding this comment.
you could also modify E2B error classes to have trace id in message instead of this
| if message is None and e.status_code not in (401, 429): | ||
| return default_exception_class(f"{e.status_code}: {e.content}").with_traceback( | ||
| stack_trace | ||
| return append_trace_id( |
There was a problem hiding this comment.
feel free to update default_exception_class instead
Address review feedback: - Error/exception classes now take an optional trace ID in the constructor and append it to the message, instead of a post-hoc append helper; apiErrorFromCode / api_exception_from_code and the envd error maps thread it through. - The CLI reuses extractTraceId exported from the JS SDK instead of keeping its own copy, and types headers with the built-in Headers. - Cover AWS raw-value fallback and header priority in the CLI tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147b6bTi2gq4Yvm7VrLrxLD
|
Addressed the review in 0adc234:
Also added CLI test cases for the AWS raw-value fallback and header priority. Generated by Claude Code |
Summary
This change adds trace ID extraction from HTTP response headers and appends them to error messages across the SDK and CLI. When API or envd requests fail, the error message now includes the trace ID (e.g.,
(trace ID: abc123)) so users can report it to E2B support for correlation with server-side traces.Usage examples
No API changes — the trace ID shows up in existing error messages when the failed response carries a trace header.
JS SDK
Python SDK (sync and async)
CLI
Key Changes
New trace ID utilities: Added
e2b/trace_id.py(Python) andsrc/traceId.ts(JavaScript) modules that:X-Trace-ID(direct),X-Cloud-Trace-Context(GCP edge),X-Amzn-Trace-Id(AWS edge)Root=1-<8 hex>-<24 hex>to the 32-hex form the server logs asedge_trace_id, so the reported ID joins directly against traces/logs in GrafanaSDK integration:
e2b/api/__init__.py(handle_api_exception) ande2b/envd/api.py(handle_envd_api_exception/ahandle_envd_api_exception) to extract and append trace IDs to exceptionssrc/api/index.ts(handleApiError) andsrc/envd/api.ts(handleEnvdApiError) to extract and append trace IDs to errorspackages/cli/src/utils/errors.tswith trace ID extraction and appending logicconnectrpc.ConnectErrorexposes no response metadata, and the JS side is kept symmetricComprehensive test coverage: Added test suites for both Python and JavaScript implementations covering:
Implementation Details
(trace ID: ...)format for consistency across all SDKsNote on the server side
No code in
e2b-dev/infraore2b-dev/beltcurrently sets a trace-ID response header (the API handlers stashtraceIDin the gin context but never write it to the response; the edge only parses the GCP/AWS request headers intoedge_trace_idlogs). If production responses don't carry one of the three headers above, error messages stay unchanged until the API emits one — e.g. a one-linec.Header("X-Trace-ID", traceID)in infra wherec.Set("traceID", ...)already happens. Worth verifying which header a real failedapi.e2b.devresponse returns.https://claude.ai/code/session_0147b6bTi2gq4Yvm7VrLrxLD