Skip to content

Commit d902967

Browse files
feat: Add integration to TaskScheduler.UnobservedTaskException (#481)
1 parent 1a54854 commit d902967

File tree

3 files changed

+153
-0
lines changed

3 files changed

+153
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Diagnostics;
3+
using Sentry.Internal;
4+
using System.Runtime.ExceptionServices;
5+
using System.Security;
6+
using Sentry.Protocol;
7+
using System.Threading.Tasks;
8+
9+
namespace Sentry.Integrations
10+
{
11+
internal class TaskUnobservedTaskExceptionIntegration : IInternalSdkIntegration
12+
{
13+
private readonly IAppDomain _appDomain;
14+
private IHub _hub;
15+
16+
internal TaskUnobservedTaskExceptionIntegration(IAppDomain appDomain = null)
17+
=> _appDomain = appDomain ?? AppDomainAdapter.Instance;
18+
19+
public void Register(IHub hub, SentryOptions _)
20+
{
21+
Debug.Assert(hub != null);
22+
_hub = hub;
23+
_appDomain.UnobservedTaskException += Handle;
24+
}
25+
26+
public void Unregister(IHub hub)
27+
{
28+
_appDomain.UnobservedTaskException -= Handle;
29+
_hub = null;
30+
}
31+
32+
// Internal for testability
33+
[HandleProcessCorruptedStateExceptions, SecurityCritical]
34+
internal void Handle(object sender, UnobservedTaskExceptionEventArgs e)
35+
{
36+
if (e.Exception != null)
37+
{
38+
e.Exception.Data[Mechanism.MechanismKey] = "UnobservedTaskException";
39+
_ = _hub?.CaptureException(e.Exception);
40+
(_hub as IDisposable)?.Dispose();
41+
}
42+
}
43+
}
44+
}

src/Sentry/Internal/AppDomainAdapter.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Runtime.ExceptionServices;
33
using System.Security;
4+
using System.Threading.Tasks;
45

56
namespace Sentry.Internal
67
{
@@ -9,6 +10,8 @@ internal interface IAppDomain
910
event UnhandledExceptionEventHandler UnhandledException;
1011

1112
event EventHandler ProcessExit;
13+
14+
event EventHandler<UnobservedTaskExceptionEventArgs> UnobservedTaskException;
1215
}
1316

1417
internal sealed class AppDomainAdapter : IAppDomain
@@ -19,15 +22,21 @@ private AppDomainAdapter()
1922
{
2023
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
2124
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
25+
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
2226
}
2327

2428
public event UnhandledExceptionEventHandler UnhandledException;
2529

2630
public event EventHandler ProcessExit;
2731

32+
public event EventHandler<UnobservedTaskExceptionEventArgs> UnobservedTaskException;
33+
2834
private void OnProcessExit(object sender, EventArgs e) => ProcessExit?.Invoke(sender, e);
2935

3036
[HandleProcessCorruptedStateExceptions, SecurityCritical]
3137
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) => UnhandledException?.Invoke(this, e);
38+
39+
[HandleProcessCorruptedStateExceptions, SecurityCritical]
40+
private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) => UnobservedTaskException?.Invoke(this, e);
3241
}
3342
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System;
2+
using NSubstitute;
3+
using Sentry.Integrations;
4+
using Sentry.Internal;
5+
using Xunit;
6+
using System.Linq;
7+
using System.Threading.Tasks;
8+
using Sentry.Extensibility;
9+
using Sentry.Protocol;
10+
using System.Threading;
11+
using Microsoft.Extensions.Logging;
12+
13+
namespace Sentry.Tests
14+
{
15+
public class TaskUnobservedTaskExceptionIntegrationTests
16+
{
17+
private class Fixture
18+
{
19+
public IHub Hub { get; set; } = Substitute.For<IHub, IDisposable>();
20+
public IAppDomain AppDomain { get; set; } = Substitute.For<IAppDomain>();
21+
22+
public Fixture() => Hub.IsEnabled.Returns(true);
23+
24+
public TaskUnobservedTaskExceptionIntegration GetSut()
25+
=> new TaskUnobservedTaskExceptionIntegration(AppDomain);
26+
}
27+
28+
private readonly Fixture _fixture = new Fixture();
29+
public SentryOptions SentryOptions { get; set; } = new SentryOptions();
30+
31+
[Fact]
32+
public void Handle_WithException_CaptureEvent()
33+
{
34+
var sut = _fixture.GetSut();
35+
sut.Register(_fixture.Hub, SentryOptions);
36+
37+
sut.Handle(this, new UnobservedTaskExceptionEventArgs(new AggregateException()));
38+
39+
_ = _fixture.Hub.Received(1).CaptureEvent(Arg.Any<SentryEvent>());
40+
}
41+
42+
// Only triggers in release mode.
43+
#if RELEASE
44+
[Fact] // Integration test.
45+
public void Handle_UnobservedTaskException_CaptureEvent()
46+
{
47+
_fixture.AppDomain = AppDomainAdapter.Instance;
48+
var evt = new ManualResetEvent(false);
49+
_fixture.Hub.When(x => x.CaptureEvent(Arg.Any<SentryEvent>()))
50+
.Do(_ => evt.Set());
51+
52+
var sut = _fixture.GetSut();
53+
sut.Register(_fixture.Hub, SentryOptions);
54+
try
55+
{
56+
Task.Factory.StartNew(() => { throw new Exception("Unhandled on Task"); });
57+
Thread.Sleep(2000);
58+
GC.Collect();
59+
GC.WaitForPendingFinalizers();
60+
61+
Assert.True(evt.WaitOne(TimeSpan.FromMilliseconds(1)));
62+
}
63+
finally
64+
{
65+
sut.Unregister(_fixture.Hub);
66+
}
67+
}
68+
#endif
69+
70+
[Fact]
71+
public void Handle_NoException_NoCaptureEvent()
72+
{
73+
var sut = _fixture.GetSut();
74+
sut.Register(_fixture.Hub, SentryOptions);
75+
76+
sut.Handle(this, new UnobservedTaskExceptionEventArgs(null));
77+
78+
_ = _fixture.Hub.DidNotReceive().CaptureEvent(Arg.Any<SentryEvent>());
79+
}
80+
81+
[Fact]
82+
public void Register_UnhandledException_Subscribes()
83+
{
84+
var sut = _fixture.GetSut();
85+
sut.Register(_fixture.Hub, SentryOptions);
86+
87+
_fixture.AppDomain.Received().UnobservedTaskException += sut.Handle;
88+
}
89+
90+
[Fact]
91+
public void Unregister_UnhandledException_Unsubscribes()
92+
{
93+
var sut = _fixture.GetSut();
94+
sut.Register(_fixture.Hub, SentryOptions);
95+
sut.Unregister(_fixture.Hub);
96+
97+
_fixture.AppDomain.Received(1).UnobservedTaskException -= sut.Handle;
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)