From 05efd92d0d83f742ca1f4b9c4588dd7945c7178d Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 22 Sep 2026 13:36:52 +1200 Subject: [PATCH 1/5] fix: record client reports for items dropped by user callbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Internal/DefaultSentryMetricEmitter.cs | 8 +++- .../Internal/DefaultSentryStructuredLogger.cs | 9 +++- src/Sentry/Internal/SentryEventHelper.cs | 12 +++++- src/Sentry/SentryClient.cs | 13 +++++- test/Sentry.Tests/SentryClientTests.cs | 42 +++++++++++++++++++ test/Sentry.Tests/SentryMetricEmitterTests.cs | 5 +++ .../SentryStructuredLoggerTests.cs | 6 +++ 7 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs index 11d2879295..e7a4876f8a 100644 --- a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs +++ b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs @@ -76,15 +76,19 @@ private protected override void CaptureMetric(SentryMetric metric) where T } catch (Exception e) { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.TraceMetric); _options.DiagnosticLogger?.LogError(e, "The BeforeSendMetric callback threw an exception. The Metric will be dropped."); return; } } - if (configuredMetric is not null) + if (configuredMetric is null) { - _batchProcessor.Enqueue(configuredMetric); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.TraceMetric); + return; } + + _batchProcessor.Enqueue(configuredMetric); } /// diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs index 5c775c775d..947a6c77db 100644 --- a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs +++ b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs @@ -70,6 +70,7 @@ private protected override void CaptureLog(SentryLogLevel level, string template } catch (Exception e) { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); _options.DiagnosticLogger?.LogError(e, "The configureLog callback threw an exception. The Log will be dropped."); return; } @@ -93,15 +94,19 @@ protected internal override void CaptureLog(SentryLog log) } catch (Exception e) { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); _options.DiagnosticLogger?.LogError(e, "The BeforeSendLog callback threw an exception. The Log will be dropped."); return; } } - if (configuredLog is not null) + if (configuredLog is null) { - _batchProcessor.Enqueue(configuredLog); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); + return; } + + _batchProcessor.Enqueue(configuredLog); } /// diff --git a/src/Sentry/Internal/SentryEventHelper.cs b/src/Sentry/Internal/SentryEventHelper.cs index bda07489e0..8c0dad38aa 100644 --- a/src/Sentry/Internal/SentryEventHelper.cs +++ b/src/Sentry/Internal/SentryEventHelper.cs @@ -17,7 +17,17 @@ internal static class SentryEventHelper foreach (var processor in processors) { - processedEvent = processor.DoProcessEvent(processedEvent, effectiveHint); + try + { + processedEvent = processor.DoProcessEvent(processedEvent, effectiveHint); + } + catch (Exception e) + { + options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, dataCategory); + options.LogError(e, "Event processor {0} threw an exception. The event will be dropped.", processor.GetType().Name); + return null; + } + if (processedEvent == null) { options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, dataCategory); diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index c3c88a062e..64e82e037f 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -212,7 +212,18 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent var processedTransaction = transaction; foreach (var processor in scope.GetAllTransactionProcessors()) { - processedTransaction = processor.DoProcessTransaction(transaction, hint); + try + { + processedTransaction = processor.DoProcessTransaction(transaction, hint); + } + catch (Exception e) + { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Transaction); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Span, spanCount); + _options.LogError(e, "Transaction processor {0} threw an exception. The transaction will be dropped.", processor.GetType().Name); + return; + } + if (processedTransaction == null) // Rejected transaction { _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Transaction); diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index 8329dad9a1..2eff11188f 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -387,6 +387,23 @@ public void CaptureEvent_EventProcessor_RejectEvent_RecordsDiscard() .RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Error); } + [Fact] + public void CaptureEvent_EventProcessorThrows_DropsEventAndRecordsDiscard() + { + var processor = Substitute.For(); + processor.Process(Arg.Any()).Throws(new InvalidOperationException()); + + _fixture.SentryOptions.AddEventProcessor(processor); + + var sut = _fixture.GetSut(); + var id = sut.CaptureEvent(new SentryEvent()); + + id.Should().Be(SentryId.Empty); + _fixture.BackgroundWorker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + _fixture.ClientReportRecorder.Received(1) + .RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Error); + } + [Fact] public void CaptureEvent_ExceptionFilter_RecordsDiscard() { @@ -1695,6 +1712,31 @@ public void CaptureTransaction_TransactionProcessorRejectsEvent_RecordDiscardedE _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(reason, DataCategory.Span, expectedDroppedSpanCount); } + [Fact] + public void CaptureTransaction_TransactionProcessorThrows_DropsTransactionAndRecordsDiscard() + { + // Arrange + var processor = Substitute.For(); + processor.Process(Arg.Any(), Arg.Any()).Throws(new InvalidOperationException()); + _fixture.SentryOptions.AddTransactionProcessor(processor); + + var hub = Substitute.For(); + var transaction = new TransactionTracer(hub, "test name", "test operation"); + transaction.StartChild("span1"); + transaction.StartChild("span2"); + transaction.EndTimestamp = DateTimeOffset.Now; // finished + + // Act + _fixture.GetSut().CaptureTransaction(new SentryTransaction(transaction)); + + // Assert + _fixture.BackgroundWorker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + var reason = DiscardReason.EventProcessor; + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(reason, DataCategory.Transaction); + var expectedDroppedSpanCount = transaction.Spans.Count + 1; + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(reason, DataCategory.Span, expectedDroppedSpanCount); + } + [Fact] public void CaptureTransaction_BeforeSendTransaction_GetsHint() { diff --git a/test/Sentry.Tests/SentryMetricEmitterTests.cs b/test/Sentry.Tests/SentryMetricEmitterTests.cs index 293bad2cdb..32902a70a8 100644 --- a/test/Sentry.Tests/SentryMetricEmitterTests.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.cs @@ -13,10 +13,12 @@ public Fixture() { DiagnosticLogger = new InMemoryDiagnosticLogger(); Hub = Substitute.For(); + ClientReportRecorder = Substitute.For(); Options = new SentryOptions { Debug = true, DiagnosticLogger = DiagnosticLogger, + ClientReportRecorder = ClientReportRecorder, }; Clock = new MockClock(new DateTimeOffset(2025, 04, 22, 14, 51, 00, 789, TimeSpan.FromHours(2))); BatchSize = 2; @@ -39,6 +41,7 @@ public Fixture() public InMemoryDiagnosticLogger DiagnosticLogger { get; } public IHub Hub { get; } + public IClientReportRecorder ClientReportRecorder { get; } public SentryOptions Options { get; } public ISystemClock Clock { get; } public int BatchSize { get; set; } @@ -137,6 +140,7 @@ public void Emit_WhenBeforeSendMetricReturnsNull_DoesNotCaptureEnvelope() _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); invocations.Should().Be(1); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.TraceMetric); } [Fact] @@ -153,6 +157,7 @@ public void Emit_InvalidBeforeSendMetric_DoesNotCaptureEnvelope() entry.Message.Should().Be("The BeforeSendMetric callback threw an exception. The Metric will be dropped."); entry.Exception.Should().BeOfType(); entry.Args.Should().BeEmpty(); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.TraceMetric); } [Fact] diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index f78266898b..33310a0b1c 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -13,10 +13,12 @@ public Fixture() { DiagnosticLogger = new InMemoryDiagnosticLogger(); Hub = Substitute.For(); + ClientReportRecorder = Substitute.For(); Options = new SentryOptions { Debug = true, DiagnosticLogger = DiagnosticLogger, + ClientReportRecorder = ClientReportRecorder, }; Clock = new MockClock(new DateTimeOffset(2025, 04, 22, 14, 51, 00, 789, TimeSpan.FromHours(2))); BatchSize = 2; @@ -39,6 +41,7 @@ public Fixture() public InMemoryDiagnosticLogger DiagnosticLogger { get; } public IHub Hub { get; } + public IClientReportRecorder ClientReportRecorder { get; } public SentryOptions Options { get; } public ISystemClock Clock { get; } public int BatchSize { get; set; } @@ -148,6 +151,7 @@ public void Log_WhenBeforeSendLogReturnsNull_DoesNotCaptureEnvelope() _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); invocations.Should().Be(1); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); } [Fact] @@ -178,6 +182,7 @@ public void Log_InvalidConfigureLog_DoesNotCaptureEnvelope() entry.Message.Should().Be("The configureLog callback threw an exception. The Log will be dropped."); entry.Exception.Should().BeOfType(); entry.Args.Should().BeEmpty(); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); } [Fact] @@ -194,6 +199,7 @@ public void Log_InvalidBeforeSendLog_DoesNotCaptureEnvelope() entry.Message.Should().Be("The BeforeSendLog callback threw an exception. The Log will be dropped."); entry.Exception.Should().BeOfType(); entry.Args.Should().BeEmpty(); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); } [Fact] From 6de2c4026cf279dcaa0d48b060999bb786b70c8f Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 23 Sep 2026 13:28:40 +1200 Subject: [PATCH 2/5] 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 --- src/Sentry/Internal/DefaultSentryMetricEmitter.cs | 4 ++++ src/Sentry/Internal/DefaultSentryStructuredLogger.cs | 1 + src/Sentry/Internal/DiscardReason.cs | 1 + test/Sentry.Tests/SentryMetricEmitterTests.Types.cs | 5 +++++ test/Sentry.Tests/SentryStructuredLoggerTests.cs | 1 + 5 files changed, 12 insertions(+) diff --git a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs index e7a4876f8a..04c2e6e4e9 100644 --- a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs +++ b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs @@ -27,12 +27,14 @@ private protected override void CaptureMetric(SentryMetricType type, string n { if (!SentryMetric.IsSupported(typeof(T))) { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.Invalid, DataCategory.TraceMetric); _options.DiagnosticLogger?.LogWarning("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.", typeof(T)); return; } if (string.IsNullOrEmpty(name)) { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.Invalid, DataCategory.TraceMetric); _options.DiagnosticLogger?.LogWarning("Name of metrics cannot be null or empty. Metric-Type: {0}; Value-Type: {1}", type.ToString(), typeof(T)); return; } @@ -46,12 +48,14 @@ private protected override void CaptureMetric(SentryMetricType type, string n { if (!SentryMetric.IsSupported(typeof(T))) { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.Invalid, DataCategory.TraceMetric); _options.DiagnosticLogger?.LogWarning("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.", typeof(T)); return; } if (string.IsNullOrEmpty(name)) { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.Invalid, DataCategory.TraceMetric); _options.DiagnosticLogger?.LogWarning("Name of metrics cannot be null or empty. Metric-Type: {0}; Value-Type: {1}", type.ToString(), typeof(T)); return; } diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs index 947a6c77db..670ff58ea8 100644 --- a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs +++ b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs @@ -39,6 +39,7 @@ private protected override void CaptureLog(SentryLogLevel level, string template } catch (FormatException e) { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.Invalid, DataCategory.LogItem); _options.DiagnosticLogger?.LogError(e, "Template string does not match the provided argument. The Log will be dropped."); return; } diff --git a/src/Sentry/Internal/DiscardReason.cs b/src/Sentry/Internal/DiscardReason.cs index a75a24566f..7ed23144ae 100644 --- a/src/Sentry/Internal/DiscardReason.cs +++ b/src/Sentry/Internal/DiscardReason.cs @@ -7,6 +7,7 @@ namespace Sentry.Internal; public static DiscardReason BufferOverflow = new("buffer_overflow"); public static DiscardReason CacheOverflow = new("cache_overflow"); public static DiscardReason EventProcessor = new("event_processor"); + public static DiscardReason Invalid = new("invalid"); public static DiscardReason NetworkError = new("network_error"); public static DiscardReason QueueOverflow = new("queue_overflow"); public static DiscardReason SendError = new("send_error"); diff --git a/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs b/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs index 2024db884c..1b13c1fa6c 100644 --- a/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs @@ -163,6 +163,7 @@ public void Emit_Decimal_DoesNotCaptureEnvelope(SentryMetricType type) var entry = _fixture.DiagnosticLogger.Dequeue(); entry.Level.Should().Be(SentryLevel.Warning); entry.Message.Should().Be("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double."); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.Invalid, DataCategory.TraceMetric); entry.Exception.Should().BeNull(); entry.Args.Should().BeEquivalentTo([typeof(decimal)]); } @@ -183,6 +184,7 @@ public void Emit_Half_DoesNotCaptureEnvelope(SentryMetricType type) var entry = _fixture.DiagnosticLogger.Dequeue(); entry.Level.Should().Be(SentryLevel.Warning); entry.Message.Should().Be("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double."); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.Invalid, DataCategory.TraceMetric); entry.Exception.Should().BeNull(); entry.Args.Should().BeEquivalentTo([typeof(Half)]); } @@ -203,6 +205,7 @@ public void Emit_Enum_DoesNotCaptureEnvelope(SentryMetricType type) var entry = _fixture.DiagnosticLogger.Dequeue(); entry.Level.Should().Be(SentryLevel.Warning); entry.Message.Should().Be("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double."); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.Invalid, DataCategory.TraceMetric); entry.Exception.Should().BeNull(); entry.Args.Should().BeEquivalentTo([typeof(StringComparison)]); } @@ -222,6 +225,7 @@ public void Emit_Name_Null_DoesNotCaptureEnvelope(SentryMetricType type, string var entry = _fixture.DiagnosticLogger.Dequeue(); entry.Level.Should().Be(SentryLevel.Warning); entry.Message.Should().Be("Name of metrics cannot be null or empty. Metric-Type: {0}; Value-Type: {1}"); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.Invalid, DataCategory.TraceMetric); entry.Exception.Should().BeNull(); entry.Args.Should().BeEquivalentTo([arg0, arg1]); } @@ -241,6 +245,7 @@ public void Emit_Name_Empty_DoesNotCaptureEnvelope(SentryMetricType type, string var entry = _fixture.DiagnosticLogger.Dequeue(); entry.Level.Should().Be(SentryLevel.Warning); entry.Message.Should().Be("Name of metrics cannot be null or empty. Metric-Type: {0}; Value-Type: {1}"); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.Invalid, DataCategory.TraceMetric); entry.Exception.Should().BeNull(); entry.Args.Should().BeEquivalentTo([arg0, arg1]); } diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index 33310a0b1c..7eca4850b0 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -167,6 +167,7 @@ public void Log_InvalidFormat_DoesNotCaptureEnvelope() entry.Message.Should().Be("Template string does not match the provided argument. The Log will be dropped."); entry.Exception.Should().BeOfType(); entry.Args.Should().BeEmpty(); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.Invalid, DataCategory.LogItem); } [Fact] From 7710900ebaeb0dfb5234cafff0f3c7f5da7dd015 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 23 Sep 2026 14:47:24 +1200 Subject: [PATCH 3/5] 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 --- src/Sentry/Internal/DefaultSentryMetricEmitter.cs | 1 + src/Sentry/Internal/DefaultSentryStructuredLogger.cs | 1 + test/Sentry.Tests/SentryMetricEmitterTests.Values.cs | 9 +++++++++ test/Sentry.Tests/SentryMetricEmitterTests.cs | 5 +++++ test/Sentry.Tests/SentryStructuredLoggerTests.cs | 5 +++++ 5 files changed, 21 insertions(+) diff --git a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs index 04c2e6e4e9..0d24befc7a 100644 --- a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs +++ b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs @@ -89,6 +89,7 @@ private protected override void CaptureMetric(SentryMetric metric) where T if (configuredMetric is null) { _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.TraceMetric); + _options.DiagnosticLogger?.LogInfo("Metric dropped by BeforeSendMetric callback."); return; } diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs index 670ff58ea8..72311c14f2 100644 --- a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs +++ b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs @@ -104,6 +104,7 @@ protected internal override void CaptureLog(SentryLog log) if (configuredLog is null) { _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); + _options.DiagnosticLogger?.LogInfo("Log dropped by BeforeSendLog callback."); return; } diff --git a/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs b/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs index 497fbffa4f..f6c924297f 100644 --- a/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs @@ -118,6 +118,7 @@ public void Emit_Unit_MeasurementUnit_Predefined(MeasurementUnit unit, string ex captured.Should().NotBeNull(); captured.Unit.Should().Be(expected); + _fixture.DiagnosticLogger.Dequeue().Message.Should().Be("Metric dropped by BeforeSendMetric callback."); } [Fact] @@ -135,6 +136,7 @@ public void Emit_Unit_MeasurementUnit_None() captured.Should().NotBeNull(); captured.Unit.Should().Be("none"); + _fixture.DiagnosticLogger.Dequeue().Message.Should().Be("Metric dropped by BeforeSendMetric callback."); } [Fact] @@ -152,6 +154,7 @@ public void Emit_Unit_MeasurementUnit_Custom() captured.Should().NotBeNull(); captured.Unit.Should().Be("custom_unit"); + _fixture.DiagnosticLogger.Dequeue().Message.Should().Be("Metric dropped by BeforeSendMetric callback."); } [Fact] @@ -169,6 +172,7 @@ public void Emit_Unit_MeasurementUnit_Empty() captured.Should().NotBeNull(); captured.Unit.Should().BeEmpty(); + _fixture.DiagnosticLogger.Dequeue().Message.Should().Be("Metric dropped by BeforeSendMetric callback."); } [Fact] @@ -186,6 +190,7 @@ public void Emit_Unit_MeasurementUnit_Null() captured.Should().NotBeNull(); captured.Unit.Should().BeNull(); + _fixture.DiagnosticLogger.Dequeue().Message.Should().Be("Metric dropped by BeforeSendMetric callback."); } [Fact] @@ -203,6 +208,7 @@ public void Emit_Unit_MeasurementUnit_Default() captured.Should().NotBeNull(); captured.Unit.Should().BeNull(); + _fixture.DiagnosticLogger.Dequeue().Message.Should().Be("Metric dropped by BeforeSendMetric callback."); } [Fact] @@ -221,6 +227,7 @@ public void Emit_Unit_String_Custom() captured.Should().NotBeNull(); captured.Unit.Should().Be("custom_unit"); + _fixture.DiagnosticLogger.Dequeue().Message.Should().Be("Metric dropped by BeforeSendMetric callback."); } [Fact] @@ -239,6 +246,7 @@ public void Emit_Unit_String_Empty() captured.Should().NotBeNull(); captured.Unit.Should().BeEmpty(); + _fixture.DiagnosticLogger.Dequeue().Message.Should().Be("Metric dropped by BeforeSendMetric callback."); } [Fact] @@ -257,6 +265,7 @@ public void Emit_Unit_String_Null() captured.Should().NotBeNull(); captured.Unit.Should().BeNull(); + _fixture.DiagnosticLogger.Dequeue().Message.Should().Be("Metric dropped by BeforeSendMetric callback."); } [SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "The generic SentryMetric type is internal. Testing via the public abstract base type.")] diff --git a/test/Sentry.Tests/SentryMetricEmitterTests.cs b/test/Sentry.Tests/SentryMetricEmitterTests.cs index 32902a70a8..e676ed0239 100644 --- a/test/Sentry.Tests/SentryMetricEmitterTests.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.cs @@ -141,6 +141,11 @@ public void Emit_WhenBeforeSendMetricReturnsNull_DoesNotCaptureEnvelope() _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); invocations.Should().Be(1); _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.TraceMetric); + var entry = _fixture.DiagnosticLogger.Dequeue(); + entry.Level.Should().Be(SentryLevel.Info); + entry.Message.Should().Be("Metric dropped by BeforeSendMetric callback."); + entry.Exception.Should().BeNull(); + entry.Args.Should().BeEmpty(); } [Fact] diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index 7eca4850b0..0ba5544c59 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -152,6 +152,11 @@ public void Log_WhenBeforeSendLogReturnsNull_DoesNotCaptureEnvelope() _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); invocations.Should().Be(1); _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); + var entry = _fixture.DiagnosticLogger.Dequeue(); + entry.Level.Should().Be(SentryLevel.Info); + entry.Message.Should().Be("Log dropped by BeforeSendLog callback."); + entry.Exception.Should().BeNull(); + entry.Args.Should().BeEmpty(); } [Fact] From 8992aa908b7225c41ed5b8022f0c3763864aa9c5 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 25 Sep 2026 09:12:24 +1200 Subject: [PATCH 4/5] 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 --- src/Sentry/Internal/DefaultSentryMetricEmitter.cs | 2 +- src/Sentry/Internal/DefaultSentryStructuredLogger.cs | 4 ++-- src/Sentry/Internal/DiscardReason.cs | 1 + src/Sentry/Internal/SentryEventHelper.cs | 4 ++-- src/Sentry/SentryClient.cs | 4 ++-- test/Sentry.Tests/SentryClientTests.cs | 6 +++--- test/Sentry.Tests/SentryMetricEmitterTests.cs | 2 +- test/Sentry.Tests/SentryStructuredLoggerTests.cs | 4 ++-- 8 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs index 0d24befc7a..2b1f423d3f 100644 --- a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs +++ b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs @@ -80,7 +80,7 @@ private protected override void CaptureMetric(SentryMetric metric) where T } catch (Exception e) { - _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.TraceMetric); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.TraceMetric); _options.DiagnosticLogger?.LogError(e, "The BeforeSendMetric callback threw an exception. The Metric will be dropped."); return; } diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs index 72311c14f2..4bcf0862a9 100644 --- a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs +++ b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs @@ -71,7 +71,7 @@ private protected override void CaptureLog(SentryLogLevel level, string template } catch (Exception e) { - _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.LogItem); _options.DiagnosticLogger?.LogError(e, "The configureLog callback threw an exception. The Log will be dropped."); return; } @@ -95,7 +95,7 @@ protected internal override void CaptureLog(SentryLog log) } catch (Exception e) { - _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.LogItem); _options.DiagnosticLogger?.LogError(e, "The BeforeSendLog callback threw an exception. The Log will be dropped."); return; } diff --git a/src/Sentry/Internal/DiscardReason.cs b/src/Sentry/Internal/DiscardReason.cs index 7ed23144ae..ac7bd9e737 100644 --- a/src/Sentry/Internal/DiscardReason.cs +++ b/src/Sentry/Internal/DiscardReason.cs @@ -4,6 +4,7 @@ namespace Sentry.Internal; { // See https://develop.sentry.dev/sdk/client-reports/ for list public static DiscardReason BeforeSend = new("before_send"); + public static DiscardReason CallbackError = new("callback_error"); public static DiscardReason BufferOverflow = new("buffer_overflow"); public static DiscardReason CacheOverflow = new("cache_overflow"); public static DiscardReason EventProcessor = new("event_processor"); diff --git a/src/Sentry/Internal/SentryEventHelper.cs b/src/Sentry/Internal/SentryEventHelper.cs index 8c0dad38aa..561ba6aea9 100644 --- a/src/Sentry/Internal/SentryEventHelper.cs +++ b/src/Sentry/Internal/SentryEventHelper.cs @@ -23,7 +23,7 @@ internal static class SentryEventHelper } catch (Exception e) { - options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, dataCategory); + options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, dataCategory); options.LogError(e, "Event processor {0} threw an exception. The event will be dropped.", processor.GetType().Name); return null; } @@ -104,7 +104,7 @@ internal static class SentryEventHelper } catch (Exception e) { - options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Feedback); + options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Feedback); options.LogError(e, "The BeforeSendFeedback callback threw an exception. The feedback will be dropped."); return null; } diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 64e82e037f..75ee38887f 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -218,8 +218,8 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent } catch (Exception e) { - _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Transaction); - _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Span, spanCount); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Transaction); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Span, spanCount); _options.LogError(e, "Transaction processor {0} threw an exception. The transaction will be dropped.", processor.GetType().Name); return; } diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index 2eff11188f..32d3427031 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -401,7 +401,7 @@ public void CaptureEvent_EventProcessorThrows_DropsEventAndRecordsDiscard() id.Should().Be(SentryId.Empty); _fixture.BackgroundWorker.DidNotReceive().EnqueueEnvelope(Arg.Any()); _fixture.ClientReportRecorder.Received(1) - .RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Error); + .RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Error); } [Fact] @@ -1143,7 +1143,7 @@ public void CaptureFeedback_BeforeSendFeedbackThrows_FeedbackDropped() result.Should().Be(CaptureFeedbackResult.DroppedByBeforeSendFeedback); id.Should().Be(SentryId.Empty); _ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); - _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Feedback); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Feedback); } [Fact] @@ -1731,7 +1731,7 @@ public void CaptureTransaction_TransactionProcessorThrows_DropsTransactionAndRec // Assert _fixture.BackgroundWorker.DidNotReceive().EnqueueEnvelope(Arg.Any()); - var reason = DiscardReason.EventProcessor; + var reason = DiscardReason.CallbackError; _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(reason, DataCategory.Transaction); var expectedDroppedSpanCount = transaction.Spans.Count + 1; _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(reason, DataCategory.Span, expectedDroppedSpanCount); diff --git a/test/Sentry.Tests/SentryMetricEmitterTests.cs b/test/Sentry.Tests/SentryMetricEmitterTests.cs index e676ed0239..391a2ff22e 100644 --- a/test/Sentry.Tests/SentryMetricEmitterTests.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.cs @@ -162,7 +162,7 @@ public void Emit_InvalidBeforeSendMetric_DoesNotCaptureEnvelope() entry.Message.Should().Be("The BeforeSendMetric callback threw an exception. The Metric will be dropped."); entry.Exception.Should().BeOfType(); entry.Args.Should().BeEmpty(); - _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.TraceMetric); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.TraceMetric); } [Fact] diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index 0ba5544c59..b140893203 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -188,7 +188,7 @@ public void Log_InvalidConfigureLog_DoesNotCaptureEnvelope() entry.Message.Should().Be("The configureLog callback threw an exception. The Log will be dropped."); entry.Exception.Should().BeOfType(); entry.Args.Should().BeEmpty(); - _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.LogItem); } [Fact] @@ -205,7 +205,7 @@ public void Log_InvalidBeforeSendLog_DoesNotCaptureEnvelope() entry.Message.Should().Be("The BeforeSendLog callback threw an exception. The Log will be dropped."); entry.Exception.Should().BeOfType(); entry.Args.Should().BeEmpty(); - _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.LogItem); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.LogItem); } [Fact] From f6ee5c567356cbddca365399d82e5fc198a38772 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 25 Sep 2026 23:07:13 +1200 Subject: [PATCH 5/5] 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