Skip to content

fix: an exception in BeforeSend or BeforeSendTransaction now drops the item instead of sending it - #5610

Open
jamescrosswell wants to merge 5 commits into
fix/callback-client-reportsfrom
fix/before-send-failure-drops
Open

jamescrosswell wants to merge 5 commits into
fix/callback-client-reportsfrom
fix/before-send-failure-drops

Conversation

@jamescrosswell

@jamescrosswell jamescrosswell commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Work item 3 ("align BeforeSend / BeforeSendTransaction with the matrix") of #5535, the last one. Stacked on #5607 — review that one first; this branch targets fix/callback-client-reports.

Three BeforeSend routes did the same thing when a user's callback threw: swallow the exception and send the item anyway.

Path Before Now
Managed BeforeSend breadcrumb with the exception attached, event sent error log naming the callback, event dropped, before_send / error
Managed BeforeSendTransaction same error log naming the callback, transaction dropped, before_send / transaction + span
Android native BeforeSend original unscrubbed Java event sent error log naming the callback, event dropped by the Java SDK

For 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 BeforeSend is 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.

The Android bridge (added after review)

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.Execute wraps its whole body in a try and returns e on 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 returns null for a user failure — which the Java SDK turns into a drop — and leaves Execute's catch doing only the serialization fail-open it was written for.

No managed client report on that path: sentry-java already records BEFORE_SEND / Error itself when the callback returns null (SentryClient.java#L163), and native events are the native SDK's to account for. Recording it again on the managed side would double-count.

Scope: Native.EnableBeforeSend is opt-in and defaults to false. Managed events never cross the JNI bridge. SuppressSegfaults installs the same wrapper without calling user code, so that route can't reach the guard.

Notes for review

  • The discard is recorded in different places for the two managed paths, because that's where each already recorded its deliberate drop. DoBeforeSend records before_send/error itself; BeforeSendTransaction returns null into CaptureTransaction's existing drop branch, which records before_send for the transaction and its spans. Duplicating the span accounting inside the callback helper would have been worse.
  • 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 an AotHelper.IsTrimmed guard — all now gone. Ben.Demystifier is still used by DebugStackTrace for StackTraceMode.Enhanced, so nothing is orphaned.
  • CaptureEvent_BeforeEventThrows_ErrorToEventBreadcrumb and CaptureTransaction_BeforeSendTransactionThrows_ErrorToEventBreadcrumb pinned the old behaviour and are deleted, along with their three Verify snapshots and the now-empty SentryClientTests.verify.cs. Replaced with ordinary assertions covering the drop, the client report, the error log, and the absence of any breadcrumb on the item.
  • iOS native crashes also run managed BeforeSend, and SentryEvent.Exception is null there, so a callback that dereferences it throws on that path only — previously reported with a breadcrumb, now dropped. ProcessOnBeforeSend had no coverage for a throwing callback; it does now. The underlying null-Exception problem 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.
  • Still user-visible for anyone whose callback throws today. They currently get a degraded event; they will now get none — but they also get an error-level diagnostic log naming the callback, and the loss is counted in client reports rather than being invisible.

Closes #5535

🤖 Generated with Claude Code

…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

codecov Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.91%. Comparing base (e25f203) to head (9d42b4b).

Additional details and impacted files
@@                       Coverage Diff                       @@
##           fix/callback-client-reports    #5610      +/-   ##
===============================================================
- Coverage                        74.96%   74.91%   -0.05%     
===============================================================
  Files                              515      515              
  Lines                            18993    18968      -25     
  Branches                          3698     3692       -6     
===============================================================
- Hits                             14238    14210      -28     
- Misses                            3874     3878       +4     
+ Partials                           881      880       -1     

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

Comment thread src/Sentry/Internal/SentryEventHelper.cs

@ric-oliv ric-oliv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

jamescrosswell added a commit that referenced this pull request Sep 23, 2026
…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>
@jamescrosswell

Copy link
Copy Markdown
Collaborator Author

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?

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.

…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>
@jamescrosswell jamescrosswell changed the title fix: an exception in BeforeSend or BeforeSendTransaction now drops the item instead of sending it with the failure attached fix: an exception in BeforeSend or BeforeSendTransaction now drops the item instead of sending it Sep 23, 2026
jamescrosswell and others added 2 commits September 24, 2026 11:46
…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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Conform to the callback error isolation spec (wrap all user callbacks)

2 participants