Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix unhandled exception not captured when hub disabled #2103

Merged
merged 8 commits into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixes

- Logging info instead of warning when skipping debug images ([#2101](https://github.com/getsentry/sentry-dotnet/pull/2101))
- Fix unhandled exception not captured when hub disabled ([#2103](https://github.com/getsentry/sentry-dotnet/pull/2103))

## 3.25.0

Expand Down
14 changes: 10 additions & 4 deletions samples/Sentry.Samples.Console.Basic/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
using Sentry;

using (SentrySdk.Init("https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537"))
using var _ = SentrySdk.Init(o =>
{
// The following exception is captured and sent to Sentry
throw null;
}
// The DSN is required.
o.Dsn = "https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537";

// When debug is enabled, the Sentry client will emit detailed debugging information to the console.
o.Debug = true;
});

// The following unhandled exception will be captured and sent to Sentry.
throw new Exception("test");
40 changes: 23 additions & 17 deletions src/Sentry/HubExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,17 +155,21 @@ public LockedScope(IHub hub)
public void Dispose() => _scope.Dispose();
}

internal static SentryId CaptureExceptionInternal(this IHub hub, Exception ex) =>
hub.CaptureEventInternal(new SentryEvent(ex));

internal static SentryId CaptureEventInternal(this IHub hub, SentryEvent evt) =>
hub is IHubEx hubEx ? hubEx.CaptureEventInternal(evt) : hub.CaptureEvent(evt);

/// <summary>
/// Captures the exception with a configurable scope callback.
/// </summary>
/// <param name="hub">The Sentry hub.</param>
/// <param name="ex">The exception.</param>
/// <param name="configureScope">The callback to configure the scope.</param>
/// <returns>The Id of the event</returns>
public static SentryId CaptureException(this IHub hub, Exception ex, Action<Scope> configureScope)
=> !hub.IsEnabled
? new SentryId()
: hub.CaptureEvent(new SentryEvent(ex), configureScope);
public static SentryId CaptureException(this IHub hub, Exception ex, Action<Scope> configureScope) =>
hub.CaptureEvent(new SentryEvent(ex), configureScope);

/// <summary>
/// Captures a message with a configurable scope callback.
Expand All @@ -175,20 +179,22 @@ public static SentryId CaptureException(this IHub hub, Exception ex, Action<Scop
/// <param name="configureScope">The callback to configure the scope.</param>
/// <param name="level">The message level.</param>
/// <returns>The Id of the event</returns>
public static SentryId CaptureMessage(
this IHub hub,
string message,
Action<Scope> configureScope,
public static SentryId CaptureMessage(this IHub hub, string message, Action<Scope> configureScope,
SentryLevel level = SentryLevel.Info)
=> !hub.IsEnabled || string.IsNullOrWhiteSpace(message)
? new SentryId()
: hub.CaptureEvent(
new SentryEvent
{
Message = message,
Level = level
},
configureScope);
{
if (string.IsNullOrWhiteSpace(message))
{
return new SentryId();
}

var sentryEvent = new SentryEvent
mattjohnsonpint marked this conversation as resolved.
Show resolved Hide resolved
{
Message = message,
Level = level
};

return hub.CaptureEvent(sentryEvent, configureScope);
}

internal static ITransaction StartTransaction(
this IHub hub,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Sentry.Extensibility;
using Sentry.Internal;
using Sentry.Protocol;

Expand Down Expand Up @@ -26,16 +27,20 @@ public void Register(IHub hub, SentryOptions options)
[SecurityCritical]
internal void Handle(object sender, UnhandledExceptionEventArgs e)
{
_options?.LogDebug("AppDomain Unhandled Exception");

if (e.ExceptionObject is Exception ex)
{
ex.Data[Mechanism.HandledKey] = false;
ex.Data[Mechanism.MechanismKey] = "AppDomain.UnhandledException";
_ = _hub?.CaptureException(ex);

// Call the internal implementation, so that we still capture even if the hub has been disabled.
_hub?.CaptureExceptionInternal(ex);
}

if (e.IsTerminating)
{
_hub?.Flush(_options!.ShutdownTimeout);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ internal void Handle(object? sender, UnobservedTaskExceptionEventArgs e)
#endif
ex.Data[Mechanism.HandledKey] = false;
ex.Data[Mechanism.MechanismKey] = "UnobservedTaskException";
_hub.CaptureException(ex);

// Call the internal implementation, so that we still capture even if the hub has been disabled.
_hub.CaptureExceptionInternal(ex);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ private void WinUIUnhandledExceptionHandler(object sender, object e)
// Set some useful data and capture the exception
exception.Data[Protocol.Mechanism.HandledKey] = handled;
exception.Data[Protocol.Mechanism.MechanismKey] = "Microsoft.UI.Xaml.UnhandledException";
_hub.CaptureException(exception);

// Call the internal implementation, so that we still capture even if the hub has been disabled.
_hub.CaptureExceptionInternal(exception);

if (!handled)
{
Expand Down
27 changes: 25 additions & 2 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Sentry.Internal;

internal class Hub : IHub, IDisposable
internal class Hub : IHubEx, IDisposable
{
private readonly object _sessionPauseLock = new();

Expand Down Expand Up @@ -281,6 +281,11 @@ public void EndSession(SessionEndStatus status = SessionEndStatus.Exited) =>

public SentryId CaptureEvent(SentryEvent evt, Action<Scope> configureScope)
{
if (!IsEnabled)
{
return SentryId.Empty;
}

try
{
var clonedScope = ScopeManager.GetCurrent().Key.Clone();
Expand All @@ -295,7 +300,10 @@ public SentryId CaptureEvent(SentryEvent evt, Action<Scope> configureScope)
}
}

public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null)
public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null) =>
IsEnabled ? ((IHubEx)this).CaptureEventInternal(evt, scope) : SentryId.Empty;

SentryId IHubEx.CaptureEventInternal(SentryEvent evt, Scope? scope)
{
try
{
Expand Down Expand Up @@ -347,6 +355,11 @@ public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null)

public void CaptureUserFeedback(UserFeedback userFeedback)
{
if (!IsEnabled)
{
return;
}

try
{
_ownedClient.CaptureUserFeedback(userFeedback);
Expand All @@ -359,6 +372,11 @@ public void CaptureUserFeedback(UserFeedback userFeedback)

public void CaptureTransaction(Transaction transaction)
{
if (!IsEnabled)
{
return;
}

try
{
// Apply scope data
Expand Down Expand Up @@ -395,6 +413,11 @@ public void CaptureTransaction(Transaction transaction)

public void CaptureSession(SessionUpdate sessionUpdate)
{
if (!IsEnabled)
{
return;
}

try
{
_ownedClient.CaptureSession(sessionUpdate);
Expand Down
6 changes: 6 additions & 0 deletions src/Sentry/Internal/IHubEx.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Sentry.Internal;

internal interface IHubEx : IHub
mattjohnsonpint marked this conversation as resolved.
Show resolved Hide resolved
{
SentryId CaptureEventInternal(SentryEvent evt, Scope? scope = null);
}
46 changes: 26 additions & 20 deletions src/Sentry/SentryClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@ public static class SentryClientExtensions
/// <param name="client">The Sentry client.</param>
/// <param name="ex">The exception.</param>
/// <returns>The Id of the event</returns>
public static SentryId CaptureException(this ISentryClient client, Exception ex)
{
return !client.IsEnabled
? new SentryId()
: client.CaptureEvent(new SentryEvent(ex));
}
public static SentryId CaptureException(this ISentryClient client, Exception ex) =>
client.IsEnabled ? client.CaptureEventInternal(new SentryEvent(ex)) : SentryId.Empty;

/// <summary>
/// Captures a message.
Expand All @@ -29,19 +25,19 @@ public static SentryId CaptureException(this ISentryClient client, Exception ex)
/// <param name="message">The message to send.</param>
/// <param name="level">The message level.</param>
/// <returns>The Id of the event</returns>
public static SentryId CaptureMessage(
this ISentryClient client,
string message,
public static SentryId CaptureMessage(this ISentryClient client, string message,
SentryLevel level = SentryLevel.Info)
{
return !client.IsEnabled || string.IsNullOrWhiteSpace(message)
? new SentryId()
: client.CaptureEvent(
new SentryEvent
{
Message = message,
Level = level
});
if (client.IsEnabled && !string.IsNullOrWhiteSpace(message))
{
return client.CaptureEventInternal(new SentryEvent
{
Message = message,
Level = level
});
}

return SentryId.Empty;
}

/// <summary>
Expand All @@ -52,14 +48,24 @@ public static SentryId CaptureMessage(
/// <param name="email">The user email.</param>
/// <param name="comments">The user comments.</param>
/// <param name="name">The optional username.</param>
public static void CaptureUserFeedback(this ISentryClient client, SentryId eventId, string email, string comments, string? name = null)
public static void CaptureUserFeedback(this ISentryClient client, SentryId eventId, string email, string comments,
string? name = null)
{
if (client.IsEnabled)
if (!client.IsEnabled)
{
client.CaptureUserFeedback(new UserFeedback(eventId, name, email, comments));
return;
}

client.CaptureUserFeedback(new UserFeedback(eventId, name, email, comments));
}

private static SentryId CaptureEventInternal(this ISentryClient client, SentryEvent sentryEvent) =>
client switch
{
IHubEx hub => hub.CaptureEventInternal(sentryEvent),
_ => client.CaptureEvent(sentryEvent)
};

/// <summary>
/// Flushes the queue of captured events until the timeout set in <see cref="SentryOptions.FlushTimeout"/>
/// is reached.
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry/SentryId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Sentry;
/// <summary>
/// An empty sentry id.
/// </summary>
public static readonly SentryId Empty = Guid.Empty;
public static readonly SentryId Empty = default;

/// <summary>
/// Creates a new instance of a Sentry Id.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@ public class AppDomainUnhandledExceptionIntegrationTests
{
private class Fixture
{
public IHub Hub { get; set; } = Substitute.For<IHub, IDisposable>();
public IAppDomain AppDomain { get; set; } = Substitute.For<IAppDomain>();
public IHubEx Hub { get; } = Substitute.For<IHubEx, IDisposable>();
public IAppDomain AppDomain { get; } = Substitute.For<IAppDomain>();

public Fixture() => Hub.IsEnabled.Returns(true);

public AppDomainUnhandledExceptionIntegration GetSut()
=> new(AppDomain);
public AppDomainUnhandledExceptionIntegration GetSut() => new(AppDomain);
}

private readonly Fixture _fixture = new();
public SentryOptions SentryOptions { get; set; } = new();
private SentryOptions SentryOptions { get; } = new();

[Fact]
public void Handle_WithException_CaptureEvent()
Expand All @@ -24,7 +21,7 @@ public void Handle_WithException_CaptureEvent()

sut.Handle(this, new UnhandledExceptionEventArgs(new Exception(), true));

_ = _fixture.Hub.Received(1).CaptureEvent(Arg.Any<SentryEvent>());
_fixture.Hub.Received(1).CaptureEventInternal(Arg.Any<SentryEvent>());
}

[Fact]
Expand All @@ -35,7 +32,7 @@ public void Handle_NoException_NoCaptureEvent()

sut.Handle(this, new UnhandledExceptionEventArgs(new object(), true));

_ = _fixture.Hub.DidNotReceive().CaptureEvent(Arg.Any<SentryEvent>());
_fixture.Hub.DidNotReceive().CaptureEventInternal(Arg.Any<SentryEvent>());
}

[Fact]
Expand All @@ -57,15 +54,15 @@ public void Handle_TerminatingTrue_IsHandledFalse()

var exception = new Exception();
sut.Handle(this, new UnhandledExceptionEventArgs(exception, true));
Assert.False((bool)exception.Data[Mechanism.HandledKey]);
Assert.Equal(false, exception.Data[Mechanism.HandledKey]);
Assert.True(exception.Data.Contains(Mechanism.MechanismKey));

var stackTraceFactory = Substitute.For<ISentryStackTraceFactory>();
var exceptionProcessor = new MainExceptionProcessor(SentryOptions, () => stackTraceFactory);
var @event = new SentryEvent(exception);

exceptionProcessor.Process(exception, @event);
Assert.NotNull(@event.SentryExceptions.ToList().Single(p => p.Mechanism.Handled == false));
Assert.NotNull(@event.SentryExceptions?.ToList().Single(p => p.Mechanism?.Handled == false));
}

[Fact]
Expand All @@ -74,7 +71,7 @@ public void Handle_TerminatingTrue_NoException_FlushesHub()
var sut = _fixture.GetSut();
sut.Register(_fixture.Hub, SentryOptions);

sut.Handle(this, new UnhandledExceptionEventArgs(null, true));
sut.Handle(this, new UnhandledExceptionEventArgs(null!, true));

_fixture.Hub.Received(1).FlushAsync(Arg.Any<TimeSpan>());
}
Expand Down
Loading