diff --git a/src/Sentry.Extensions.Hosting/Sentry.Extensions.Hosting.csproj b/src/Sentry.Extensions.Hosting/Sentry.Extensions.Hosting.csproj index e03cb914fa..7ab457ec9e 100644 --- a/src/Sentry.Extensions.Hosting/Sentry.Extensions.Hosting.csproj +++ b/src/Sentry.Extensions.Hosting/Sentry.Extensions.Hosting.csproj @@ -20,6 +20,12 @@ + + + + + + diff --git a/src/Sentry.Extensions.Hosting/SentryHostBuilderExtensions.cs b/src/Sentry.Extensions.Hosting/SentryHostBuilderExtensions.cs index bef06229fc..cc91cd755e 100644 --- a/src/Sentry.Extensions.Hosting/SentryHostBuilderExtensions.cs +++ b/src/Sentry.Extensions.Hosting/SentryHostBuilderExtensions.cs @@ -183,6 +183,7 @@ private static void AddSentryGenericHostServices( : new SentryStructuredLoggerProvider(c.GetRequiredService())); services.TryAddSingleton(); services.AddSentry(); + services.TryAddEnumerable(ServiceDescriptor.Singleton, SentrySdkInitializer>()); services.TryAddEnumerable(ServiceDescriptor.Singleton(c => new SentryHostedService(c, app == App.Web && !HasHostIntegration(services)))); diff --git a/src/Sentry.Extensions.Hosting/SentrySdkInitializer.cs b/src/Sentry.Extensions.Hosting/SentrySdkInitializer.cs new file mode 100644 index 0000000000..6c3d330af9 --- /dev/null +++ b/src/Sentry.Extensions.Hosting/SentrySdkInitializer.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace Sentry.Extensions.Hosting; + +/// +/// Starts the SDK while the host is built. The host's constructor reads IOptions<HostOptions>, which runs +/// this, so the SDK starts even when the app removes or replaces Sentry's logger providers. +/// +internal sealed class SentrySdkInitializer(IServiceProvider services) : IConfigureOptions +{ + public void Configure(HostOptions options) => _ = services.GetRequiredService>(); +} diff --git a/test/Sentry.Extensions.Hosting.Tests/SentryHostBuilderExtensionsTests.cs b/test/Sentry.Extensions.Hosting.Tests/SentryHostBuilderExtensionsTests.cs index 087b9bc587..715c7d0e10 100644 --- a/test/Sentry.Extensions.Hosting.Tests/SentryHostBuilderExtensionsTests.cs +++ b/test/Sentry.Extensions.Hosting.Tests/SentryHostBuilderExtensionsTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Sentry.Extensions.Logging; @@ -297,6 +298,30 @@ public void UseSentry_CapturedEvent_ReportsHostingSdk(BuilderKind kind) sdk.Packages.Should().Contain(package => package.Name == "nuget:Sentry.Extensions.Hosting"); } + [Theory, MemberData(nameof(TestHost.Builders), MemberType = typeof(TestHost))] + public void UseSentry_LoggerProvidersClearedAfterwards_CapturesBeforeHostStarts(BuilderKind kind) + { + var testHost = new TestHost().UseSentry(Capture).ClearLoggerProviders(); + using var host = testHost.Build(kind); + + SentrySdk.CaptureMessage("captured before the host starts"); + + _events.Should().ContainSingle(e => e.Message!.Message == "captured before the host starts"); + } + + [Theory, MemberData(nameof(TestHost.Builders), MemberType = typeof(TestHost))] + public void UseSentry_LoggerFactoryReplaced_CapturesBeforeHostStarts(BuilderKind kind) + { + var testHost = new TestHost() + .UseSentry(Capture) + .ConfigureServices(services => services.AddSingleton(NullLoggerFactory.Instance)); + using var host = testHost.Build(kind); + + SentrySdk.CaptureMessage("captured before the host starts"); + + _events.Should().ContainSingle(e => e.Message!.Message == "captured before the host starts"); + } + [Theory, MemberData(nameof(TestHost.Builders), MemberType = typeof(TestHost))] public async Task UseSentry_EmptyDsnInJson_StartsHostWithSentryDisabled(BuilderKind kind) { diff --git a/test/Sentry.Extensions.Hosting.Tests/SentryHostedServiceTests.cs b/test/Sentry.Extensions.Hosting.Tests/SentryHostedServiceTests.cs index 52b5df0c33..c410ffbced 100644 --- a/test/Sentry.Extensions.Hosting.Tests/SentryHostedServiceTests.cs +++ b/test/Sentry.Extensions.Hosting.Tests/SentryHostedServiceTests.cs @@ -74,14 +74,18 @@ public async Task StartAsync_WorkerHost_LogsNoWarning(BuilderKind kind) standardError.ToString().Should().NotContain("Sentry:"); } + private static void RemoveSdkInitializer(IServiceCollection services) + => services.Remove(services.Single(d => d.ImplementationType == typeof(SentrySdkInitializer))); + [Theory, MemberData(nameof(TestHost.Builders), MemberType = typeof(TestHost))] - public async Task StartAsync_LoggerProvidersCleared_SdkRunsBeforeAppHostedServiceStarts(BuilderKind kind) + public async Task StartAsync_SdkNotStartedDuringBuild_SdkRunsBeforeAppHostedServiceStarts(BuilderKind kind) { var appService = new SdkStateAtStart(); var testHost = new TestHost() .ConfigureServices(services => services.AddSingleton(appService)) .UseSentry(Capture) - .ClearLoggerProviders(); + .ClearLoggerProviders() + .ConfigureServices(RemoveSdkInitializer); using var host = testHost.Build(kind); SentrySdk.IsEnabled.Should().BeFalse(); @@ -93,7 +97,7 @@ public async Task StartAsync_LoggerProvidersCleared_SdkRunsBeforeAppHostedServic #if NET8_0_OR_GREATER [Theory, MemberData(nameof(TestHost.Builders), MemberType = typeof(TestHost))] - public async Task StartAsync_ServicesStartConcurrently_SdkRunsBeforeAppHostedServiceStarts(BuilderKind kind) + public async Task StartAsync_SdkNotStartedDuringBuildAndServicesStartConcurrently_SdkRunsBeforeAppHostedServiceStarts(BuilderKind kind) { var appService = new SdkStateAtStart(); var testHost = new TestHost() @@ -103,7 +107,8 @@ public async Task StartAsync_ServicesStartConcurrently_SdkRunsBeforeAppHostedSer services.AddSingleton(appService); }) .UseSentry(Capture) - .ClearLoggerProviders(); + .ClearLoggerProviders() + .ConfigureServices(RemoveSdkInitializer); using var host = testHost.Build(kind); SentrySdk.IsEnabled.Should().BeFalse();