Skip to content

feat: Link errors to the active streamed span - #3936

Draft
buenaflor wants to merge 5 commits into
mainfrom
feat/link-errors-to-streamed-spans
Draft

feat: Link errors to the active streamed span#3936
buenaflor wants to merge 5 commits into
mainfrom
feat/link-errors-to-streamed-spans

Conversation

@buenaflor

@buenaflor buenaflor commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

📜 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.

CoreScope.applyToEvent stamps contexts.trace from the active span-v2 and takes the transaction name from its segment. Hub attaches the span an event belongs to onto a per-capture scope copy, in captureEvent/captureException only. The error envelope's DSC now comes from the segment's frozen header instead of incomplete scope baggage.

Integrations that capturehttp, 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, isar and file, and restores under streaming what Hub._throwableToSpan already does for static tracing.

Where several links are possible the most specific wins: an explicit withScope span, then the span the throwable aborted, then the ambient active span.

No public API changes; InstrumentationSpan.applyToScope, RequestSpanRegistry and ThrowableSpanRegistry are all @internal.

💡 Motivation and Context

Under SentryTraceLifecycle.stream there is no scope.span, so errors fell through to SentryTraceContext.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 in applySpanToEvent.

StreamingInstrumentationSpan.throwable also wrote to a field nothing read, so every span.throwable = e in 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-flutter with the span name as transaction — trace 2b5e767b74594702abf391f2bd5ba1b3.

📝 Checklist

  • I reviewed submitted code
  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPii is enabled
  • I updated the docs if needed
  • All tests passing
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec
  • No breaking changes

🔮 Next steps

  • Throwables that Expando cannot 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.
  • Non-error envelopes (logs, metrics, outbound baggage headers) still carry an incomplete DSC. Fixable generally by freezing the root span's DSC into the propagation context.
  • captureMessage and captureFeedback don't link to spans yet — additive follow-up if wanted.
  • Dio's FailedRequestInterceptor only 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

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


Features

  • Link errors to the active streamed span by buenaflor in #3936

🤖 This preview updates automatically when you update the PR.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.31250% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.51%. Comparing base (c27e6fa) to head (ef25d26).
⚠️ Report is 6 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
.../tracing/instrumentation/instrumentation_span.dart 85.71% 1 Missing ⚠️
...tracing/instrumentation/request_span_registry.dart 80.00% 1 Missing ⚠️
...acing/instrumentation/throwable_span_registry.dart 93.33% 1 Missing ⚠️
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     
Flag Coverage Δ
sentry 88.30% <94.54%> (+0.67%) ⬆️
sentry_dio 97.76% <100.00%> (+0.03%) ⬆️
sentry_drift 93.57% <ø> (ø)
sentry_file 65.29% <ø> (ø)
sentry_firebase_remote_config 100.00% <ø> (ø)
sentry_flutter 92.16% <ø> (ø)
sentry_grpc 99.10% <100.00%> (+<0.01%) ⬆️
sentry_hive ?
sentry_isar 74.37% <ø> (ø)
sentry_link ?
sentry_logging 97.01% <ø> (ø)
sentry_sqflite 88.81% <ø> (ø)
sentry_supabase 98.20% <100.00%> (+0.93%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

buenaflor and others added 2 commits July 30, 2026 14:09
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
buenaflor force-pushed the feat/link-errors-to-streamed-spans branch from 1caa86c to c75fe4f Compare July 30, 2026 12:24
@buenaflor
buenaflor changed the base branch from chore/pre-commit-fvm-dart to main July 30, 2026 12:24
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

iOS Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1235.31 ms 1244.29 ms 8.98 ms
Size 5.83 MiB 6.28 MiB 460.02 KiB

Baseline results on branch: main

Startup times

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

Previous results on branch: feat/link-errors-to-streamed-spans

Startup times

Revision Plain With Sentry Diff
1caa86c 1230.40 ms 1234.38 ms 3.98 ms

App size

Revision Plain With Sentry Diff
1caa86c 5.83 MiB 6.28 MiB 461.65 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>
Comment thread packages/grpc/test/sentry_grpc_interceptor_test.dart
buenaflor and others added 2 commits July 30, 2026 22:45
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>
@github-actions

Copy link
Copy Markdown
Contributor

Android Performance metrics 🚀

  Plain With Sentry Diff
Startup time 370.92 ms 367.14 ms -3.78 ms
Size 14.55 MiB 15.87 MiB 1.31 MiB

Baseline results on branch: main

Startup times

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

Previous results on branch: feat/link-errors-to-streamed-spans

Startup times

Revision Plain With Sentry Diff
1caa86c 364.81 ms 365.78 ms 0.97 ms

App size

Revision Plain With Sentry Diff
1caa86c 14.55 MiB 15.87 MiB 1.31 MiB

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.

1 participant