getsentry / getsentry/sentry-dotnet
Auto-create traces for MAUI UI events
- Dominant language
- C#
- Stars
- 770
- Forks
- 248
- Avg merge
- 3d 4h
- Merged PRs (30d)
- 49
Description
### Description
Part of: getsentry/sentry-dotnet#5109
* getsentry/sentry-dotnet#5109
Follow-up for: getsentry/sentry-dotnet#5111
* getsentry/sentry-dotnet#5111
getsentry/sentry-dotnet#5111 only added traces for navigation events. This issue would be to follow up with traces for things like click and scroll events.
## Android implementation
The Android implementation is conceptually straight forward:
* GestureDetector.onSingleTapUp / onUp fire with the touched View
* Get the resource name from Resources.getResourceEntryName([view.id]())
* Transaction name = ActivityName.viewId, op = ui.action.click / ui.action.scroll
### Challenges in MAUI
**No unified gesture callback** - Android has a single GestureDetector sitting below all views. MAUI uses GestureRecognizer instances attached per-element (TapGestureRecognizer, PanGestureRecognizer, etc.). There's already an IMauiElementEventBinder infrastructure for this, but the gesture binder tests are all platform-skipped — meaning the gesture binders don't currently work in unit tests, which makes TDD harder.
**No guaranteed view ID** - Android has integer resource IDs → string names. MAUI has AutomationId and StyleId, which are optional and frequently unset. We'd need fallback logic (AutomationId → StyleId → type name), and the "no view ID, skip the transaction" scenario (currently marked N/A) becomes a real requirement we'd need to implement and warn about.
**No ambient "current screen" name** - Android reads activity.getClass().getSimpleName(). In MAUI, from inside a gesture event handler on an arbitrary element, getting the current page/route name requires either passing it as context or querying Shell.Current (which isn't always available and is a static dependency).
**ScrollView vs gesture scroll** - Android intercepts raw scroll gestures. MAUI's ScrollView.Scrolled fires after content scrolls, not at the gesture level. The semantics are similar enough but the plumbing is different.
Also the "ongoing screen load transaction — don't bind new UI event transaction to scope" scenario, would need to be implemented.
### References
Essentially we'd need to implement something like what's covered in [https://github.com/getsentry/sentry-java/pull/1975]() in these tests:
* sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt
* sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt
## Implementation plan — Button click transactions
Follow-up to getsentry/sentry-dotnet#5111 (navigation transactions). Scope is `Button.Clicked` only — `ImageButton`, scrolling, and gesture-recognizer taps are deferred.
### Behaviour (per getsentry/sentry-dotnet#5109 Scenarios)
* Clicking a `Button` starts an auto-generated transaction with op `ui.action.click`.
* Transaction name: `{PageTypeName}.{identifier}` (e.g. `MainPage.feedback`), where the page is found by walking `element.Parent` and `identifier` is `AutomationId` ?? `StyleId`. If **neither** is set, the SDK **skips the transaction and logs a warning** (no fall-back to type name, matching the Android/iOS contract).
* Transaction uses `TransactionNameSource.Component` and the shared `AutoTransactionIdleTimeout` (default 3s), leveraging the idle-timeout infrastructure already added in getsentry/sentry-dotnet#5111.
* Same element clicked again → `ResetIdleTimeout()`, no new transaction. Different element → previous click transaction finishes `Ok`, new one starts.
* If a navigation (`ui.load`) transaction or a user-owned transaction is already on scope → the click transaction is still created and reported, but **not bound to scope**.
* If a click transaction is live when `Shell.Navigating` fires a new navigation transaction → the click transaction is finished with `SpanStatus.Cancelled`.
* `Window.Stopped` finishes any live click transaction.
### Options
Because getsentry/sentry-dotnet#5111 is unreleased, we rename first to clean up the public surface:
* `EnableNavigationTransactions` → `EnableAutoTransactions` (governs both navigation and user-interaction tracing)
* `NavigationTransactionIdleTimeout` → `AutoTransactionIdleTimeout`
* **New**: `EnableUserInteractionTracing` (default `true`, gated by `EnableAutoTransactions`)
Mirror all three in `BindableSentryMauiOptions`.
### Files
* `src/Sentry.Maui/SentryMauiOptions.cs` — rename + new flag
* `src/Sentry.Maui/BindableSentryMauiOptions.cs` — mirror
* `src/Sentry.Maui/Internal/Extensions.cs` — add `FindContainingPage` helper (walks `.Parent` with hop limit)
* `src/Sentry.Maui/Internal/MauiEventsBinder.cs`:
* New field `_currentInteractionTransaction` (separate from `_currentTransaction`)
* New constant `UserInteractionClickOp = "ui.action.click"`
* Hook `Button.Clicked` for the transaction path in `OnApplicationOnDescendantAdded` (parallel to the existing `MauiButtonEventsBinder` breadcrumb path — no change to that public interface)
* New `StartUserInteractionTransaction` method mirroring `StartNavigationTransaction`
* `StartNavigationTransaction` cancels any live click transaction at the top
* `OnWindowOnStopped` also finishes `_currentInteractionTransaction`
* `test/Sentry.Maui.Tests/MauiEventsBinderTests.Button.cs` — new file mirroring `MauiEventsBinderTests.Shell.cs`
* Rename usages in existing `MauiEventsBinderTests.Shell.cs` / `MauiEventsBinderTests.Application.cs`
* Accept updated `ApiApprovalTests.Run.DotNet9_0.verified.txt` / `...DotNet10_0.verified.txt` snapshots
* `CHANGELOG.md` — feature + rename note (both under unreleased)
### Verification
1. `dotnet test test/Sentry.Maui.Tests/` — all new `Button_*` tests pass.
2. Run `pwsh ./scripts/accept-verifier-changes.ps1`, inspect API diff, commit.
3. `dotnet format` per [CLAUDE.md]() command.
4. End-to-end in `Sentry.Samples.Maui`: set `AutomationId="feedback"` on the feedback button, click it, verify a `ui.action.click` transaction named `MainPage.feedback` plus the subsequent `ui.load` transaction (with the click transaction finished as `Cancelled`).
5. Remove `AutomationId`/`StyleId`, click, verify no transaction + warning in the diagnostic log.
Contributor guide
Assessment
This issue has not been assessed yet.