feat: Automatic trace instrumentation for MAUI#5138
Conversation
…inished transactions
Resolves 5116: - #5116
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog. Features ✨
Fixes 🐛
Dependencies ⬆️Deps
Other
🤖 This preview updates automatically when you update the PR. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #5138 +/- ##
==========================================
+ Coverage 74.06% 74.14% +0.08%
==========================================
Files 501 509 +8
Lines 18113 18473 +360
Branches 3521 3642 +121
==========================================
+ Hits 13415 13697 +282
- Misses 3838 3888 +50
- Partials 860 888 +28 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…Span test Without this, StartNavigationSpan always returned null and the test never verified the navigation span finishing behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
ResetIdleTimeout is unreachable when GetSpan() returns null in StartNavigationSpan
Move CurrentUiTx?.ResetIdleTimeout() before the GetSpan() guard so the idle timer is reset whenever navigation begins, even if there is no active span currently on the scope.
Verification
In src/Sentry.Maui/Internal/MauiEventsBinder.cs, StartNavigationSpan reads: if (_hub.GetSpan() is not { IsFinished: false } parentSpan) { return null; } then CurrentUiTx?.ResetIdleTimeout(). When GetSpan() returns null the method exits before the reset call. The unit test OnShellOnNavigating_ActiveUiTransaction_ResetsIdleTimeout in MauiEventsBinderTests.Shell.cs calls StartUiTransaction, which sets scope.Transaction = uiTransaction via ConfigureScope, but the mock Hub.GetSpan() is never configured to return a non-null value (unlike the other Shell tests that all add _fixture.Hub.GetSpan().Returns(mockTransaction)). Because the mock returns null, the guard fires and ResetIdleTimeout is never called, so uiTransaction.Received(1).ResetIdleTimeout() will throw a substitution failure. In production the real Hub.GetSpan() reads CurrentScope.Span, which normally returns the UI transaction after StartUiTransaction binds it to scope — so the failure is rare but the test failure is deterministic.
Identified by Warden find-bugs
We don't want to do that. Only child spans of a transaction should reset the idle timeout. |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4b482a7. Configure here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rface impl Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| { | ||
| Volatile.Write(ref _isFinished, false); | ||
| _endTimestamp = null; | ||
| } |
There was a problem hiding this comment.
Bug: In the SpanTracer.EndTimestamp setter, the null case has a memory ordering violation. Volatile.Write to _isFinished occurs before _endTimestamp is set to null, which is inconsistent and unsafe for future refactoring.
Severity: LOW
Suggested Fix
Reverse the order of operations in the null case of the SpanTracer.EndTimestamp setter. First, set _endTimestamp = null;, and then perform the Volatile.Write(ref _isFinished, false);. This will make the memory ordering consistent with the non-null case and align with the documented intention, preventing potential future race conditions.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: src/Sentry/SpanTracer.cs#L49-L52
Potential issue: When the `SpanTracer.EndTimestamp` property is set to `null`, a memory
ordering violation occurs. The code performs a volatile write to set `_isFinished` to
`false` before setting the `_endTimestamp` field to `null`. This is the reverse of the
intended and documented memory barrier pattern, where the data write should precede the
volatile 'gate' write. While current access patterns prevent this from causing a bug, as
the getter checks `_isFinished` before reading `_endTimestamp`, it is a latent issue.
Future refactoring that accesses `_endTimestamp` without this specific gating logic
could lead to race conditions.
There was a problem hiding this comment.
This is not a real issue - it's mitigated by the getter (and even documented in the setter):
sentry-dotnet/src/Sentry/SpanTracer.cs
Lines 41 to 59 in f5d5bb4

Resolves #5116
Description
Adds automatic tracing support in Sentry.Maui applications.
This initial PR is fairly lean and adds only adds tracing for button clicks and Shell navigation events. So if you click on a button or an image button that results in a Shell navigation, this will be captured automatically as a transaction, which is useful to identify performance problems with application navigation.
Trivial button clicks (with no child spans) wil be filtered automatically and not captured.
The plan is to expand on this functionality to also automatically create traces for Scroll events and capture things like Mobile Vitals at startup:
Sample
An example of the output from

Sentry.Samples.Mauiwhen opening and closing theSubmitFeedbackmodal:Note
Tracing for button clicks only works if the buttons have some id associated with them (
x:Name)... for example:sentry-dotnet/samples/Sentry.Samples.Maui/SubmitFeedback.xaml
Lines 20 to 21 in bc1c8ab
Resolves: #5109
Implementation
Tracing for UI applications is a little bit complex.
Typically very little work will be done on the main UI thread, to avoid freezing/blocking this. So a UI interaction like a button click might navigate to a new view, a spinner icon animation kicks off and, technically, at that point, the navigation is completed. In reality the app may be implementing some kind of progressive display and loading of portions of that display is still taking place in the background (getting some stuff from a database to load up in the main panel, pulling access control credentials from a remote source to determine what actions should be available on the menu etc.).
The solution we have then is to start a new transaction with an idle timer on it:
This won't be accurate 100% of the time, but it should be a pretty good approximation most of the time.
Concurrency
This PR also introduces and has to resolve a bunch of concurrency issues. Those were non-trivial so did them in a separate PR that I merged into this one. Reviewers can see those changes in isolation (along with a description of some of the challenges) in that PR: