diff --git a/CHANGELOG.md b/CHANGELOG.md index dafbc925a..4f651266a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Linux native crash support ([#734](https://github.com/getsentry/sentry-unity/pull/734)) - Collect context information synchronously during init to capture it for very early events ([#744](https://github.com/getsentry/sentry-unity/pull/744)) +- Automatic user IDs on native crashes & .NET events ([#728](https://github.com/getsentry/sentry-unity/pull/728)) ## 0.16.0 diff --git a/package-dev/Plugins/iOS/SentryNativeBridge.m b/package-dev/Plugins/iOS/SentryNativeBridge.m index 4fbf9afd3..cdaeb82ed 100644 --- a/package-dev/Plugins/iOS/SentryNativeBridge.m +++ b/package-dev/Plugins/iOS/SentryNativeBridge.m @@ -1,3 +1,4 @@ +#import #import NS_ASSUME_NONNULL_BEGIN @@ -121,4 +122,15 @@ void SentryNativeBridgeUnsetUser() [SentrySDK configureScope:^(SentryScope *scope) { [scope setUser:nil]; }]; } +char *SentryNativeBridgeGetInstallationId() +{ + // Create a null terminated C string on the heap as expected by marshalling. + // See Tips for iOS in https://docs.unity3d.com/Manual/PluginsForIOS.html + const char *nsStringUtf8 = [[PrivateSentrySDKOnly installationID] UTF8String]; + size_t len = strlen(nsStringUtf8) + 1; + char *cString = (char *)malloc(len); + memcpy(cString, nsStringUtf8, len); + return cString; +} + NS_ASSUME_NONNULL_END diff --git a/package-dev/Plugins/macOS/SentryNativeBridge.m b/package-dev/Plugins/macOS/SentryNativeBridge.m index b43e67e1d..35e76470e 100644 --- a/package-dev/Plugins/macOS/SentryNativeBridge.m +++ b/package-dev/Plugins/macOS/SentryNativeBridge.m @@ -7,6 +7,7 @@ static Class SentryScope; static Class SentryBreadcrumb; static Class SentryUser; +static Class PrivateSentrySDKOnly; #define LOAD_CLASS_OR_BREAK(name) \ name = (__bridge Class)dlsym(dylib, "OBJC_CLASS_$_" #name); \ @@ -32,6 +33,7 @@ int SentryNativeBridgeLoadLibrary() LOAD_CLASS_OR_BREAK(SentryScope) LOAD_CLASS_OR_BREAK(SentryBreadcrumb) LOAD_CLASS_OR_BREAK(SentryUser) + LOAD_CLASS_OR_BREAK(PrivateSentrySDKOnly) // everything above passed - mark as successfully loaded loadStatus = 1; @@ -223,3 +225,15 @@ void SentryNativeBridgeUnsetUser() SentryConfigureScope( ^(id scope) { [scope performSelector:@selector(setUser:) withObject:nil]; }); } + +char *SentryNativeBridgeGetInstallationId() +{ + // Create a null terminated C string on the heap as expected by marshalling. + // See Tips for iOS in https://docs.unity3d.com/Manual/PluginsForIOS.html + const char *nsStringUtf8 = + [[PrivateSentrySDKOnly performSelector:@selector(installationID)] UTF8String]; + size_t len = strlen(nsStringUtf8) + 1; + char *cString = (char *)malloc(len); + memcpy(cString, nsStringUtf8, len); + return cString; +} diff --git a/package-dev/Runtime/SentryInitialization.cs b/package-dev/Runtime/SentryInitialization.cs index c908ebf26..5ac72bebf 100644 --- a/package-dev/Runtime/SentryInitialization.cs +++ b/package-dev/Runtime/SentryInitialization.cs @@ -43,7 +43,7 @@ public static void Init() try { #if SENTRY_NATIVE_COCOA - SentryNativeCocoa.Configure(options); + SentryNativeCocoa.Configure(options, sentryUnityInfo); #elif SENTRY_NATIVE_ANDROID SentryNativeAndroid.Configure(options, sentryUnityInfo); #elif SENTRY_NATIVE diff --git a/samples/unity-of-bugs/Assets/Scripts/SmokeTester.cs b/samples/unity-of-bugs/Assets/Scripts/SmokeTester.cs index abb641699..e0519019d 100644 --- a/samples/unity-of-bugs/Assets/Scripts/SmokeTester.cs +++ b/samples/unity-of-bugs/Assets/Scripts/SmokeTester.cs @@ -125,6 +125,7 @@ public static void SmokeTest() t.ExpectMessage(currentMessage, "'type':'event'"); t.ExpectMessage(currentMessage, $"LogError(GUID)={guid}"); + t.ExpectMessage(currentMessage, "'user':{'id':'"); // non-null automatic ID t.ExpectMessage(currentMessage, "'filename':'screenshot.jpg','attachment_type':'event.attachment'"); t.ExpectMessageNot(currentMessage, "'length':0"); diff --git a/scripts/smoke-test-webgl.py b/scripts/smoke-test-webgl.py index d5119cba3..d7f3ff9da 100644 --- a/scripts/smoke-test-webgl.py +++ b/scripts/smoke-test-webgl.py @@ -63,6 +63,9 @@ def Expect(self, message, result): raise Exception(info) def CheckMessage(self, index, substring, negate): + if len(self.__requests) <= index: + raise Exception('HTTP Request #{} not captured.'.format(index)) + message = self.__requests[index]["body"] contains = substring in message or substring.replace( "'", "\"") in message @@ -183,6 +186,7 @@ def waitUntil(condition, interval=0.1, timeout=1): currentMessage += 1 t.ExpectMessage(currentMessage, "'type':'event'") t.ExpectMessage(currentMessage, "LogError(GUID)") +t.ExpectMessage(currentMessage, "'user':{'id':'") # t.ExpectMessage( # currentMessage, "'filename':'screenshot.jpg','attachment_type':'event.attachment'") # t.ExpectMessageNot(currentMessage, "'length':0") diff --git a/scripts/unity-utils.ps1 b/scripts/unity-utils.ps1 index fa0cf96e8..6933296fe 100644 --- a/scripts/unity-utils.ps1 +++ b/scripts/unity-utils.ps1 @@ -66,11 +66,13 @@ function RunUnity([string] $unityPath, [string[]] $arguments, [switch] $ReturnLo break } } while ($stopwatch.Elapsed.TotalSeconds -lt $RunUnityLicenseRetryTimeoutSeconds) - + if ($process.ExitCode -ne 0) { Throw "Unity exited with code $($process.ExitCode)" } + + Write-Host -ForegroundColor Green "Unity finished successfully. Time taken: $($stopwatch.Elapsed.ToString('hh\:mm\:ss\.fff'))" return $ReturnLogOutput ? $stdout : $null } diff --git a/src/Sentry.Unity.Android/SentryJava.cs b/src/Sentry.Unity.Android/SentryJava.cs index adb72a60d..f861cb0f9 100644 --- a/src/Sentry.Unity.Android/SentryJava.cs +++ b/src/Sentry.Unity.Android/SentryJava.cs @@ -3,15 +3,28 @@ namespace Sentry.Unity.Android { /// - /// P/Invoke to `sentry-java` methods. + /// JNI access to `sentry-java` methods. /// /// /// The `sentry-java` SDK on Android is brought in through the `sentry-android-core` /// and `sentry-java` maven packages. /// /// - public static class SentryJava + internal static class SentryJava { + internal static string? GetInstallationId() + { + if (!Attach()) + { + return null; + } + + using var sentry = GetSentryJava(); + using var hub = sentry.CallStatic("getCurrentHub"); + using var options = hub?.Call("getOptions"); + return options?.Call("getDistinctId"); + } + /// /// Returns whether or not the last run resulted in a crash. /// @@ -24,15 +37,25 @@ public static class SentryJava /// public static bool? CrashedLastRun() { - using var jo = new AndroidJavaObject("io.sentry.Sentry"); - return jo.CallStatic("isCrashedLastRun") - ?.Call("booleanValue"); + if (!Attach()) + { + return null; + } + using var sentry = GetSentryJava(); + using var jo = sentry.CallStatic("isCrashedLastRun"); + return jo?.Call("booleanValue"); } public static void Close() { - using var jo = new AndroidJavaObject("io.sentry.Sentry"); - jo.CallStatic("close"); + if (Attach()) + { + using var sentry = GetSentryJava(); + sentry.CallStatic("close"); + } } + + private static bool Attach() => AndroidJNI.AttachCurrentThread() == 0; + private static AndroidJavaObject GetSentryJava() => new AndroidJavaClass("io.sentry.Sentry"); } } diff --git a/src/Sentry.Unity.Android/SentryNativeAndroid.cs b/src/Sentry.Unity.Android/SentryNativeAndroid.cs index 4142f494f..8768f3741 100644 --- a/src/Sentry.Unity.Android/SentryNativeAndroid.cs +++ b/src/Sentry.Unity.Android/SentryNativeAndroid.cs @@ -51,6 +51,8 @@ public static void Configure(SentryUnityOptions options, ISentryUnityInfo sentry options.DiagnosticLogger?.LogDebug("Closing the sentry-java SDK"); SentryJava.Close(); }; + + options.DefaultUserId = SentryJava.GetInstallationId(); } } } diff --git a/src/Sentry.Unity.Native/SentryNative.cs b/src/Sentry.Unity.Native/SentryNative.cs index 7b5ffd3e0..c91841f4c 100644 --- a/src/Sentry.Unity.Native/SentryNative.cs +++ b/src/Sentry.Unity.Native/SentryNative.cs @@ -2,6 +2,7 @@ using Sentry.Unity.Integrations; using System.Collections.Generic; using UnityEngine; +using UnityEngine.Analytics; namespace Sentry.Unity.Native { @@ -40,6 +41,13 @@ public static void Configure(SentryUnityOptions options) options.ScopeObserver = new NativeScopeObserver(options); options.EnableScopeSync = true; + // Use AnalyticsSessionInfo.userId as the default UserID in native & dotnet + options.DefaultUserId = AnalyticsSessionInfo.userId; + if (options.DefaultUserId is not null) + { + options.ScopeObserver.SetUser(new User { 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. diff --git a/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs b/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs index 9f3c7314a..27e100005 100644 --- a/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs +++ b/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs @@ -100,5 +100,8 @@ public static bool Init(SentryUnityOptions options) [DllImport("__Internal")] public static extern void SentryNativeBridgeUnsetUser(); + + [DllImport("__Internal", EntryPoint = "SentryNativeBridgeGetInstallationId")] + public static extern string GetInstallationId(); } } diff --git a/src/Sentry.Unity.iOS/SentryNative.cs b/src/Sentry.Unity.iOS/SentryNative.cs index 4375c2e93..f38b66fef 100644 --- a/src/Sentry.Unity.iOS/SentryNative.cs +++ b/src/Sentry.Unity.iOS/SentryNative.cs @@ -13,9 +13,10 @@ public static class SentryNativeCocoa /// Configures the native support. /// /// The Sentry Unity options to use. - public static void Configure(SentryUnityOptions options) => Configure(options, ApplicationAdapter.Instance.Platform); + public static void Configure(SentryUnityOptions options, ISentryUnityInfo sentryUnityInfo) => + Configure(options, sentryUnityInfo, ApplicationAdapter.Instance.Platform); - internal static void Configure(SentryUnityOptions options, RuntimePlatform platform) + internal static void Configure(SentryUnityOptions options, ISentryUnityInfo sentryUnityInfo, RuntimePlatform platform) { switch (platform) { @@ -58,6 +59,10 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf options.DiagnosticLogger?.LogDebug("Closing the sentry-cocoa SDK"); SentryCocoaBridgeProxy.Close(); }; + if (sentryUnityInfo.IL2CPP) + { + options.DefaultUserId = SentryCocoaBridgeProxy.GetInstallationId(); + } } } } diff --git a/src/Sentry.Unity/Sentry.Unity.csproj b/src/Sentry.Unity/Sentry.Unity.csproj index 765410eac..397b77fdb 100644 --- a/src/Sentry.Unity/Sentry.Unity.csproj +++ b/src/Sentry.Unity/Sentry.Unity.csproj @@ -27,4 +27,9 @@ Overwrite="true"/> + + + <_Parameter1>Sentry.Unity.Native + + diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index c94b85d4d..a63c32601 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Text.RegularExpressions; using Sentry.Unity.Integrations; +using Sentry.Extensibility; using CompressionLevel = System.IO.Compression.CompressionLevel; namespace Sentry.Unity @@ -111,6 +112,26 @@ public sealed class SentryUnityOptions : SentryOptions /// public bool LinuxNativeSupportEnabled { get; set; } = true; + + // Initialized by native SDK binding code to set the User.ID in .NET (UnityEventProcessor). + internal string? _defaultUserId; + internal string? DefaultUserId + { + get => _defaultUserId; + set + { + _defaultUserId = value; + if (_defaultUserId is null) + { + DiagnosticLogger?.LogWarning("Couldn't set the default user ID - the value is NULL."); + } + else + { + DiagnosticLogger?.LogDebug("Setting '{0}' as the default user ID.", _defaultUserId); + } + } + } + public SentryUnityOptions() : this(ApplicationAdapter.Instance, false) { } diff --git a/src/Sentry.Unity/UnityEventProcessor.cs b/src/Sentry.Unity/UnityEventProcessor.cs index 193efbc54..33396f70b 100644 --- a/src/Sentry.Unity/UnityEventProcessor.cs +++ b/src/Sentry.Unity/UnityEventProcessor.cs @@ -19,12 +19,12 @@ internal static class UnitySdkInfo internal class UnityEventProcessor : ISentryEventProcessor { - private readonly SentryOptions _sentryOptions; + private readonly SentryUnityOptions _sentryOptions; private readonly MainThreadData _mainThreadData; private readonly IApplication _application; - public UnityEventProcessor(SentryOptions sentryOptions, SentryMonoBehaviour sentryMonoBehaviour, IApplication? application = null) + public UnityEventProcessor(SentryUnityOptions sentryOptions, SentryMonoBehaviour sentryMonoBehaviour, IApplication? application = null) { _sentryOptions = sentryOptions; _mainThreadData = sentryMonoBehaviour.MainThreadData; @@ -42,6 +42,7 @@ public SentryEvent Process(SentryEvent @event) PopulateGpu(@event.Contexts.Gpu); PopulateUnity((Protocol.Unity)@event.Contexts.GetOrAdd(Protocol.Unity.Type, _ => new Protocol.Unity())); PopulateTags(@event); + PopulateUser(@event); } catch (Exception ex) { @@ -211,6 +212,17 @@ private void PopulateTags(SentryEvent @event) @event.SetTag("unity.is_main_thread", _mainThreadData.IsMainThread().ToTagValue()); } + private void PopulateUser(SentryEvent @event) + { + if (_sentryOptions.DefaultUserId is not null) + { + if (@event.User.Id is null) + { + @event.User.Id = _sentryOptions.DefaultUserId; + } + } + } + /// /// - If UI thread, extract the value (can be null) /// - If non-UI thread, check if value is created, then extract diff --git a/src/Sentry.Unity/WebGL/SentryWebGL.cs b/src/Sentry.Unity/WebGL/SentryWebGL.cs index 93bdd6490..e56355986 100644 --- a/src/Sentry.Unity/WebGL/SentryWebGL.cs +++ b/src/Sentry.Unity/WebGL/SentryWebGL.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using UnityEngine.Analytics; namespace Sentry.Unity.WebGL { @@ -36,6 +37,9 @@ public static void Configure(SentryUnityOptions options) options.DiagnosticLogger?.LogWarning("Attaching screenshots on WebGL is disabled - " + "it currently produces blank screenshots mid-frame."); } + + // Use AnalyticsSessionInfo.userId as the default UserID in native & dotnet + options.DefaultUserId = AnalyticsSessionInfo.userId; } } } diff --git a/test/Sentry.Unity.Tests/UnityEventProcessorTests.cs b/test/Sentry.Unity.Tests/UnityEventProcessorTests.cs index 32f2816b4..c6769dd3c 100644 --- a/test/Sentry.Unity.Tests/UnityEventProcessorTests.cs +++ b/test/Sentry.Unity.Tests/UnityEventProcessorTests.cs @@ -198,7 +198,7 @@ public sealed class UnityEventProcessorTests { private GameObject _gameObject = null!; private SentryMonoBehaviour _sentryMonoBehaviour = null!; - private SentryOptions _sentryOptions = null!; + private SentryUnityOptions _sentryOptions = null!; private TestApplication _testApplication = null!; [SetUp] @@ -206,7 +206,7 @@ public void SetUp() { _gameObject = new GameObject("ProcessorTest"); _sentryMonoBehaviour = _gameObject.AddComponent(); - _sentryOptions = new SentryOptions + _sentryOptions = new SentryUnityOptions { Debug = true, DiagnosticLogger = new TestLogger() @@ -279,7 +279,7 @@ public void Process_ServerName_IsNull() public void Process_DeviceUniqueIdentifierWithSendDefaultPii_IsNotNull() { // arrange - var sentryOptions = new SentryOptions { SendDefaultPii = true }; + var sentryOptions = new SentryUnityOptions { SendDefaultPii = true }; var sut = new UnityEventProcessor(sentryOptions, _sentryMonoBehaviour, _testApplication); var sentryEvent = new SentryEvent(); @@ -299,7 +299,7 @@ public void Process_AppProtocol_Assigned() { MainThreadId = 1 }; - var unityEventProcessor = new UnityEventProcessor(new SentryOptions(), _sentryMonoBehaviour, _testApplication); + var unityEventProcessor = new UnityEventProcessor(new(), _sentryMonoBehaviour, _testApplication); var sentryEvent = new SentryEvent(); // act @@ -310,6 +310,39 @@ public void Process_AppProtocol_Assigned() Assert.IsNotNull(sentryEvent.Contexts.App.StartTime); } + [Test] + public void Process_UserId_SetIfEmpty() + { + // arrange + var options = new SentryUnityOptions { DefaultUserId = "foo" }; + var processor = new UnityEventProcessor(options, _sentryMonoBehaviour, _testApplication); + var sentryEvent = new SentryEvent(); + + // act + _sentryMonoBehaviour.CollectData(); + processor.Process(sentryEvent); + + // assert + Assert.AreEqual(sentryEvent.User.Id, options.DefaultUserId); + } + + [Test] + public void Process_UserId_UnchangedIfNonEmpty() + { + // arrange + var options = new SentryUnityOptions { DefaultUserId = "foo" }; + var processor = new UnityEventProcessor(options, _sentryMonoBehaviour, _testApplication); + var sentryEvent = new SentryEvent(); + sentryEvent.User.Id = "bar"; + + // act + _sentryMonoBehaviour.CollectData(); + processor.Process(sentryEvent); + + // assert + Assert.AreEqual(sentryEvent.User.Id, "bar"); + } + [Test] public void Process_Tags_Set() { @@ -322,7 +355,7 @@ public void Process_Tags_Set() InstallMode = ApplicationInstallMode.Store.ToString() }; - var sentryOptions = new SentryOptions { SendDefaultPii = true }; + var sentryOptions = new SentryUnityOptions { SendDefaultPii = true }; var unityEventProcessor = new UnityEventProcessor(sentryOptions, _sentryMonoBehaviour, _testApplication); var sentryEvent = new SentryEvent(); diff --git a/test/Sentry.Unity.iOS.Tests/SentryNativeIosTests.cs b/test/Sentry.Unity.iOS.Tests/SentryNativeIosTests.cs index 7585b176f..c0d35b703 100644 --- a/test/Sentry.Unity.iOS.Tests/SentryNativeIosTests.cs +++ b/test/Sentry.Unity.iOS.Tests/SentryNativeIosTests.cs @@ -4,13 +4,26 @@ namespace Sentry.Unity.iOS.Tests { + public class TestSentryUnityInfo : ISentryUnityInfo + { + public bool IL2CPP { get; set; } + } + public class SentryNativeCocoaTests { + private TestSentryUnityInfo _sentryUnityInfo = null!; + + [SetUp] + public void SetUp() + { + _sentryUnityInfo = new TestSentryUnityInfo { IL2CPP = false }; + } + [Test] public void Configure_DefaultConfiguration_iOS() { var options = new SentryUnityOptions(); - SentryNativeCocoa.Configure(options, RuntimePlatform.IPhonePlayer); + SentryNativeCocoa.Configure(options, _sentryUnityInfo, RuntimePlatform.IPhonePlayer); Assert.IsAssignableFrom(options.ScopeObserver); Assert.IsNotNull(options.CrashedLastRun); Assert.True(options.EnableScopeSync); @@ -20,7 +33,7 @@ public void Configure_DefaultConfiguration_iOS() public void Configure_NativeSupportDisabled_iOS() { var options = new SentryUnityOptions { IosNativeSupportEnabled = false }; - SentryNativeCocoa.Configure(options, RuntimePlatform.IPhonePlayer); + SentryNativeCocoa.Configure(options, _sentryUnityInfo, RuntimePlatform.IPhonePlayer); Assert.Null(options.ScopeObserver); Assert.Null(options.CrashedLastRun); Assert.False(options.EnableScopeSync); @@ -33,14 +46,14 @@ public void Configure_DefaultConfiguration_macOS() // Note: can't test macOS - throws because it tries to call SentryCocoaBridgeProxy.Init() // but the bridge isn't loaded now... Assert.Throws(() => - SentryNativeCocoa.Configure(options, RuntimePlatform.OSXPlayer)); + SentryNativeCocoa.Configure(options, _sentryUnityInfo, RuntimePlatform.OSXPlayer)); } [Test] public void Configure_NativeSupportDisabled_macOS() { var options = new SentryUnityOptions { MacosNativeSupportEnabled = false }; - SentryNativeCocoa.Configure(options, RuntimePlatform.OSXPlayer); + SentryNativeCocoa.Configure(options, _sentryUnityInfo, RuntimePlatform.OSXPlayer); Assert.Null(options.ScopeObserver); Assert.Null(options.CrashedLastRun); Assert.False(options.EnableScopeSync);