spike: Sentry tracing API as a shim over Activities (experiment, do not merge)#5383
Draft
jamescrosswell wants to merge 2 commits into
Draft
spike: Sentry tracing API as a shim over Activities (experiment, do not merge)#5383jamescrosswell wants to merge 2 commits into
jamescrosswell wants to merge 2 commits into
Conversation
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 Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/ISpanas a thin facade over Activities during a 1–2 major deprecation runway — this spike validates that the facade actually works.Question this spike answers
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
ActivitySourceusage becomes incremental cleanup.Design
Lifecycle, timing and parenting go through the Activity; rich Sentry-only state delegates directly to the shadow tracer the
SentryActivityListenercreates. 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 beforeOnStartruns sampling):SpanStatus—Finish(SpanStatus.Cancelled)survives even though Activity only has Unset/Ok/Error.customSamplingContext— this solves theTracesSamplergap identified in spike: Activity-based tracing — SentrySpanProcessor as a raw ActivityListener in core (experiment, do not merge) #5382:ActivityListener.Samplehas no channel for it, but the fused side-channel hands it back to Sentry's sampling pipeline intact (proven by test).DynamicSamplingContext— continued traces keep their DSC.Routing mechanics worth reviewing:
Hub.StartTransactionroutes throughSentryOptions.ActivityShimFactory— a plain delegate with no Activity types, so it compiles in core on all TFMs while the shim implementation ships inSentry.DiagnosticSourcefor legacy TFMs. This validates the legacy-TFM wiring model.Instrumenter.OpenTelemetryand fall through to the classic path (no recursion). A null factory result (no listener running) also falls back to classic tracing.hub.GetSpan()/hub.StartSpan(the integration path) also routes children through Activities."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
src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTransactionShim.csITransactionTracerfacade over an Activity + factory invoked byHubsrc/Sentry.DiagnosticSource/Internal/Tracing/ActivitySpanShim.csISpanfacade; documents the delegation design principlesrc/Sentry.DiagnosticSource/Internal/Tracing/ActivityShim.cs"Sentry"ActivitySource + side-channel keyssrc/Sentry/SentryOptions.cs,src/Sentry/Internal/Hub.csSentryActivityProcessor.cstest/Sentry.DiagnosticSource.Tests/Tracing/ActivityShimTests.csTest evidence (local, macOS, net10.0)
ActivityShimTests: 8/8 ✅ — rich-state round-trip (rename +SpanStatus.Cancelled+ measurement), Activity-backed children,hub.StartSpanrouting, external-ActivitySource interleaving, unsampled flow, classic-path fallback,customSamplingContext→TracesSamplerround-tripSentry.DiagnosticSource.Tests: 127/127 ✅ ·Sentry.Tests: 2402 passed ✅ ·Sentry.OpenTelemetry.Tests: 42/42 ✅CreateActivity/SetStatus/IsStoppedall present in DiagnosticSource 8.0.1's legacy assets) ✅Known limitations / findings for the real implementation
ISpan.Operationmutations 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.TransactionContext.TraceId/SpanIdare not honored — the Activity generates its own ids (except for continued remote traces, which work viaActivityContext(isRemote: true)). Anyone relying on pre-generated ids would see a behavior change.Scope.ResetTransactionon finish: the tracer'sFinishpasses 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.SpanTracerreference (rather than via shim/scope) still bypass Activities — only relevant to internal code, which is exactly what gets migrated toActivitySourceover time.#skip-changelog
🤖 Generated with Claude Code