Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5afe8bd
fix: exceptions thrown from BeforeBreadcrumb, log filters and BeforeS…
jamescrosswell Sep 22, 2026
7561f8d
ref: name the platform in native BeforeBreadcrumb failure logs, add A…
jamescrosswell Sep 22, 2026
05efd92
fix: record client reports for items dropped by user callbacks
jamescrosswell Sep 22, 2026
450f1db
fix: an exception in BeforeSend or BeforeSendTransaction now drops th…
jamescrosswell Sep 22, 2026
6de2c40
fix: record client reports for logs and metrics dropped by validation
jamescrosswell Sep 23, 2026
7710900
fix: log when BeforeSendLog or BeforeSendMetric drops an item
jamescrosswell Sep 23, 2026
eae8fc3
fix: a throwing log entry filter now drops the entry instead of letti…
jamescrosswell Sep 23, 2026
9dac9c2
ref: evaluate log entry filters once per log call instead of twice
jamescrosswell Sep 23, 2026
7ff549d
Merge branch 'fix/callback-client-reports' into fix/before-send-failu…
jamescrosswell Sep 23, 2026
ab58162
fix: a throwing BeforeSend on the Android bridge no longer sends the …
jamescrosswell Sep 23, 2026
3546adc
Merge remote-tracking branch 'origin/main' into fix/isolate-user-call…
jamescrosswell Sep 23, 2026
e25f203
Merge remote-tracking branch 'origin/fix/isolate-user-callbacks' into…
jamescrosswell Sep 23, 2026
1a9271e
Merge remote-tracking branch 'origin/fix/callback-client-reports' int…
jamescrosswell Sep 23, 2026
9d42b4b
test: move the Cocoa BeforeSend tests in with the existing ProcessOnB…
jamescrosswell Sep 24, 2026
8992aa9
fix: report callback failures with the callback_error discard reason
jamescrosswell Sep 24, 2026
ecff49e
Merge remote-tracking branch 'origin/fix/callback-client-reports' int…
jamescrosswell Sep 24, 2026
3f11a6c
fix: report BeforeSend and BeforeSendTransaction failures as callback…
jamescrosswell Sep 24, 2026
f6ee5c5
chore: rebuild the pull request merge ref
jamescrosswell Sep 25, 2026
5e6a369
Merge remote-tracking branch 'origin/main' into fix/isolate-user-call…
jamescrosswell Sep 27, 2026
171cc52
Merge remote-tracking branch 'origin/fix/isolate-user-callbacks' into…
jamescrosswell Sep 27, 2026
f55e72e
Merge remote-tracking branch 'origin/fix/callback-client-reports' int…
jamescrosswell Sep 27, 2026
61f72bf
Merge remote-tracking branch 'origin/main' into fix/before-send-failu…
jamescrosswell Sep 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 3 additions & 23 deletions src/Sentry/Internal/SentryEventHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ internal static class SentryEventHelper
return processedEvent;
}

#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026: RequiresUnreferencedCode", Justification = AotHelper.AvoidAtRuntime)]
Comment thread
jamescrosswell marked this conversation as resolved.
#endif
public static SentryEvent? DoBeforeSend(SentryEvent? @event, SentryHint hint, SentryOptions options)
{
if (@event is null || options.BeforeSendInternal is null)
Expand All @@ -60,26 +57,9 @@ internal static class SentryEventHelper
}
catch (Exception e)
{
if (!AotHelper.IsTrimmed)
{
// Attempt to demystify exceptions before adding them as breadcrumbs.
e.Demystify();
}

options.LogError(e, "The BeforeSend callback threw an exception. It will be added as breadcrumb and continue.");
var data = new Dictionary<string, string>
{
{"message", e.Message}
};
if (e.StackTrace is not null)
{
data.Add("stackTrace", e.StackTrace);
}
@event?.AddBreadcrumb(
"BeforeSend callback failed.",
category: "SentryClient",
data: data,
level: BreadcrumbLevel.Error);
options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Error);
options.LogError(e, "The BeforeSend callback threw an exception. The event will be dropped.");
return null;
}

return @event;
Expand Down
18 changes: 15 additions & 3 deletions src/Sentry/Platforms/Android/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,21 @@ private static void InitSentryAndroidSdk(SentryOptions options)
}

// Call the user defined BeforeSend callback, if it's defined - otherwise return the event as-is
return (options.Native.EnableBeforeSend && options.BeforeSendInternal is { } beforeSend)
? beforeSend(evt, hint)
: evt;
if (!options.Native.EnableBeforeSend || options.BeforeSendInternal is not { } beforeSend)
{
return evt;
}

