Skip to content

Add trace ID extraction and appending to error messages - #1664

Open
djeebus wants to merge 4 commits into
mainfrom
claude/trace-id-error-messages-cbzqwl
Open

Add trace ID extraction and appending to error messages#1664
djeebus wants to merge 4 commits into
mainfrom
claude/trace-id-error-messages-cbzqwl

Conversation

@djeebus

@djeebus djeebus commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

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

The trace ID shows up in existing error messages when the failed response carries a trace header, and is readable off the error itself.

JS SDK

import { Sandbox } from 'e2b'

try {
  await Sandbox.connect('already-dead-sandbox-id')
} catch (err) {
  console.error(err.message)
  // 404: sandbox not found (trace ID: 105445aa7843bc8bf206b12000100000)

  // or read it off the error, without parsing the message
  reportToSentry({ traceId: (err as SandboxError).traceId })
}

Python SDK (sync and async)

from e2b import Sandbox, SandboxException

try:
    Sandbox.connect("already-dead-sandbox-id")
except SandboxException as e:
    print(e)
    # 404: sandbox not found (trace ID: 105445aa7843bc8bf206b12000100000)

    # or read it off the exception, without parsing the message
    report_to_sentry(trace_id=e.trace_id)

CLI

$ e2b sandbox logs already-dead-sandbox-id
Error while getting sandbox logs: [404] not found: sandbox not found (trace ID: 105445aa7843bc8bf206b12000100000)

Key Changes

  • Trace ID parsing: packages/js-sdk/src/traceId.ts (single implementation, exported as extractTraceId from the e2b package and reused by the CLI) and e2b/trace_id.py (exported as extract_trace_id for JS/Python parity):

    • Extract trace IDs from response headers in priority order: X-Trace-ID (direct), X-Cloud-Trace-Context (GCP edge), X-Amzn-Trace-Id (AWS edge)
    • Normalize AWS trace IDs from Root=1-<8 hex>-<24 hex> to the 32-hex form the server logs as edge_trace_id
  • Error classes take the trace ID as constructor context and append it to the message: a trailing options object in JS (new SandboxError(message, { traceId }), uniform across every class) and a keyword-only argument in Python (SandboxException(message, trace_id=...)). Two option types are exported: ErrorOpts (traceId) and ErrorOptsWithStackTrace (adds stackTrace). Only the four classes actually handed a stack trace take the latter — InvalidArgumentError, TemplateError, BuildError, FileUploadError — so refactor(js-sdk): drop unused stackTrace params from error constructors, reparent volume not-found errors #1732's narrowing is preserved and passing stackTrace to any other class is a type error rather than silently ignored. apiErrorFromCode swaps in AuthenticationError/RateLimitError for 401/429 and deliberately does not forward the caller's frame to them: a bad key or a rate limit is a property of the request, not of the builder step that made it. apiErrorFromCode / api_exception_from_code and the envd error maps thread both through.

  • Breaking (JS, hence the minor changeset): the options object replaces the positional stackTrace parameter on the four classes that had one, so new TemplateError(message, stackTrace) becomes new TemplateError(message, { stackTrace }) (likewise InvalidArgumentError, BuildError, FileUploadError). TypeScript callers get a compile error on the old form; plain-JS callers silently lose the stack trace. Python is unaffected — its exceptions already took a single positional argument.

  • Direct 404 raises: VolumeNotFoundError / VolumePathNotFoundError / SecretNotFoundError (and their Python counterparts) are thrown at the call site before handleApiError sees the response — per T-60, 404 meaning is decided per call site — so each of those raise sites reads the header itself. 11 sites in JS (volume/index.ts, secret.ts) and 11 in each Python flavor (volume_sync/volume_async, secret_sync/secret_async).

  • Wiring: handleApiError (JS), handle_api_exception (Python), handleEnvdApiError / handle_envd_api_exception / ahandle_envd_api_exception (envd HTTP), and the CLI's handleE2BRequestError read the response headers and pass the extracted ID into the error constructors.

  • Not covered — envd RPC (connect) paths: sandbox.commands / sandbox.files RPC errors do not carry the trace ID, because Python's connectrpc.ConnectError exposes no response metadata, and the JS side is kept symmetric. This is where most in-sandbox failures surface, so the feature currently reaches HTTP API and envd HTTP (file transfer) errors only.

  • Test coverage: header extraction and normalization edge cases, priority ordering, case-insensitivity, missing-header fallbacks, custom errorClass/error_map pass-through, and CLI parsing — in JS, Python (sync + async), and the CLI.

