diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff183e48..a63b978c3 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. ([#2625](https://github.com/getsentry/sentry-unity/pull/2625)) + ### Fixes - When targeting iOS or macOS, the SDK now correctly passes on the `CaptureFailedRequests` flag and set status code ranged ([#2619](https://github.com/getsentry/sentry-unity/pull/2619)) 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..84d771261 100644 --- a/src/Sentry.Unity/Integrations/UnityScopeIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityScopeIntegration.cs @@ -159,12 +159,17 @@ private void PopulateUnity(Protocol.Unity unity) private void PopulateUser(Scope scope) { + if (scope.User.Id is not null) + { + return; + } + + // 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) { - if (scope.User.Id is null) - { - scope.User.Id = _options.DefaultUserId; - } + scope.User.Id = _options.DefaultUserId; } } } 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/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 63af99c13..6f756e3e4 100644 --- a/test/Sentry.Unity.Tests/UnityEventScopeTests.cs +++ b/test/Sentry.Unity.Tests/UnityEventScopeTests.cs @@ -358,6 +358,65 @@ public void UserId_UnchangedIfNonEmpty() Assert.AreEqual(scope.User.Id, "bar"); } + [Test] + public void UserId_DefaultUserIdIsSet() + { + // arrange + var options = new SentryUnityOptions(application: _testApplication) { DefaultUserId = "native-id" }; + + var sut = new UnityScopeUpdater(options, _testApplication); + var scope = new Scope(options); + + // act + sut.ConfigureScope(scope); + + // assert + 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_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 +659,21 @@ 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 AddFileAttachmentImpl(string filePath, string fileName, string? contentType) { } + public override void AddByteAttachmentImpl(byte[] data, string fileName, string? contentType) { } + public override void ClearAttachmentsImpl() { } +}