From 74f5618abcf59fa1e859e2f8503895d9d5994b64 Mon Sep 17 00:00:00 2001 From: Ricardo Colombo Oliveira Date: Thu, 22 Oct 2026 12:51:43 +0200 Subject: [PATCH] fix(hosting): start the SDK during Build() when logging is cleared or replaced UseSentry relied on Sentry's logger provider to start the SDK while the host was built. When an app called ClearProviders() after UseSentry, or replaced the logger factory with Services.AddSerilog(), nothing started the SDK until the host started, so a host that was built but never started captured nothing. SentrySdkInitializer is an IConfigureOptions that resolves Func. The host's constructor reads IOptions, so it runs during Build() whatever the app did to its logging. SentryHostedService keeps resolving Func as a fallback. HostOptions is in Microsoft.Extensions.Hosting, so the package now depends on it, at the lowest version each target needs: one per runtime makes apps that reference an older version fail with NU1605. Refs #5755 Co-Authored-By: Claude Opus 5.5 --- .../Sentry.Extensions.Hosting.csproj | 6 +++++ .../SentryHostBuilderExtensions.cs | 1 + .../SentrySdkInitializer.cs | 14 +++++++++++ .../SentryHostBuilderExtensionsTests.cs | 25 +++++++++++++++++++ .../SentryHostedServiceTests.cs | 13 +++++++--- 5 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 src/Sentry.Extensions.Hosting/SentrySdkInitializer.cs 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();