diff --git a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs index 11d2879295..2b1f423d3f 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; } @@ -76,15 +80,20 @@ private protected override void CaptureMetric(SentryMetric metric) where T } catch (Exception e) { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, 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); + _options.DiagnosticLogger?.LogInfo("Metric dropped by BeforeSendMetric callback."); + return; } + + _batchProcessor.Enqueue(configuredMetric); } /// diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs index 5c775c775d..4bcf0862a9 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; } @@ -70,6 +71,7 @@ private protected override void CaptureLog(SentryLogLevel level, string template } catch (Exception e) { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.LogItem); _options.DiagnosticLogger?.LogError(e, "The configureLog callback threw an exception. The Log will be dropped."); return; } @@ -93,15 +95,20 @@ protected internal override void CaptureLog(SentryLog log) } catch (Exception e) { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, 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); + _options.DiagnosticLogger?.LogInfo("Log dropped by BeforeSendLog callback."); + return; } + + _batchProcessor.Enqueue(configuredLog); } /// diff --git a/src/Sentry/Internal/DiscardReason.cs b/src/Sentry/Internal/DiscardReason.cs index a75a24566f..ac7bd9e737 100644 --- a/src/Sentry/Internal/DiscardReason.cs +++ b/src/Sentry/Internal/DiscardReason.cs @@ -4,9 +4,11 @@ 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"); + 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/src/Sentry/Internal/SentryEventHelper.cs b/src/Sentry/Internal/SentryEventHelper.cs index bda07489e0..561ba6aea9 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.CallbackError, 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); @@ -94,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 c3c88a062e..75ee38887f 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.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; + } + 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..32d3427031 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.CallbackError, DataCategory.Error); + } + [Fact] public void CaptureEvent_ExceptionFilter_RecordsDiscard() { @@ -1126,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] @@ -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.CallbackError; + _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.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/SentryMetricEmitterTests.Values.cs b/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs index e7884e1a2b..3420e607e7 100644 --- a/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs @@ -131,6 +131,7 @@ private void AssertEmittedUnit(MeasurementUnit unit, string expected) captured.Should().NotBeNull(); captured.Unit.Should().Be(expected); + _fixture.DiagnosticLogger.Dequeue().Message.Should().Be("Metric dropped by BeforeSendMetric callback."); } [Fact] @@ -148,6 +149,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] @@ -165,6 +167,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] @@ -182,6 +185,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] @@ -199,6 +203,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] @@ -216,6 +221,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] @@ -234,6 +240,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] @@ -252,6 +259,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] @@ -270,6 +278,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 293bad2cdb..391a2ff22e 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,12 @@ 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] @@ -153,6 +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.CallbackError, DataCategory.TraceMetric); } [Fact] diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index f78266898b..b140893203 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,12 @@ 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] @@ -163,6 +172,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] @@ -178,6 +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.CallbackError, DataCategory.LogItem); } [Fact] @@ -194,6 +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.CallbackError, DataCategory.LogItem); } [Fact]