Note on the server side

Verified against production (2026-08-13): no failed api.e2b.dev or sandbox-edge response currently returns any trace header, so this change is inert until the server emits one. The API handlers already compute the trace ID (c.Set("traceID", traceID) in infra) — a one-line c.Header("X-Trace-ID", traceID) there would activate the primary path. The GCP/AWS edge-header branches are speculative (those are request-side conventions); whether to keep them or trim to X-Trace-ID-only once the infra change lands is an open reviewer question for @djeebus.

https://claude.ai/code/session_0147b6bTi2gq4Yvm7VrLrxLD

@djeebus
djeebus requested a review from mishushakov as a code owner August 12, 2026 00:38
@cla-bot cla-bot Bot added the cla-signed label Aug 12, 2026
@changeset-bot

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d2e48e4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
e2b Minor
@e2b/python-sdk Patch
@e2b/cli Patch

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

@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

PR Summary

Medium Risk
Public JS error constructors change shape (breaking for the four classes that took a positional stackTrace). Error-message formatting is user-facing across SDKs and CLI, so mistakes here affect support correlation and existing catch sites.

Overview
JS TemplateError/BuildError/FileUploadError/InvalidArgumentError constructors now take { stackTrace } instead of a positional second argument; plain JS callers will silently drop the stack. Template build poll failures and file-upload PUT failures still construct errors without reading response headers, so those paths never get a trace ID. Envd RPC errors are also left without one. CLI E2BRequestError only appends the ID to the message and has no traceId field.

Reviewed by Cursor Bugbot for commit d2e48e4. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 1917333. Download artifacts from this workflow run.

JS SDK (e2b@2.45.1-claude-trace-id-error-messages-cbzqwl.0):

npm install ./e2b-2.45.1-claude-trace-id-error-messages-cbzqwl.0.tgz

CLI (@e2b/cli@2.17.2-claude-trace-id-error-messages-cbzqwl.0):

npm install ./e2b-cli-2.17.2-claude-trace-id-error-messages-cbzqwl.0.tgz

Python SDK (e2b==2.45.1+claude.trace.id.error.messages.cbzqwl):

pip install ./e2b-2.45.1+claude.trace.id.error.messages.cbzqwl-py3-none-any.whl

@djeebus
djeebus enabled auto-merge (squash) August 12, 2026 00:39
Comment thread packages/cli/src/utils/errors.ts Outdated
Comment thread packages/cli/src/utils/errors.ts Outdated

@mishushakov mishushakov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Comment thread packages/cli/src/utils/errors.ts Outdated
Comment thread packages/js-sdk/src/api/index.ts Outdated
Comment thread packages/js-sdk/src/traceId.ts Outdated
Comment thread packages/js-sdk/src/traceId.ts Outdated
Comment thread packages/python-sdk/e2b/api/__init__.py Outdated

djeebus commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review in 0adc234:

  • Duplicate method removedextractTraceId now lives only in the JS SDK (src/traceId.ts), is exported from the package index, and the CLI imports it from e2b.
  • Trace ID via constructors — the error/exception classes in both SDKs now take an optional trace ID in the constructor and append it to the message (SandboxError(message, stackTrace, traceId) / SandboxException(message, trace_id)); the appendTraceId/append_trace_id side-effect helpers are gone. apiErrorFromCode / api_exception_from_code and the envd error maps thread the trace ID through, per the inline suggestions.
  • Built-in Headers type — the CLI (and the SDK helper) now type headers with the standard Headers instead of a custom shape.

Also added CLI test cases for the AWS raw-value fallback and header priority.


Generated by Claude Code

@mishushakov

Copy link
Copy Markdown
Member

@djeebus Second pass — I re-reviewed after 0adc234 and verified the build locally. All five of my earlier comments are addressed; details plus a few independent findings below.

My earlier comments

