Skip to content

feat: Automatic trace instrumentation for MAUI#5138

Open
jamescrosswell wants to merge 57 commits into
mainfrom
maui-trace-ui-5116
Open

feat: Automatic trace instrumentation for MAUI#5138
jamescrosswell wants to merge 57 commits into
mainfrom
maui-trace-ui-5116

Conversation

@jamescrosswell
Copy link
Copy Markdown
Collaborator

@jamescrosswell jamescrosswell commented Apr 17, 2026

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.Maui when opening and closing the SubmitFeedback modal:
image

Note

Tracing for button clicks only works if the buttons have some id associated with them (x:Name)... for example:

<Button x:Name="CancelBtn" Text="Cancel" Clicked="OnCancelClicked" BackgroundColor="Red" TextColor="White" />
<Button x:Name="SubmitBtn" Text="Submit" Clicked="OnSubmitClicked" BackgroundColor="Green" TextColor="White" />

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:

  • If the idle timeout is reached before any other activity happens on the transaction (child spans added) then we simply discard the transaction as trivial/irrelevant.
  • If a child span kicks off on the transaction though, we reset the idle timeout... and we keep doing that as long as there is activity
  • When the idle timeout finally triggers, if there are child spans on the transaction, the end date/time for the transaction is set as the end date/time of the last child span to finish

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:

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

Semver Impact of This PR

None (no version bump detected)

📋 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 ✨

  • feat: Automatic trace instrumentation for MAUI by jamescrosswell in #5138

Fixes 🐛

  • fix: prevent redundant native exceptions on Android/CoreCLR by jpnurmi in #5127

Dependencies ⬆️

Deps

  • chore(deps): update Java SDK to v8.39.0 by github-actions in #5137
  • chore(deps): update Native SDK to v0.13.7 by github-actions in #5136

Other

  • perf(logs): avoid string allocation when no parameters are passed by Flash0ver in #4697
  • chore: fix missing skill by jamescrosswell in #5134

🤖 This preview updates automatically when you update the PR.

@jamescrosswell jamescrosswell changed the base branch from maui-transactions-5109 to main April 17, 2026 01:32
@jamescrosswell jamescrosswell changed the base branch from main to maui-transactions-5109 April 17, 2026 01:32
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 17, 2026

Codecov Report

❌ Patch coverage is 80.20305% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.14%. Comparing base (32a55e3) to head (f5d5bb4).
⚠️ Report is 33 commits behind head on main.

Files with missing lines Patch % Lines
src/Sentry/TransactionTracer.cs 82.02% 10 Missing and 6 partials ⚠️
src/Sentry.Maui/Internal/MauiEventsBinder.cs 82.19% 4 Missing and 9 partials ⚠️
src/Sentry/Infrastructure/SystemTimer.cs 0.00% 6 Missing ⚠️
src/Sentry/Extensibility/DisabledHub.cs 0.00% 1 Missing ⚠️
src/Sentry/Extensibility/HubAdapter.cs 0.00% 1 Missing ⚠️
src/Sentry/Internal/Hub.cs 0.00% 1 Missing ⚠️
src/Sentry/SentrySdk.cs 0.00% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/Sentry.Maui/Internal/MauiEventsBinder.cs Outdated
…limit (#5227)

Wraps AddChildSpan in a lock so concurrent span creation cannot exceed
the limit or set IsSampled incorrectly under contention.

Fixes #5173

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/Sentry/TransactionTracer.cs Outdated
Comment thread src/Sentry.Maui/Internal/MauiEventsBinder.cs
Comment thread test/Sentry.Maui.Tests/MauiEventsBinderTests.Shell.cs
Comment thread src/Sentry/TransactionTracer.cs
…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>
Comment thread src/Sentry/ITransactionTracer.cs Outdated
Copy link
Copy Markdown
Contributor

@sentry-warden sentry-warden Bot left a comment

Choose a reason for hiding this comment

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

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

Comment thread src/Sentry.Maui/Internal/MauiEventsBinder.cs Outdated
Comment thread test/Sentry.Maui.Tests/MauiEventsBinderTests.Shell.cs
@jamescrosswell
Copy link
Copy Markdown
Collaborator Author

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.

We don't want to do that. Only child spans of a transaction should reset the idle timeout.

Comment thread test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt Outdated
Comment thread src/Sentry/SpanTracer.cs
Comment thread test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt Outdated
Comment thread test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt Outdated
Comment thread src/Sentry/TransactionTracer.cs Outdated
Comment thread src/Sentry.Maui/Internal/MauiEventsBinder.cs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

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.

Reviewed by Cursor Bugbot for commit 4b482a7. Configure here.

Comment thread src/Sentry/TransactionTracer.cs Outdated
Comment thread src/Sentry.Maui/Internal/MauiEventsBinder.cs Outdated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jamescrosswell jamescrosswell marked this pull request as draft May 18, 2026 03:10
…rface impl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/Sentry/TransactionTracer.cs
@jamescrosswell jamescrosswell marked this pull request as ready for review May 21, 2026 11:58
Comment thread src/Sentry/SpanTracer.cs
Comment on lines +49 to +52
{
Volatile.Write(ref _isFinished, false);
_endTimestamp = null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is not a real issue - it's mitigated by the getter (and even documented in the setter):

public DateTimeOffset? EndTimestamp
{
get => Volatile.Read(ref _isFinished) ? _endTimestamp : null;
internal set
{
// Ordering is load-bearing: the gate-flip is release-ordered against the data write,
// so lock-free readers gating on `_isFinished` see a consistent (timestamp, finished) pair.
if (value is null)
{
Volatile.Write(ref _isFinished, false);
_endTimestamp = null;
}
else
{
_endTimestamp = value;
Volatile.Write(ref _isFinished, true);
}
}
}

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.

Auto-create traces for MAUI UI events Transactions for UI events in MAUI

2 participants