Skip to content

spike: Activity-based tracing — SentrySpanProcessor as a raw ActivityListener in core (experiment, do not merge)#5382

Draft
jamescrosswell wants to merge 4 commits into
mainfrom
spike/activity-listener-core
Draft

spike: Activity-based tracing — SentrySpanProcessor as a raw ActivityListener in core (experiment, do not merge)#5382
jamescrosswell wants to merge 4 commits into
mainfrom
spike/activity-listener-core

Conversation

@jamescrosswell

@jamescrosswell jamescrosswell commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Experiment — not intended to be merged

This is part (a) of a two-part spike exploring whether System.Diagnostics.Activity can become the single source of truth for tracing in the .NET SDK (context: #3238, #3288, #5350, #4859, and the internal discussion about the Sentry-vs-OTel instrumentation ecosystems bifurcating). It exists to inform that discussion with working code; it will be closed, not merged.

Question this spike answers

Can the Activity→Sentry conversion logic in Sentry.OpenTelemetry's SentrySpanProcessor live in the core Sentry package, driven by a raw System.Diagnostics.ActivityListener, with zero dependency on the OpenTelemetry SDK — on all TFMs — and pass the existing SentrySpanProcessorTests suite unchanged?

Answer: yes. The port was near-verbatim. Exactly two things needed to change, both predicted in advance:

  1. No BaseProcessor<Activity> base classActivityListener.ActivityStarted/ActivityStopped are drop-in replacements for OnStart/OnEnd.
  2. OTel Resource detection (ParentProvider.GetResource()) has no System.Diagnostics equivalent — replaced with an injectable resourceAttributeResolver (defaults to empty).

Everything else — the shadow span map, transaction-vs-child parent inference (including mixed Sentry/OTel parenting), DSC-from-baggage, DSN self-request filtering, scope restoration across async, pruning/leak guards, error generation from exception events, op/description/status mapping — is pure System.Diagnostics code and ported as-is.

Packaging model: all TFMs, no new core dependency

The sources live in src/Sentry.DiagnosticSource/Internal/Tracing/ and ship through the same split the EF/SqlClient DiagnosticSource integration already uses:

  • Modern TFMs: compiled directly into the core Sentry package via the existing <Compile Include> in Sentry.csprojActivityListener is part of the shared framework there, so core takes no new dependency.
  • Legacy TFMs (netstandard2.0/2.1, net462): the code ships in the Sentry.DiagnosticSource package, which carries the System.Diagnostics.DiagnosticSource package reference. Legacy apps opt in by adding that package — exactly as they do today for EF/SQL instrumentation.

📌 Version finding: ActivityListener needs DiagnosticSource 5.0 and ActivityStatusCode needs 6.0, but the netstandard2.0/net462 assets of the 6.0 package lag the modern API surface (no Activity.HasRemoteParent), so 8.0.1 is the effective minimum (bumped from 4.5.0).

A HAS_ACTIVITY_LISTENER constant (defined in both csproj files) gates compilation; only two small source concessions were needed for the legacy TFMs (hex-string id conversion branches, no KeyValuePair deconstruction).

What's here

File What it is
src/Sentry.DiagnosticSource/Internal/Tracing/SentryActivityProcessor.cs Port of SentrySpanProcessor (kept diffable against the original)
src/Sentry.DiagnosticSource/Internal/Tracing/SentryActivityListener.cs Replaces the OTel TracerProvider registration glue with a raw ActivityListener
src/Sentry.DiagnosticSource/Internal/Tracing/ActivityIdExtensions.cs, ActivityAttributeExtensions.cs, ISentryActivityEnricher.cs Ports of the small helpers the processor needs (distinct namespace to avoid InternalsVisibleTo collisions with the copies in the OTel packages)
test/Sentry.DiagnosticSource.Tests/Tracing/SentryActivityProcessorTests.cs All 33 tests from SentrySpanProcessorTests ported 1:1 — bodies kept diffable — passing against the core port
test/Sentry.DiagnosticSource.Tests/Tracing/SentryActivityListenerTests.cs 4 new end-to-end tests: root activity → captured transaction, child activity → correctly-parented span, unsampled → dropped, unmatched source → StartActivity returns null (the zero-cost HasListeners property). No OTel SDK anywhere in the pipeline.

The tests live in Sentry.DiagnosticSource.Tests deliberately: modern TFMs exercise the compiled-into-core path, while net48 (Windows CI) exercises the standalone-package path — one suite covers both packagings.

Test evidence (local, macOS)

  • Original suite: SentrySpanProcessorTests 33/33 ✅
  • Ported suite against the core processor: 33/33 ✅ (+ 4/4 new listener e2e tests; Sentry.DiagnosticSource.Tests 119/119 on net10.0)
  • Full Sentry.Tests (net10.0): 2402 passed, 0 failed ✅
  • src/Sentry builds clean on all TFMs ✅; Sentry.DiagnosticSource builds clean on netstandard2.0/2.1/net462 with DiagnosticSource 8.0.1 ✅
  • OTel packages build unchanged ✅
  • net48 test execution happens in Windows CI (not runnable locally on macOS)

What this spike deliberately does NOT address

  • Sampling at creation time — the listener's Sample callback returns AllDataAndRecorded for parity with today's POTEL behaviour (record everything, Sentry samples at conversion). Moving Sentry's sampling into the Sample callback is future work and needs answers for the TracesSampler customSamplingContext gap and multi-listener composition (the runtime takes the most permissive result).
  • The Instrumenter.OpenTelemetry constructor guard is retained as-is for test parity; in a real implementation it inverts (Activity-based becomes the default, not the opt-in).
  • Idle/deadline transactions (MAUI, feat: Automatic trace instrumentation for MAUI #5138) — that's part (b) of the spike.
  • Migrating any integration to emit Activities, propagation, or public API shape.

#skip-changelog

🤖 Generated with Claude Code

Experiment for the Activity-as-single-source-of-truth discussion (see #3238,
#5350, #4859): proves that the Activity->Sentry conversion logic in
Sentry.OpenTelemetry's SentrySpanProcessor can live in the core Sentry package,
driven by a System.Diagnostics.ActivityListener, with zero dependency on the
OpenTelemetry SDK.

- Sentry.Internal.Tracing.SentryActivityProcessor: near-verbatim port of
  SentrySpanProcessor's OnStart/OnEnd state machine. Only two changes:
  no BaseProcessor<Activity> base class, and OTel Resource detection replaced
  by an injectable resourceAttributeResolver (the only two OTel SDK touch
  points in the original).
- Sentry.Internal.Tracing.SentryActivityListener: replaces the OTel
  TracerProvider registration glue with a raw ActivityListener
  (ShouldListenTo / Sample / ActivityStarted / ActivityStopped).
- HAS_ACTIVITY_LISTENER constant gates everything to modern TFMs;
  netstandard2.0/2.1/net462 compile the new files out, so no new package
  dependency is introduced anywhere.
- Tests: all 33 SentrySpanProcessorTests ported 1:1 and passing against the
  core port, plus 4 new end-to-end SentryActivityListener tests (no OTel SDK
  involved). Full Sentry.Tests suite green (2439 passed).

#skip-changelog

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

Validates the packaging model for supporting Activity-based tracing on every
TFM without adding a dependency to the core Sentry package:

- The tracing sources move to src/Sentry.DiagnosticSource/Internal/Tracing/,
  so the existing compile-include in Sentry.csproj ships them inside core for
  modern TFMs (where ActivityListener is part of the shared framework), while
  legacy TFMs (netstandard2.0/2.1/net462) get them by referencing the
  Sentry.DiagnosticSource package - the same split the EF/SqlClient
  DiagnosticSource integration uses today.
- Sentry.DiagnosticSource bumps System.Diagnostics.DiagnosticSource from
  4.5.0 to 8.0.1. ActivityListener needs 5.0 and ActivityStatusCode needs
  6.0, but the netstandard2.0/net462 assets of 6.0 lag the modern API surface
  (no Activity.HasRemoteParent), making 8.0.1 the effective minimum.
- ActivityIdExtensions regains the non-NET8 hex-string conversion branches
  and the processor avoids KeyValuePair deconstruction, so the sources
  compile on the legacy TFMs.
- Tests move to Sentry.DiagnosticSource.Tests, where modern TFMs exercise the
  core-compiled path and net48 (Windows CI) exercises the standalone package
  path: one suite covers both packagings.

Verified locally (macOS): Sentry builds on all TFMs; Sentry.DiagnosticSource
builds on netstandard2.0/2.1/net462; Sentry.DiagnosticSource.Tests 119/119 on
net10.0; Sentry.Tests 2402 passed; OTel packages build unchanged.

#skip-changelog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/Sentry.DiagnosticSource/Internal/Tracing/ActivityIdExtensions.cs Outdated
Addresses review feedback on the PR: now that the tracing sources live in
Sentry.DiagnosticSource, ActivityListener is available in every compilation
context that sees them - the standalone package always references
System.Diagnostics.DiagnosticSource 8.0.1, and the compile-include into core
Sentry only fires on modern TFMs where the APIs ship in the framework.

This also matches the existing convention: the EF/SqlClient DiagnosticSource
sources carry no availability guards either; HAS_DIAGNOSTIC_INTEGRATION is
only used by core files (e.g. SentryOptions.cs) that are compiled on all TFMs
and reference those types. If/when core gains wiring that references the
Activity tracing types (e.g. SDK init), it will need an equivalent constant -
reintroduce it then, on the referencing side.

#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 70.68966% with 85 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.98%. Comparing base (11068e0) to head (5d22058).
⚠️ Report is 42 commits behind head on main.

Files with missing lines Patch % Lines
...Source/Internal/Tracing/SentryActivityProcessor.cs 70.00% 52 Missing and 17 partials ⚠️
...ticSource/Internal/Tracing/ActivityIdExtensions.cs 12.50% 14 Missing ⚠️
...cSource/Internal/Tracing/SentryActivityListener.cs 90.47% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5382      +/-   ##
==========================================
- Coverage   74.15%   73.98%   -0.18%     
==========================================
  Files         508      513       +5     
  Lines       18353    18710     +357     
  Branches     3586     3673      +87     
==========================================
+ Hits        13610    13842     +232     
- Misses       3870     3970     +100     
- Partials      873      898      +25     

☔ 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.

On net9.0/net10.0-android, the Android implicit usings bring Android.App into
scope, making the bare `Activity` identifier ambiguous with
System.Diagnostics.Activity (which the repo-wide global usings import).
Alias it explicitly in the three tracing files that use the bare type name.

First time core has compiled System.Diagnostics.Activity usage on mobile
TFMs, so no precedent existed. Verified locally: src/Sentry builds for
net9.0-android35.0 (the TFM CI failed on).

#skip-changelog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant