fix: an exception in BeforeSend or BeforeSendTransaction now drops the item instead of sending it - #5610
Conversation
…creenshotCapture no longer reach the application Completes the callback-isolation work item from #5535 that #5545 started for TracesSampler. Each callback now runs inside a recovery boundary that logs an error naming the callback and applies the spec's fallback: - BeforeBreadcrumb (Scope.AddBreadcrumb, Android JNI, Cocoa) drops the breadcrumb - ILogEntryFilter.Filter treats a failing filter as "did not filter" - SetBeforeScreenshotCapture skips the screenshot and keeps the event Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ndroid device tests Addresses review feedback on #5606: distinguish native callback failures from managed ones in the diagnostic log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Work item 2 of #5535. Items lost to a user callback — whether it returned null or threw — were disappearing without a client report, and a throwing event or transaction processor skipped the report entirely by unwinding to the Hub catch-all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e item Work item 3 of #5535. Both callbacks previously demystified the exception, stapled its message and stack trace onto the item as a breadcrumb, and sent the item anyway. The spec says a callback failure MUST NOT be attached to the item, and that filters drop — a callback that threw part-way may not have applied the redaction the user wrote it to apply, so the partially-scrubbed item plus the exception detail were both reaching Sentry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5610 +/- ##
==========================================
- Coverage 74.94% 74.92% -0.03%
==========================================
Files 515 515
Lines 18994 18973 -21
Branches 3698 3693 -5
==========================================
- Hits 14236 14215 -21
- Misses 3877 3878 +1
+ Partials 881 880 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
A log whose template doesn't match its arguments, and a metric with an unsupported value type or an empty name, were dropped with a diagnostic log and no client report. Records the spec's `invalid` reason, which the SDK did not previously carry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every other deliberate drop by a user callback logs at info level naming the callback; logs and metrics were the exception. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ric-oliv
left a comment
There was a problem hiding this comment.
Looks good! Just two notes my agent flagged:
Android native BeforeSend still fails open. Platforms/Android/Callbacks/BeforeSendCallback.cs catches the exception, logs "Before Send Error" and returns the original native event (return e), so a scrubber that throws partway still lets the event through. It only applies to apps that opt into Native.EnableBeforeSend, and it wasn't in #5535's conformance table, but it's the same gap this PR closes elsewhere. Could we align it here, or track it separately and change Closes #5535 to Part of?
Worth a line in the release note (not a blocker). On iOS, BeforeSend also runs on native crashes (Platforms/Cocoa/SentrySdk.cs:321), where @event.Exception is null. A callback that dereferences it currently still gets the crash reported, with a breadcrumb. After this PR it's silently dropped. Something like "BeforeSend also runs on iOS native crashes, where Exception is null; a throwing callback now drops the event" would help people notice. A ProcessOnBeforeSend test with a throwing callback would also pin down that path.
…ng it through Review feedback on #5606. Failing open meant a filter written to exclude entries would, once broken, start sending them. Before this PR the exception reached the app and nothing was sent, so dropping keeps that outcome while removing the crash, and matches the filter rule applied to BeforeSend in #5610. Also aligns the internal screenshot callback names with the public SetBeforeScreenshotCapture, and fixes a doc sample naming a method that no longer exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ah, that's actually a problem. We use serialisation to convert the Java object to a managed one, so we can run the managed beforesend callback. We don't want issues with that serialisation to prevent the event from being sent. However that catches issues in the beforesend and then still sends the original event - meaning any problems in the user's beforesend callback (maybe a callback they wrote to strip PII) would result in all events being sent unredacted... not good. It's not introduced by this PR but it is the kind of thing this PR is supposed to fix, so we should address it. |
ShouldCaptureEvent and ShouldAddBreadcrumb each ran the whole filter chain, so a user filter was invoked twice for every entry that cleared both thresholds — and, since the previous commit, logged its failure twice. The two predicates now test only their level threshold, and the shared guard runs once in Log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…unscrubbed native event The JNI bridge wrapped its whole body in a catch that returns the original Java event, so a scrubber that threw part-way sent the completely unredacted native event. That catch was added for serialization safety, so the guard goes around the user callback inside BeforeSendWrapper instead: it returns null, which the Java SDK turns into a drop and accounts for in its own client report, and Execute's catch keeps failing open only for serialization. Also covers the Cocoa native-crash path, where a throwing callback now drops the event and previously had no test at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… fix/callback-client-reports
…o fix/before-send-failure-drops
…eforeSend family Sentry.Tests.SentrySdkTests already has six ProcessOnBeforeSend tests inside an #if __IOS__ block. A second SentrySdkTests class under Platforms/iOS was a confusing duplicate name in a parallel namespace. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the hooks spec revision in getsentry/sentry-docs#19189 (b53c3009): a throwing callback must be distinguishable from an explicit drop, so every item a failure drops is recorded as callback_error with the item's category. Null returns keep their existing reasons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o fix/before-send-failure-drops
…_error Follows the hooks spec revision in getsentry/sentry-docs#19189 (b53c3009). BeforeSendTransaction now records its own discard so a throw is distinguishable from a null return, which the shared caller could not tell apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GitHub stopped rebuilding refs/pull/5607/merge, so no pull_request workflow could be created and the PR reported a conflict that git shows does not exist. A new head commit forces the ref to be recomputed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… fix/callback-client-reports # Conflicts: # src/Sentry/SentryClient.cs
…o fix/before-send-failure-drops
…creenshotCapture no longer reach the application (#5606) * fix: exceptions thrown from BeforeBreadcrumb, log filters and BeforeScreenshotCapture no longer reach the application Completes the callback-isolation work item from #5535 that #5545 started for TracesSampler. Each callback now runs inside a recovery boundary that logs an error naming the callback and applies the spec's fallback: - BeforeBreadcrumb (Scope.AddBreadcrumb, Android JNI, Cocoa) drops the breadcrumb - ILogEntryFilter.Filter treats a failing filter as "did not filter" - SetBeforeScreenshotCapture skips the screenshot and keeps the event Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ref: name the platform in native BeforeBreadcrumb failure logs, add Android device tests Addresses review feedback on #5606: distinguish native callback failures from managed ones in the diagnostic log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: a throwing log entry filter now drops the entry instead of letting it through Review feedback on #5606. Failing open meant a filter written to exclude entries would, once broken, start sending them. Before this PR the exception reached the app and nothing was sent, so dropping keeps that outcome while removing the crash, and matches the filter rule applied to BeforeSend in #5610. Also aligns the internal screenshot callback names with the public SetBeforeScreenshotCapture, and fixes a doc sample naming a method that no longer exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ref: evaluate log entry filters once per log call instead of twice ShouldCaptureEvent and ShouldAddBreadcrumb each ran the whole filter chain, so a user filter was invoked twice for every entry that cleared both thresholds — and, since the previous commit, logged its failure twice. The two predicates now test only their level threshold, and the shared guard runs once in Log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…nted in client reports (#5607) * fix: exceptions thrown from BeforeBreadcrumb, log filters and BeforeScreenshotCapture no longer reach the application Completes the callback-isolation work item from #5535 that #5545 started for TracesSampler. Each callback now runs inside a recovery boundary that logs an error naming the callback and applies the spec's fallback: - BeforeBreadcrumb (Scope.AddBreadcrumb, Android JNI, Cocoa) drops the breadcrumb - ILogEntryFilter.Filter treats a failing filter as "did not filter" - SetBeforeScreenshotCapture skips the screenshot and keeps the event Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ref: name the platform in native BeforeBreadcrumb failure logs, add Android device tests Addresses review feedback on #5606: distinguish native callback failures from managed ones in the diagnostic log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: record client reports for items dropped by user callbacks Work item 2 of #5535. Items lost to a user callback — whether it returned null or threw — were disappearing without a client report, and a throwing event or transaction processor skipped the report entirely by unwinding to the Hub catch-all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: record client reports for logs and metrics dropped by validation A log whose template doesn't match its arguments, and a metric with an unsupported value type or an empty name, were dropped with a diagnostic log and no client report. Records the spec's `invalid` reason, which the SDK did not previously carry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: log when BeforeSendLog or BeforeSendMetric drops an item Every other deliberate drop by a user callback logs at info level naming the callback; logs and metrics were the exception. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: a throwing log entry filter now drops the entry instead of letting it through Review feedback on #5606. Failing open meant a filter written to exclude entries would, once broken, start sending them. Before this PR the exception reached the app and nothing was sent, so dropping keeps that outcome while removing the crash, and matches the filter rule applied to BeforeSend in #5610. Also aligns the internal screenshot callback names with the public SetBeforeScreenshotCapture, and fixes a doc sample naming a method that no longer exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ref: evaluate log entry filters once per log call instead of twice ShouldCaptureEvent and ShouldAddBreadcrumb each ran the whole filter chain, so a user filter was invoked twice for every entry that cleared both thresholds — and, since the previous commit, logged its failure twice. The two predicates now test only their level threshold, and the shared guard runs once in Log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: report callback failures with the callback_error discard reason Follows the hooks spec revision in getsentry/sentry-docs#19189 (b53c3009): a throwing callback must be distinguishable from an explicit drop, so every item a failure drops is recorded as callback_error with the item's category. Null returns keep their existing reasons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: rebuild the pull request merge ref GitHub stopped rebuilding refs/pull/5607/merge, so no pull_request workflow could be created and the PR reported a conflict that git shows does not exist. A new head commit forces the ref to be recomputed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…re-drops # Conflicts: # test/Sentry.Tests/SentryClientTests.cs
Work item 3 ("align
BeforeSend/BeforeSendTransactionwith the matrix") of #5535, the last one. Now that #5606 and #5607 have landed this targetsmaindirectly.Three
BeforeSendroutes did the same thing when a user's callback threw: swallow the exception and send the item anyway.BeforeSendcallback_error/errorBeforeSendTransactioncallback_error/transaction+spanBeforeSendFor the two managed callbacks this deviated from the spec twice — a failure MUST NOT be attached to the item, and the matrix says filters 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 in .NET, where the commonest job for
BeforeSendis PII scrubbing. A callback that threw mid-scrub meant both the partially-redacted event and the exception message and stack trace we stapled to it were sent — a user's redaction failure turning into two kinds of unintended data landing in their org. Hence handling this as a bug rather than holding it for a major.Both managed paths now match
DoBeforeSendFeedback, which was already conformant, and use thecallback_errorreason #5607 introduced for the revised spec's "a failure must be distinguishable from an explicit drop".The Android bridge
Not in #5535's conformance table, and not a regression from this PR, but the same gap and a worse outcome — so it's fixed here rather than left behind a
Closes.BeforeSendCallback.Executewraps its whole body in atryand returnseon failure — the original Java event, not the managed one the callback was mutating. So a scrubber that threw part-way didn't send a partially-redacted event; it sent the completely unredacted native one. That catch arrived in #4022 aimed at serialization safety ("native types tend to move before dotnet does"), and swallowing the user callback was collateral.The fix keeps those two concerns apart. The user callback is invoked one level in, inside
BeforeSendWrapper, so guarding it there returnsnullfor a user failure — which the Java SDK turns into a drop — and leavesExecute's catch doing only the serialization fail-open it was written for.No managed client report is recorded on that path: sentry-java records the drop itself when the callback returns null (SentryClient.java#L171-177), and native events are the native SDK's to account for. It records
before_sendrather thancallback_error, so on this route a failure isn't distinguishable from an explicit drop — tracked as item 2 of #5634 and blocked on getsentry/sentry-java#6141, which adds the reason upstream.Client report accounting on native events
The two bridges end up asymmetric, both cases covered by #5634:
callback_errorrecordedBeforeSendWrapperbefore_sendDoBeforeSendbefore_sendThe Android side avoids a double count because the guard sits in the bridge's own wrapper; the Cocoa side routes through
SentryEventHelper.DoBeforeSend, which records for every caller. Main already double-counts anullreturn on the iOS path, so this adds the throw case to an existing problem rather than a new one — but it is a behaviour change, hence flagging it here. #5634 item 1 is explicitly marked as needing a decision: suppressing the managed record fixes the count but loses the reason until item 2 ships upstream, so it isn't a fix to slip into this PR.Scope:
Native.EnableBeforeSendis opt-in and defaults tofalse. Managed events never cross the JNI bridge.SuppressSegfaultsinstalls the same wrapper without calling user code, so that route can't reach the guard.Notes for review
e.Demystify()goes with the breadcrumb. It existed only to make the stack trace readable in that breadcrumb, and was the reason both methods carried an[UnconditionalSuppressMessage("Trimming", "IL2026")]and anAotHelper.IsTrimmedguard — all now gone.Ben.Demystifieris still used byDebugStackTraceforStackTraceMode.Enhanced, so nothing is orphaned.BeforeSendTransactionnow takesspanCountand owns its own reporting for both the null return and the throw, so the caller's drop branch just returns. Previously the span accounting lived at the call site, which meant a throw either duplicated it or logged twice.CaptureEvent_BeforeEventThrows_ErrorToEventBreadcrumbandCaptureTransaction_BeforeSendTransactionThrows_ErrorToEventBreadcrumbpinned the old behaviour and are deleted, along with their three Verify snapshots and the now-emptySentryClientTests.verify.cs. Replaced with ordinary assertions covering the drop, the client report, the error log, and the absence of any breadcrumb on the item.BeforeSend, andSentryEvent.Exceptionis null there, so a callback that dereferences it throws on that path only — previously reported with a breadcrumb, now dropped. The throwing case had no coverage in the existingProcessOnBeforeSendtests; it does now. The underlying null-Exceptionproblem is not introduced here and is tracked separately in iOS: SentryEvent.Exception is null for native crashes, so a BeforeSend callback that dereferences it throws #5620.Verified on device as well as the host: the Android guard by the
Run Android API-34/36 Testjobs, and the Cocoa path byios-tests(both new tests confirmed present and passing in the results artifact, not just inferred from a green job).Closes #5535
🤖 Generated with Claude Code