Skip to content
Open
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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

question: compliant with new PII rules?

See https://develop.sentry.dev/sdk/foundations/client/data-collection/.
I haven't checked the spec yet ... currently in the draft phase ... need to go though it.

I just wanted to make sure we don't paint ourselves into a corner when - later this year - we implement the new PII rules via a new dataCollection-Options.

Copy link
Copy Markdown
Collaborator Author

@jamescrosswell jamescrosswell May 6, 2026

Choose a reason for hiding this comment

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

Without the installation id, some functionality like Session Health simply doesn't work. So I can't see how we can avoid setting this.

This PR also doesn't introduce the collection of any new information - it just shifts when we're collecting it. So if we no longer want to generate installation ids and send these with events etc. that's fine - but I don't think related to this PR (that would be a separate issue and separate PR in the backlog).

Arguably this PR improves the situation since previously users couldn't opt out of the installation id being set/collected. Now, since we set it early, they can override it by nulling it out if they want.

}
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.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

These were never really part of the test anyway (which is to ensure all the default integrations get registered)

},
{
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