diff --git a/src/Sentry/Integrations/GlobalRootScopeIntegration.cs b/src/Sentry/Integrations/GlobalRootScopeIntegration.cs new file mode 100644 index 0000000000..0a5a47c77f --- /dev/null +++ b/src/Sentry/Integrations/GlobalRootScopeIntegration.cs @@ -0,0 +1,14 @@ +namespace Sentry.Integrations; + +internal class GlobalRootScopeIntegration : ISdkIntegration +{ + public void Register(IHub hub, SentryOptions options) + { + if (!options.IsGlobalModeEnabled) + { + return; + } + + hub.ConfigureScope(scope => scope.User.Id ??= options.InstallationId); + } +} diff --git a/src/Sentry/Internal/Enricher.cs b/src/Sentry/Internal/Enricher.cs index 3d3629934a..480b92c608 100644 --- a/src/Sentry/Internal/Enricher.cs +++ b/src/Sentry/Internal/Enricher.cs @@ -12,7 +12,7 @@ internal class Enricher private readonly Lazy _runtimeLazy = new(() => { - var current = PlatformAbstractions.SentryRuntime.Current; + var current = SentryRuntime.Current; return new Runtime { Name = current.Name, @@ -36,7 +36,7 @@ public void Apply(IEventLike eventLike) if (!eventLike.Contexts.ContainsKey(OperatingSystem.Type)) { // RuntimeInformation.OSDescription is throwing on Mono 5.12 - if (!PlatformAbstractions.SentryRuntime.Current.IsMono()) + if (!SentryRuntime.Current.IsMono()) { #if NETFRAMEWORK // RuntimeInformation.* throws on .NET Framework on macOS/Linux @@ -58,9 +58,8 @@ public void Apply(IEventLike eventLike) } } - // SDK // SDK Name/Version might have be already set by an outer package - // e.g: ASP.NET Core can set itself as the SDK + // e.g.: ASP.NET Core can set itself as the SDK if (eventLike.Sdk.Version is null && eventLike.Sdk.Name is null) { eventLike.Sdk.Name = Constants.SdkName; @@ -92,7 +91,12 @@ public void Apply(IEventLike eventLike) eventLike.User.IpAddress ??= DefaultIpAddress; } - eventLike.User.Id ??= _options.InstallationId; + // Set by the GlobalRootScopeIntegration in global mode so that it can be overridden by the user. + // In non-global mode (e.g. ASP.NET Core) the enricher sets it here as a fallback. + if (!_options.IsGlobalModeEnabled) + { + eventLike.User.Id ??= _options.InstallationId; + } //Apply App startup and Boot time eventLike.Contexts.App.StartTime ??= ProcessInfo.Instance?.StartupTime; diff --git a/src/Sentry/PlatformAbstractions/FrameworkInfo.cs b/src/Sentry/PlatformAbstractions/FrameworkInfo.cs index 6eb543c443..5b301bff90 100644 --- a/src/Sentry/PlatformAbstractions/FrameworkInfo.cs +++ b/src/Sentry/PlatformAbstractions/FrameworkInfo.cs @@ -36,6 +36,7 @@ public static partial class FrameworkInfo {528372, "4.8"}, {528449, "4.8"}, {533320, "4.8.1"}, - {533325, "4.8.1"} + {533325, "4.8.1"}, + {533509, "4.8.1"} }; } diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 25a6eb3183..070728089a 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -220,6 +220,11 @@ internal IEnumerable Integrations } #endif + if ((_defaultIntegrations & DefaultIntegrations.GlobalRootScopeIntegration) != 0) + { + yield return new GlobalRootScopeIntegration(); + } + foreach (var integration in _integrations) { yield return integration; @@ -1362,6 +1367,7 @@ public SentryOptions() #if NET8_0_OR_GREATER | DefaultIntegrations.SystemDiagnosticsMetricsIntegration #endif + | DefaultIntegrations.GlobalRootScopeIntegration ; #if ANDROID @@ -1829,6 +1835,7 @@ internal enum DefaultIntegrations #if NET8_0_OR_GREATER SystemDiagnosticsMetricsIntegration = 1 << 7, #endif + GlobalRootScopeIntegration = 1 << 8, } internal void SetupLogging() diff --git a/test/Sentry.Tests/Integrations/GlobalRootScopeIntegrationTests.cs b/test/Sentry.Tests/Integrations/GlobalRootScopeIntegrationTests.cs new file mode 100644 index 0000000000..45efb94980 --- /dev/null +++ b/test/Sentry.Tests/Integrations/GlobalRootScopeIntegrationTests.cs @@ -0,0 +1,145 @@ +using Sentry.Integrations; + +namespace Sentry.Tests.Integrations; + +public class GlobalRootScopeIntegrationTests +{ + [Fact] + public void Register_GlobalModeEnabled_SetsInstallationIdOnRootScope() + { + // Arrange + var options = new SentryOptions + { + Dsn = ValidDsn, + IsGlobalModeEnabled = true, + AutoSessionTracking = false + }; + + var hub = Substitute.For(); + var integration = new GlobalRootScopeIntegration(); + + // Act + integration.Register(hub, options); + + // Assert + hub.Received(1).ConfigureScope(Arg.Any>()); + } + + [Fact] + public void Register_GlobalModeDisabled_DoesNotConfigureScope() + { + // Arrange + var options = new SentryOptions + { + Dsn = ValidDsn, + IsGlobalModeEnabled = false, + AutoSessionTracking = false + }; + + var hub = Substitute.For(); + var integration = new GlobalRootScopeIntegration(); + + // Act + integration.Register(hub, options); + + // Assert + hub.DidNotReceive().ConfigureScope(Arg.Any>()); + } + + [Fact] + public void Register_GlobalModeEnabled_SetsUserIdFromInstallationId() + { + // Arrange + var options = new SentryOptions + { + Dsn = ValidDsn, + IsGlobalModeEnabled = true, + AutoSessionTracking = false + }; + + // Capture the action passed to ConfigureScope + Action capturedAction = null; + var hub = Substitute.For(); + hub.When(h => h.ConfigureScope(Arg.Any>())) + .Do(call => capturedAction = call.Arg>()); + + var integration = new GlobalRootScopeIntegration(); + integration.Register(hub, options); + + // Apply the captured action to a real scope + var scope = new Scope(options); + + capturedAction.Should().NotBeNull(); + capturedAction(scope); + + // The scope's User.Id should be set to the InstallationId + scope.User.Id.Should().Be(options.InstallationId); + } + + [Fact] + public void Register_GlobalModeEnabled_DoesNotOverwriteExistingUserId() + { + // Arrange + var options = new SentryOptions + { + Dsn = ValidDsn, + IsGlobalModeEnabled = true, + AutoSessionTracking = false + }; + + // Capture the action passed to ConfigureScope + Action capturedAction = null; + var hub = Substitute.For(); + hub.When(h => h.ConfigureScope(Arg.Any>())) + .Do(call => capturedAction = call.Arg>()); + + var integration = new GlobalRootScopeIntegration(); + integration.Register(hub, options); + + // Apply the captured action to a scope that already has a User.Id + var scope = new Scope(options); + const string existingUserId = "my-custom-user-id"; + scope.User.Id = existingUserId; + + capturedAction.Should().NotBeNull(); + capturedAction(scope); + + // The existing User.Id should not be overwritten + scope.User.Id.Should().Be(existingUserId); + } + + [Fact] + public void Enricher_GlobalModeEnabled_DoesNotSetInstallationId() + { + // Verify the enricher no longer sets User.Id when global mode is enabled, + // ensuring users can clear the User.Id set by GlobalRootScopeIntegration. + var options = new SentryOptions { IsGlobalModeEnabled = true }; + var enricher = new Sentry.Internal.Enricher(options); + + var eventLike = Substitute.For(); + eventLike.Sdk.Returns(new SdkVersion()); + eventLike.User = new SentryUser(); + eventLike.Contexts = new SentryContexts(); + + enricher.Apply(eventLike); + + eventLike.User.Id.Should().BeNull(); + } + + [Fact] + public void Enricher_GlobalModeDisabled_SetsInstallationIdAsFallback() + { + // Verify the enricher still sets User.Id when global mode is disabled (e.g. ASP.NET Core). + var options = new SentryOptions { IsGlobalModeEnabled = false }; + var enricher = new Sentry.Internal.Enricher(options); + + var eventLike = Substitute.For(); + eventLike.Sdk.Returns(new SdkVersion()); + eventLike.User = new SentryUser(); + eventLike.Contexts = new SentryContexts(); + + enricher.Apply(eventLike); + + eventLike.User.Id.Should().Be(options.InstallationId); + } +} diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index 1fedab2ff8..865f2f7c5e 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -1,4 +1,3 @@ -using NSubstitute.ReceivedExtensions; using Sentry.Internal.Http; using BackgroundWorker = Sentry.Internal.BackgroundWorker; @@ -302,10 +301,17 @@ public void CaptureEvent_EventAndScope_CopyScopeIntoEvent() Assert.Equal(scope.Breadcrumbs, @event.Breadcrumbs); } - [Fact] + [SkippableFact] public void CaptureEvent_UserIsNull_SetsFallbackUserId() { +#if NET5_0_OR_GREATER + // In global mode the userid gets set at app startup via the GlobalRootScopeIntegration, rather than by an + // enricher during capture... so this functionality in SentryClient only works when IsGlobalModeEnabled is false + Skip.If(System.OperatingSystem.IsAndroid() || System.OperatingSystem.IsIOS(), + "On mobile, User.Id is set by GlobalRootScopeIntegration at startup, not the enricher."); +#endif // Arrange + _fixture.SentryOptions.IsGlobalModeEnabled = false; var scope = new Scope(_fixture.SentryOptions); var @event = new SentryEvent(); @@ -1293,10 +1299,17 @@ public void CaptureTransaction_ScopeContainsAttachments_GetAppliedToHint() hint.Attachments.Should().Contain(attachments); } - [Fact] + [SkippableFact] public void CaptureTransaction_UserIsNull_SetsFallbackUserId() { +#if NET5_0_OR_GREATER + Skip.If(System.OperatingSystem.IsAndroid() || System.OperatingSystem.IsIOS(), + "On mobile, User.Id is set by GlobalRootScopeIntegration at startup, not the enricher."); +#endif // Arrange + // In global mode the userid gets set at app startup via the GlobalRootScopeIntegration, rather than by an + // enricher during capture... so this functionality in SentryClient only works when IsGlobalModeEnabled is false + _fixture.SentryOptions.IsGlobalModeEnabled = false; var transaction = new SentryTransaction("name", "operation") { IsSampled = true, diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt index f477fddad0..ad1db68cad 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt @@ -1,13 +1,4 @@ [ - { - Message: Initializing Hub for Dsn: '{0}'., - Args: [ - https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 - ] - }, - { - Message: Starting BackpressureMonitor. - }, { Message: Registering integration: '{0}'., Args: [ @@ -37,5 +28,11 @@ Args: [ SentryDiagnosticListenerIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + GlobalRootScopeIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt index c1fa278003..a9a8912ac6 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt @@ -1,13 +1,4 @@ [ - { - Message: Initializing Hub for Dsn: '{0}'., - Args: [ - https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 - ] - }, - { - Message: Starting BackpressureMonitor. - }, { Message: Registering integration: '{0}'., Args: [ @@ -43,5 +34,11 @@ Args: [ WinUIUnhandledExceptionIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + GlobalRootScopeIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt index f477fddad0..ad1db68cad 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt @@ -1,13 +1,4 @@ [ - { - Message: Initializing Hub for Dsn: '{0}'., - Args: [ - https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 - ] - }, - { - Message: Starting BackpressureMonitor. - }, { Message: Registering integration: '{0}'., Args: [ @@ -37,5 +28,11 @@ Args: [ SentryDiagnosticListenerIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + GlobalRootScopeIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt index c1fa278003..a9a8912ac6 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt @@ -1,13 +1,4 @@ [ - { - Message: Initializing Hub for Dsn: '{0}'., - Args: [ - https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 - ] - }, - { - Message: Starting BackpressureMonitor. - }, { Message: Registering integration: '{0}'., Args: [ @@ -43,5 +34,11 @@ Args: [ WinUIUnhandledExceptionIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + GlobalRootScopeIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.DotNet.verified.txt index f477fddad0..ad1db68cad 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.DotNet.verified.txt @@ -1,13 +1,4 @@ [ - { - Message: Initializing Hub for Dsn: '{0}'., - Args: [ - https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 - ] - }, - { - Message: Starting BackpressureMonitor. - }, { Message: Registering integration: '{0}'., Args: [ @@ -37,5 +28,11 @@ Args: [ SentryDiagnosticListenerIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + GlobalRootScopeIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.Windows.DotNet.verified.txt index c1fa278003..a9a8912ac6 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.Windows.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.Windows.DotNet.verified.txt @@ -1,13 +1,4 @@ [ - { - Message: Initializing Hub for Dsn: '{0}'., - Args: [ - https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 - ] - }, - { - Message: Starting BackpressureMonitor. - }, { Message: Registering integration: '{0}'., Args: [ @@ -43,5 +34,11 @@ Args: [ WinUIUnhandledExceptionIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + GlobalRootScopeIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt index e9aa248faa..9914267969 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt @@ -1,13 +1,4 @@ [ - { - Message: Initializing Hub for Dsn: '{0}'., - Args: [ - https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 - ] - }, - { - Message: Starting BackpressureMonitor. - }, { Message: Registering integration: '{0}'., Args: [ @@ -37,5 +28,11 @@ Args: [ NetFxInstallationsIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + GlobalRootScopeIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.verify.cs b/test/Sentry.Tests/SentryOptionsTests.verify.cs index 3e66a887ba..0ce5d0ce0c 100644 --- a/test/Sentry.Tests/SentryOptionsTests.verify.cs +++ b/test/Sentry.Tests/SentryOptionsTests.verify.cs @@ -16,7 +16,7 @@ public Task Integrations_default_ones_are_properly_registered() }; Hub _ = new(options, Substitute.For()); - var settingsTask = Verify(logger.Entries) + var settingsTask = Verify(logger.Entries.Where(e => e.Message.Contains("Registering integration"))) .UniqueForTargetFrameworkAndVersion() .UniqueForRuntime() .AutoVerify(includeBuildServer: false);