Repository navigation
Description
Summary
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.
Audited against main @ f2df15b.
Implementation / Status
- fix: exceptions thrown from BeforeBreadcrumb, log filters and BeforeScreenshotCapture no longer reach the application #5606
- fix: telemetry dropped by user callbacks and by validation is now counted in client reports #5607
- fix: an exception in BeforeSend or BeforeSendTransaction now drops the item instead of sending it #5610
Conformance table
| Callback | Spec behaviour on failure | sentry-dotnet today | |
|---|---|---|---|
BeforeSend |
Drop event, report before_send/error |
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 |
|
BeforeSendCheckIn |
Drop check-in | Not implemented (#4538) | n/a |
BeforeSendSpan |
Emit the span | Not implemented | n/a |
ProfilesSampler |
Fall back as if unconfigured | Not implemented (we only have ProfilesSampleRate) |
n/a |
ErrorSampler |
Fall back to SampleRate |
Not implemented (spec says MAY) | n/a |
Two .NET-specific callbacks are not in the spec's matrix but are covered by "applies to every user-provided callback":
| Callback | Sensible fallback | Today | |
|---|---|---|---|
ILogEntryFilter.Filter (Sentry.Extensions.Logging) |
Treat as "did not filter" | Unguarded — throws into the application's own ILogger.Log call |
❌ |
SetBeforeScreenshotCapture (Sentry.Maui) |
Skip the screenshot | Unguarded; unwinds to the Hub.CaptureEvent catch-all and drops the whole event |
❌ |
Work 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.
- fix: exceptions thrown from BeforeBreadcrumb, log filters and BeforeScreenshotCapture no longer reach the application #5606
-
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:- ASP.NET classic —
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. - OpenTelemetry —
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. - Any direct
SentrySdk.StartTransaction/hub.StartTransactioncall in user code.
- ASP.NET classic —
-
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.
-
- fix: exceptions thrown from BeforeBreadcrumb, log filters and BeforeScreenshotCapture no longer reach the application #5606
-
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".
- fix: telemetry dropped by user callbacks and by validation is now counted in client reports #5607
-
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. - Event processors — a throw skips the
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 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
- fix: an exception in BeforeSend or BeforeSendTransaction now drops the item instead of sending it #5610
-
Internal/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 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
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.- 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 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, ProfilesSampler or ErrorSampler land, they should be born inside a recovery boundary rather than retrofitted.
Refs:
Activity
linear-code commentedon Sep 3, 2026
NET-800
fix: exceptions thrown from BeforeBreadcrumb, log filters and BeforeS…
fix: telemetry dropped by user callbacks and by validation is now cou…
fix: an exception in BeforeSend or BeforeSendTransaction now drops th…