From 447aa7f77e2fef742a453e4028bad17b2db03cd6 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 2 Apr 2026 16:37:52 +0200 Subject: [PATCH 1/6] get rid of unity.analytics --- .../SentryNativeAndroid.cs | 26 +--- src/Sentry.Unity.Native/SentryNative.cs | 31 ----- src/Sentry.Unity.iOS/SentryNativeCocoa.cs | 25 +--- .../Integrations/UnityScopeIntegration.cs | 14 +- src/Sentry.Unity/WebGL/SentryWebGL.cs | 4 - .../SentryNativeAndroidTests.cs | 15 ++- .../UnityEventScopeTests.cs | 121 ++++++++++++++++++ 7 files changed, 157 insertions(+), 79 deletions(-) diff --git a/src/Sentry.Unity.Android/SentryNativeAndroid.cs b/src/Sentry.Unity.Android/SentryNativeAndroid.cs index f86fbb456..468e796f4 100644 --- a/src/Sentry.Unity.Android/SentryNativeAndroid.cs +++ b/src/Sentry.Unity.Android/SentryNativeAndroid.cs @@ -3,7 +3,6 @@ using Sentry.Unity.Integrations; using Sentry.Unity.NativeUtils; using UnityEngine; -using UnityEngine.Analytics; namespace Sentry.Unity.Android; @@ -113,25 +112,14 @@ public static void Configure(SentryUnityOptions options) Logger?.LogDebug("Fetching installation ID"); - options.DefaultUserId = SentryJava.GetInstallationId(); - if (string.IsNullOrEmpty(options.DefaultUserId)) + var installationId = SentryJava.GetInstallationId(); + if (!string.IsNullOrEmpty(installationId)) { - // In case we can't get an installation ID we create one and sync that down to the native layer - Logger?.LogDebug( - "Failed to fetch 'Installation ID' from the native SDK. Creating new 'Default User ID'."); - - // We fall back to Unity's Analytics Session Info: https://docs.unity3d.com/ScriptReference/Analytics.AnalyticsSessionInfo-userId.html - // It's a randomly generated GUID that gets created immediately after installation helping - // to identify the same instance of the game - options.DefaultUserId = AnalyticsSessionInfo.userId; - if (options.DefaultUserId is not null) - { - options.ScopeObserver.SetUser(new SentryUser { Id = options.DefaultUserId }); - } - else - { - Logger?.LogDebug("Failed to create new 'Default User ID'."); - } + options.DefaultUserId = installationId; + } + else + { + Logger?.LogDebug("Failed to fetch 'Installation ID' from the native SDK."); } Logger?.LogInfo("Successfully configured the Android SDK"); diff --git a/src/Sentry.Unity.Native/SentryNative.cs b/src/Sentry.Unity.Native/SentryNative.cs index 1ecf08c23..0ccade4cc 100644 --- a/src/Sentry.Unity.Native/SentryNative.cs +++ b/src/Sentry.Unity.Native/SentryNative.cs @@ -3,7 +3,6 @@ using Sentry.Unity.Integrations; using System.Collections.Generic; using UnityEngine; -using UnityEngine.Analytics; namespace Sentry.Unity.Native; @@ -67,12 +66,6 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf options.NativeContextWriter = new NativeContextWriter(); options.NativeDebugImageProvider = new NativeDebugImageProvider(); - options.DefaultUserId = GetInstallationId(); - if (options.DefaultUserId is not null) - { - options.ScopeObserver.SetUser(new SentryUser { Id = options.DefaultUserId }); - } - // Note: we must actually call the function now and on every other call use the value we get here. // Additionally, we cannot call this multiple times for the same directory, because the result changes // on subsequent runs. Therefore, we cache the value during the whole runtime of the application. @@ -123,28 +116,4 @@ private static void ReinstallBackend() Logger?.LogError(e, "Native dependency not found. Did you delete sentry.dll or move files around?"); } } - - private static string? GetInstallationId(IApplication? application = null) - { - application ??= ApplicationAdapter.Instance; - switch (application.Platform) - { - case RuntimePlatform.Switch: - case RuntimePlatform.PS5: - case RuntimePlatform.XboxOne: - case RuntimePlatform.GameCoreXboxSeries: - case RuntimePlatform.GameCoreXboxOne: - // TODO: Fetch the installation ID from sentry-native - // See https://github.com/getsentry/sentry-native/issues/1324 - return null; - - case RuntimePlatform.WindowsPlayer: - case RuntimePlatform.WindowsEditor: - case RuntimePlatform.LinuxPlayer: - case RuntimePlatform.LinuxEditor: - return AnalyticsSessionInfo.userId; - default: - return null; - } - } } diff --git a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs index 566332d3e..89aa52f04 100644 --- a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs +++ b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs @@ -1,7 +1,6 @@ using Sentry.Extensibility; using Sentry.Unity.Integrations; using UnityEngine; -using UnityEngine.Analytics; namespace Sentry.Unity.iOS; @@ -72,24 +71,14 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf options.NativeSupportCloseCallback += () => Close(options); if (options.UnityInfo.IL2CPP) { - options.DefaultUserId = SentryCocoaBridgeProxy.GetInstallationId(); - if (string.IsNullOrEmpty(options.DefaultUserId)) + var installationId = SentryCocoaBridgeProxy.GetInstallationId(); + if (!string.IsNullOrEmpty(installationId)) { - // In case we can't get an installation ID we create one and sync that down to the native layer - Logger?.LogDebug("Failed to fetch 'Installation ID' from the native SDK. Creating new 'Default User ID'."); - - // We fall back to Unity's Analytics Session Info: https://docs.unity3d.com/ScriptReference/Analytics.AnalyticsSessionInfo-userId.html - // It's a randomly generated GUID that gets created immediately after installation helping - // to identify the same instance of the game - options.DefaultUserId = AnalyticsSessionInfo.userId; - if (options.DefaultUserId is not null) - { - options.ScopeObserver.SetUser(new SentryUser { Id = options.DefaultUserId }); - } - else - { - Logger?.LogDebug("Failed to create new 'Default User ID'."); - } + options.DefaultUserId = installationId; + } + else + { + Logger?.LogDebug("Failed to fetch 'Installation ID' from the native SDK."); } } diff --git a/src/Sentry.Unity/Integrations/UnityScopeIntegration.cs b/src/Sentry.Unity/Integrations/UnityScopeIntegration.cs index 2c5c08ee6..cce5f495c 100644 --- a/src/Sentry.Unity/Integrations/UnityScopeIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityScopeIntegration.cs @@ -159,12 +159,16 @@ private void PopulateUnity(Protocol.Unity unity) private void PopulateUser(Scope scope) { - if (_options.DefaultUserId is not null) + if (scope.User.Id is not null) { - if (scope.User.Id is null) - { - scope.User.Id = _options.DefaultUserId; - } + return; + } + + // Prefer the native installation ID set by platform Configure methods + var userId = _options.DefaultUserId ?? _options.InstallationId; + if (userId is not null) + { + scope.User.Id = userId; } } } diff --git a/src/Sentry.Unity/WebGL/SentryWebGL.cs b/src/Sentry.Unity/WebGL/SentryWebGL.cs index fd3dca9de..842a7bd40 100644 --- a/src/Sentry.Unity/WebGL/SentryWebGL.cs +++ b/src/Sentry.Unity/WebGL/SentryWebGL.cs @@ -1,6 +1,5 @@ using Sentry.Extensibility; using Sentry.Unity.Integrations; -using UnityEngine.Analytics; namespace Sentry.Unity.WebGL; @@ -46,9 +45,6 @@ public static void Configure(SentryUnityOptions options) options.LogWarning("IL2CPP line number support is unsupported on WebGL - disabling."); } - // Use AnalyticsSessionInfo.userId as the default UserID - options.DefaultUserId = AnalyticsSessionInfo.userId; - // Indicate that this platform doesn't support running background threads. options.MultiThreading = false; } diff --git a/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs b/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs index c34468f31..09d2057fb 100644 --- a/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs +++ b/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs @@ -111,14 +111,25 @@ public void Configure_NativeAndroidSupportDisabled_DoesNotReInitializeNativeBack } [Test] - public void Configure_NoInstallationIdReturned_SetsNewDefaultUserId() + public void Configure_InstallationIdReturned_SetsDefaultUserId() + { + var options = new SentryUnityOptions(); + _testSentryJava.InstallationId = "test-installation-id"; + + SentryNativeAndroid.Configure(options); + + Assert.AreEqual("test-installation-id", options.DefaultUserId); + } + + [Test] + public void Configure_NoInstallationIdReturned_DoesNotSetDefaultUserId() { var options = new SentryUnityOptions(); _testSentryJava.InstallationId = string.Empty; SentryNativeAndroid.Configure(options); - Assert.False(string.IsNullOrEmpty(options.DefaultUserId)); + Assert.IsNull(options.DefaultUserId); } [Test] diff --git a/test/Sentry.Unity.Tests/UnityEventScopeTests.cs b/test/Sentry.Unity.Tests/UnityEventScopeTests.cs index 63af99c13..572740028 100644 --- a/test/Sentry.Unity.Tests/UnityEventScopeTests.cs +++ b/test/Sentry.Unity.Tests/UnityEventScopeTests.cs @@ -358,6 +358,110 @@ public void UserId_UnchangedIfNonEmpty() Assert.AreEqual(scope.User.Id, "bar"); } + [Test] + public void UserId_FallsBackToInstallationId_WhenDefaultUserIdIsNull() + { + // arrange - no DefaultUserId set, InstallationId resolves from .NET SDK + var options = new SentryUnityOptions(application: _testApplication); + Assert.IsNull(options.DefaultUserId); + var expectedId = options.InstallationId; + Assert.IsNotNull(expectedId, "InstallationId should resolve from the .NET SDK"); + + var sut = new UnityScopeUpdater(options, _testApplication); + var scope = new Scope(options); + + // act + sut.ConfigureScope(scope); + + // assert + Assert.AreEqual(expectedId, scope.User.Id); + } + + [Test] + public void UserId_DefaultUserIdPreferredOverInstallationId() + { + // arrange + var options = new SentryUnityOptions(application: _testApplication) { DefaultUserId = "native-id" }; + var installationId = options.InstallationId; + Assert.IsNotNull(installationId, "InstallationId should resolve from the .NET SDK"); + Assert.AreNotEqual("native-id", installationId); + + var sut = new UnityScopeUpdater(options, _testApplication); + var scope = new Scope(options); + + // act + sut.ConfigureScope(scope); + + // assert - DefaultUserId wins over InstallationId + Assert.AreEqual("native-id", scope.User.Id); + } + + [Test] + public void UserId_ScopeSync_TriggeredWhenUserIdSet() + { + // arrange - enable scope sync with a tracking observer + var options = new SentryUnityOptions(application: _testApplication) { DefaultUserId = "sync-test-id" }; + var observer = new TestScopeObserver(options); + options.ScopeObserver = observer; + options.EnableScopeSync = true; + + var sut = new UnityScopeUpdater(options, _testApplication); + var scope = new Scope(options); + + // act + sut.ConfigureScope(scope); + + // assert - the observer should have received the SetUser call via PropertyChanged + Assert.IsNotNull(observer.LastUser, "ScopeObserver.SetUser should have been called"); + Assert.AreEqual("sync-test-id", observer.LastUser!.Id); + } + + [Test] + public void UserId_ScopeSync_TriggeredWithInstallationIdFallback() + { + // arrange - no DefaultUserId, should fall back to InstallationId and still sync + var options = new SentryUnityOptions(application: _testApplication); + var observer = new TestScopeObserver(options); + options.ScopeObserver = observer; + options.EnableScopeSync = true; + + var expectedId = options.InstallationId; + Assert.IsNotNull(expectedId); + + var sut = new UnityScopeUpdater(options, _testApplication); + var scope = new Scope(options); + + // act + sut.ConfigureScope(scope); + + // assert + Assert.IsNotNull(observer.LastUser, "ScopeObserver.SetUser should have been called"); + Assert.AreEqual(expectedId, observer.LastUser!.Id); + } + + [Test] + public void UserId_ScopeSync_NotTriggeredWhenUserAlreadySet() + { + // arrange - User.Id already set, PopulateUser should early-return + var options = new SentryUnityOptions(application: _testApplication) { DefaultUserId = "should-not-sync" }; + var observer = new TestScopeObserver(options); + options.ScopeObserver = observer; + options.EnableScopeSync = true; + + var sut = new UnityScopeUpdater(options, _testApplication); + var scope = new Scope(options); + scope.User.Id = "already-set"; + + // Reset observer after the initial scope.User.Id set above triggered it + observer.LastUser = null; + + // act + sut.ConfigureScope(scope); + + // assert - PopulateUser should not have set a new user + Assert.IsNull(observer.LastUser, "ScopeObserver.SetUser should not be called when User.Id is already set"); + } + [Test] public void OperatingSystemProtocol_Assigned() { @@ -600,3 +704,20 @@ internal sealed class TestSentrySystemInfo : ISentrySystemInfo public Lazy? RenderingThreadingMode { get; set; } public Lazy? StartTime { get; set; } } + +internal sealed class TestScopeObserver : ScopeObserver +{ + public SentryUser? LastUser { get; set; } + + public TestScopeObserver(SentryOptions options) : base("Test", options) { } + + public override void AddBreadcrumbImpl(Breadcrumb breadcrumb) { } + public override void SetExtraImpl(string key, string? value) { } + public override void SetTagImpl(string key, string value) { } + public override void UnsetTagImpl(string key) { } + public override void SetUserImpl(SentryUser user) => LastUser = user; + public override void UnsetUserImpl() => LastUser = null; + public override void SetTraceImpl(SentryId traceId, SpanId spanId) { } + public override void AddAttachmentImpl(SentryAttachment attachment) { } + public override void ClearAttachmentsImpl() { } +} From eb03be3be9607b225ab2ada870dd6f6fcd6b8d25 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 2 Apr 2026 17:17:51 +0200 Subject: [PATCH 2/6] updated changelog.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d569dd18..a19d2ac7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Behavioral Changes + +- The SDK no longer relies on UnityEngine.Analytics.AnalyticsSessionInfo to determine unique users but uses SDK-internal mechanisms instead. ([#2617](https://github.com/getsentry/sentry-unity/pull/2625)) + ### Fixes - The SDK now correctly resolves the storage path on Xbox during initialization, enabling offline caching and native crash capturing without user setup out of the box. ([#2617](https://github.com/getsentry/sentry-unity/pull/2617)) From 166abe9bac3a7e9485b72141eb0d39e3a60c37c2 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 2 Apr 2026 20:49:25 +0200 Subject: [PATCH 3/6] Only set DefaultUserId in PopulateUser, not InstallationId PopulateUser was falling back to options.InstallationId which caused User.Id to be set on the scope before the .NET SDK Enricher runs. This made HasUser() return true, preventing IsEnvironmentUser from setting the Username when SendDefaultPii is enabled. The Enricher already sets User.Id from InstallationId after the HasUser() check, so the ID is still always present on every event. --- src/Sentry.Unity/Integrations/UnityScopeIntegration.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Sentry.Unity/Integrations/UnityScopeIntegration.cs b/src/Sentry.Unity/Integrations/UnityScopeIntegration.cs index cce5f495c..84d771261 100644 --- a/src/Sentry.Unity/Integrations/UnityScopeIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityScopeIntegration.cs @@ -164,11 +164,12 @@ private void PopulateUser(Scope scope) return; } - // Prefer the native installation ID set by platform Configure methods - var userId = _options.DefaultUserId ?? _options.InstallationId; - if (userId is not null) + // Only set the native installation ID here. The .NET SDK's Enricher handles + // the fallback to InstallationId after the HasUser() check, which allows + // IsEnvironmentUser/SendDefaultPii to set the Username first. + if (_options.DefaultUserId is not null) { - scope.User.Id = userId; + scope.User.Id = _options.DefaultUserId; } } } From 9226cce5b446433d715a132c89c62882e6b0c417 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 2 Apr 2026 21:26:23 +0200 Subject: [PATCH 4/6] removed erronous tests --- .../UnityEventScopeTests.cs | 49 +------------------ 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/test/Sentry.Unity.Tests/UnityEventScopeTests.cs b/test/Sentry.Unity.Tests/UnityEventScopeTests.cs index 572740028..a28efa61f 100644 --- a/test/Sentry.Unity.Tests/UnityEventScopeTests.cs +++ b/test/Sentry.Unity.Tests/UnityEventScopeTests.cs @@ -359,32 +359,10 @@ public void UserId_UnchangedIfNonEmpty() } [Test] - public void UserId_FallsBackToInstallationId_WhenDefaultUserIdIsNull() - { - // arrange - no DefaultUserId set, InstallationId resolves from .NET SDK - var options = new SentryUnityOptions(application: _testApplication); - Assert.IsNull(options.DefaultUserId); - var expectedId = options.InstallationId; - Assert.IsNotNull(expectedId, "InstallationId should resolve from the .NET SDK"); - - var sut = new UnityScopeUpdater(options, _testApplication); - var scope = new Scope(options); - - // act - sut.ConfigureScope(scope); - - // assert - Assert.AreEqual(expectedId, scope.User.Id); - } - - [Test] - public void UserId_DefaultUserIdPreferredOverInstallationId() + public void UserId_DefaultUserIdIsSet() { // arrange var options = new SentryUnityOptions(application: _testApplication) { DefaultUserId = "native-id" }; - var installationId = options.InstallationId; - Assert.IsNotNull(installationId, "InstallationId should resolve from the .NET SDK"); - Assert.AreNotEqual("native-id", installationId); var sut = new UnityScopeUpdater(options, _testApplication); var scope = new Scope(options); @@ -392,7 +370,7 @@ public void UserId_DefaultUserIdPreferredOverInstallationId() // act sut.ConfigureScope(scope); - // assert - DefaultUserId wins over InstallationId + // assert Assert.AreEqual("native-id", scope.User.Id); } @@ -416,29 +394,6 @@ public void UserId_ScopeSync_TriggeredWhenUserIdSet() Assert.AreEqual("sync-test-id", observer.LastUser!.Id); } - [Test] - public void UserId_ScopeSync_TriggeredWithInstallationIdFallback() - { - // arrange - no DefaultUserId, should fall back to InstallationId and still sync - var options = new SentryUnityOptions(application: _testApplication); - var observer = new TestScopeObserver(options); - options.ScopeObserver = observer; - options.EnableScopeSync = true; - - var expectedId = options.InstallationId; - Assert.IsNotNull(expectedId); - - var sut = new UnityScopeUpdater(options, _testApplication); - var scope = new Scope(options); - - // act - sut.ConfigureScope(scope); - - // assert - Assert.IsNotNull(observer.LastUser, "ScopeObserver.SetUser should have been called"); - Assert.AreEqual(expectedId, observer.LastUser!.Id); - } - [Test] public void UserId_ScopeSync_NotTriggeredWhenUserAlreadySet() { From e7f0f9645ddcf3cb429efd19595f9e19e3ab76d5 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 2 Apr 2026 22:05:37 +0200 Subject: [PATCH 5/6] tests --- test/Sentry.Unity.Tests/ContextWriterTests.cs | 3 +++ test/Sentry.Unity.Tests/UnityEventScopeTests.cs | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/Sentry.Unity.Tests/ContextWriterTests.cs b/test/Sentry.Unity.Tests/ContextWriterTests.cs index cd3b5bafe..95b7ade18 100644 --- a/test/Sentry.Unity.Tests/ContextWriterTests.cs +++ b/test/Sentry.Unity.Tests/ContextWriterTests.cs @@ -78,6 +78,9 @@ public void Arguments() Debug = true, DiagnosticLogger = new TestLogger(), NativeContextWriter = context, + CreateHttpMessageHandler = () => new TestHttpClientHandler(), + AutoSessionTracking = false, + CacheDirectoryPath = null, }; // act diff --git a/test/Sentry.Unity.Tests/UnityEventScopeTests.cs b/test/Sentry.Unity.Tests/UnityEventScopeTests.cs index a28efa61f..6f756e3e4 100644 --- a/test/Sentry.Unity.Tests/UnityEventScopeTests.cs +++ b/test/Sentry.Unity.Tests/UnityEventScopeTests.cs @@ -673,6 +673,7 @@ public override void UnsetTagImpl(string key) { } public override void SetUserImpl(SentryUser user) => LastUser = user; public override void UnsetUserImpl() => LastUser = null; public override void SetTraceImpl(SentryId traceId, SpanId spanId) { } - public override void AddAttachmentImpl(SentryAttachment attachment) { } + public override void AddFileAttachmentImpl(string filePath, string fileName, string? contentType) { } + public override void AddByteAttachmentImpl(byte[] data, string fileName, string? contentType) { } public override void ClearAttachmentsImpl() { } } From 70ff58ea553e0831885a4ea3091c09235f8ef8a7 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 2 Apr 2026 22:14:12 +0200 Subject: [PATCH 6/6] updated changelog.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b09e679..a63b978c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Behavioral Changes -- The SDK no longer relies on UnityEngine.Analytics.AnalyticsSessionInfo to determine unique users but uses SDK-internal mechanisms instead. ([#2617](https://github.com/getsentry/sentry-unity/pull/2625)) +- The SDK no longer relies on UnityEngine.Analytics.AnalyticsSessionInfo to determine unique users but uses SDK-internal mechanisms instead. ([#2625](https://github.com/getsentry/sentry-unity/pull/2625)) ### Fixes