Comment Status
Remove duplicate extractTraceId in the CLI ✅ One copy in js-sdk/src/traceId.ts, exported from index.ts, CLI imports from 'e2b'.
Modify default Error classes to take a trace field in the constructor ✅ Both SDKs; appendTraceId/append_trace_id deleted. ⚠️ see B
maybe just change apiErrorFromCode instead ✅ Threads traceId. Extraction staying in handleApiError is right — apiErrorFromCode also serves errors embedded in bodies (per-fork results) and has no response to read.
Use built-in Headers type ✅ Global Headers in both. ⚠️ traceId.ts:12 still runtime-guards typeof headers.get !== 'function' — leftover from the old HeadersLike shape and now unreachable, since the !headers check already covers every mock. Please drop it.
feel free to update default_exception_class instead ✅ Base SandboxException/AuthenticationException/BuildException/VolumeException take trace_id; api_exception_from_code passes it through.

A. The feature is inert against production today, and 2 of its 3 header paths likely never fire

You flagged this in the description and asked someone to verify which header a real failed response returns. I checked against live prod:

Request Result Response headers
GET api.e2b.dev/sandboxes/does-not-exist (no auth) 401 content-type, date, content-length, via, alt-svc
same, with a real API key 400 identical set
GET 49999-<bogus>.e2b.app/files?path=/x (sandbox edge) 502 identical set

No trace header on any path. And via: 1.1 google matters here: X-Cloud-Trace-Context and X-Amzn-Trace-Id are request-side conventions — the LB injects them toward the backend, neither is echoed back on the response. So only X-Trace-ID has a plausible future, and it needs the infra one-liner you described.

Suggestion: land c.Header("X-Trace-ID", traceID) in infra first (or in parallel), and drop the GCP/AWS branches plus the AWS normalization regex and their tests unless we can name a deployment that actually returns them. That's roughly half the new code, and the JS/Python duplication of the AWS parser is the part most likely to drift.

B. JS error-class arity is now inconsistent, and it's a live trap

SandboxError(message, stackTrace?, traceId?) but AuthenticationError(message, traceId?) / GitAuthError(message, traceId?) — trace ID in slot 2 vs slot 3. Meanwhile apiErrorFromCode/handleApiError declare errorClass: new (message, stackTrace?, traceId?) => Error, which AuthenticationError satisfies structurally (fewer params is assignable). Confirmed by construction:

apiErrorFromCode(500, 'boom', AuthenticationError, 'STACK', 'realtrace')
// typechecks clean; message === "500: boom (trace ID: STACK)"

No current call site hits it — handleApiError is only ever passed BuildError/FileUploadError/TemplateError/VolumeError — so it's latent rather than broken, but it's the kind of thing that bites the next person. Python has no equivalent problem; it's uniformly (message, trace_id). Please give AuthenticationError/GitAuthError the same 3-arg shape.

Related: six call sites now read new NotFoundError(message, undefined, traceId). An options bag — new SandboxError(message, { stackTrace, traceId }) — removes the undefined padding and the arity trap in one move, and matches our convention that optional parameters go in a trailing options object. Still a constructor field, so it satisfies the original request.

C. Unrelated change bundled in

VolumeError gained this.stack = stackTrace (errors.ts:182-184), which has nothing to do with trace IDs. It also introduces a JS↔Python divergence: Python's VolumeException.__init__ is (message, trace_id) with no stack-trace param. No caller passes a stack trace to VolumeError (every site is handleApiError(res, VolumeError)), so it's dead today. Drop it, or mirror it in Python.

D. extractTraceId is now public JS API with no Python counterpart

export { extractTraceId } in src/index.ts puts an internal parser on the public e2b surface, while extract_trace_id is absent from e2b/__init__.py. We treat JS/Python parity of the public surface as non-negotiable. The CLI's tsconfig maps only bare e2b../js-sdk/src (no e2b/*), so index was the only zero-config route — that's fine, but it should be a deliberate call, and neither the changeset nor the PR description mentions the new export.

