-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(core): Attribute errors to the span they escaped #23666
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
5d3f587
dbe138c
d1a5baf
b9b5cd5
b1d8b5d
1285e11
4099fde
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { getTraceContextFromScope } from '../currentScopes'; | ||
| import type { Scope } from '../scope'; | ||
| import type { TraceContext } from '../types/context'; | ||
| import type { Event, EventHint } from '../types/event'; | ||
| import type { Span } from '../types/span'; | ||
| import { isPrimitive } from './is'; | ||
| import { spanIsSampled, spanToTraceContext } from './spanUtils'; | ||
|
|
||
| /** | ||
| * The trace context of the span an error escaped, keyed by the error itself. | ||
| * | ||
| * We store the trace context rather than the span because that is the shape we apply to the event | ||
| * later, and it snapshots the span as it failed instead of reading it back once it has ended. | ||
| */ | ||
| const escapedSpanTraceContexts = new WeakMap<object, TraceContext>(); | ||
|
|
||
| /** | ||
| * A `WeakMap` can only be keyed by an object, so an error thrown as a primitive (`throw 'boom'`) | ||
| * has nothing we can hang the span on and is left unattributed. | ||
| */ | ||
| function toWeakMapKey(error: unknown): object | undefined { | ||
| return isPrimitive(error) ? undefined : error; | ||
| } | ||
|
|
||
| /** | ||
| * Remember which span an error escaped, so a later `captureException` can attribute the error to | ||
| * the span that actually failed instead of whichever span happens to be active at capture time. | ||
| * | ||
| * The first span to see the error wins: as an error unwinds through nested spans, the innermost | ||
| * one is the one that failed. Unsampled spans are skipped because they are never sent, so their | ||
| * span id would point at a span that does not exist. Sampling rather than `isRecording()` is what | ||
| * matters here: a span ended before the error escaped it, which is the norm for `startSpanManual`, | ||
| * has stopped recording but is still sent. | ||
| */ | ||
| export function recordEscapedErrorSpan(error: unknown, span: Span): void { | ||
| const key = toWeakMapKey(error); | ||
|
|
||
| if (!key || !spanIsSampled(span) || escapedSpanTraceContexts.has(key)) { | ||
| return; | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| escapedSpanTraceContexts.set(key, spanToTraceContext(span)); | ||
| } | ||
|
|
||
| /** | ||
| * Attribute an error event to the span the error escaped, if we recorded one. | ||
| * | ||
| * This only applies within the error's own trace. The stored span id is meaningless in another | ||
| * trace, and the event's dynamic sampling context (which the envelope header is built from) is | ||
| * derived from the root span of the trace the event is already on. Rewriting the trace id here | ||
| * would leave the envelope header and body naming different traces. | ||
| */ | ||
| export function applyEscapedErrorSpanToEvent(event: Event, hint: EventHint, scope: Scope | undefined): void { | ||
| const key = toWeakMapKey(hint.originalException); | ||
| const traceContext = key && escapedSpanTraceContexts.get(key); | ||
|
|
||
| if (!traceContext) { | ||
| return; | ||
| } | ||
|
|
||
| // An error captured with no active span has no trace context yet: the scope's is merged in | ||
| // further downstream. Resolve the trace the event will end up on the same way that merge does, | ||
| // so the check below still knows which trace we are on. | ||
| const eventTraceContext = event.contexts?.trace; | ||
| const eventTraceId = eventTraceContext?.trace_id ?? (scope && getTraceContextFromScope(scope).trace_id); | ||
|
|
||
| if (eventTraceId !== traceContext.trace_id) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trace check uses the wrong scopeMedium Severity The same-trace fallback reads Additional Locations (1)Reviewed by Cursor Bugbot for commit 4099fde. Configure here. |
||
| return; | ||
| } | ||
|
|
||
| event.contexts = { | ||
| ...event.contexts, | ||
| trace: { | ||
| ...eventTraceContext, | ||
| ...traceContext, | ||
| }, | ||
| }; | ||
|
logaretm marked this conversation as resolved.
|
||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The error span attribution fix is incomplete. Next.js route handlers call
handleCallbackErrorsdirectly, bypassing thestartSpanlogic, so errors are not correctly attributed to the handler's span.Severity: MEDIUM
Suggested Fix
To ensure consistent error attribution, wrap the route handler execution in
wrapRouteHandlerWithSentry.tswith astartSpancall. This will ensure thatrunCallbackis executed, which in turn callsrecordEscapedErrorSpanupon an error, correctly associating the error with the active span before it's captured. This pattern should be applied to any other framework integrations that currently callhandleCallbackErrorsdirectly.Prompt for AI Agent