try
{
return beforeSend(evt, hint);
}
catch (Exception e)
{
// Dropping the event is left to the Java SDK, which also records the client report for it.
options.LogError(e, "Android BeforeSend callback failed.");
return null;
}
};
}

Expand Down
49 changes: 15 additions & 34 deletions src/Sentry/SentryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,9 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent
}
}

processedTransaction = BeforeSendTransaction(processedTransaction, hint);
processedTransaction = BeforeSendTransaction(processedTransaction, hint, spanCount);
if (processedTransaction is null) // Rejected transaction
{
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Transaction);
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Span, spanCount);
_options.LogInfo("Transaction dropped by BeforeSendTransaction callback.");
return;
}

Expand All @@ -253,10 +250,7 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent
CaptureEnvelope(Envelope.FromTransaction(processedTransaction, _options.DiagnosticLogger, attachments));
}

#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026: RequiresUnreferencedCode", Justification = AotHelper.AvoidAtRuntime)]
#endif
private SentryTransaction? BeforeSendTransaction(SentryTransaction transaction, SentryHint hint)
private SentryTransaction? BeforeSendTransaction(SentryTransaction transaction, SentryHint hint, int spanCount)
{
if (_options.BeforeSendTransactionInternal is null)
{
Expand All @@ -267,36 +261,23 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent

try
{
return _options.BeforeSendTransactionInternal?.Invoke(transaction, hint);
}
catch (Exception e)
{
if (!AotHelper.IsTrimmed)
var processedTransaction = _options.BeforeSendTransactionInternal.Invoke(transaction, hint);
if (processedTransaction is null) // Rejected transaction
{
// Attempt to demystify exceptions before adding them as breadcrumbs.
e.Demystify();
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Transaction);
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Span, spanCount);
_options.LogInfo("Transaction dropped by BeforeSendTransaction callback.");
}

_options.LogError(e, "The BeforeSendTransaction callback threw an exception. It will be added as breadcrumb and continue.");

var data = new Dictionary<string, string>
{
{"message", e.Message}
};

if (e.StackTrace is not null)
{
data.Add("stackTrace", e.StackTrace);
}

transaction.AddBreadcrumb(
message: "BeforeSendTransaction callback failed.",
category: "SentryClient",
data: data,
level: BreadcrumbLevel.Error);
return processedTransaction;
}
catch (Exception e)
{
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Transaction);
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Span, spanCount);
_options.LogError(e, "The BeforeSendTransaction callback threw an exception. The transaction will be dropped.");
return null;
}

return transaction;
}

/// <inheritdoc />
Expand Down
23 changes: 23 additions & 0 deletions test/Sentry.Tests/Platforms/Android/SentrySdkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,28 @@ public void BeforeSendWrapper_NativeBeforeSendDisabled_ReturnsEvent()
beforeSend.DidNotReceive().Invoke(Arg.Any<SentryEvent>(), Arg.Any<SentryHint>());
result.Should().Be(evt);
}

[Fact]
public void BeforeSendWrapper_BeforeSendThrows_ReturnsNullAndLogsError()
{
// Arrange
var exception = new InvalidOperationException("callback failed");
var logger = new InMemoryDiagnosticLogger();
var options = new SentryOptions { Debug = true, DiagnosticLogger = logger };
options.Native.EnableBeforeSend = true;
options.SetBeforeSend((SentryEvent _, SentryHint _) => throw exception);
var evt = new SentryEvent();
var hint = new SentryHint();

// Act
var result = SentrySdk.BeforeSendWrapper(options).Invoke(evt, hint);

// Assert
result.Should().BeNull();
logger.Entries.Should().ContainSingle(entry =>
entry.Level == SentryLevel.Error &&
entry.Exception == exception &&
entry.Message == "Android BeforeSend callback failed.");
}
}
#endif

This file was deleted.

This file was deleted.

This file was deleted.

58 changes: 58 additions & 0 deletions test/Sentry.Tests/SentryClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,36 @@ public void CaptureEvent_BeforeEvent_RejectEvent_RecordsDiscard()
.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Error);
}