E. Smaller notes

  • SupportsApiErrorResponse not extendedhandle_api_exception reaches for getattr(e, "headers", None), but the generated Response dataclass does declare headers: MutableMapping[str, str], and every real caller (including the hand-built volume ones) passes httpx.Headers. Adding headers to the Protocol would let ty check it.
  • Trace ID silently dropped on empty messages — both formatMessage and format_message_with_trace_id require a truthy message, so new SandboxError(undefined, undefined, 'abc') loses it. Marginal.
  • Connect-RPC paths uncovered — documented as intentional, but that's where most sandbox.commands/sandbox.files errors surface, so the feature won't reach the majority of in-sandbox failures even once infra sets the header. Worth calling out explicitly in the description.
  • No test that a custom errorClass (BuildError/VolumeError) receives the trace ID through handleApiError.
  • Python's _FILESYSTEM_HTTP_ERROR_MAP needed no change (it holds classes, not lambdas) while the JS one did — correct, just asymmetric.

Verification I ran locally

  • Lint/format: js-sdk oxlint clean; ruff check + ruff format --check clean (406 files); prettier --check clean on js-sdk src+tests and cli src.
  • Typecheck: js-sdk tsc --noEmit clean, cli clean, python ty check clean.
  • Tests: js-sdk traceId + handleApiError + handleEnvdApiError → 53 passed. CLI vitest run → 114 passed (only template/create.test.ts fails, which needs E2B_API_KEY; unrelated). Python test_trace_id.py → 20 passed.
  • CI: the single red check, Build Packages, is a transient GitHub 503 while uv downloaded CPython 3.10.19 — unrelated to this PR, just needs a re-run.

Verdict

The feedback was implemented faithfully and the mechanics are sound. Before merge I'd like B fixed (real trap, two-line change) and C dropped. A is the judgment call worth your input: as it stands this ships ~690 lines that produce no trace ID against any live E2B endpoint, and two-thirds of the parsing targets headers that GCP-fronted responses don't carry. Pairing it with the infra header change and cutting the edge-header branches would leave a much smaller, actually-exercised feature.

djeebus commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Second-pass items addressed in b7eeae2:

  • B (arity trap)AuthenticationError/GitAuthError now take the same (message, stackTrace?, traceId?) shape as the other classes, so a custom errorClass can no longer receive the stack trace in the trace ID slot. The construction you demonstrated now produces 500: boom (trace ID: realtrace).
  • CVolumeError no longer applies the stack trace (pre-existing behavior restored); the parameter remains only to fit the errorClass constructor shape, with a comment saying so.
  • Leftover guard — the unreachable typeof headers.get check in extractTraceId is gone.
  • D (parity)extract_trace_id is now exported from the Python package (e2b/__init__.py + __all__), and both exports are mentioned in the changeset and PR description.
  • ESupportsApiErrorResponse declares headers (so ty checks the access, getattr dropped); added a handleApiError test for trace ID pass-through with a custom errorClass (BuildError); the PR description now calls out the RPC-path limitation explicitly.

A is @djeebus's call, so I've left the GCP/AWS branches in place for now and updated the PR description with your production findings (no trace header on any live path, via: 1.1 google, request-side conventions). If the decision is to trim to X-Trace-ID-only and pair with the infra c.Header("X-Trace-ID", traceID) change, that's a small follow-up commit — happy to do it on this branch once decided.

Build Packages failure on 0adc234 was the transient 503 you diagnosed; the new push re-runs CI.


Generated by Claude Code

@mishushakov

mishushakov commented Aug 17, 2026

Copy link
Copy Markdown
Member

@djeebus Consolidating Finding A into one self-contained write-up, since it's the last open call on this PR and the surrounding decisions have now shifted what's at stake. Everything you need to decide is below — no need to dig back through the thread.


What this PR does, and where Finding A sits

The PR reads a trace ID off the headers of a failed HTTP response and appends (trace ID: ...) to the error message, so a user can quote the ID in a support ticket and we can join it against server-side traces. It checks three headers in priority order: X-Trace-ID, then GCP's X-Cloud-Trace-Context, then AWS's X-Amzn-Trace-Id (normalising Root=1-<8 hex>-<24 hex> into the 32-hex form).

The rest of the review is settled — error classes now take a trailing options object in JS / keyword-only trace_id in Python, the parser lives in one place rather than being duplicated into the CLI, and it's exported as documented public API (extractTraceId / extract_trace_id) for callers who handle an E2B response themselves.

Finding A is the one thing left: do the GCP and AWS branches belong in the shipped surface at all?

The evidence: none of the three headers comes back from any live E2B endpoint

Request Result Response headers
GET api.e2b.dev/sandboxes/does-not-exist (no auth) 401 content-type, date, content-length, via, alt-svc
same, with a real API key 400 identical set
GET 49999-<bogus>.e2b.app/files?path=/x (sandbox edge) 502 identical set

No X-Trace-ID, no X-Cloud-Trace-Context, no X-Amzn-Trace-Id on any of them. As shipped, the feature is inert against production — which matches the note in your own PR description that nothing in infra/belt writes a trace ID onto the response.

Why the two edge branches are structurally unlikely, not just currently unused

X-Cloud-Trace-Context and X-Amzn-Trace-Id are request-side conventions. The load balancer injects them on the way to the backend; neither is echoed back on the response. via: 1.1 google confirms we sit behind a GCP LB, which doesn't echo its own header.

So X-Trace-ID is the only branch with a plausible future, and it needs the one-liner you identified — c.Header("X-Trace-ID", traceID) in infra, where c.Set("traceID", ...) already happens.

Why this now matters more than when I first raised it

When the parser was internal, shipping unused branches was just dead weight — delete it later, nobody notices. Now that extractTraceId / extract_trace_id are exported with docs and a worked @example, the three-header priority order and the AWS normalisation become a public contract. Removing them later stops being a cleanup and becomes a deprecation cycle on documented behaviour, for parsing that has never once produced a value in production.

That's the crux: publishing it freezes it. Worth getting right in this PR rather than the next one.

What trimming to X-Trace-ID-only would remove

  • traceId.ts: the GCP split('/') block and the AWS ;-field loop with /^1-([0-9a-f]{8})-([0-9a-f]{24})$/i, plus their doc lines — roughly half the file.
  • trace_id.py: import re, _AWS_ROOT_PATTERN, the equivalent two blocks and doc lines — again about half.
  • 14 test cases across five files: 4 in js-sdk/tests/traceId.test.ts, 4 in test_trace_id.py, 4 in cli/tests/utils/errors.test.ts, plus one GCP case each in handleApiError.test.ts and handleEnvdApiError.test.ts.

It would also retire a divergence the two implementations already carry: JS does field.trim().split('=') and destructures [key, value], so a Root value containing = gets truncated, while Python's partition("=") keeps it. Harmless for well-formed AWS values — but it's two implementations that already disagree on a malformed input, inside the code that cannot currently fire.

What's left after the trim is essentially one line:

headers?.get('x-trace-id')?.trim() || undefined

The alternative I'd genuinely push for

The SDKs send no trace or request-ID header today — no traceparent, x-request-id, x-cloud-trace-context or x-amzn-trace-id anywhere in either SDK's source. Combined with your finding that our edge parses the GCP/AWS headers off the request and logs them as edge_trace_id, the design inverts:

If the edge logs whatever trace header arrives on the request, the client can pick the ID and know it up front. Nothing needs to be echoed back.

That's strictly better for the use case this PR exists to serve:

  1. It covers failures with no response at all — connection resets, request timeouts, DNS failures, handleEnvdApiFetchError, the Connect-RPC paths. Response-header extraction can never reach those, and they're a large share of what users actually open tickets about. formatSandboxTimeoutError and formatRequestTimeoutError fire with no response in hand, so under the current approach they stay trace-less permanently.
  2. No infra change needed, versus waiting on the c.Header one-liner.
  3. One ID spans retries, so a flaky request correlates as a single story.

Caveat: I haven't verified the edge-logging behaviour myself — that's from your reading of infra/belt, and I don't have those repos to hand. Worth confirming with whoever owns the edge before betting on it. If infra is on OpenTelemetry, traceparent (W3C Trace Context) is likely the right header to send rather than the GCP-specific one.

The fair counter-argument

Self-hosted / BYOC. An E2B behind API Gateway can return X-Amzn-Trace-Id in some configurations, so the AWS branch isn't dead code forever — it's speculative for E2B Cloud specifically. If we keep it, I'd want that anchored to a named deployment rather than "cloud providers generally," because as it stands it's three header formats maintained in two languages serving zero live endpoints — and now doing so as public API.

Recommendation

  1. Ship X-Trace-ID only, landing the infra one-liner in the same window. Half the parser and 14 tests lighter, nothing speculative, and the public surface we freeze is one header instead of three.
  2. Follow-up: SDK generates and sends the ID, attaching it to every error including timeouts and connection failures. Gated on confirming the edge logs it.
  3. What I'd avoid: merging the three-header parser as public API, since it commits us to a documented priority order and an AWS normalisation that produce nothing today and that either option above would want to remove.

Happy to do the trim on this branch if you want option 1 — it's a small commit. One wrinkle if we go all the way to inlining the one-liner: JS is safe, since Headers.get is spec-guaranteed case-insensitive, but Python isn't — handle_api_exception types headers as Optional[Mapping[str, str]] and the tests pass plain dicts keyed "X-Trace-ID". Every real caller passes httpx.Headers, which is case-insensitive, so I'd keep the small Python helper rather than inline it, to keep that contract in one place.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2da2723. Configure here.

Comment thread packages/js-sdk/src/errors.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

TASTE.md review

The shape of this change is right where TASTE points. The move from (message, stackTrace?, traceId?) to a trailing options object is the golden rule applied correctly, ErrorOpts/StackTraceErrorOpts use the Opts suffix and are named and exported rather than inlined, traceId/trace_id mirror across JS and Python, Id is cased as a word while prose keeps "trace ID" all-caps, JSDoc uses @param/@returns/@example against Python's :param:/:return:, and both the JS extractTraceId and the Python extract_trace_id return absence rather than throwing.

I verified the change locally: tsc --noEmit and ty check are clean, and the 58 JS plus 23 Python unit tests covering the new paths pass.

Six inline comments below. Two are worth acting on before merge (the dropped stack trace for 401/429, and the changeset severity), the rest are consistency nits.

One gap that has no diff line to attach to

Nine control-plane methods in packages/js-sdk/src/sandbox/sandboxApi.ts short-circuit 404 before handleApiError ever runs:

if (res.error?.code === 404) {
  throw new SandboxNotFoundError(`Sandbox ${sandboxId} not found`)
}

const err = handleApiError(res)

So getInfo, setTimeout, getMetrics, pause/resume and friends never pick up a trace ID, even though res.response.headers is in scope one line above. The same holds for the nine sync and nine async sites in e2b/sandbox_sync/sandbox_api.py and e2b/sandbox_async/sandbox_api.py.

This matters more than a normal coverage gap because it is exactly the example the PR description leads with:

await Sandbox.connect('already-dead-sandbox-id')
// 404: sandbox not found (trace ID: 105445aa7843bc8bf206b12000100000)

As written, that call produces no trace ID. Deciding the 404 class at the call site is what TASTE asks for, but the trace ID is orthogonal to which class you pick — it can be attached at each of those sites ({ traceId: extractTraceId(res.response.headers) }) without changing the centralized status mapping. It is a mechanical change across the three files, and it would move the feature from "HTTP errors other than sandbox-not-found" to "the failure users actually hit".

Non-blocking notes

  • The (trace ID: …) suffix is now written out in three places: formatMessage in js-sdk/src/errors.ts, format_message_with_trace_id in e2b/exceptions.py, and throwE2BRequestError in cli/src/utils/errors.ts. The two SDK copies are unavoidable, but the CLI already imports extractTraceId from e2b, so its copy of the message shape can drift from the SDK's without anything noticing. Relatedly, the CLI's E2BRequestError interpolates the ID into the message but does not expose a traceId field the way the SDK errors now do.
  • TASTE asks that error messages say what to do, not just what failed. (trace ID: abc123) is a bare fact — a reader who has not seen the changeset does not know it is meant to be handed to E2B support. Worth considering whether the suffix should carry that instruction, weighed against how much longer it makes every message.
  • The PR description notes that envd connect-RPC errors are not covered because connectrpc.ConnectError exposes no response metadata in Python and the JS side is kept symmetric. That is the right call under the parity rule — better to have both surfaces miss it than to have JS-only trace IDs on RPC errors.
Open in Web View Automation 

Sent by Cursor Automation: SDK complies with TASTE.md

