You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Hooks spec gained a Callback Error Isolation section (spec 1.1.0, candidate) in getsentry/sentry-docs#19189, following INC-2332, where an exception thrown from a user-provided traces_sampler took down an ingest path.
sentry-dotnet partially implements the spec and deviates from it in several places. This issue tracks bringing us into conformance.
The spec requires, for every user-provided callback:
invoke it inside a recovery boundary — a failure MUST NOT reach the host application;
emit an error-level internal log naming the callback;
MUST NOT re-throw, capture the failure as an event, or attach it to the item;
apply the per-callback fallback matrix (filters drop, samplers fall back as if unconfigured);
record the same client report a deliberate drop would produce.
Logs, adds a breadcrumb containing the exception message + stack trace, keeps and sends the event
❌
BeforeSendTransaction
Drop transaction, report before_send/transaction + spans
Same as above — breadcrumb, keeps and sends
❌
BeforeSendFeedback
Drop feedback, report before_send/feedback
Logs, drops, correct client report
✅
BeforeSendLog
Drop log, report before_send/log_item
Logs, drops, no client report
⚠️
BeforeSendMetric
Drop metric, report before_send/trace_metric
Logs, drops, no client report
⚠️
BeforeBreadcrumb
Drop breadcrumb, no report
Managed: dropped only by the Hub.ConfigureScope catch-all, which logs "Failure to ConfigureScope" rather than naming the callback. Native bridges (Android/Cocoa): unguarded
❌
TracesSampler
Fall back as if unconfigured, report sample_rate if sampled out
Unguarded on all three paths (managed, Android, Cocoa)
❌
Event processors
Drop event, report event_processor + category
Dropped by the Hub.CaptureEvent catch-all; no client report; log names the capture, not the processor
TracesSampler — Internal/Hub.cs:208. Invoked bare inside StartTransaction, so a throwing sampler propagates out of SentrySdk.StartTransaction into the caller. The fallback the spec asks for is already there: leaving isSampled null falls through to the branch that honours an inherited decision first (context.IsSampled ?? …) and only then the static TracesSampleRate, which is exactly "fall back as if unconfigured, including any inherited decision that takes precedence over the static rate".
Where that actually reaches the host application — not ASP.NET Core, which is already isolated: SentryTracingMiddleware.TryStartTransaction wraps the call, logs "Failed to start transaction." and returns null. (It drops the transaction rather than falling back, and the log names the caller rather than the callback, so it still deviates from the spec — but it does not reach the app.) The paths that do:
ASP.NET classic — Sentry.AspNet/HttpContextExtensions.cs:120 is unguarded, and StartSentryTransaction() is called from the user's own Application_BeginRequest. That is a 500 on every request — the same shape as RUBY-49 and the incident itself.
OpenTelemetry — SentrySpanProcessor.OnStart is unguarded, so the exception surfaces inside the user's ActivitySource.StartActivity. Reachable through UseOpenTelemetry(), whose disableSentryTracing parameter defaults to false; the Exporter integration sets it true, after which Hub.StartTransaction returns before the sampler runs.
Any direct SentrySdk.StartTransaction / hub.StartTransaction call in user code.
BeforeBreadcrumb — guard Scope.cs:338 itself. Managed BeforeBreadcrumb is only accidentally isolated: HubExtensions.AddBreadcrumb happens to route through Hub.ConfigureScope, which catches everything. Scope.AddBreadcrumb is public, so calling it directly is unprotected — and the rescue logs "Failure to ConfigureScope", which doesn't tell anyone their breadcrumb callback is broken (spec: the log MUST name the callback).
2. Emit the missing client reports
The spec is explicit that "isolating a callback without reporting the loss is a conformance failure".
BeforeSendLog — Internal/DefaultSentryStructuredLogger.cs:92. The catch is a bare return, and so is the ordinary return null path — a user dropping logs via BeforeSendLog currently produces no before_send discards at all. Same for the configureLog callback at line 69. DataCategory.LogItem already exists.
Event processors — a throw skips the RecordDiscardedEvent(EventProcessor, …) branch in Internal/SentryEventHelper.cs:23 and lands in the Hub.CaptureEvent catch-all at Hub.cs:687, which records nothing. This is precisely the control-flow trap the Linear write-up calls out ("a catch that returns directly bypasses that branch and drops silently").
No new DiscardReason is needed — the spec rules out internal_sdk_error and says sampler failure is deliberately indistinguishable from ordinary sampling.
3. Align BeforeSend / BeforeSendTransaction with the matrix
This deviates twice over. The spec says a failure MUST NOT be attached to the item, and the matrix says drop — with the rationale spelled out: "filters drop because a callback that failed part-way may not have applied the redaction the user wrote it to apply."
That's the realistic case here, and it's a data-protection bug rather than a style preference: the commonest job for BeforeSend in .NET is PII scrubbing, so a callback that throws mid-scrub currently causes us to send both the partially-redacted event and the exception message and stack trace we stapled to it. A user's redaction failure turns into two kinds of unintended data landing in Sentry.
This is therefore being treated as a bug/security fix and can ship in a minor — it is not held back for 7.0.0, and it does not gate on the spec leaving candidate.
Notes for whoever picks this up:
Still user-visible for anyone whose BeforeSend throws today (they currently get a degraded event; they will now get none), so it needs a clear changelog line and is worth calling out in the release notes.
CaptureTransaction_BeforeSendTransactionThrows_ErrorToEventBreadcrumb in SentryClientTests.verify.cs pins the current behaviour and will need replacing.
The "defensive copy" SHOULD in the spec only applies to callbacks that keep the item, i.e. before_send_span, which we don't implement — nothing to do there.
Already conformant — please don't re-do these
ConfigureScope / ConfigureScopeAsync — guarded in Hub (this is the .NET issue cited in the Linear project).
OnCrashedLastRun (Cocoa) and BeforeSend (Android bridge) — both wrapped.
ProcessOnBeforeSend (Cocoa native events) — has its own try/catch.
Scope.OnEvaluating — guarded.
And when BeforeSendCheckIn (#4538), BeforeSendSpan, ProfilesSampler or ErrorSampler land, they should be born inside a recovery boundary rather than retrofitted.
Summary
The
Hooksspec gained a Callback Error Isolation section (spec 1.1.0, candidate) in getsentry/sentry-docs#19189, following INC-2332, where an exception thrown from a user-providedtraces_samplertook down an ingest path.sentry-dotnet partially implements the spec and deviates from it in several places. This issue tracks bringing us into conformance.
The spec requires, for every user-provided callback:
Audited against
main@ f2df15b.Implementation / Status
Conformance table
BeforeSendbefore_send/errorBeforeSendTransactionbefore_send/transaction+ spansBeforeSendFeedbackbefore_send/feedbackBeforeSendLogbefore_send/log_itemBeforeSendMetricbefore_send/trace_metricBeforeBreadcrumbHub.ConfigureScopecatch-all, which logs"Failure to ConfigureScope"rather than naming the callback. Native bridges (Android/Cocoa): unguardedTracesSamplersample_rateif sampled outevent_processor+ categoryHub.CaptureEventcatch-all; no client report; log names the capture, not the processorBeforeSendCheckInBeforeSendSpanProfilesSamplerProfilesSampleRate)ErrorSamplerSampleRateTwo .NET-specific callbacks are not in the spec's matrix but are covered by "applies to every user-provided callback":
ILogEntryFilter.Filter(Sentry.Extensions.Logging)ILogger.LogcallSetBeforeScreenshotCapture(Sentry.Maui)Hub.CaptureEventcatch-all and drops the whole eventWork items
1. Isolate the callbacks that can reach the host app
The highest-value fix. No public API change, and no behaviour change for anyone whose callbacks don't throw.
TracesSampler—Internal/Hub.cs:208. Invoked bare insideStartTransaction, so a throwing sampler propagates out ofSentrySdk.StartTransactioninto the caller. The fallback the spec asks for is already there: leavingisSamplednull falls through to the branch that honours an inherited decision first (context.IsSampled ?? …) and only then the staticTracesSampleRate, which is exactly "fall back as if unconfigured, including any inherited decision that takes precedence over the static rate".Where that actually reaches the host application — not ASP.NET Core, which is already isolated:
SentryTracingMiddleware.TryStartTransactionwraps the call, logs"Failed to start transaction."and returnsnull. (It drops the transaction rather than falling back, and the log names the caller rather than the callback, so it still deviates from the spec — but it does not reach the app.) The paths that do:Sentry.AspNet/HttpContextExtensions.cs:120is unguarded, andStartSentryTransaction()is called from the user's ownApplication_BeginRequest. That is a 500 on every request — the same shape as RUBY-49 and the incident itself.SentrySpanProcessor.OnStartis unguarded, so the exception surfaces inside the user'sActivitySource.StartActivity. Reachable throughUseOpenTelemetry(), whosedisableSentryTracingparameter defaults tofalse; the Exporter integration sets ittrue, after whichHub.StartTransactionreturns before the sampler runs.SentrySdk.StartTransaction/hub.StartTransactioncall in user code.TracesSampler—Platforms/Android/Callbacks/TracesSamplerCallback.cs:17. Called back from Java over JNI; an escaping managed exception surfaces as an app crash.TracesSampler—Platforms/Cocoa/SentrySdk.cs:83. Same, invoked from Objective-C.BeforeBreadcrumb—Platforms/Android/Callbacks/BeforeBreadcrumbCallback.cs:21andPlatforms/Cocoa/SentrySdk.cs:65. NoteBeforeSendCallbackright next door is wrapped, with a comment saying "because this can go out to user code, we want to prevent external crashing" — the pattern exists, it just wasn't applied to its neighbours.ILogEntryFilter.Filter—Sentry.Extensions.Logging/SentryLogger.cs:164,179.SetBeforeScreenshotCapture—Sentry.Maui/Internal/SentryMauiScreenshotProcessor.cs:21. Log and skip the screenshot rather than losing the error the user was trying to report.BeforeBreadcrumb— guardScope.cs:338itself. ManagedBeforeBreadcrumbis only accidentally isolated:HubExtensions.AddBreadcrumbhappens to route throughHub.ConfigureScope, which catches everything.Scope.AddBreadcrumbis public, so calling it directly is unprotected — and the rescue logs"Failure to ConfigureScope", which doesn't tell anyone their breadcrumb callback is broken (spec: the log MUST name the callback).2. Emit the missing client reports
The spec is explicit that "isolating a callback without reporting the loss is a conformance failure".
BeforeSendLog—Internal/DefaultSentryStructuredLogger.cs:92. The catch is a barereturn, and so is the ordinaryreturn nullpath — a user dropping logs viaBeforeSendLogcurrently produces nobefore_senddiscards at all. Same for theconfigureLogcallback at line 69.DataCategory.LogItemalready exists.BeforeSendMetric—Internal/DefaultSentryMetricEmitter.cs:75. Identical;DataCategory.TraceMetricalready exists.RecordDiscardedEvent(EventProcessor, …)branch inInternal/SentryEventHelper.cs:23and lands in theHub.CaptureEventcatch-all atHub.cs:687, which records nothing. This is precisely the control-flow trap the Linear write-up calls out ("a catch that returns directly bypasses that branch and drops silently").No new
DiscardReasonis needed — the spec rules outinternal_sdk_errorand says sampler failure is deliberately indistinguishable from ordinary sampling.3. Align
BeforeSend/BeforeSendTransactionwith the matrixInternal/SentryEventHelper.cs:51andSentryClient.cs:259currently demystify the exception, add a"BeforeSend callback failed."breadcrumb carrying its message and stack trace, and send the item anyway.This deviates twice over. The spec says a failure MUST NOT be attached to the item, and the matrix says drop — with the rationale spelled out: "filters drop because a callback that failed part-way may not have applied the redaction the user wrote it to apply."
That's the realistic case here, and it's a data-protection bug rather than a style preference: the commonest job for
BeforeSendin .NET is PII scrubbing, so a callback that throws mid-scrub currently causes us to send both the partially-redacted event and the exception message and stack trace we stapled to it. A user's redaction failure turns into two kinds of unintended data landing in Sentry.This is therefore being treated as a bug/security fix and can ship in a minor — it is not held back for 7.0.0, and it does not gate on the spec leaving
candidate.Notes for whoever picks this up:
BeforeSendthrows today (they currently get a degraded event; they will now get none), so it needs a clear changelog line and is worth calling out in the release notes.CaptureTransaction_BeforeSendTransactionThrows_ErrorToEventBreadcrumbinSentryClientTests.verify.cspins the current behaviour and will need replacing.before_send_span, which we don't implement — nothing to do there.Already conformant — please don't re-do these
ConfigureScope/ConfigureScopeAsync— guarded inHub(this is the .NET issue cited in the Linear project).BeforeSendFeedback— logs, drops, correct discard reason.CrashedLastRun— guarded inGlobalSessionManager.OnCrashedLastRun(Cocoa) andBeforeSend(Android bridge) — both wrapped.ProcessOnBeforeSend(Cocoa native events) — has its own try/catch.Scope.OnEvaluating— guarded.And when
BeforeSendCheckIn(#4538),BeforeSendSpan,ProfilesSamplerorErrorSamplerland, they should be born inside a recovery boundary rather than retrofitted.Refs: