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
14 changes: 14 additions & 0 deletions src/Sentry/Integrations/GlobalRootScopeIntegration.cs
Comment thread
jamescrosswell marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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);
Comment thread
Flash0ver marked this conversation as resolved.
Copy link
Copy Markdown
Member

@Flash0ver Flash0ver May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use the non-Closure-allocating overload:

-hub.ConfigureScope(scope => scope.User.Id ??= options.InstallationId);
+hub.ConfigureScope(static (scope, options) => scope.User.Id ??= options.InstallationId, options);

But let's do this in a follow-up, as this would also need a change to the tests, and it's not really worth going through the entire cycle again.

Opened #5219.

}
Comment thread
jamescrosswell marked this conversation as resolved.
}
14 changes: 9 additions & 5 deletions src/Sentry/Internal/Enricher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal class Enricher

private readonly Lazy<Runtime> _runtimeLazy = new(() =>
{
var current = PlatformAbstractions.SentryRuntime.Current;
var current = SentryRuntime.Current;
return new Runtime
{
Name = current.Name,
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Comment thread
jamescrosswell marked this conversation as resolved.
if (!_options.IsGlobalModeEnabled)
{
eventLike.User.Id ??= _options.InstallationId;
}

//Apply App startup and Boot time
eventLike.Contexts.App.StartTime ??= ProcessInfo.Instance?.StartupTime;
Expand Down
7 changes: 7 additions & 0 deletions src/Sentry/SentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ internal IEnumerable<ISdkIntegration> Integrations
}
#endif

if ((_defaultIntegrations & DefaultIntegrations.GlobalRootScopeIntegration) != 0)
{
yield return new GlobalRootScopeIntegration();
}

foreach (var integration in _integrations)
{
if (DisableSentryTracing && integration is ISentryTracingIntegration)
Expand Down Expand Up @@ -1383,6 +1388,7 @@ public SentryOptions()
#if NET8_0_OR_GREATER
| DefaultIntegrations.SystemDiagnosticsMetricsIntegration
#endif
| DefaultIntegrations.GlobalRootScopeIntegration
;
Comment thread
jamescrosswell marked this conversation as resolved.

#if ANDROID
Expand Down Expand Up @@ -1850,6 +1856,7 @@ internal enum DefaultIntegrations
#if NET8_0_OR_GREATER
SystemDiagnosticsMetricsIntegration = 1 << 7,
#endif
GlobalRootScopeIntegration = 1 << 8,
}

internal void SetupLogging()
Expand Down
120 changes: 120 additions & 0 deletions test/Sentry.Tests/Integrations/GlobalRootScopeIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using Sentry.Integrations;

namespace Sentry.Tests.Integrations;

public class GlobalRootScopeIntegrationTests
{
[Fact]
public void Register_GlobalModeDisabled_DoesNotConfigureScope()
{
// Arrange
var options = new SentryOptions
{
Dsn = ValidDsn,
IsGlobalModeEnabled = false,
AutoSessionTracking = false
};
var scope = new Scope();

var hub = Substitute.For<IHub>();
hub.SubstituteConfigureScope(scope);
var integration = new GlobalRootScopeIntegration();

// Act
integration.Register(hub, options);

// Assert
hub.DidNotReceive().ConfigureScope(Arg.Any<Action<Scope>>());
Comment thread
jamescrosswell marked this conversation as resolved.
scope.User.Id.Should().BeNull();
}

[Fact]
public void Register_GlobalModeEnabled_SetsInstallationIdOnRootScope()
{
// Arrange
var options = new SentryOptions
{
Dsn = ValidDsn,
IsGlobalModeEnabled = true,
AutoSessionTracking = false
};
var scope = new Scope();

var hub = Substitute.For<IHub>();
hub.SubstituteConfigureScope(scope);
var integration = new GlobalRootScopeIntegration();

// Act
integration.Register(hub, options);

// Assert
hub.Received(1).ConfigureScope(Arg.Any<Action<Scope>>());
scope.User.Id.Should().Be(options.InstallationId);
}

[Fact]
public void Register_GlobalModeEnabled_DoesNotOverwriteExistingUserId()
{
// Arrange
var options = new SentryOptions
{
Dsn = ValidDsn,
IsGlobalModeEnabled = true,
AutoSessionTracking = false
};
var oldId = "old-id";
var scope = new Scope
{
User =
{
Id = oldId
}
};

var hub = Substitute.For<IHub>();
hub.SubstituteConfigureScope(scope);
var integration = new GlobalRootScopeIntegration();

// Act
integration.Register(hub, options);

// Assert
hub.Received(1).ConfigureScope(Arg.Any<Action<Scope>>());
scope.User.Id.Should().Be(oldId);
}

[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<IEventLike>();
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<IEventLike>();
eventLike.Sdk.Returns(new SdkVersion());
eventLike.User = new SentryUser();
eventLike.Contexts = new SentryContexts();

enricher.Apply(eventLike);

eventLike.User.Id.Should().Be(options.InstallationId);
}
}
19 changes: 16 additions & 3 deletions test/Sentry.Tests/SentryClientTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using NSubstitute.ReceivedExtensions;
using Sentry.Internal.Http;
using BackgroundWorker = Sentry.Internal.BackgroundWorker;

Expand Down Expand Up @@ -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
Skip.If(System.OperatingSystem.IsAndroid() || System.OperatingSystem.IsIOS(),
$"On mobile, User.Id is set by {nameof(GlobalRootScopeIntegration)} at startup, not the enricher.");
#endif
Comment thread
jamescrosswell marked this conversation as resolved.
// 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 scope = new Scope(_fixture.SentryOptions);
var @event = new SentryEvent();

Expand Down Expand Up @@ -1294,10 +1300,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 {nameof(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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
[
{
Message: Initializing Hub for Dsn: '{0}'.,
Args: [
https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647
]
},
{
Message: Starting BackpressureMonitor.
Comment thread
Flash0ver marked this conversation as resolved.
},
{
Message: Registering integration: '{0}'.,
Args: [
Expand Down Expand Up @@ -37,5 +28,11 @@
Args: [
SentryDiagnosticListenerIntegration
]
},
{
Message: Registering integration: '{0}'.,
Args: [
GlobalRootScopeIntegration
]
}
]
Original file line number Diff line number Diff line change
@@ -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: [
Expand Down Expand Up @@ -43,5 +34,11 @@
Args: [
WinUIUnhandledExceptionIntegration
]
},
{
Message: Registering integration: '{0}'.,
Args: [
GlobalRootScopeIntegration
]
}
]
Original file line number Diff line number Diff line change
@@ -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: [
Expand Down Expand Up @@ -37,5 +28,11 @@
Args: [
SentryDiagnosticListenerIntegration
]
},
{
Message: Registering integration: '{0}'.,
Args: [
GlobalRootScopeIntegration
]
}
]
Original file line number Diff line number Diff line change
@@ -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: [
Expand Down Expand Up @@ -43,5 +34,11 @@
Args: [
WinUIUnhandledExceptionIntegration
]
},
{
Message: Registering integration: '{0}'.,
Args: [
GlobalRootScopeIntegration
]
}
]
Original file line number Diff line number Diff line change
@@ -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: [
Expand Down Expand Up @@ -37,5 +28,11 @@
Args: [
SentryDiagnosticListenerIntegration
]
},
{
Message: Registering integration: '{0}'.,
Args: [
GlobalRootScopeIntegration
]
}
]
Loading
Loading