Skip to content

spike: MAUI idle transactions over Activities — shim battle-test + direct prototype (experiment, do not merge)#5384

Draft
jamescrosswell wants to merge 61 commits into
spike/activity-tracing-shimfrom
spike/maui-idle-over-shim
Draft

spike: MAUI idle transactions over Activities — shim battle-test + direct prototype (experiment, do not merge)#5384
jamescrosswell wants to merge 61 commits into
spike/activity-tracing-shimfrom
spike/maui-idle-over-shim

Conversation

@jamescrosswell

Copy link
Copy Markdown
Collaborator

⚠️ Experiment — not intended to be merged

Third spike in the Activity-tracing series, stacked on #5383 (which stacks on #5382). This one merges #5138 (MAUI automatic trace instrumentation) onto the shim branch and answers the two questions posed for it — as two commits, one per question:

Q1: Does #5138 work as-is over the shim? — Yes (commit 1)

MauiEventsBinder needed zero changes. All idle-transaction mechanics — idle timeout, reset-on-interaction, pause-while-children-active, discard-if-empty, end-time trimming — work while every transaction and span is backed by an Activity. The shim needed four small additions:

  1. Idle timeout rides the fused side-channel (like customSamplingContext): Hub → shim factory → fused onto the Activity before Start() → processor hands it to Hub.StartTransaction → the shadow tracer runs feat: Automatic trace instrumentation for MAUI #5138's real idle machinery unmodified.
  2. ActivityTransactionShim implements IAutoTimeoutTracer, delegating ResetIdleTimeout() (the binder casts for this).
  3. TransactionTracer.OnFinished (internal callback): when the idle timer finishes the transaction out-of-band (capture or discard), the backing Activity gets stopped. Without this the Activity leaks — and worse, stays Activity.Current and silently re-parents later spans.
  4. Root detachment: StartTransaction explicitly detaches from any ambient Activity, so a second UI click is an independent trace, not an implicit child of the still-idling first activity.

MauiActivityShimTests (real Hub + listener + real MauiEventsBinder + MockTimer, 5 scenarios) proves it end-to-end, including the trimmed end-timestamp and ui.load child span in the captured transaction.

Q2: How hard is removing the shim afterwards? — Mechanical (commit 2)

MauiUiActivityTracing is the direct-Activity rewrite of the binder's tracing methods, and MauiDirectActivityTests runs the same five scenarios with identical assertions — both implementations produce the same captured transactions. The conversion table:

Shim (today) Direct (after)
internalHub.StartTransaction(context, idleTimeout) Source.StartRootActivity(op, name, idleTimeout)
autoTimeoutTracer.ResetIdleTimeout() activity.ResetIdleTimeout()
parentSpan.StartChild("ui.load", name) Source.StartActivity("ui.load") + DisplayName
navSpan.Finish(SpanStatus.Ok) activity.Stop(SpanStatus.Ok)

The Sentry-specific semantics raw ActivitySource calls can't express live in a small internal extension class (ActivityTracingExtensions): starting an independent root, idle timeouts, rich span statuses. The idle mechanics stay on the shadow tracer either way — no reimplementation was needed for the direct path (the stop-Activity-on-out-of-band-finish hook generalized from the shim into the processor, covering both).

Findings worth remembering

  • The idle-transaction machinery from feat: Automatic trace instrumentation for MAUI #5138 ports to the Activity world untouched — it runs tracer-side, driven by a fused idle timeout, for both shim and direct instrumentation. The "idle transactions have no Activity equivalent" risk from the original feasibility report is resolved by this cooperation pattern rather than reimplementation.
  • Out-of-band tracer finishes must stop the backing Activity (TransactionTracer.OnFinished) — otherwise stale Activity.Current silently re-parents subsequent spans. This generalizes beyond MAUI to anything that auto-finishes transactions.
  • Gotcha: ActivityListener.ShouldListenTo fires from inside the ActivitySource constructor, so a filter comparing a static ActivitySource field by reference races that field's initialization (reads null, permanently declines the source). Compare by name.
  • Unsampled idle transactions never auto-finish (no timer on UnsampledTransaction) — their Activity stays current until the next interaction detaches. Harmless today, but the real implementation should stop those Activities too.

Test evidence (local, macOS, net10.0)

#skip-changelog

🤖 Generated with Claude Code

jamescrosswell and others added 30 commits April 2, 2026 14:35
jamescrosswell and others added 27 commits April 22, 2026 15:15
…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>
…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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rface impl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Merges #5138 (maui-trace-ui-5116) onto the Activity shim spike and proves the
MAUI automatic trace instrumentation works over the shim WITHOUT ANY CHANGES
to MauiEventsBinder itself. The shim needed four additions:

- The idle timeout rides the same fused side-channel as the custom sampling
  context: Hub passes it to the shim factory, the shim fuses it onto the
  Activity before Start, and the processor hands it to Hub.StartTransaction
  so the shadow tracer runs the real #5138 idle machinery (timer,
  pause-while-children-active, discard-if-empty, end-time trimming).
