Skip to content

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

Merged
jamescrosswell merged 22 commits into
mainfrom
fix/before-send-failure-drops
Sep 29, 2026
Merged

jamescrosswell merged 22 commits into
mainfrom
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. Now that #5606 and #5607 have landed this targets main directly.

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, callback_error / error
Managed BeforeSendTransaction same error log naming the callback, transaction dropped, callback_error / 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, and use the callback_error reason #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.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 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_send rather than callback_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:

Route callback_error recorded Native SDK also records Net
Android no — the guard lives in BeforeSendWrapper before_send counted once, under the pre-revision reason (#5634 item 2)
iOS yes — the throw goes through the shared DoBeforeSend before_send counted twice, reasons disagreeing (#5634 item 1)

The 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 a null return 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.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

  • 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.
  • BeforeSendTransaction now takes spanCount and 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_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. The throwing case had no coverage in the existing ProcessOnBeforeSend tests; 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.

Verified on device as well as the host: the Android guard by the Run Android API-34/36 Test jobs, and the Cocoa path by ios-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

jamescrosswell and others added 4 commits September 22, 2026 12:31
…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

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.92%. Comparing base (cc9ec24) to head (61f72bf).

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.
📢 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
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>
@jamescrosswell
jamescrosswell marked this pull request as ready for review September 23, 2026 01:28
@github-actions github-actions Bot added the risk: medium PR risk score: medium label Sep 23, 2026
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 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.

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

jamescrosswell and others added 2 commits September 24, 2026 10:45
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>
@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 4 commits September 24, 2026 11:42
…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>
jamescrosswell and others added 2 commits September 25, 2026 09:12
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>
…_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>
@jamescrosswell
jamescrosswell removed this pull request from stack #5618 September 27, 2026 22:01
@jamescrosswell
jamescrosswell added this pull request to stack #5628 September 27, 2026 22:01
@jamescrosswell
jamescrosswell removed this pull request from stack #5628 September 27, 2026 22:01
jamescrosswell added a commit that referenced this pull request Sep 28, 2026
…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>
jamescrosswell added a commit that referenced this pull request Sep 28, 2026
…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>
Base automatically changed from fix/callback-client-reports to main September 28, 2026 22:02
…re-drops

# Conflicts:
#	test/Sentry.Tests/SentryClientTests.cs
@jamescrosswell
jamescrosswell merged commit 4af9645 into main Sep 29, 2026
52 checks passed
@jamescrosswell
jamescrosswell deleted the fix/before-send-failure-drops branch September 29, 2026 00:25
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