Skip to content
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 @@ -15,6 +15,7 @@
- Custom ISentryEventProcessors are now run for native iOS events ([#4318](https://github.com/getsentry/sentry-dotnet/pull/4318))
- Crontab validation when capturing checkins ([#4314](https://github.com/getsentry/sentry-dotnet/pull/4314))
- Native AOT: link to static `lzma` on Linux/MUSL ([#4326](https://github.com/getsentry/sentry-dotnet/pull/4326))
- AppDomain.CurrentDomain.ProcessExit hook is now removed on shutdown ([#4323](https://github.com/getsentry/sentry-dotnet/pull/4323))

### Dependencies

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Sentry.Integrations;

internal class AppDomainProcessExitIntegration : ISdkIntegration
internal class AppDomainProcessExitIntegration : ISdkIntegration, IDisposable
{
private readonly IAppDomain _appDomain;
private IHub? _hub;
Expand All @@ -24,4 +24,9 @@ internal void HandleProcessExit(object? sender, EventArgs e)
_options?.LogInfo("AppDomain process exited: Disposing SDK.");
(_hub as IDisposable)?.Dispose();
}

public void Dispose()
{
_appDomain.ProcessExit -= HandleProcessExit;
}
}
18 changes: 18 additions & 0 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Sentry.Extensibility;
using Sentry.Infrastructure;
using Sentry.Integrations;
using Sentry.Protocol.Envelopes;
using Sentry.Protocol.Metrics;

Expand All @@ -14,6 +15,7 @@ internal class Hub : IHub, IDisposable
private readonly SentryOptions _options;
private readonly RandomValuesFactory _randomValuesFactory;
private readonly IReplaySession _replaySession;
private readonly List<IDisposable> _integrationsToCleanup = new();

#if MEMORY_DUMP_SUPPORTED
private readonly MemoryMonitor? _memoryMonitor;
Expand Down Expand Up @@ -84,6 +86,10 @@ internal Hub(
{
options.LogDebug("Registering integration: '{0}'.", integration.GetType().Name);
integration.Register(this, options);
if (integration is IDisposable disposableIntegration)
{
_integrationsToCleanup.Add(disposableIntegration);
}
}
}

Expand Down Expand Up @@ -772,6 +778,18 @@ public void Dispose()
return;
}

foreach (var integration in _integrationsToCleanup)
{
try
{
integration.Dispose();
}
catch (Exception e)
{
_options.LogError("Failed to dispose integration {0}: {1}", integration.GetType().Name, e);
}
}

#if MEMORY_DUMP_SUPPORTED
_memoryMonitor?.Dispose();
#endif
Expand Down
94 changes: 94 additions & 0 deletions test/Sentry.Tests/HubTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1988,6 +1988,100 @@ public void CaptureUserFeedback_InvalidEmail_FeedbackDropped(string email)
_fixture.Client.Received(1).CaptureUserFeedback(Arg.Is<UserFeedback>(f => f.Email.IsNull()));
#pragma warning restore CS0618 // Type or member is obsolete
}

private class TestDisposableIntegration : ISdkIntegration, IDisposable
{
public int Registered { get; private set; }
public int Disposed { get; private set; }

public void Register(IHub hub, SentryOptions options)
{
Registered++;
}

protected virtual void Cleanup()
{
Disposed++;
}

public void Dispose()
{
Cleanup();
}
}

private class TestFlakyDisposableIntegration : TestDisposableIntegration
{
protected override void Cleanup()
{
throw new InvalidOperationException("Cleanup failed");
}
}

[Fact]
public void Dispose_IntegrationsWithCleanup_CleanupCalled()
{
// Arrange
var integration1 = new TestDisposableIntegration();
var integration2 = Substitute.For<ISdkIntegration>();
var integration3 = new TestDisposableIntegration();
_fixture.Options.AddIntegration(integration1);
_fixture.Options.AddIntegration(integration2);
_fixture.Options.AddIntegration(integration3);
var hub = _fixture.GetSut();

// Act
hub.Dispose();

// Assert
integration1.Disposed.Should().Be(1);
integration3.Disposed.Should().Be(1);
}

[Fact]
public void Dispose_CleanupThrowsException_ExceptionHandledAndLogged()
{
// Arrange
var integration1 = new TestDisposableIntegration();
var integration2 = new TestFlakyDisposableIntegration();
var integration3 = new TestDisposableIntegration();
_fixture.Options.AddIntegration(integration1);
_fixture.Options.AddIntegration(integration2);
_fixture.Options.AddIntegration(integration3);
_fixture.Options.Debug = true;
_fixture.Options.DiagnosticLogger = Substitute.For<IDiagnosticLogger>();
_fixture.Options.DiagnosticLogger!.IsEnabled(Arg.Any<SentryLevel>()).Returns(true);
var hub = _fixture.GetSut();

// Act
hub.Dispose();

// Assert
integration1.Disposed.Should().Be(1);
integration2.Disposed.Should().Be(0);
integration3.Disposed.Should().Be(1);
_fixture.Options.DiagnosticLogger.Received(1).Log(
SentryLevel.Error,
Arg.Is<string>(s => s.Contains("Failed to dispose integration")),
Arg.Any<InvalidOperationException>(),
Arg.Any<object[]>());
}

[Fact]
public void Dispose_CalledMultipleTimes_CleanupCalledOnlyOnce()
{
// Arrange
var integration = new TestDisposableIntegration();
_fixture.Options.AddIntegration(integration);
var hub = _fixture.GetSut();

// Act
hub.Dispose();
hub.Dispose();

// Assert
integration.Disposed.Should().Be(1);
}
}

#if NET6_0_OR_GREATER
Expand Down