- ActivityTransactionShim implements IAutoTimeoutTracer, delegating
  ResetIdleTimeout to the shadow tracer (MauiEventsBinder casts for this).
- TransactionTracer gains an internal OnFinished callback, letting the shim
  stop the backing Activity when the idle timer finishes the transaction
  out-of-band (capture or discard). Without this the Activity leaks and -
  worse - stays Activity.Current, silently re-parenting later spans.
- ActivityTransactionShim.Create detaches from the ambient Activity so a
  new Sentry-API transaction is an independent root: a second UI click must
  start a new trace, not become an implicit child of the still-idling first
  activity (Activity.Current parenting would otherwise kick in).

Also adds SentryOptions.IdleTimerFactory (internal) so tests can inject
MockTimer through the real Hub.

MauiActivityShimTests (real Hub + listener + MauiEventsBinder, 5 scenarios):
activity-backed idle transaction with correct timeout; idle-fire captures a
trimmed transaction with the ui.load child and stops the Activity;
ResetIdleTimeout flows through the shim; discard-if-empty stops the Activity
and clears the scope; second click starts an independent trace.

Verified: Sentry.Maui.Tests 164/164, Sentry.Tests 2410, DiagnosticSource
tests 127/127 (net10.0).

#skip-changelog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he shim)

Answers the second question: converting the MAUI tracing from the shim to
direct Activity instrumentation is mechanical. MauiUiActivityTracing is the
direct-Activity equivalent of MauiEventsBinder's tracing methods, and
MauiDirectActivityTests runs the same five behavioral scenarios as the shim
battle-tests with identical assertions - both implementations produce the
same captured transactions.

The conversion table (documented in MauiUiActivityTracing):
  internalHub.StartTransaction(context, idleTimeout) -> Source.StartRootActivity(op, name, idleTimeout)
  autoTimeoutTracer.ResetIdleTimeout()               -> activity.ResetIdleTimeout()
  parentSpan.StartChild("ui.load", name)             -> Source.StartActivity("ui.load") + DisplayName
  navSpan.Finish(SpanStatus.Ok)                      -> activity.Stop(SpanStatus.Ok)

Supporting pieces:
- ActivityTracingExtensions: the API surface Sentry integrations use when
  instrumenting directly with Activities - StartRootActivity (detach from
  ambient + idle timeout via the fused side-channel), ResetIdleTimeout, and
  Stop(SpanStatus) for rich statuses. The idle mechanics themselves (timer,
  pause-while-children-active, discard-if-empty, end-time trimming) are
  unchanged: they run on the shadow tracer, driven by the fused idle timeout.
- The stop-the-Activity-on-out-of-band-finish hook moved from the shim into
  SentryActivityProcessor.CreateRootSpan, where it now covers every
  activity-derived transaction (shim-created or direct).

Gotcha discovered: ActivityListener.ShouldListenTo fires from inside the
ActivitySource constructor, so a listener filter comparing against a static
ActivitySource field by reference races that field's initialization (it
reads null and permanently declines the source). Filters must compare by
name; documented on MauiUiActivityTracing.SourceName.

Verified: Sentry.Maui.Tests 169/169 (5 shim battle-tests + 5 direct-Activity
tests + all #5138 tests), Sentry.Tests 2410, DiagnosticSource tests 127/127;
core + standalone build clean on all TFMs.

#skip-changelog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.84791% with 53 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.79%. Comparing base (f0e3e5b) to head (ece5dad).
⚠️ Report is 1 commits behind head on spike/activity-tracing-shim.

Files with missing lines Patch % Lines
src/Sentry.Maui/Internal/MauiEventsBinder.cs 82.19% 4 Missing and 9 partials ⚠️
src/Sentry/TransactionTracer.cs 86.02% 7 Missing and 6 partials ⚠️
src/Sentry.Maui/Internal/MauiUiActivityTracing.cs 71.42% 3 Missing and 3 partials ⚠️
src/Sentry/Infrastructure/SystemTimer.cs 0.00% 6 Missing ⚠️
src/Sentry/SentrySdk.cs 0.00% 5 Missing and 1 partial ⚠️
...urce/Internal/Tracing/ActivityTracingExtensions.cs 71.42% 1 Missing and 3 partials ⚠️
...Source/Internal/Tracing/ActivityTransactionShim.cs 71.42% 1 Missing and 1 partial ⚠️
src/Sentry/Extensibility/DisabledHub.cs 0.00% 1 Missing ⚠️
src/Sentry/Extensibility/HubAdapter.cs 0.00% 1 Missing ⚠️
src/Sentry/HubExtensions.cs 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@                       Coverage Diff                       @@
##           spike/activity-tracing-shim    #5384      +/-   ##
===============================================================
+ Coverage                        73.73%   73.79%   +0.05%     
===============================================================
  Files                              515      518       +3     
  Lines                            18777    18987     +210     
  Branches                          3673     3746      +73     
===============================================================
+ Hits                             13846    14011     +165     
- Misses                            4031     4055      +24     
- Partials                           900      921      +21     

☔ View full report in Codecov by Harness.
📢 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants