Skip to content

feat(tracing): Add Sentry.startNewTrace - #4036

Open
gbolahan507 wants to merge 8 commits into
getsentry:mainfrom
gbolahan507:ahabeeblahi/feat/start-new-trace
Open

gbolahan507 wants to merge 8 commits into
getsentry:mainfrom
gbolahan507:ahabeeblahi/feat/start-new-trace

Conversation

@gbolahan507

Copy link
Copy Markdown
Contributor

Closes #3930

Description

Adds Sentry.startNewTrace<T>(T Function() callback), a public API for
manually starting a new trace scoped to a callback — parity with the JS
SDK's Sentry.startNewTrace().

Forks the current scope with a fresh PropagationContext and runs the
callback in a Zone, reusing the same runZoned + _scopeKey
mechanism Hub.startSpan already uses. Transactions, events, and
outgoing trace headers created inside the callback pick up the new
trace; the global scope is untouched, so concurrent work outside the
callback keeps its trace. The zone survives awaits, so async
callbacks and Timer.periodic polls (the reporter's use case) work.

Motivation

Users need to segment background work — polling loops, per-message
handlers, per-job units — into distinct traces without clobbering the
global trace state. The existing internal Hub.generateNewTrace()
mutates the global scope, which isn't callback-scoped and breaks
concurrent work.

Design follows @buenaflor's proposal in #3930.

How tested

  • 14 new tests in `hub_test.dart`: sync/async callbacks, global scope
    left untouched, transactions inside the callback share the new
    trace id, awaits survive, nested calls, streaming spans, captured
    events pick up the new trace, `rethrow` still ends the span
  • 1 test each in `sentry_test.dart` and `tracing_utils_test.dart`
  • Full `packages/dart` suite: 1728 passed, 4 skipped, 0 failed
  • `packages/dio` analyze clean (mock hub updated)

Deliberate choice

This does not dispatch `OnGenerateNewTrace` to native SDKs. Doing
so would push the callback-scoped trace id into the native layer
globally and break the "doesn't override the global scope" contract
this API is built around. If native sync is desired for this API,
happy to do it as a follow-up with a separate scoping story.

Changelog Entry

Features

  • Add `Sentry.startNewTrace` for manually starting a callback-scoped
    trace (#3930)

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/dart/lib/src/hub.dart Resolved
Comment thread packages/dart/lib/src/hub.dart Resolved

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/dart/lib/src/hub.dart Resolved
Comment thread packages/dart/lib/src/hub.dart Resolved

@buenaflor buenaflor 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.

Thanks for the contribution. Please have a look at the AI flagged reviews first.

@gbolahan507

Copy link
Copy Markdown
Contributor Author

Thanks for the pointer @buenaflor — the bots were spot on, they were all really pointing at the same problem. I was swapping in a fresh propagation context for the new trace but leaving three references to the outer span in place, so anything that reached for those (nested spans, HTTP headers, error events) ended up back on the old trace.

Pushed a fix in 146da91. Short version:

The zone-forked scope was inheriting _activeSpan from the parent scope, and getActiveSpan() was also falling back to the hub's idle span, so any startSpan inside the callback parented onto whatever was already running — that's the Flutter performance scenario Seer flagged. And on the events side, applyToEvent was reading the hub's bound transaction (e.g. a navigation transaction), which pinned captured events to the old trace regardless of the new propagation context.

I added a small zone sentinel that lets getActiveSpan, getSpan, and the event-scope path recognise they're inside a startNewTrace callback and skip those outer references. Nothing changes for normal startSpan — that keeps its parent-chain behaviour. Also added tests for the two Flutter-shaped cases (idle span active, and a hub-bound transaction with an event captured inside the callback). Full suite passes.

Let me know if you'd like me to split anything out or reshape the API.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/dart/lib/src/hub.dart Resolved
@gbolahan507

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in 255946c. bindToScope inside startNewTrace now writes to the zone-forked scope so the bound tx is visible to getSpan/events during the callback and doesn't leak to the hub scope. Test added.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/dart/lib/src/hub.dart Resolved
@gbolahan507

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in 64c1582. Logger and metrics setup integrations now wire scopeProvider to hub.traceScope, so logs and metrics emitted inside startNewTrace pick up the callback-scoped trace id. Tests added (verified they fail without the fix).

@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.15%. Comparing base (abc9712) to head (64c1582).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/dart/lib/src/hub_adapter.dart 0.00% 4 Missing ⚠️
packages/dart/lib/src/noop_hub.dart 0.00% 4 Missing ⚠️
packages/dart/lib/src/hub.dart 97.22% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4036      +/-   ##
==========================================
- Coverage   88.17%   88.15%   -0.02%     
==========================================
  Files         347      347              
  Lines       12955    12996      +41     
==========================================
+ Hits        11423    11457      +34     
- Misses       1532     1539       +7     
Flag Coverage Δ
sentry 87.82% <83.33%> (-0.04%) ⬇️
sentry_dio 97.73% <ø> (ø)
sentry_drift 93.57% <ø> (ø)
sentry_file 65.29% <ø> (ø)
sentry_firebase_remote_config 100.00% <ø> (ø)
sentry_flutter 92.46% <ø> (ø)
sentry_grpc 99.09% <ø> (ø)
sentry_hive 77.48% <ø> (ø)
sentry_isar 74.37% <ø> (ø)
sentry_link 21.50% <ø> (ø)
sentry_logging 97.01% <ø> (ø)
sentry_sqflite 88.81% <ø> (ø)
sentry_supabase 97.27% <ø> (ø)

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.

@gbolahan507

Copy link
Copy Markdown
Contributor Author

Friendly ping @buenaflor : all AI-flagged reviews are addressed, and CI is green (the two failures are the known changelog-preview / android-beta infra flakes). Happy to rebase on the main whenever you have a moment to take another look.

@buenaflor

Copy link
Copy Markdown
Contributor

@gbolahan507 thanks, we will be releasing a new major next week so let's wait for that to get out before continuing with this

@buenaflor

Copy link
Copy Markdown
Contributor

Pls rebase to main

Public API for manually starting a new trace, scoped to a
callback. Forks the current scope with a fresh
PropagationContext and runs the callback in a Zone using the
same _scopeKey mechanism startSpan uses, so transactions,
events, and outgoing trace headers inside the callback pick up
the new trace while the global scope is untouched. Async
callbacks are covered — the zone survives awaits.

Closes getsentry#3930
Cursor Bugbot and Seer flagged that startNewTrace let the outer
trace leak into the callback through three span references on the
hub scope:

- Scope.clone copied _activeSpan onto the forked scope, so
  getActiveSpan still returned an outer span.
- getActiveSpan fell back to _currentIdleSpan unconditionally,
  so nested startSpan calls parented onto the outer idle span
  and inherited its trace id (Flutter idle-span case).
- getSpan and Scope.applyToEvent read the hub scope's bound
  transaction, so getSpan(), HTTP header propagation and
  captured events inside the callback stayed on the outer
  trace (Flutter navigation-transaction case).

Add a Zone sentinel _newTraceKey set alongside _scopeKey in
startNewTrace. Inside that zone:

- Also clearActiveSpan() on the forked scope.
- getActiveSpan skips the _currentIdleSpan fallback.
- getSpan reads from the zone-forked scope, not the hub scope.
- _cloneAndRunWithScope clears the incoming scope's span and
  active span, so applyToEvent derives trace context from the
  fresh PropagationContext instead of an outer span.

Three new tests cover: idle span active on hub, hub-bound
transaction with an event captured inside the callback, and
getSpan() inside the callback.

Refs getsentry#3930
Bugbot flagged that startTransaction(bindToScope: true) inside a
startNewTrace callback wrote the tracer to the hub's scope stack,
so the previous fixup — which taught getSpan and applyToEvent to
read from the zone-forked scope — no longer saw it. Errors and
outgoing HTTP requests inside the callback would not attach to
that transaction, and the write leaked into the hub scope for
code running outside.

Route the bindToScope write to the zone-forked scope when the
current zone was established by startNewTrace, so the bound
transaction is visible for the duration of the callback and the
hub scope stays untouched.

Refs getsentry#3930
Bugbot flagged that structured logs and metrics still stamped
the outer trace id when emitted inside a startNewTrace callback:
DefaultSentryLogger and DefaultSentryMetrics build the log/metric
synchronously (before hub.captureLog/captureMetric runs) and read
propagationContext from the scope provider — which was wired to
hub.scope, not the zone-forked scope.

Wire the scope provider to hub.traceScope in both setup
integrations, so the logger and metrics implementations pick up
the callback-scoped propagation context inside startNewTrace and
fall back to the hub scope everywhere else.

Refs getsentry#3930
@gbolahan507
gbolahan507 force-pushed the ahabeeblahi/feat/start-new-trace branch from 64c1582 to 2c94a73 Compare October 15, 2026 00:53

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 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 reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

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

Comment thread packages/dart/lib/src/hub.dart Resolved
Bugbot flagged that in static mode, markActiveSpanAsErrored used
configureScope which only reaches the hub scope. A transaction
bound inside a startNewTrace callback lives on the zone-forked
scope, so an unhandled error silently failed to mark it as
errored while the outer hub span stayed clean.

Read the span from traceScope directly, same pattern the rest of
startNewTrace already uses for getSpan and event capture.

Refs getsentry#3930
@gbolahan507

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in 58b7cb8. markActiveSpanAsErrored in static mode now reads from traceScope.span directly instead of going through configureScope, so a transaction bound inside a startNewTrace callback gets marked when an error happens. Test added (verified it fails without the fix).

@buenaflor buenaflor 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.

Nice, two things to fix:

  • A transaction bound inside startNewTrace stays bound after finish(): it binds to the zone scope, but cleanup still clears the hub scope. getSpan() keeps returning the finished transaction.
  • Idle spans still bind globally. One started inside startNewTrace is invisible inside the callback but becomes active outside it.

Please add regression tests for both.

Two leaks flagged by @buenaflor:

1. A transaction bound inside startNewTrace via bindToScope was
   written to the zone-forked scope, but SentryTracer.finish went
   through configureScope which only reaches the hub scope. The
   finished tx stayed on the zone scope and getSpan kept returning
   it. Add Hub.unbindSpanFromScope that clears from either scope
   and call it from finish instead of configureScope.

2. startIdleSpan always wrote to the hub-level _idleSpan, so an
   idle span started inside the callback (which was invisible
   inside, since getActiveSpan skips the idle-span fallback in a
   new-trace zone) became active for all code outside once the
   callback exited. Store the idle span in a zone-local holder
   when inside startNewTrace, and have _currentIdleSpan read from
   the holder in that zone.

Regression tests for both.

Refs getsentry#3930
@gbolahan507

Copy link
Copy Markdown
Contributor Author

Both fixed in 5cc7559. SentryTracer.finish now goes through a new Hub.unbindSpanFromScope that clears from the zone scope too, and idle spans started inside the callback live on a zone-local holder so they don't leak out. Regression tests added for each (verified they fail without the fix).

This branch has not been deployed

No deployments
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.

Expose a public API to manually start a new trace (parity with JS SDK's startNewTrace())

2 participants