feat: Link errors to the active streamed span - #3936
Draft
buenaflor wants to merge 5 commits into
Draft
Conversation
Contributor
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. Features
🤖 This preview updates automatically when you update the PR. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3936 +/- ##
==========================================
+ Coverage 87.92% 89.51% +1.59%
==========================================
Files 346 330 -16
Lines 12852 12461 -391
==========================================
- Hits 11300 11155 -145
+ Misses 1552 1306 -246
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Under the stream trace lifecycle, errors fell through to SentryTraceContext.fromPropagationContext, which mints a span id belonging to no span, so the backend could place the error in the trace but not link it to a span. Errors now carry the active span's trace context, and failed HTTP requests link to their http.client span in the http and Dio integrations. Co-Authored-By: Cursor <cursoragent@cursor.com>
Two buttons request a 404 inside a span, through the http and Dio integrations, so the error-to-http.client-span link can be checked in Sentry. The dialog shows the trace id to search for. Co-Authored-By: Cursor <cursoragent@cursor.com>
buenaflor
force-pushed
the
feat/link-errors-to-streamed-spans
branch
from
July 30, 2026 12:24
1caa86c to
c75fe4f
Compare
Contributor
iOS Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| e5ae2a6 | 1240.48 ms | 1253.26 ms | 12.78 ms |
| 114239b | 1225.74 ms | 1230.17 ms | 4.43 ms |
| 6062654 | 1240.98 ms | 1244.56 ms | 3.58 ms |
| 6ad8fc4 | 1263.70 ms | 1266.06 ms | 2.36 ms |
| 79df88f | 1255.16 ms | 1263.51 ms | 8.35 ms |
| c002f00 | 1252.47 ms | 1258.78 ms | 6.31 ms |
| 1e46846 | 1260.48 ms | 1269.23 ms | 8.75 ms |
| c1e775e | 1263.08 ms | 1275.32 ms | 12.24 ms |
| ec78888 | 1251.37 ms | 1269.40 ms | 18.04 ms |
| 5b9a0da | 1249.69 ms | 1250.71 ms | 1.03 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| e5ae2a6 | 5.65 MiB | 6.09 MiB | 446.96 KiB |
| 114239b | 5.53 MiB | 5.96 MiB | 444.85 KiB |
| 6062654 | 5.83 MiB | 6.28 MiB | 460.75 KiB |
| 6ad8fc4 | 5.53 MiB | 6.01 MiB | 487.65 KiB |
| 79df88f | 5.83 MiB | 6.28 MiB | 461.20 KiB |
| c002f00 | 5.65 MiB | 6.09 MiB | 448.38 KiB |
| 1e46846 | 5.73 MiB | 6.18 MiB | 463.54 KiB |
| c1e775e | 20.70 MiB | 22.46 MiB | 1.75 MiB |
| ec78888 | 7.86 MiB | 9.44 MiB | 1.58 MiB |
| 5b9a0da | 5.53 MiB | 5.96 MiB | 444.32 KiB |
Extends error-to-span linking beyond the http and Dio integrations. gRPC captures in the same interceptor that creates the span, so it applies the span to the capture scope directly. Supabase and GraphQL split span creation and error capture across layers, so they reuse the request span registry: the tracing layer registers, the error layer above it looks up the same request object. Co-Authored-By: Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Integrations that only mark their span and rethrow, such as the database ones, are not the capture site. The error is reported later by application code or a global handler, when the span has ended and is reachable from nothing. The throwable is the only handle that travels that far, so the streaming span registers itself against it. The capture path then resolves that span and stamps the event through the existing scope path, giving drift, sqflite, hive, isar and file the same link the HTTP integrations already had. Co-Authored-By: Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
The helper only ever resolves span-v2 instances, which static tracing cannot create, so return before the registry lookup. Renamed since it no longer links just the active span. Co-Authored-By: Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
Android Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| f80073a | 368.87 ms | 362.77 ms | -6.10 ms |
| 1e46846 | 419.06 ms | 429.65 ms | 10.59 ms |
| d789735 | 363.95 ms | 358.09 ms | -5.87 ms |
| 79f6b41 | 469.66 ms | 525.90 ms | 56.24 ms |
| e5b87f8 | 371.22 ms | 377.22 ms | 6.00 ms |
| af26bcd | 352.64 ms | 347.08 ms | -5.56 ms |
| 8775665 | 415.88 ms | 439.26 ms | 23.38 ms |
| c27e6fa | 357.51 ms | 363.06 ms | 5.55 ms |
| 765aa8b | 493.51 ms | 531.23 ms | 37.72 ms |
| 73dca78 | 476.53 ms | 522.21 ms | 45.68 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| f80073a | 14.31 MiB | 15.56 MiB | 1.25 MiB |
| 1e46846 | 14.31 MiB | 15.56 MiB | 1.25 MiB |
| d789735 | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| 79f6b41 | 6.54 MiB | 7.69 MiB | 1.15 MiB |
| e5b87f8 | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| af26bcd | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 8775665 | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| c27e6fa | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 765aa8b | 6.54 MiB | 7.70 MiB | 1.16 MiB |
| 73dca78 | 6.54 MiB | 7.69 MiB | 1.15 MiB |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📜 Description
Errors captured while a streamed span is active now carry that span's trace context, so Sentry can link them to a span instead of only placing them in the trace.
Core —
Scope.applyToEventstampscontexts.tracefrom the active span-v2 and takes the transaction name from its segment.Hubattaches the span an event belongs to onto a per-capture scope copy, incaptureEvent/captureExceptiononly. The error envelope's DSC now comes from the segment's frozen header instead of incomplete scope baggage.Integrations that capture —
http, Dio, gRPC, Supabase and GraphQL (sentry_link) report failures themselves, so they hand their span to that capture and the error links to its own span rather than to the parent. gRPC does it directly; the others register the span per request because span creation and error capture sit in different layers.Integrations that rethrow — the database ones mark the span and rethrow, so the error is reported later by application code or a global handler, when the span has ended and is reachable from nothing. The throwable is the only handle that travels that far, so it becomes the key: the streaming span registers itself against it, and the capture path resolves it back and stamps the event through the same scope path. This covers
drift,sqflite,hive,isarandfile, and restores under streaming whatHub._throwableToSpanalready does for static tracing.Where several links are possible the most specific wins: an explicit
withScopespan, then the span the throwable aborted, then the ambient active span.No public API changes;
InstrumentationSpan.applyToScope,RequestSpanRegistryandThrowableSpanRegistryare all@internal.💡 Motivation and Context
Under
SentryTraceLifecycle.streamthere is noscope.span, so errors fell through toSentryTraceContext.fromPropagationContext, which mints a span id belonging to no span. Trace grouping worked, but the error-to-span link did not. This is the streaming equivalent of what the JS SDK does inapplySpanToEvent.StreamingInstrumentationSpan.throwablealso wrote to a field nothing read, so everyspan.throwable = ein the database integrations was silently inert — the static-mode link had no streaming counterpart.💚 How did you test it?
Unit tests per piece (hub seeding and precedence, scope apply, envelope DSC) plus end-to-end tests per integration: each asserts the error's trace context matches the instrumentation span's own id, and that nothing is linked when no span is involved. Asserting the exact span id matters — a weaker assertion would pass on the ambient-span fallback alone. The drift test drives a real failing query and reports the error after it escaped the span, which is the path that was broken.
Full suites pass:
sentry(1724),sentry_flutter(1077),sentry_sqflite(113),sentry_isar(108),sentry_supabase(96),sentry_hive(94),sentry_dio(88),sentry_grpc(38),sentry_drift(36),sentry_file(29),sentry_logging(23),sentry_link(9).Manually on an iOS simulator using two new demo buttons in the Flutter example. Both errors arrived in
sentry-flutterwith the span name astransaction— trace2b5e767b74594702abf391f2bd5ba1b3.📝 Checklist
sendDefaultPiiis enabled🔮 Next steps
Expandocannot key — strings, numbers, booleans, records — are skipped rather than wrapped. Static tracing wraps them, but at the cost of a set that grows without bound, so this is deliberate.baggageheaders) still carry an incomplete DSC. Fixable generally by freezing the root span's DSC into the propagation context.captureMessageandcaptureFeedbackdon't link to spans yet — additive follow-up if wanted.FailedRequestInterceptoronly captures failures with a matching status code, so connection errors are never captured or linked there. Pre-existing behaviour, not changed here.Made with Cursor