Skip to content

spike: Sentry tracing API as a shim over Activities (experiment, do not merge)#5383

Draft
jamescrosswell wants to merge 2 commits into
spike/activity-listener-corefrom
spike/activity-tracing-shim
Draft

spike: Sentry tracing API as a shim over Activities (experiment, do not merge)#5383
jamescrosswell wants to merge 2 commits into
spike/activity-listener-corefrom
spike/activity-tracing-shim

Conversation

@jamescrosswell

Copy link
Copy Markdown
Collaborator

⚠️ Experiment — not intended to be merged

Follow-up to #5382 (and stacked on its branch, so this diff shows only the shim). Given the Phase-0 decisions — bump the DiagnosticSource dependency for legacy TFMs, and keep SentrySdk.StartTransaction/ISpan as a thin facade over Activities during a 1–2 major deprecation runway — this spike validates that the facade actually works.

Question this spike answers

Can existing Sentry tracing API calls (SentrySdk.StartTransaction, ISpan.StartChild, hub.StartSpan) keep working unchanged while creating Activities under the hood — so the Activity is the single source of truth for the span tree regardless of which API created it?

Answer: yes. All 8 end-to-end scenarios pass, including the headline one: an external ActivitySource (think MongoDB driver, Aspire, any OTel-instrumented NuGet) starts an Activity while a Sentry-API transaction is in flight, and it lands as a correctly-parented child span in the captured transaction — one trace from two instrumentation worlds, the exact thing that's impossible today (#5106).

If this shape holds up, every integration that funnels through the Hub (which is all of them) is effectively migrated to OTel instrumentation in one stroke; replacing shim calls with direct ActivitySource usage becomes incremental cleanup.

Design

Lifecycle, timing and parenting go through the Activity; rich Sentry-only state delegates directly to the shadow tracer the SentryActivityListener creates. Nothing is squeezed lossily through Activity tags — measurements, tags, data, contexts and breadcrumbs go straight to the tracer that will be serialized.

Three Sentry concepts must survive the Activity lifecycle boundary and travel as side-channel values fused onto the Activity (set via CreateActivity + fuse + Start(), so they're in place before OnStart runs sampling):

  1. Rich SpanStatusFinish(SpanStatus.Cancelled) survives even though Activity only has Unset/Ok/Error.
  2. customSamplingContext — this solves the TracesSampler gap identified in spike: Activity-based tracing — SentrySpanProcessor as a raw ActivityListener in core (experiment, do not merge) #5382: ActivityListener.Sample has no channel for it, but the fused side-channel hands it back to Sentry's sampling pipeline intact (proven by test).
  3. Inbound DynamicSamplingContext — continued traces keep their DSC.

Routing mechanics worth reviewing:

  • Hub.StartTransaction routes through SentryOptions.ActivityShimFactory — a plain delegate with no Activity types, so it compiles in core on all TFMs while the shim implementation ships in Sentry.DiagnosticSource for legacy TFMs. This validates the legacy-TFM wiring model.
  • Processor-created transactions arrive with Instrumenter.OpenTelemetry and fall through to the classic path (no recursion). A null factory result (no listener running) also falls back to classic tracing.
  • The shim replaces the shadow tracer with itself on the scope, so hub.GetSpan()/hub.StartSpan (the integration path) also routes children through Activities.
  • The processor skips OTel semantic-convention parsing for shim-created Activities (source name "Sentry") — they're already Sentry-native, and the parse would clobber names/descriptions set through the Sentry API. Foreign Activities still get the full semantic mapping.

What's here

File What it is
src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTransactionShim.cs ITransactionTracer facade over an Activity + factory invoked by Hub
src/Sentry.DiagnosticSource/Internal/Tracing/ActivitySpanShim.cs ISpan facade; documents the delegation design principle
src/Sentry.DiagnosticSource/Internal/Tracing/ActivityShim.cs The "Sentry" ActivitySource + side-channel keys
src/Sentry/SentryOptions.cs, src/Sentry/Internal/Hub.cs The routing hook (plain delegate; ~10 lines in Hub)
SentryActivityProcessor.cs Reads the side-channels; reverse-fuses tracer↔Activity; skips semantic parse for shim Activities
test/Sentry.DiagnosticSource.Tests/Tracing/ActivityShimTests.cs 8 end-to-end scenarios

Test evidence (local, macOS, net10.0)

  • ActivityShimTests: 8/8 ✅ — rich-state round-trip (rename + SpanStatus.Cancelled + measurement), Activity-backed children, hub.StartSpan routing, external-ActivitySource interleaving, unsampled flow, classic-path fallback, customSamplingContextTracesSampler round-trip
  • Sentry.DiagnosticSource.Tests: 127/127 ✅ · Sentry.Tests: 2402 passed ✅ · Sentry.OpenTelemetry.Tests: 42/42 ✅
  • Core + standalone build clean on all TFMs (CreateActivity/SetStatus/IsStopped all present in DiagnosticSource 8.0.1's legacy assets) ✅

Known limitations / findings for the real implementation

  • ISpan.Operation mutations after creation are clobbered at OnEnd for shim spans is avoided by the skip-parse rule — but a foreign Activity's Sentry-set operation would still be re-derived. Fine for the shim, worth a convention later.
  • Explicit TransactionContext.TraceId/SpanId are not honored — the Activity generates its own ids (except for continued remote traces, which work via ActivityContext(isRemote: true)). Anyone relying on pre-generated ids would see a behavior change.
  • Scope.ResetTransaction on finish: the tracer's Finish passes itself (not the shim) as the expected current transaction; the scope self-heals because finished transactions are filtered out of the getter, but the real implementation should tidy this hand-off.
  • Spans created by holding a raw SpanTracer reference (rather than via shim/scope) still bypass Activities — only relevant to internal code, which is exactly what gets migrated to ActivitySource over time.

#skip-changelog

🤖 Generated with Claude Code

Validates part two of the Activity-as-single-source-of-truth plan: existing
Sentry tracing API calls (SentrySdk.StartTransaction, ISpan.StartChild,
hub.StartSpan) keep working unchanged while creating Activities under the
hood, giving SDK users a long deprecation runway and effectively migrating
every integration that funnels through the Hub to OTel instrumentation in
one stroke.

Design: lifecycle, timing and parenting go through the Activity (making it
the single source of truth for the span tree); rich Sentry-only state
(measurements, tags, data, contexts, breadcrumbs) delegates directly to the
shadow tracer the SentryActivityListener creates - no lossy round-trip
through Activity tags. Sentry concepts needed across the Activity lifecycle
boundary travel as side-channel values fused onto the Activity:
- an explicit rich SpanStatus passed to Finish (Activity only has
  Unset/Ok/Error),
- the customSamplingContext dictionary (solving the TracesSampler gap that
  ActivityListener.Sample has no channel for),
- an inbound DynamicSamplingContext.
These are fused before Activity.Start() (via CreateActivity + Start), so the
processor sees them at sampling time in OnStart.

Mechanics:
- ActivityTransactionShim/ActivitySpanShim implement ITransactionTracer/ISpan
  over an Activity plus the shadow tracer (resolved via a new reverse fused
  association set by the processor).
- Hub.StartTransaction routes Sentry-API transactions through
  SentryOptions.ActivityShimFactory (a plain delegate, so core needs no
  Activity types - this is also the hook that works for legacy TFMs where
  the shim ships in Sentry.DiagnosticSource). Processor-created transactions
  arrive with Instrumenter.OpenTelemetry and fall through, avoiding
  recursion. A null factory result (no listener) falls back to classic
  tracing.
- The shim replaces the shadow tracer with itself on the scope, so
  integrations using hub.GetSpan()/StartSpan also route through Activities.
- The processor skips OTel semantic-convention parsing for shim-created
  activities (they are already Sentry-native and the parse would clobber
  descriptions/names set through the Sentry API).

Tests (ActivityShimTests, 8 scenarios): transaction round-trip with rich
state (incl. SpanStatus.Cancelled and a rename), Activity-backed children,
hub.StartSpan routing, unsampled flows, classic-path fallback,
customSamplingContext reaching the TracesSampler, and the headline scenario:
an external ActivitySource (e.g. MongoDB driver) parenting naturally under a
Sentry-API transaction via Activity.Current - one trace from two worlds.

Verified: Sentry.DiagnosticSource.Tests 127/127 (net10.0), Sentry.Tests
2402 passed, Sentry.OpenTelemetry.Tests 42/42, core + standalone build clean
on all TFMs (CreateActivity/SetStatus/IsStopped all present in the
DiagnosticSource 8.0.1 legacy assets).

#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 53.14685% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.72%. Comparing base (5d22058) to head (51c7430).

Files with missing lines Patch % Lines
...Source/Internal/Tracing/ActivityTransactionShim.cs 33.89% 37 Missing and 2 partials ⚠️
...gnosticSource/Internal/Tracing/ActivitySpanShim.cs 48.14% 25 Missing and 3 partials ⚠️
Additional details and impacted files
@@                       Coverage Diff                        @@
##           spike/activity-listener-core    #5383      +/-   ##
================================================================
- Coverage                         73.98%   73.72%   -0.26%     
================================================================
  Files                               513      515       +2     
  Lines                             18710    18777      +67     
  Branches                           3673     3673              
================================================================
+ Hits                              13842    13844       +2     
- Misses                             3970     4032      +62     
- Partials                            898      901       +3     

☔ 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