Skip to content

fix(core): Attribute errors to the span they escaped - #23666

Open
logaretm wants to merge 3 commits into
developfrom
awad/error-span-attribution
Open

fix(core): Attribute errors to the span they escaped#23666
logaretm wants to merge 3 commits into
developfrom
awad/error-span-attribution

Conversation

@logaretm

@logaretm logaretm commented Aug 27, 2026

Copy link
Copy Markdown
Member

Errors are now attributed to the span they were thrown in, not whichever span happened to be active when captureException ran. startSpan records the escaping error's span in a WeakMap and we prefer that when building the event.

It only applies inside the error's own trace, this was flagged as an unlikely edge case by the clanker so I decided to add a sanity check for it regardless of how unlikely it is.

We seemed to have tests asserting the old behavior, so those needed to change to match the new one.

closes #16206

Adds failing coverage for #16206: an error is attributed to whatever span
is active at captureException time rather than the span it was thrown in.

Three cases fail on develop (sync nested span, concurrent group, deepest
escaped span). The fourth asserts the cross-trace bail-out, which passes
today and must keep passing.
An error was attributed to whichever span happened to be active when
captureException ran, not to the span that actually failed. Record the
span's trace context in a WeakMap keyed on the error as it unwinds, and
prefer that when building the error event.

The first (deepest) span wins, non-recording spans are skipped, and the
attribution only applies within the error's own trace so the envelope
header and body can never name different traces.
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.64 kB +0.26% +73 B 🔺
@sentry/browser - with treeshaking flags 26.99 kB +0.27% +71 B 🔺
@sentry/browser - with treeshaking flags tracing without tracing 26.89 kB +0.27% +70 B 🔺
@sentry/browser (incl. Tracing) 48.68 kB +0.16% +75 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 48.69 kB +0.14% +65 B 🔺
@sentry/browser (incl. Tracing, Profiling) 51.6 kB +0.13% +65 B 🔺
@sentry/browser (incl. Tracing, Replay) 88.15 kB +0.07% +53 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 77.55 kB +0.09% +68 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 92.86 kB +0.07% +64 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 105.55 kB +0.07% +66 B 🔺
@sentry/browser (incl. Feedback) 45.86 kB +0.16% +69 B 🔺
@sentry/browser (incl. sendFeedback) 33.43 kB +0.23% +74 B 🔺
@sentry/browser (incl. FeedbackAsync) 38.52 kB +0.17% +62 B 🔺
@sentry/browser (incl. Metrics) 29.59 kB +0.29% +83 B 🔺
@sentry/browser (incl. Logs) 29.86 kB +0.22% +65 B 🔺
@sentry/browser (incl. Metrics & Logs) 30.5 kB +0.21% +63 B 🔺
@sentry/react 30.38 kB +0.24% +72 B 🔺
@sentry/react (incl. Tracing) 50.89 kB +0.16% +77 B 🔺
@sentry/vue 35.76 kB +0.21% +74 B 🔺
@sentry/vue (incl. Tracing) 50.94 kB +0.15% +73 B 🔺
@sentry/svelte 28.66 kB +0.25% +70 B 🔺
CDN Bundle 30.44 kB +0.26% +77 B 🔺
CDN Bundle (incl. Tracing) 49.2 kB +0.21% +103 B 🔺
CDN Bundle (incl. Logs, Metrics) 32.63 kB +0.21% +68 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 51.09 kB +0.21% +107 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 73.08 kB +0.11% +79 B 🔺
CDN Bundle (incl. Tracing, Replay) 86.69 kB +0.11% +89 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 88.59 kB +0.11% +94 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 92.48 kB +0.12% +103 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 94.41 kB +0.12% +107 B 🔺
CDN Bundle - uncompressed 90.22 kB +0.29% +253 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 147.11 kB +0.24% +343 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 96.53 kB +0.28% +262 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 152.8 kB +0.23% +343 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 225.69 kB +0.12% +262 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 266.61 kB +0.13% +343 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 272.28 kB +0.13% +343 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 280.3 kB +0.13% +343 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 285.97 kB +0.13% +343 B 🔺
@sentry/nextjs (client) 53.45 kB +0.12% +59 B 🔺
@sentry/sveltekit (client) 49.13 kB +0.15% +71 B 🔺
@sentry/core/server 65.42 kB +0.19% +122 B 🔺
@sentry/core/browser 52.45 kB +0.2% +101 B 🔺
@sentry/node 122.83 kB +0.11% +130 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 85.22 kB - -
@sentry/node - without tracing 88.08 kB +0.16% +135 B 🔺
@sentry/aws-serverless 96.29 kB +0.12% +114 B 🔺
@sentry/cloudflare (withSentry) - minified 200.01 kB +0.22% +438 B 🔺
@sentry/cloudflare (withSentry) 496.91 kB +0.25% +1.19 kB 🔺

View base workflow run

The error thrown in a hapi route handler escapes the router span, so it is
now attributed to that span rather than to the request span. Assert the new
relationship (error span is a child of the transaction's span, and is the
router span) instead of the old identity.
@logaretm
logaretm marked this pull request as ready for review August 27, 2026 02:47
@logaretm
logaretm requested review from Lms24 and mydea August 27, 2026 02:47
*/
const escapedSpanTraceContexts = new WeakMap<object, TraceContext>();

function toKey(error: unknown): object | undefined {

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.

l: maybe add a comment here why we do this

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.

without a bit more context, from the outside it seems as if we're converting the error to something else, which I was confused about initially 😅

/**
* The trace context of the span an error escaped, keyed by the error itself.
*
* We store the plain trace context rather than the span, so that an error object cannot keep a

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.

l/m: is this necessary? Since this is a weakmap, should this be garbage collected anyhow?

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.

actually, I think the implementation is fine but I would change the comment - it is not necessary to keep this as trace context for gc reasons, but we apply the trace context directly later so this is fine.

trace: {
...eventTraceContext,
span_id: traceContext.span_id,
parent_span_id: traceContext.parent_span_id,

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.

m: IMHO we should invert this, if a span is already set on this, likely we should not overwrite it I think? 🤔 but this also makes the logic a bit trickier, because we cannot just do:

trace: {
  span_id: traceContext.span_id,
  parent_span_id: traceContext.parent_span_id
  ...eventTraceContext
}

because that could lead to a case where eventTraceContext has a span_id but no parent_span_id and then they would be incorrectly in sync.

I guess we generally do have a traceContext here already set, right, even if we do not actually have the span? 🤔

Would it work if we move the invocation of applyEscapedErrorSpanToEvent to prepareEvent.ts like this:

if (span) {
    applySpanToEvent(prepared, span);
  } else {
   applyEscapedErrorSpanToEvent(prepared, hint);
}

or something along these lines? 🤔

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.

Experiment: Thrown errors passing through startSpan should be associated with first passed through span

2 participants