return new AuthenticationError(
content ? `${message} - ${content}` : message
content ? `${message} - ${content}` : message,
{ traceId: opts?.traceId }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The 401 and 429 branches silently drop opts.stackTrace.

This is the pattern TASTE warns about in the dual-method section: forward the whole options surface you were handed rather than destructuring the one field you happen to care about, because the fields you don't name disappear without a sound.

It has a real effect now that it didn't have before this PR. AuthenticationError and RateLimitError both accept and apply opts.stackTrace as of this change, and getFileUploadLink calls handleApiError(fileUploadLinkRes, FileUploadError, { stackTrace }). So an expired API key during a template build produces an error whose stack points into the SDK instead of at the user's builder call — which is the thing stackTrace exists to prevent.

Both branches can just pass opts through:

if (code === 401) {
  const message = 'Unauthorized, please check your credentials.'
  return new AuthenticationError(
    content ? `${message} - ${content}` : message,
    opts
  )
}

if (code === 429) {
  const message = 'Rate limit exceeded, please try again later'
  return new RateLimitError(content ? `${message} - ${content}` : message, opts)
}

The Python side has the mirror of this: api_exception_from_code returns AuthenticationException(text, trace_id=trace_id) and RateLimitException(text, trace_id=trace_id) without the .with_traceback(stack_trace) that the default branch applies. Worth fixing in both so they stay in step.

Comment thread packages/js-sdk/src/traceId.ts Outdated
* }
* ```
*/
export function extractTraceId(headers?: Headers | null): string | undefined {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Headers | null in a public signature.

TASTE is explicit that absence is undefined and never null, and that where an API hands back null you normalize at the boundary so the union never reaches a consumer. extractTraceId is exported from index.ts, so this union is part of the published surface.

Nothing in the SDK passes null today — api/index.ts and envd/api.ts pass response.response.headers, and the CLI passes res.response?.headers, which is Headers | undefined. The only caller is tests/traceId.test.ts:14, which asserts extractTraceId(null) works. So headers?: Headers costs nothing but that one assertion.

There is also a small parity gap in the same signature: JS makes the parameter optional (extractTraceId() is legal), while Python declares headers: Optional[Mapping[str, str]] with no default, so extract_trace_id() is a TypeError. Adding = None on the Python side lines the two up.

Comment thread .changeset/trace-id-error-messages.md Outdated
@@ -0,0 +1,47 @@
---
"e2b": patch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

patch understates this — the JS error constructors changed shape.

The changeset itself spells out the migration two paragraphs down: new SandboxError(message, stackTrace) becomes new SandboxError(message, { stackTrace }). Every error class in errors.ts is exported from the package entry point, e2b is at 2.39.0, and the old positional call now silently lands a string in the opts slot — opts?.traceId and opts?.stackTrace both come back undefined, so a caller who was re-pointing a stack trace loses it with no type error at the boundary and no runtime signal.

The same is true in the other direction for VolumeError, which used to accept anything Error accepts and now rejects stackTrace outright.

A changeset that documents a migration path isn't a patch. Either bump the severity so the release notes carry the break, or keep the old form working — constructor(message?: string, opts?: string | StackTraceErrorOpts) with the string form marked @deprecated and a "use { stackTrace } instead" note would match how TASTE says to retire an identifier.

Python is fine here in practice: SandboxException.__init__ narrows to one positional argument, but I scanned every construction of the affected classes across the SDK and no internal call site passes more than one, so only third-party code doing SandboxException(a, b) would break.

Comment thread packages/js-sdk/src/envd/api.ts Outdated
import { createApiLogger } from '../logs'
import {
SandboxError,
ErrorOpts,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ErrorOpts is a type in a value import.

Two lines above, this file uses import type { components, paths } from './schema.gen', and js-sdk/tsconfig.json sets isolatedModules: true, which is exactly the setting that makes the value/type distinction load-bearing rather than cosmetic. TASTE calls out the same split on the export side ("runtime values use export and type-only names use export type — never mixed") and index.ts follows it in this PR with export type { ErrorOpts, StackTraceErrorOpts }.

A separate import type { ErrorOpts } from '../errors' keeps this consistent. Same applies to StackTraceErrorOpts in api/index.ts and ErrorOpts in sandbox/filesystem/index.ts.

Comment thread packages/python-sdk/e2b/exceptions.py Outdated
) -> Exception: ...


def format_message_with_trace_id(message: str, trace_id: Optional[str] = None) -> str:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should be _format_message_with_trace_id.

e2b.exceptions is an importable public module and this name has no underscore, so it reads as supported API even though it isn't in __all__. Its JS counterpart, formatMessage in errors.ts, is module-private and not exported — the two surfaces should agree on what's reachable.

The siblings already use the convention: _DEFAULT_API_ERROR_MAP in envd/api.py, _API_KEY_PATTERN and _API_KEY_EXAMPLE in api/__init__.py, _AWS_ROOT_PATTERN in the new trace_id.py right in this PR.

(ExceptionFactory above it is a different case — it appears in the public signatures of handle_api_exception and format_envd_api_exception, so it does need to be nameable.)

Comment thread packages/js-sdk/src/errors.ts Outdated
* Context for errors that can point somewhere other than where they were
* constructed.
*/
export interface StackTraceErrorOpts extends ErrorOpts {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit on the name: StackTraceErrorOpts parses as "options for a StackTraceError", by analogy with SandboxConnectOpts for Sandbox.connect or FilesystemWriteOpts for a filesystem write. There is no StackTraceError; what this actually means is "ErrorOpts, plus a stack trace".

Something like TracedErrorOpts or ErrorOptsWithStackTrace reads closer to the split you're drawing. Both are exported from index.ts, so the name is the whole story for anyone reading the type list.

The split itself is right — making VolumeError reject stackTrace at the type level instead of accepting and ignoring it is the correct fix for what the previous commit left behind.

@mishushakov

Copy link
Copy Markdown
Member

@devin-ai-integration please check

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
error message now ends with "(trace ID: ...)" so users can include the ID
when reporting a failure, and it is readable off the error itself as
`error.traceId` / `exception.trace_id`.

The header parser is exported for callers that handle an E2B response
themselves: `extractTraceId` in JS, `extract_trace_id` in Python.

JS error classes take their constructor context as a trailing options
object. Only the classes actually handed a stack trace accept one —
InvalidArgumentError, TemplateError, BuildError, FileUploadError, plus
AuthenticationError/RateLimitError, which a 401 or 429 during a template
file upload can produce; the rest take `ErrorOpts` (traceId only), keeping
the narrowing from #1732.

Not covered: envd RPC (connect) paths, because Python's ConnectError
exposes no response metadata and the JS side is kept symmetric.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mishushakov
mishushakov force-pushed the claude/trace-id-error-messages-cbzqwl branch from f463f67 to 8c5d68b Compare August 21, 2026 17:07
@@ -94,18 +128,27 @@ export class SandboxNotFoundError extends NotFoundError {
* Thrown when authentication fails.
*/
export class AuthenticationError extends Error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this error has no callers that pass a stack trace ?

Comment thread packages/js-sdk/src/errors.ts Outdated
export class RateLimitError extends SandboxError {
constructor(message: string) {
super(message)
constructor(message: string, opts?: ErrorOptsWithStackTrace) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this error has no callers that pass stack trace ?

mishushakov and others added 3 commits August 21, 2026 19:21
AuthenticationError and RateLimitError take ErrorOptsWithStackTrace
because getFileUploadLink is a real caller: it forwards the builder's
captured frame into handleApiError, which routes 401 and 429 through
apiErrorFromCode. Cover it end to end against a mocked upload-link
response so the reason those two classes differ from the rest of the
#1732 narrowing is visible in a test rather than only in review.

Also spell out in the changeset that traceId lives on one root per
domain (T-57), so readers narrow to the right one instead of assuming
SandboxError covers every call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audited every construction of an exported error class. Only
InvalidArgumentError, TemplateError, BuildError, and FileUploadError are
ever handed a stack trace, so those four keep ErrorOptsWithStackTrace and
the rest take ErrorOpts.

AuthenticationError and RateLimitError were the odd pair: nothing
constructs them with a frame directly, they only inherited one because
apiErrorFromCode swaps in a different class for 401/429 and forwarded the
opts it was given. A bad key or a rate limit is a property of the request
rather than of the builder step that made it, so both branches now pass
the trace ID alone and the two classes no longer advertise a parameter
they never get. Same on the Python side, where 401/429 stop calling
with_traceback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VolumeNotFoundError, VolumePathNotFoundError, and SecretNotFoundError are
thrown at the call site before handleApiError sees the response — 404
meaning is decided per call site (T-60) — so they were the one group of
errors the feature missed. Each raise site now reads the header itself.

11 sites in JS (volume/index.ts, secret.ts) and 11 in each Python flavor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants