diff --git a/src/Sentry.AspNet/Internal/SystemWebRequestEventProcessor.cs b/src/Sentry.AspNet/Internal/SystemWebRequestEventProcessor.cs index ba4cfbd735..229c855353 100644 --- a/src/Sentry.AspNet/Internal/SystemWebRequestEventProcessor.cs +++ b/src/Sentry.AspNet/Internal/SystemWebRequestEventProcessor.cs @@ -4,7 +4,7 @@ namespace Sentry.AspNet.Internal; -internal class SystemWebRequestEventProcessor : ISentryEventProcessor +internal class SystemWebRequestEventProcessor : ISentryEventProcessor, ISdkProcessor { private static readonly SdkVersion SdkVersion = typeof(SystemWebRequestEventProcessor).Assembly.GetNameAndVersion(); diff --git a/src/Sentry.AspNetCore.Grpc/SentryBuilderExtensions.cs b/src/Sentry.AspNetCore.Grpc/SentryBuilderExtensions.cs index 76df59bdaf..17457a5cc1 100644 --- a/src/Sentry.AspNetCore.Grpc/SentryBuilderExtensions.cs +++ b/src/Sentry.AspNetCore.Grpc/SentryBuilderExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Sentry.Extensibility; +using Sentry.Internal; using Sentry.Reflection; namespace Sentry.AspNetCore.Grpc; @@ -28,7 +29,7 @@ public static ISentryBuilder AddGrpc(this ISentryBuilder builder) return builder; } - private class SentryGrpcEventProcessor : ISentryEventProcessor + private class SentryGrpcEventProcessor : ISentryEventProcessor, ISdkProcessor { private static readonly SdkVersion NameAndVersion = typeof(SentryGrpcInterceptor).Assembly.GetNameAndVersion(); diff --git a/src/Sentry.AspNetCore/AspNetCoreEventProcessor.cs b/src/Sentry.AspNetCore/AspNetCoreEventProcessor.cs index 0851991710..78d41e90e6 100644 --- a/src/Sentry.AspNetCore/AspNetCoreEventProcessor.cs +++ b/src/Sentry.AspNetCore/AspNetCoreEventProcessor.cs @@ -1,8 +1,9 @@ using Sentry.Extensibility; +using Sentry.Internal; namespace Sentry.AspNetCore; -internal class AspNetCoreEventProcessor : ISentryEventProcessor +internal class AspNetCoreEventProcessor : ISentryEventProcessor, ISdkProcessor { public SentryEvent Process(SentryEvent @event) { diff --git a/src/Sentry.AspNetCore/AspNetCoreExceptionProcessor.cs b/src/Sentry.AspNetCore/AspNetCoreExceptionProcessor.cs index ace6284570..261ebe01c2 100644 --- a/src/Sentry.AspNetCore/AspNetCoreExceptionProcessor.cs +++ b/src/Sentry.AspNetCore/AspNetCoreExceptionProcessor.cs @@ -1,9 +1,10 @@ using Sentry.Extensibility; +using Sentry.Internal; using Sentry.Protocol; namespace Sentry.AspNetCore; -internal class AspNetCoreExceptionProcessor : ISentryEventExceptionProcessor +internal class AspNetCoreExceptionProcessor : ISentryEventExceptionProcessor, ISdkProcessor { public void Process(Exception exception, SentryEvent @event) { diff --git a/src/Sentry.AspNetCore/ExceptionHandlerFeatureProcessor.cs b/src/Sentry.AspNetCore/ExceptionHandlerFeatureProcessor.cs index 095accabae..97091ada53 100644 --- a/src/Sentry.AspNetCore/ExceptionHandlerFeatureProcessor.cs +++ b/src/Sentry.AspNetCore/ExceptionHandlerFeatureProcessor.cs @@ -1,11 +1,12 @@ using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Routing; using Sentry.Extensibility; +using Sentry.Internal; namespace Sentry.AspNetCore; #if NET6_0_OR_GREATER -internal class ExceptionHandlerFeatureProcessor : ISentryEventExceptionProcessor +internal class ExceptionHandlerFeatureProcessor : ISentryEventExceptionProcessor, ISdkProcessor { private readonly string _originalMethod; private readonly IExceptionHandlerFeature _exceptionHandlerFeature; diff --git a/src/Sentry.EntityFramework/ErrorProcessors/DbConcurrencyExceptionProcessor.cs b/src/Sentry.EntityFramework/ErrorProcessors/DbConcurrencyExceptionProcessor.cs index 4495350bda..83f66eb3ea 100644 --- a/src/Sentry.EntityFramework/ErrorProcessors/DbConcurrencyExceptionProcessor.cs +++ b/src/Sentry.EntityFramework/ErrorProcessors/DbConcurrencyExceptionProcessor.cs @@ -1,9 +1,11 @@ +using Sentry.Internal; + namespace Sentry.EntityFramework.ErrorProcessors; /// /// Exception processor for Entity Framework . /// -public class DbConcurrencyExceptionProcessor : SentryEventExceptionProcessor +public class DbConcurrencyExceptionProcessor : SentryEventExceptionProcessor, ISdkProcessor { /// /// Extracts RowCount and RowError from . diff --git a/src/Sentry.EntityFramework/ErrorProcessors/DbEntityValidationExceptionProcessor.cs b/src/Sentry.EntityFramework/ErrorProcessors/DbEntityValidationExceptionProcessor.cs index 3979220318..f5917f329d 100644 --- a/src/Sentry.EntityFramework/ErrorProcessors/DbEntityValidationExceptionProcessor.cs +++ b/src/Sentry.EntityFramework/ErrorProcessors/DbEntityValidationExceptionProcessor.cs @@ -1,9 +1,11 @@ +using Sentry.Internal; + namespace Sentry.EntityFramework.ErrorProcessors; /// /// Exception processor for . /// -public class DbEntityValidationExceptionProcessor : SentryEventExceptionProcessor +public class DbEntityValidationExceptionProcessor : SentryEventExceptionProcessor, ISdkProcessor { /// /// Extracts details from into the . diff --git a/src/Sentry.Google.Cloud.Functions/SentryStartup.cs b/src/Sentry.Google.Cloud.Functions/SentryStartup.cs index ad7e485043..b4b6ce4b82 100644 --- a/src/Sentry.Google.Cloud.Functions/SentryStartup.cs +++ b/src/Sentry.Google.Cloud.Functions/SentryStartup.cs @@ -8,6 +8,7 @@ using Sentry; using Sentry.AspNetCore; using Sentry.Extensibility; +using Sentry.Internal; using Sentry.Reflection; namespace Google.Cloud.Functions.Framework; @@ -94,7 +95,7 @@ public override void Configure(WebHostBuilderContext context, IApplicationBuilde app.UseSentryTracing(); } - private class SentryGoogleCloudFunctionEventProcessor : ISentryEventProcessor + private class SentryGoogleCloudFunctionEventProcessor : ISentryEventProcessor, ISdkProcessor { private static readonly SdkVersion NameAndVersion = typeof(SentryStartup).Assembly.GetNameAndVersion(); diff --git a/src/Sentry.Maui/Internal/SentryMauiEventProcessor.cs b/src/Sentry.Maui/Internal/SentryMauiEventProcessor.cs index 6048da72fc..1e50972cd2 100644 --- a/src/Sentry.Maui/Internal/SentryMauiEventProcessor.cs +++ b/src/Sentry.Maui/Internal/SentryMauiEventProcessor.cs @@ -1,8 +1,9 @@ using Sentry.Extensibility; +using Sentry.Internal; namespace Sentry.Maui.Internal; -internal class SentryMauiEventProcessor : ISentryEventProcessor +internal class SentryMauiEventProcessor : ISentryEventProcessor, ISdkProcessor { public static bool? InForeground { get; set; } diff --git a/src/Sentry.Maui/Internal/SentryMauiScreenshotProcessor.cs b/src/Sentry.Maui/Internal/SentryMauiScreenshotProcessor.cs index 621b6ab95b..07df59d5ef 100644 --- a/src/Sentry.Maui/Internal/SentryMauiScreenshotProcessor.cs +++ b/src/Sentry.Maui/Internal/SentryMauiScreenshotProcessor.cs @@ -1,8 +1,9 @@ using Sentry.Extensibility; +using Sentry.Internal; namespace Sentry.Maui.Internal; -internal class SentryMauiScreenshotProcessor : ISentryEventProcessorWithHint +internal class SentryMauiScreenshotProcessor : ISentryEventProcessorWithHint, ISdkProcessor { private readonly SentryMauiOptions _options; diff --git a/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs b/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs index d7d285eebb..36912d8338 100644 --- a/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs +++ b/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs @@ -3,7 +3,7 @@ namespace Sentry.OpenTelemetry; -internal class OpenTelemetryTransactionProcessor : ISentryTransactionProcessor +internal class OpenTelemetryTransactionProcessor : ISentryTransactionProcessor, ISdkProcessor { public SentryTransaction Process(SentryTransaction transaction) { diff --git a/src/Sentry.Serilog/SerilogScopeEventProcessor.cs b/src/Sentry.Serilog/SerilogScopeEventProcessor.cs index 4b3de83fe0..0951cf816d 100644 --- a/src/Sentry.Serilog/SerilogScopeEventProcessor.cs +++ b/src/Sentry.Serilog/SerilogScopeEventProcessor.cs @@ -5,7 +5,7 @@ namespace Sentry.Serilog; /// /// Sentry event processor that applies properties from the Serilog scope to Sentry events. /// -internal class SerilogScopeEventProcessor : ISentryEventProcessor +internal class SerilogScopeEventProcessor : ISentryEventProcessor, ISdkProcessor { private readonly SentryOptions _options; diff --git a/src/Sentry/Internal/DuplicateEventDetectionEventProcessor.cs b/src/Sentry/Internal/DuplicateEventDetectionEventProcessor.cs index 8e781a46a9..4025b3d9bf 100644 --- a/src/Sentry/Internal/DuplicateEventDetectionEventProcessor.cs +++ b/src/Sentry/Internal/DuplicateEventDetectionEventProcessor.cs @@ -2,7 +2,7 @@ namespace Sentry.Internal; -internal class DuplicateEventDetectionEventProcessor : ISentryEventProcessor +internal class DuplicateEventDetectionEventProcessor : ISentryEventProcessor, ISdkProcessor { private readonly SentryOptions _options; private readonly ConditionalWeakTable _capturedObjects = new(); diff --git a/src/Sentry/Internal/Extensions/SentryStackTraceFactoryExtensions.cs b/src/Sentry/Internal/Extensions/SentryStackTraceFactoryExtensions.cs new file mode 100644 index 0000000000..e51f033e1c --- /dev/null +++ b/src/Sentry/Internal/Extensions/SentryStackTraceFactoryExtensions.cs @@ -0,0 +1,19 @@ +using Sentry.Extensibility; + +namespace Sentry.Internal.Extensions; + +internal static class SentryStackTraceFactoryExtensions +{ + public static SentryStackTrace? TryCreate(this ISentryStackTraceFactory factory, SentryOptions options, Exception? exception = null) + { + try + { + return factory.Create(exception); + } + catch (Exception e) + { + options.LogError(e, "Stack trace factory {0} threw an exception. The stack trace will be omitted.", factory.GetType().Name); + return null; + } + } +} diff --git a/src/Sentry/Internal/ISdkProcessor.cs b/src/Sentry/Internal/ISdkProcessor.cs new file mode 100644 index 0000000000..7f85e22f27 --- /dev/null +++ b/src/Sentry/Internal/ISdkProcessor.cs @@ -0,0 +1,5 @@ +namespace Sentry.Internal; + +// Marker interface for SDK processors. +// Must not be applied to processors that run user code (e.g. DelegateEventProcessor) +internal interface ISdkProcessor; diff --git a/src/Sentry/Internal/MainExceptionProcessor.cs b/src/Sentry/Internal/MainExceptionProcessor.cs index 8f4899763c..c53d644274 100644 --- a/src/Sentry/Internal/MainExceptionProcessor.cs +++ b/src/Sentry/Internal/MainExceptionProcessor.cs @@ -4,7 +4,7 @@ namespace Sentry.Internal; -internal class MainExceptionProcessor : ISentryEventExceptionProcessor +internal class MainExceptionProcessor : ISentryEventExceptionProcessor, ISdkProcessor { private const string ExceptionDataKeyPrefix = "sentry:"; internal const string ExceptionDataTagKey = ExceptionDataKeyPrefix + "tag:"; @@ -155,7 +155,7 @@ private SentryException BuildSentryException(Exception exception, int id, int? p sentryEx.Mechanism = mechanism; } - sentryEx.Stacktrace ??= SentryStackTraceFactoryAccessor().Create(exception); + sentryEx.Stacktrace ??= SentryStackTraceFactoryAccessor().TryCreate(_options, exception); return sentryEx; } diff --git a/src/Sentry/Internal/MainSentryEventProcessor.cs b/src/Sentry/Internal/MainSentryEventProcessor.cs index c0259d74b2..7d7b1ed0a5 100644 --- a/src/Sentry/Internal/MainSentryEventProcessor.cs +++ b/src/Sentry/Internal/MainSentryEventProcessor.cs @@ -1,9 +1,10 @@ using Sentry.Extensibility; +using Sentry.Internal.Extensions; using Sentry.Reflection; namespace Sentry.Internal; -internal class MainSentryEventProcessor : ISentryEventProcessor +internal class MainSentryEventProcessor : ISentryEventProcessor, ISdkProcessor { internal const string CultureInfoKey = "Current Culture"; internal const string CurrentUiCultureKey = "Current UI Culture"; @@ -87,7 +88,7 @@ public SentryEvent Process(SentryEvent @event) if (@event.Exception?.StackTrace is null && @event.SentryThreads?.Any(t => t.Current == true) != true) { var stackTrace = @event.SentryExceptions?.FirstOrDefault()?.Stacktrace - ?? SentryStackTraceFactoryAccessor().Create(); + ?? SentryStackTraceFactoryAccessor().TryCreate(_options); if (stackTrace != null) { var currentThread = Thread.CurrentThread; diff --git a/src/Sentry/Internal/SentryEventHelper.cs b/src/Sentry/Internal/SentryEventHelper.cs index cc12cbb40c..54e8af85cd 100644 --- a/src/Sentry/Internal/SentryEventHelper.cs +++ b/src/Sentry/Internal/SentryEventHelper.cs @@ -21,6 +21,10 @@ internal static class SentryEventHelper { processedEvent = processor.DoProcessEvent(processedEvent, effectiveHint); } + catch (Exception e) when (processor is ISdkProcessor) + { + options.LogError(e, "Event processor {0} threw an exception. Continuing with the remaining processors.", processor.GetType().Name); + } catch (Exception e) { options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, dataCategory); diff --git a/src/Sentry/Internal/TraceIgnoreStatusCodeTransactionProcessor.cs b/src/Sentry/Internal/TraceIgnoreStatusCodeTransactionProcessor.cs index a6dc881562..3dfe4a39a2 100644 --- a/src/Sentry/Internal/TraceIgnoreStatusCodeTransactionProcessor.cs +++ b/src/Sentry/Internal/TraceIgnoreStatusCodeTransactionProcessor.cs @@ -3,7 +3,7 @@ namespace Sentry.Internal; -internal class TraceIgnoreStatusCodeTransactionProcessor : ISentryTransactionProcessor +internal class TraceIgnoreStatusCodeTransactionProcessor : ISentryTransactionProcessor, ISdkProcessor { private const string IncomingRequestOperation = "http.server"; diff --git a/src/Sentry/PlatformAbstractions/NetFxInstallationsEventProcessor.cs b/src/Sentry/PlatformAbstractions/NetFxInstallationsEventProcessor.cs index dd57ccd646..2ec131658f 100644 --- a/src/Sentry/PlatformAbstractions/NetFxInstallationsEventProcessor.cs +++ b/src/Sentry/PlatformAbstractions/NetFxInstallationsEventProcessor.cs @@ -1,9 +1,10 @@ #if NETFRAMEWORK using Sentry.Extensibility; +using Sentry.Internal; namespace Sentry.PlatformAbstractions; -internal class NetFxInstallationsEventProcessor : ISentryEventProcessor +internal class NetFxInstallationsEventProcessor : ISentryEventProcessor, ISdkProcessor { internal static readonly string NetFxInstallationsKey = ".NET Framework"; diff --git a/src/Sentry/Platforms/Android/AndroidEventProcessor.cs b/src/Sentry/Platforms/Android/AndroidEventProcessor.cs index cba15fd78e..c95f226641 100644 --- a/src/Sentry/Platforms/Android/AndroidEventProcessor.cs +++ b/src/Sentry/Platforms/Android/AndroidEventProcessor.cs @@ -1,10 +1,11 @@ using Sentry.Android.Extensions; using Sentry.Extensibility; +using Sentry.Internal; using Sentry.JavaSdk.Android.Core; namespace Sentry.Android; -internal class AndroidEventProcessor : ISentryEventProcessor, IDisposable +internal class AndroidEventProcessor : ISentryEventProcessor, IDisposable, ISdkProcessor { private readonly JavaSdk.IEventProcessor? _androidProcessor; private readonly JavaSdk.Hint _hint = new(); diff --git a/src/Sentry/Platforms/Android/LogCatAttachmentEventProcessor.cs b/src/Sentry/Platforms/Android/LogCatAttachmentEventProcessor.cs index 89879f813d..2da3e28bdd 100644 --- a/src/Sentry/Platforms/Android/LogCatAttachmentEventProcessor.cs +++ b/src/Sentry/Platforms/Android/LogCatAttachmentEventProcessor.cs @@ -1,9 +1,10 @@ using Sentry.Extensibility; +using Sentry.Internal; using Runtime = Java.Lang.Runtime; namespace Sentry.Android; -internal class LogCatAttachmentEventProcessor : ISentryEventProcessorWithHint +internal class LogCatAttachmentEventProcessor : ISentryEventProcessorWithHint, ISdkProcessor { private static bool SendLogcatLogs = true; private readonly LogCatIntegrationType _logCatIntegrationType; diff --git a/src/Sentry/Platforms/Cocoa/CocoaEventProcessor.cs b/src/Sentry/Platforms/Cocoa/CocoaEventProcessor.cs index 03d45e81c9..a122e9ee07 100644 --- a/src/Sentry/Platforms/Cocoa/CocoaEventProcessor.cs +++ b/src/Sentry/Platforms/Cocoa/CocoaEventProcessor.cs @@ -1,9 +1,10 @@ using Sentry.Cocoa.Extensions; using Sentry.Extensibility; +using Sentry.Internal; namespace Sentry.Cocoa; -internal class CocoaEventProcessor : ISentryEventProcessor, IDisposable +internal class CocoaEventProcessor : ISentryEventProcessor, IDisposable, ISdkProcessor { public SentryEvent Process(SentryEvent @event) { diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index d49ac28b5b..09d9a4c3e5 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -216,6 +216,10 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent { processedTransaction = processor.DoProcessTransaction(processedTransaction, hint); } + catch (Exception e) when (processor is ISdkProcessor) + { + _options.LogError(e, "Transaction processor {0} threw an exception. Continuing with the remaining processors.", processor.GetType().Name); + } catch (Exception e) { _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Transaction); @@ -331,7 +335,12 @@ public SentryId CaptureCheckIn( private SentryId DoSendEvent(SentryEvent @event, SentryHint? hint, Scope? scope) { - var filteredExceptions = ApplyExceptionFilters(@event.Exception); + var filteredExceptions = ApplyExceptionFilters(@event.Exception, out var filterThrew); + if (filterThrew) + { + return SentryId.Empty; + } + if (filteredExceptions?.Count > 0) { _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Error); @@ -363,9 +372,20 @@ private SentryId DoSendEvent(SentryEvent @event, SentryHint? hint, Scope? scope) // after the SDK is initialized. Useful for example once a DI container is up foreach (var processor in scope.GetAllExceptionProcessors()) { - processor.Process(@event.Exception, @event); - - // NOTE: Exception processors can't drop events, but exception filters (above) can. + try + { + processor.Process(@event.Exception, @event); + } + catch (Exception e) when (processor is ISdkProcessor) + { + _options.LogError(e, "Exception processor {0} threw an exception. Continuing with the remaining processors.", processor.GetType().Name); + } + catch (Exception e) + { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Error); + _options.LogError(e, "Exception processor {0} threw an exception. The event will be dropped.", processor.GetType().Name); + return SentryId.Empty; + } } } @@ -437,8 +457,9 @@ private SentryId DoSendEvent(SentryEvent @event, SentryHint? hint, Scope? scope) return SentryId.Empty; } - private IReadOnlyCollection? ApplyExceptionFilters(Exception? exception) + private IReadOnlyCollection? ApplyExceptionFilters(Exception? exception, out bool filterThrew) { + filterThrew = false; var filters = _options.ExceptionFilters; if (exception == null || filters == null || filters.Count == 0) { @@ -446,21 +467,39 @@ private SentryId DoSendEvent(SentryEvent @event, SentryHint? hint, Scope? scope) return null; } - if (filters.Any(f => f.Filter(exception))) + foreach (var filter in filters) { - // The event should be filtered based on the given exception - return new[] { exception }; + try + { + if (filter.Filter(exception)) + { + // The event should be filtered based on the given exception + return new[] { exception }; + } + } + catch (Exception e) + { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Error); + _options.LogError(e, "Exception filter {0} threw an exception. The event will be dropped.", filter.GetType().Name); + filterThrew = true; + return null; + } } if (exception is AggregateException aggregate) { // Flatten the tree of aggregates such that all the inner exceptions are non-aggregates. var innerExceptions = aggregate.Flatten().InnerExceptions; - if (innerExceptions.All(e => ApplyExceptionFilters(e) != null)) + foreach (var innerException in innerExceptions) { - // All inner exceptions matched a filter, so the event should be filtered. - return innerExceptions; + if (ApplyExceptionFilters(innerException, out filterThrew) == null) + { + return null; + } } + + // All inner exceptions matched a filter, so the event should be filtered. + return innerExceptions; } // The event should not be filtered. diff --git a/test/Sentry.Tests/Internals/MainExceptionProcessorTests.cs b/test/Sentry.Tests/Internals/MainExceptionProcessorTests.cs index 9a65da4dbf..215697db68 100644 --- a/test/Sentry.Tests/Internals/MainExceptionProcessorTests.cs +++ b/test/Sentry.Tests/Internals/MainExceptionProcessorTests.cs @@ -47,6 +47,24 @@ public void Process_ExceptionsWithData_MechanismDataIsPopulated() Assert.Contains(data, pair => pair.Key == "bar" && pair.Value.Equals(456)); } + [Fact] + public void Process_StackTraceFactoryThrows_KeepsExceptionWithoutStackTrace() + { + var factoryException = new InvalidOperationException("factory failed"); + _fixture.SentryStackTraceFactory.Create(Arg.Any()).Throws(factoryException); + _fixture.SentryOptions.AddDiagnosticLoggerSubstitute(); + var sut = _fixture.GetSut(); + var evt = new SentryEvent(); + + sut.Process(new InvalidOperationException("captured"), evt); + + var sentryException = evt.SentryExceptions.Should().ContainSingle().Subject; + sentryException.Type.Should().Be(typeof(InvalidOperationException).FullName); + sentryException.Value.Should().Be("captured"); + sentryException.Stacktrace.Should().BeNull(); + _fixture.SentryOptions.ReceivedLogError(factoryException, "Stack trace factory {0} threw an exception. The stack trace will be omitted."); + } + [Fact] public void Process_ExceptionWithout_Handled() { diff --git a/test/Sentry.Tests/Internals/MainSentryEventProcessorTests.cs b/test/Sentry.Tests/Internals/MainSentryEventProcessorTests.cs index 89e3a4379f..17ae308278 100644 --- a/test/Sentry.Tests/Internals/MainSentryEventProcessorTests.cs +++ b/test/Sentry.Tests/Internals/MainSentryEventProcessorTests.cs @@ -35,6 +35,23 @@ public void Process_SendDefaultPiiTrueIdEnvironmentDefault_UserNameSet() Assert.Equal(Environment.UserName, evt.User.Username); } + [Fact] + public void Process_StackTraceFactoryThrows_StillEnrichesEvent() + { + var factoryException = new InvalidOperationException("factory failed"); + _fixture.SentryStackTraceFactory.Create(Arg.Any()).Throws(factoryException); + _fixture.SentryOptions.AddDiagnosticLoggerSubstitute(); + var sut = _fixture.GetSut(); + var evt = new SentryEvent(); + + _ = sut.Process(evt); + + evt.SentryThreads.Should().BeNullOrEmpty(); + evt.Sdk.Name.Should().Be(Sentry.Internal.Constants.SdkName); + evt.Environment.Should().NotBeNull(); + _fixture.SentryOptions.ReceivedLogError(factoryException, "Stack trace factory {0} threw an exception. The stack trace will be omitted."); + } + [Fact] public void EnsureThreadPoolInfoExists() { diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index 1eb558c1d4..9a681933a6 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -226,6 +226,46 @@ public void CaptureEvent_ExceptionProcessorsOnScope_Invoked() exceptionProcessor.Received(1).Process(evt.Exception!, evt); } + [Fact] + public void CaptureEvent_ExceptionProcessorThrows_DropsEventAndRecordsDiscard() + { + var exception = new InvalidOperationException("processor failed"); + var processor = Substitute.For(); + processor.When(p => p.Process(Arg.Any(), Arg.Any())).Throw(exception); + _fixture.SentryOptions.AddExceptionProcessor(processor); + _fixture.SentryOptions.AddDiagnosticLoggerSubstitute(); + + var id = _fixture.GetSut().CaptureEvent(new SentryEvent(new Exception())); + + id.Should().Be(SentryId.Empty); + _ = _fixture.BackgroundWorker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + _fixture.ClientReportRecorder.Received(1) + .RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Error); + _fixture.SentryOptions.ReceivedLogError(exception, "Exception processor {0} threw an exception. The event will be dropped."); + } + + [Fact] + public void CaptureEvent_SdkExceptionProcessorThrows_SendsEventWithoutDiscard() + { + var exception = new InvalidOperationException("processor failed"); + var sdkProcessor = Substitute.For(); + sdkProcessor.When(p => p.Process(Arg.Any(), Arg.Any())).Throw(exception); + var nextProcessor = Substitute.For(); + _fixture.SentryOptions.AddExceptionProcessor(sdkProcessor); + _fixture.SentryOptions.AddExceptionProcessor(nextProcessor); + _fixture.SentryOptions.AddDiagnosticLoggerSubstitute(); + var evt = new SentryEvent(new Exception()); + + var id = _fixture.GetSut().CaptureEvent(evt); + + id.Should().Be(evt.EventId); + nextProcessor.Received(1).Process(evt.Exception!, evt); + _ = _fixture.BackgroundWorker.Received(1).EnqueueEnvelope(Arg.Any()); + _fixture.ClientReportRecorder.DidNotReceive() + .RecordDiscardedEvent(Arg.Any(), Arg.Any(), Arg.Any()); + _fixture.SentryOptions.ReceivedLogError(exception, "Exception processor {0} threw an exception. Continuing with the remaining processors."); + } + [Fact] public void CaptureEvent_NullEventWithScope_EmptyGuid() { @@ -434,6 +474,42 @@ public void CaptureEvent_EventProcessorThrows_DropsEventAndRecordsDiscard() .RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Error); } + [Fact] + public void CaptureEvent_DelegateEventProcessorThrows_DropsEventAndRecordsDiscard() + { + var scope = new Scope(_fixture.SentryOptions); + scope.AddEventProcessor(_ => throw new InvalidOperationException()); + + var id = _fixture.GetSut().CaptureEvent(new SentryEvent(), scope); + + id.Should().Be(SentryId.Empty); + _fixture.ClientReportRecorder.Received(1) + .RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Error); + } + + [Fact] + public void CaptureEvent_SdkEventProcessorThrows_SendsEventWithoutDiscard() + { + var exception = new InvalidOperationException("processor failed"); + var sdkProcessor = Substitute.For(); + sdkProcessor.Process(Arg.Any()).Throws(exception); + var nextProcessor = Substitute.For(); + nextProcessor.Process(Arg.Any()).Returns(c => c.Arg()); + _fixture.SentryOptions.AddEventProcessor(sdkProcessor); + _fixture.SentryOptions.AddEventProcessor(nextProcessor); + _fixture.SentryOptions.AddDiagnosticLoggerSubstitute(); + var evt = new SentryEvent(); + + var id = _fixture.GetSut().CaptureEvent(evt); + + id.Should().Be(evt.EventId); + nextProcessor.Received(1).Process(evt); + _ = _fixture.BackgroundWorker.Received(1).EnqueueEnvelope(Arg.Any()); + _fixture.ClientReportRecorder.DidNotReceive() + .RecordDiscardedEvent(Arg.Any(), Arg.Any(), Arg.Any()); + _fixture.SentryOptions.ReceivedLogError(exception, "Event processor {0} threw an exception. Continuing with the remaining processors."); + } + [Fact] public void CaptureEvent_ExceptionFilter_RecordsDiscard() { @@ -449,6 +525,41 @@ public void CaptureEvent_ExceptionFilter_RecordsDiscard() .RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Error); } + [Fact] + public void CaptureEvent_ExceptionFilterThrows_DropsEventAndRecordsDiscard() + { + var exception = new InvalidOperationException("filter failed"); + var filter = Substitute.For(); + filter.Filter(Arg.Any()).Throws(exception); + _fixture.SentryOptions.AddExceptionFilter(filter); + _fixture.SentryOptions.AddDiagnosticLoggerSubstitute(); + + var id = _fixture.GetSut().CaptureException(new Exception()); + + id.Should().Be(SentryId.Empty); + _ = _fixture.BackgroundWorker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + _fixture.ClientReportRecorder.Received(1) + .RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Error); + _fixture.SentryOptions.ReceivedLogError(exception, "Exception filter {0} threw an exception. The event will be dropped."); + } + + [Fact] + public void CaptureEvent_ExceptionFilterThrowsForInnerException_DropsEventAndRecordsDiscard() + { + var filter = Substitute.For(); + filter.Filter(Arg.Is(e => e is InvalidOperationException)).Throws(new ArgumentException("filter failed")); + _fixture.SentryOptions.AddExceptionFilter(filter); + + var id = _fixture.GetSut().CaptureException(new AggregateException(new InvalidOperationException())); + + id.Should().Be(SentryId.Empty); + _ = _fixture.BackgroundWorker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + _fixture.ClientReportRecorder.Received(1) + .RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Error); + _fixture.ClientReportRecorder.DidNotReceive() + .RecordDiscardedEvent(DiscardReason.EventProcessor, Arg.Any(), Arg.Any()); + } + [Fact] public void CaptureEvent_BeforeSend_GetsHint() { @@ -1803,6 +1914,56 @@ public void CaptureTransaction_TransactionProcessorThrows_DropsTransactionAndRec _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(reason, DataCategory.Span, expectedDroppedSpanCount); } + [Fact] + public void CaptureTransaction_DelegateTransactionProcessorThrows_DropsTransactionAndRecordsDiscard() + { + // Arrange + var scope = new Scope(_fixture.SentryOptions); + scope.AddTransactionProcessor(_ => throw new InvalidOperationException()); + + var tracer = new TransactionTracer(Substitute.For(), "test name", "test operation") + { + EndTimestamp = DateTimeOffset.Now // finished + }; + var transaction = new SentryTransaction(tracer) { IsSampled = true }; + + // Act + _fixture.GetSut().CaptureTransaction(transaction, scope, null); + + // Assert + _ = _fixture.BackgroundWorker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + _fixture.ClientReportRecorder.Received(1) + .RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Transaction); + _fixture.ClientReportRecorder.Received(1) + .RecordDiscardedEvent(DiscardReason.CallbackError, DataCategory.Span, transaction.Spans.Count + 1); + } + + [Fact] + public void CaptureTransaction_SdkTransactionProcessorThrows_SendsTransactionWithoutDiscard() + { + // Arrange + var exception = new InvalidOperationException("processor failed"); + var sdkProcessor = Substitute.For(); + sdkProcessor.Process(Arg.Any()).Throws(exception); + _fixture.SentryOptions.AddTransactionProcessor(sdkProcessor); + _fixture.SentryOptions.AddDiagnosticLoggerSubstitute(); + + var tracer = new TransactionTracer(Substitute.For(), "test name", "test operation") + { + EndTimestamp = DateTimeOffset.Now // finished + }; + var transaction = new SentryTransaction(tracer) { IsSampled = true }; + + // Act + _fixture.GetSut().CaptureTransaction(transaction); + + // Assert + _ = _fixture.BackgroundWorker.Received(1).EnqueueEnvelope(Arg.Any()); + _fixture.ClientReportRecorder.DidNotReceive() + .RecordDiscardedEvent(Arg.Any(), Arg.Any(), Arg.Any()); + _fixture.SentryOptions.ReceivedLogError(exception, "Transaction processor {0} threw an exception. Continuing with the remaining processors."); + } + [Fact] public void CaptureTransaction_BeforeSendTransactionThrows_DropsTransactionAndRecordsDiscard() {