[Fact]
public void CaptureEvent_BeforeSendThrows_DropsEventAndRecordsDiscard()
{
var exception = new InvalidOperationException("callback failed");
_fixture.SentryOptions.SetBeforeSend((_, _) => throw exception);
_fixture.SentryOptions.AddDiagnosticLoggerSubstitute();
var @event = new SentryEvent();

var sut = _fixture.GetSut();
var id = sut.CaptureEvent(@event);

id.Should().Be(SentryId.Empty);
_ = _fixture.BackgroundWorker.DidNotReceive().EnqueueEnvelope(Arg.Any<Envelope>());
_fixture.ClientReportRecorder.Received(1)
.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Error);
_fixture.SentryOptions.ReceivedLogError(exception, "The BeforeSend callback threw an exception. The event will be dropped.");
}

[Fact]
public void CaptureEvent_BeforeSendThrows_DoesNotAttachFailureToEvent()
{
_fixture.SentryOptions.SetBeforeSend((_, _) => throw new InvalidOperationException("callback failed"));
var @event = new SentryEvent();

var sut = _fixture.GetSut();
_ = sut.CaptureEvent(@event);

@event.Breadcrumbs.Should().BeEmpty();
}

[Fact]
public void CaptureEvent_EventProcessor_RejectEvent_RecordsDiscard()
{
Expand Down Expand Up @@ -1773,6 +1803,34 @@ public void CaptureTransaction_TransactionProcessorThrows_DropsTransactionAndRec
_fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(reason, DataCategory.Span, expectedDroppedSpanCount);
}

[Fact]
public void CaptureTransaction_BeforeSendTransactionThrows_DropsTransactionAndRecordsDiscard()
{
// Arrange
var exception = new InvalidOperationException("callback failed");
_fixture.SentryOptions.SetBeforeSendTransaction((_, _) => throw exception);
_fixture.SentryOptions.AddDiagnosticLoggerSubstitute();

var hub = Substitute.For<IHub>();
var tracer = new TransactionTracer(hub, "test name", "test operation");
tracer.StartChild("span1");
tracer.StartChild("span2");
tracer.EndTimestamp = DateTimeOffset.Now; // finished
var transaction = new SentryTransaction(tracer) { IsSampled = true };

// Act
_fixture.GetSut().CaptureTransaction(transaction);

// Assert
_ = _fixture.BackgroundWorker.DidNotReceive().EnqueueEnvelope(Arg.Any<Envelope>());
_fixture.ClientReportRecorder.Received(1)
.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Transaction);
_fixture.ClientReportRecorder.Received(1)
.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Span, transaction.Spans.Count + 1);
_fixture.SentryOptions.ReceivedLogError(exception, "The BeforeSendTransaction callback threw an exception. The transaction will be dropped.");
transaction.Breadcrumbs.Should().BeEmpty();
}

[Fact]
public void CaptureTransaction_BeforeSendTransaction_GetsHint()
{
Expand Down
35 changes: 0 additions & 35 deletions test/Sentry.Tests/SentryClientTests.verify.cs

This file was deleted.

38 changes: 38 additions & 0 deletions test/Sentry.Tests/SentrySdkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,44 @@ public void ProcessOnBeforeSend_EventProcessorsInvoked()
// Assert
eventProcessor.Invoked.Should().BeTrue();
}

[Fact]
public void ProcessOnBeforeSend_BeforeSendThrows_DropsNativeEvent()
{
// Arrange
var exception = new InvalidOperationException("callback failed");
var logger = new InMemoryDiagnosticLogger();
var options = new SentryOptions { Debug = true, DiagnosticLogger = logger };
options.SetBeforeSend((SentryEvent _, SentryHint _) => throw exception);

var native = new Sentry.CocoaSdk.SentryObjCEvent();

// Act
var result = SentrySdk.ProcessOnBeforeSend(options, native, Substitute.For<IHub>());

// Assert
result.Should().BeNull();
logger.Entries.Should().ContainSingle(entry =>
entry.Level == SentryLevel.Error &&
entry.Exception == exception &&
entry.Message == "The BeforeSend callback threw an exception. The event will be dropped.");
}

[Fact]
public void ProcessOnBeforeSend_BeforeSendReturnsEvent_KeepsNativeEvent()
{
// Arrange
var options = new SentryOptions();
options.SetBeforeSend((SentryEvent @event, SentryHint _) => @event);

var native = new Sentry.CocoaSdk.SentryObjCEvent();

// Act
var result = SentrySdk.ProcessOnBeforeSend(options, native, Substitute.For<IHub>());

// Assert
result.Should().BeSameAs(native);
}
#endif

public void Dispose()
Expand Down
Loading