diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c2af15f..60a3d4d6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,12 @@ ### API Changes -- The native layer on iOS no longer self-initializes before the Unity game starts. Instead, it accepts the options at the end of the configure call. To restore the old behavior, users can opt-in to initializing native first via `IosInitializeNativeFirst`. Note that using this option comes with the limitation of baking the options into the generated Xcode project at build-time. ([#1915](https://github.com/getsentry/sentry-unity/pull/1915)) +- The native layer on mobile platforms (iOS and Android) no longer self-initializes before the Unity game starts. Previously, the SDK would use the options at build-time and bake them into the native layer. Instead, the SDK will now take the options passed into the `Configure` callback and use those to initialize the native SDKs. This allows users to modify the native SDK's options at runtime programmatically. +The initialization behaviour is controlled by `IosNativeInitializationType` and `AndroidNativeInitializationType` options. These can be set from `Runtime` (default) to `BuildTime` to restore the previous flow and bake the options into the native projects. ([#1915](https://github.com/getsentry/sentry-unity/pull/1915), [#1924](https://github.com/getsentry/sentry-unity/pull/1924)) + +### Fixes + +- On Android, the SDK not longer freezes the game when failing to sync with the native SDK ([#1927](https://github.com/getsentry/sentry-unity/pull/1927)) ### Dependencies diff --git a/src/Sentry.Unity.Android/IJniExecutor.cs b/src/Sentry.Unity.Android/IJniExecutor.cs index a12c4f1bc..f465602cd 100644 --- a/src/Sentry.Unity.Android/IJniExecutor.cs +++ b/src/Sentry.Unity.Android/IJniExecutor.cs @@ -4,6 +4,6 @@ namespace Sentry.Unity.Android; internal interface IJniExecutor : IDisposable { - public TResult? Run(Func jniOperation); - public void Run(Action jniOperation); + public TResult? Run(Func jniOperation, TimeSpan? timeout = null); + public void Run(Action jniOperation, TimeSpan? timeout = null); } diff --git a/src/Sentry.Unity.Android/JniExecutor.cs b/src/Sentry.Unity.Android/JniExecutor.cs index f141ad8d3..079484b6d 100644 --- a/src/Sentry.Unity.Android/JniExecutor.cs +++ b/src/Sentry.Unity.Android/JniExecutor.cs @@ -1,26 +1,37 @@ using System; using System.Threading; using System.Threading.Tasks; +using Sentry.Extensibility; using UnityEngine; namespace Sentry.Unity.Android; internal class JniExecutor : IJniExecutor { + // We're capping out at 16ms - 1 frame at 60 frames per second + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromMilliseconds(16); + private readonly CancellationTokenSource _shutdownSource; private readonly AutoResetEvent _taskEvent; + private readonly IDiagnosticLogger? _logger; + private Delegate _currentTask = null!; // The current task will always be set together with the task event private TaskCompletionSource? _taskCompletionSource; private readonly object _lock = new object(); - public JniExecutor() + private bool _isDisposed; + private Thread? _workerThread; + + public JniExecutor(IDiagnosticLogger? logger) { + _logger = logger; _taskEvent = new AutoResetEvent(false); _shutdownSource = new CancellationTokenSource(); - new Thread(DoWork) { IsBackground = true, Name = "SentryJniExecutorThread" }.Start(); + _workerThread = new Thread(DoWork) { IsBackground = true, Name = "SentryJniExecutorThread" }; + _workerThread.Start(); } private void DoWork() @@ -29,7 +40,7 @@ private void DoWork() var waitHandles = new[] { _taskEvent, _shutdownSource.Token.WaitHandle }; - while (true) + while (!_isDisposed) { var index = WaitHandle.WaitAny(waitHandles); if (index > 0) @@ -50,74 +61,122 @@ private void DoWork() _taskCompletionSource?.SetResult(null); break; } - case Func func1: + case Func func1: { var result = func1.Invoke(); _taskCompletionSource?.SetResult(result); break; } - case Func func2: + case Func func2: { var result = func2.Invoke(); _taskCompletionSource?.SetResult(result); break; } + case Func func3: + { + var result = func3.Invoke(); + _taskCompletionSource?.SetResult(result); + break; + } default: - throw new ArgumentException("Invalid type for _currentTask."); + throw new NotImplementedException($"Task type '{_currentTask?.GetType()}' with value '{_currentTask}' is not implemented in the JniExecutor."); } } catch (Exception e) { - Debug.unityLogger.Log(LogType.Exception, UnityLogger.LogTag, $"Error during JNI execution: {e}"); + _logger?.LogError(e, "Error during JNI execution."); + _taskCompletionSource?.SetException(e); } } AndroidJNI.DetachCurrentThread(); } - public TResult? Run(Func jniOperation) + public TResult? Run(Func jniOperation, TimeSpan? timeout = null) { lock (_lock) { + timeout ??= DefaultTimeout; + using var timeoutCts = new CancellationTokenSource(timeout.Value); _taskCompletionSource = new TaskCompletionSource(); _currentTask = jniOperation; _taskEvent.Set(); try { - return (TResult?)_taskCompletionSource.Task.GetAwaiter().GetResult(); + _taskCompletionSource.Task.Wait(timeoutCts.Token); + return (TResult?)_taskCompletionSource.Task.Result; + } + catch (OperationCanceledException) + { + _logger?.LogError("JNI execution timed out."); + return default; } catch (Exception e) { - Debug.unityLogger.Log(LogType.Exception, UnityLogger.LogTag, $"Error during JNI execution: {e}"); + _logger?.LogError(e, "Error during JNI execution."); + return default; + } + finally + { + _currentTask = null!; } - - return default; } } - public void Run(Action jniOperation) + public void Run(Action jniOperation, TimeSpan? timeout = null) { lock (_lock) { + timeout ??= DefaultTimeout; + using var timeoutCts = new CancellationTokenSource(timeout.Value); _taskCompletionSource = new TaskCompletionSource(); _currentTask = jniOperation; _taskEvent.Set(); try { - _taskCompletionSource.Task.Wait(); + _taskCompletionSource.Task.Wait(timeoutCts.Token); + } + catch (OperationCanceledException) + { + _logger?.LogError("JNI execution timed out."); } catch (Exception e) { - Debug.unityLogger.Log(LogType.Exception, UnityLogger.LogTag, $"Error during JNI execution: {e}"); + _logger?.LogError(e, "Error during JNI execution."); + } + finally + { + _currentTask = null!; } } } public void Dispose() { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + _shutdownSource.Cancel(); + try + { + _workerThread?.Join(100); + } + catch (ThreadStateException) + { + _logger?.LogError("JNI Executor Worker thread was never started during disposal"); + } + catch (ThreadInterruptedException) + { + _logger?.LogError("JNI Executor Worker thread was interrupted during disposal"); + } + _taskEvent.Dispose(); } } diff --git a/src/Sentry.Unity.Android/SentryJava.cs b/src/Sentry.Unity.Android/SentryJava.cs index 9e185f6f8..6f047f586 100644 --- a/src/Sentry.Unity.Android/SentryJava.cs +++ b/src/Sentry.Unity.Android/SentryJava.cs @@ -1,10 +1,15 @@ using System; +using System.Diagnostics; +using Sentry.Extensibility; using UnityEngine; +using Debug = UnityEngine.Debug; namespace Sentry.Unity.Android; internal interface ISentryJava { + public bool IsEnabled(IJniExecutor jniExecutor); + public bool Init(IJniExecutor jniExecutor, SentryUnityOptions options, TimeSpan timeout); public string? GetInstallationId(IJniExecutor jniExecutor); public bool? CrashedLastRun(IJniExecutor jniExecutor); public void Close(IJniExecutor jniExecutor); @@ -40,6 +45,93 @@ internal class SentryJava : ISentryJava { private static AndroidJavaObject GetSentryJava() => new AndroidJavaClass("io.sentry.Sentry"); + public bool IsEnabled(IJniExecutor jniExecutor) + { + return jniExecutor.Run(() => + { + using var sentry = GetSentryJava(); + return sentry.CallStatic("isEnabled"); + }); + } + + public bool Init(IJniExecutor jniExecutor, SentryUnityOptions options, TimeSpan timeout) + { + jniExecutor.Run(() => + { + using var sentry = new AndroidJavaClass("io.sentry.android.core.SentryAndroid"); + using var context = new AndroidJavaClass("com.unity3d.player.UnityPlayer") + .GetStatic("currentActivity"); + + sentry.CallStatic("init", context, new AndroidOptionsConfiguration(androidOptions => + { + androidOptions.Call("setDsn", options.Dsn); + androidOptions.Call("setDebug", options.Debug); + androidOptions.Call("setRelease", options.Release); + androidOptions.Call("setEnvironment", options.Environment); + + var sentryLevelClass = new AndroidJavaClass("io.sentry.SentryLevel"); + var levelString = GetLevelString(options.DiagnosticLevel); + var sentryLevel = sentryLevelClass.GetStatic(levelString); + androidOptions.Call("setDiagnosticLevel", sentryLevel); + + if (options.SampleRate.HasValue) + { + androidOptions.SetIfNotNull("setSampleRate", options.SampleRate.Value); + } + + androidOptions.Call("setMaxBreadcrumbs", options.MaxBreadcrumbs); + androidOptions.Call("setMaxCacheItems", options.MaxCacheItems); + androidOptions.Call("setSendDefaultPii", options.SendDefaultPii); + androidOptions.Call("setEnableNdk", options.NdkIntegrationEnabled); + androidOptions.Call("setEnableScopeSync", options.NdkScopeSyncEnabled); + + // Options that are not to be set by the user + // We're disabling some integrations as to not duplicate event or because the SDK relies on the .NET SDK + // implementation of certain feature - i.e. Session Tracking + + // Note: doesn't work - produces a blank (white) screenshot + androidOptions.Call("setAttachScreenshot", false); + androidOptions.Call("setEnableAutoSessionTracking", false); + androidOptions.Call("setEnableActivityLifecycleBreadcrumbs", false); + androidOptions.Call("setAnrEnabled", false); + androidOptions.Call("setEnableScopePersistence", false); + }, options.DiagnosticLogger)); + }, timeout); + + return IsEnabled(jniExecutor); + } + + internal class AndroidOptionsConfiguration : AndroidJavaProxy + { + private readonly Action _callback; + private readonly IDiagnosticLogger? _logger; + + public AndroidOptionsConfiguration(Action callback, IDiagnosticLogger? logger) + : base("io.sentry.Sentry$OptionsConfiguration") + { + _callback = callback; + _logger = logger; + } + + public override AndroidJavaObject? Invoke(string methodName, AndroidJavaObject[] args) + { + try + { + if (methodName != "configure" || args.Length != 1) + { + throw new Exception($"Invalid invocation: {methodName}({args.Length} args)"); + } + + _callback(args[0]); + } + catch (Exception e) + { + _logger?.LogError(e, "Error invoking {0} ’.", methodName); + } + return null; + } + } + public string? GetInstallationId(IJniExecutor jniExecutor) { return jniExecutor.Run(() => @@ -165,6 +257,17 @@ public ScopeCallback(Action callback) : base("io.sentry.Scope return null; } } + + // https://github.com/getsentry/sentry-java/blob/db4dfc92f202b1cefc48d019fdabe24d487db923/sentry/src/main/java/io/sentry/SentryLevel.java#L4-L9 + internal static string GetLevelString(SentryLevel level) => level switch + { + SentryLevel.Debug => "DEBUG", + SentryLevel.Error => "ERROR", + SentryLevel.Fatal => "FATAL", + SentryLevel.Info => "INFO", + SentryLevel.Warning => "WARNING", + _ => "DEBUG" + }; } internal static class AndroidJavaObjectExtension @@ -186,6 +289,8 @@ public static void SetIfNotNull(this AndroidJavaObject javaObject, string pro } public static void SetIfNotNull(this AndroidJavaObject javaObject, string property, int? value) => SetIfNotNull(javaObject, property, value, "java.lang.Integer"); + public static void SetIfNotNull(this AndroidJavaObject javaObject, string property, bool value) => + SetIfNotNull(javaObject, property, value, "java.lang.Boolean"); public static void SetIfNotNull(this AndroidJavaObject javaObject, string property, bool? value) => SetIfNotNull(javaObject, property, value, "java.lang.Boolean"); } diff --git a/src/Sentry.Unity.Android/SentryNativeAndroid.cs b/src/Sentry.Unity.Android/SentryNativeAndroid.cs index a6db01640..350f828d1 100644 --- a/src/Sentry.Unity.Android/SentryNativeAndroid.cs +++ b/src/Sentry.Unity.Android/SentryNativeAndroid.cs @@ -37,7 +37,18 @@ public static void Configure(SentryUnityOptions options, ISentryUnityInfo sentry return; } - JniExecutor ??= new JniExecutor(); + JniExecutor ??= new JniExecutor(options.DiagnosticLogger); + + if (SentryJava.IsEnabled(JniExecutor)) + { + options.DiagnosticLogger?.LogDebug("The Android SDK is already initialized"); + } + // Local testing had Init at an average of about 25ms. + else if (!SentryJava.Init(JniExecutor, options, TimeSpan.FromMilliseconds(200))) + { + options.DiagnosticLogger?.LogError("Failed to initialize Android Native Support"); + return; + } options.NativeContextWriter = new NativeContextWriter(JniExecutor, SentryJava); options.ScopeObserver = new AndroidJavaScopeObserver(options, JniExecutor); @@ -98,6 +109,8 @@ public static void Configure(SentryUnityOptions options, ISentryUnityInfo sentry options.DiagnosticLogger?.LogDebug("Failed to create new 'Default User ID'."); } } + + options.DiagnosticLogger?.LogInfo("Successfully configured the Android SDK"); } /// @@ -119,7 +132,7 @@ internal static void Close(SentryUnityOptions options, ISentryUnityInfo sentryUn // This is an edge-case where the Android SDK has been enabled and setup during build-time but is being // shut down at runtime. In this case Configure() has not been called and there is no JniExecutor yet - JniExecutor ??= new JniExecutor(); + JniExecutor ??= new JniExecutor(options.DiagnosticLogger); SentryJava?.Close(JniExecutor); JniExecutor.Dispose(); } diff --git a/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs b/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs index c461662d4..bacab1b88 100644 --- a/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs +++ b/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs @@ -72,6 +72,13 @@ internal AndroidManifestConfiguration( public void OnPostGenerateGradleAndroidProject(string basePath) { + if (_options is null) + { + _logger.LogWarning("Android native support disabled because Sentry has not been configured. " + + "You can do that through the editor: {0}", SentryWindow.EditorMenuPath); + return; + } + if (_scriptingImplementation != ScriptingImplementation.IL2CPP) { if (_options is { AndroidNativeSupportEnabled: true }) @@ -130,7 +137,18 @@ internal void ModifyManifest(string basePath) androidManifest.AddDisclaimerComment(); - _logger.LogDebug("Configuring Sentry options on AndroidManifest: {0}", basePath); + if (_options?.AndroidNativeInitializationType is NativeInitializationType.Runtime) + { + _logger.LogDebug("Setting 'auto-init' to 'false'. The Android SDK will be initialized at runtime."); + androidManifest.SetAutoInit(false); + _ = androidManifest.Save(); + + return; + } + + _logger.LogInfo("Adding Sentry options to the AndroidManifest."); + _logger.LogDebug("Modifying AndroidManifest: {0}", basePath); + androidManifest.SetSDK("sentry.java.android.unity"); _logger.LogDebug("Setting DSN: {0}", _options!.Dsn); androidManifest.SetDsn(_options.Dsn!); @@ -419,6 +437,9 @@ internal void RemovePreviousConfigurations() public void AddDisclaimerComment() => _applicationElement.AppendChild(_applicationElement.OwnerDocument.CreateComment(Disclaimer)); + internal void SetAutoInit(bool enableAutoInit) + => SetMetaData($"{SentryPrefix}.auto-init", enableAutoInit.ToString()); + internal void SetDsn(string dsn) => SetMetaData($"{SentryPrefix}.dsn", dsn); internal void SetSampleRate(float sampleRate) => diff --git a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs index 32c2a39f6..176baff83 100644 --- a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs +++ b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs @@ -87,7 +87,7 @@ internal static void Configure(SentryUnityOptions options, ISentryUnityInfo sent } } - options.DiagnosticLogger?.LogInfo("Successfully initialized the native SDK"); + options.DiagnosticLogger?.LogInfo("Successfully configured the native SDK"); } /// diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 608e427a9..0bf35ad6c 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -150,9 +150,15 @@ public sealed class SentryUnityOptions : SentryOptions public bool IosNativeSupportEnabled { get; set; } = true; /// - /// Whether the SDK should initialize the native SDK before the Unity game. This bakes the options at build-time into - /// the generated Xcode project + /// Whether the SDK should initialize the native SDK before the game starts. This bakes the options at build-time into + /// the generated Xcode project. Modifying the options at runtime will not affect the options used to initialize + /// the native SDK. /// + /// + /// When set to , the options are written and hardcoded into the + /// Xcode project during the build process. This means that the options cannot be changed at runtime, as they are + /// embedded into the project itself. + /// public NativeInitializationType IosNativeInitializationType { get; set; } = NativeInitializationType.Runtime; /// @@ -160,6 +166,18 @@ public sealed class SentryUnityOptions : SentryOptions /// public bool AndroidNativeSupportEnabled { get; set; } = true; + /// + /// Whether the SDK should initialize the native SDK before the game starts. This bakes the options at build-time into + /// the generated Gradle project. Modifying the options at runtime will not affect the options used to initialize + /// the native SDK. + /// + /// + /// When set to , the options are written and hardcoded into the + /// Gradle project during the build process. This means that the options cannot be changed at runtime, as they are + /// embedded into the project itself. + /// + public NativeInitializationType AndroidNativeInitializationType { get; set; } = NativeInitializationType.Runtime; + /// /// Whether the SDK should add the NDK integration for Android /// diff --git a/test/Sentry.Unity.Android.Tests/JniExecutorTests.cs b/test/Sentry.Unity.Android.Tests/JniExecutorTests.cs new file mode 100644 index 000000000..30e296e91 --- /dev/null +++ b/test/Sentry.Unity.Android.Tests/JniExecutorTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Threading; +using NUnit.Framework; +using Sentry.Unity.Tests.SharedClasses; + +namespace Sentry.Unity.Android.Tests +{ + [TestFixture] + public class JniExecutorTests + { + private TestLogger _logger = null!; // Set during SetUp + private JniExecutor _sut = null!; // Set during SetUp + + [SetUp] + public void SetUp() + { + _logger = new TestLogger(); + _sut = new JniExecutor(_logger); + } + + [TearDown] + public void TearDown() + { + _sut.Dispose(); + } + + [Test] + public void Run_Action_ExecutesSuccessfully() + { + // Arrange + var executed = false; + var action = () => executed = true; + + // Act + _sut.Run(action); + + // Assert + Assert.That(executed, Is.True); + } + + [Test] + public void Run_FuncBool_ReturnsExpectedResult() + { + // Arrange + var func = () => true; + + // Act + var result = _sut.Run(func); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void Run_FuncString_ReturnsExpectedResult() + { + // Arrange + const string? expected = "Hello"; + var func = () => expected; + + // Act + var result = _sut.Run(func); + + // Assert + Assert.AreEqual(expected, result); + } + + [Test] + public void Run_WithTimeout_LogsErrorOnTimeout() + { + // Arrange + var slowAction = () => Thread.Sleep(100); + var timeout = TimeSpan.FromMilliseconds(50); + + // Act + _sut.Run(slowAction, timeout); + + // Assert + Assert.IsTrue(_logger.Logs.Any(log => + log.logLevel == SentryLevel.Error && + log.message.Contains("JNI execution timed out."))); + } + + [Test] + public void Run_ThrowingOperation_LogsError() + { + // Arrange + var exception = new Exception("Test exception"); + Action throwingAction = () => throw exception; + + // Act + _sut.Run(throwingAction); + + // Assert + Assert.IsTrue(_logger.Logs.Any(log => + log.logLevel == SentryLevel.Error && + log.message.Contains("Error during JNI execution."))); + } + + [Test] + public void Run_Generic_ReturnsDefaultOnException() + { + // Arrange + Func throwingFunc = () => throw new Exception("Test exception"); + + // Act + var result = _sut.Run(throwingFunc); + + // Assert + Assert.That(result, Is.Null); + } + } +} diff --git a/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs b/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs index 6e10d8328..8c04a9d68 100644 --- a/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs +++ b/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs @@ -1,6 +1,8 @@ using System; +using System.Linq; using System.Threading; using NUnit.Framework; +using Sentry.Unity.Tests.SharedClasses; namespace Sentry.Unity.Android.Tests; @@ -10,6 +12,9 @@ public class SentryNativeAndroidTests private Action? _originalReinstallSentryNativeBackendStrategy; private Action _fakeReinstallSentryNativeBackendStrategy; private TestUnityInfo _sentryUnityInfo = null!; + private TestSentryJava _testSentryJava = null!; + private readonly TestLogger _logger = new(); + private SentryUnityOptions _options = null!; public SentryNativeAndroidTests() => _fakeReinstallSentryNativeBackendStrategy = () => _reinstallCalled = true; @@ -23,8 +28,16 @@ public void SetUp() _reinstallCalled = false; _sentryUnityInfo = new TestUnityInfo { IL2CPP = false }; - SentryNativeAndroid.JniExecutor = new TestJniExecutor(); - SentryNativeAndroid.SentryJava = new TestSentryJava(); + SentryNativeAndroid.JniExecutor ??= new JniExecutor(_logger); + _testSentryJava = new TestSentryJava(); + SentryNativeAndroid.SentryJava = _testSentryJava; + + _options = new SentryUnityOptions + { + Debug = true, + DiagnosticLevel = SentryLevel.Debug, + DiagnosticLogger = _logger + }; } [TearDown] @@ -36,41 +49,38 @@ public void TearDown() => [Test] public void Configure_DefaultConfiguration_SetsScopeObserver() { - var options = new SentryUnityOptions(); - SentryNativeAndroid.Configure(options, _sentryUnityInfo); - Assert.IsAssignableFrom(options.ScopeObserver); + SentryNativeAndroid.Configure(_options, _sentryUnityInfo); + Assert.IsAssignableFrom(_options.ScopeObserver); } [Test] public void Configure_DefaultConfiguration_SetsCrashedLastRun() { - var options = new SentryUnityOptions(); - SentryNativeAndroid.Configure(options, _sentryUnityInfo); - Assert.IsNotNull(options.CrashedLastRun); + SentryNativeAndroid.Configure(_options, _sentryUnityInfo); + Assert.IsNotNull(_options.CrashedLastRun); } [Test] public void Configure_NativeAndroidSupportDisabled_ObserverIsNull() { - var options = new SentryUnityOptions { AndroidNativeSupportEnabled = false }; - SentryNativeAndroid.Configure(options, _sentryUnityInfo); - Assert.Null(options.ScopeObserver); + _options.AndroidNativeSupportEnabled = false; + SentryNativeAndroid.Configure(_options, _sentryUnityInfo); + Assert.Null(_options.ScopeObserver); } [Test] public void Configure_DefaultConfiguration_EnablesScopeSync() { - var options = new SentryUnityOptions(); - SentryNativeAndroid.Configure(options, _sentryUnityInfo); - Assert.True(options.EnableScopeSync); + SentryNativeAndroid.Configure(_options, _sentryUnityInfo); + Assert.True(_options.EnableScopeSync); } [Test] public void Configure_NativeAndroidSupportDisabled_DisabledScopeSync() { - var options = new SentryUnityOptions { AndroidNativeSupportEnabled = false }; - SentryNativeAndroid.Configure(options, _sentryUnityInfo); - Assert.False(options.EnableScopeSync); + _options.AndroidNativeSupportEnabled = false; + SentryNativeAndroid.Configure(_options, _sentryUnityInfo); + Assert.False(_options.EnableScopeSync); } [Test] @@ -81,7 +91,7 @@ public void Configure_IL2CPP_ReInitializesNativeBackend(bool il2cpp, bool expect _sentryUnityInfo.IL2CPP = il2cpp; Assert.False(_reinstallCalled); // Sanity check - SentryNativeAndroid.Configure(new(), _sentryUnityInfo); + SentryNativeAndroid.Configure(_options, _sentryUnityInfo); Assert.AreEqual(expectedReinstall, _reinstallCalled); } @@ -89,20 +99,60 @@ public void Configure_IL2CPP_ReInitializesNativeBackend(bool il2cpp, bool expect [Test] public void Configure_NativeAndroidSupportDisabled_DoesNotReInitializeNativeBackend() { - var options = new SentryUnityOptions { AndroidNativeSupportEnabled = false }; - SentryNativeAndroid.Configure(options, _sentryUnityInfo); + _options.AndroidNativeSupportEnabled = false; + SentryNativeAndroid.Configure(_options, _sentryUnityInfo); Assert.False(_reinstallCalled); } [Test] public void Configure_NoInstallationIdReturned_SetsNewDefaultUserId() { - var options = new SentryUnityOptions(); - var sentryJava = SentryNativeAndroid.SentryJava as TestSentryJava; - Assert.NotNull(sentryJava); - sentryJava!.InstallationId = string.Empty; + _testSentryJava.InstallationId = string.Empty; + + SentryNativeAndroid.Configure(_options, _sentryUnityInfo); + Assert.False(string.IsNullOrEmpty(_options.DefaultUserId)); + } + + [Test] + public void Configure_DefaultConfigurationSentryJavaNotPresent_LogsErrorAndReturns() + { + _testSentryJava.SentryPresent = false; + + SentryNativeAndroid.Configure(_options, _sentryUnityInfo); + + Assert.IsTrue(_logger.Logs.Any(log => + log.logLevel == SentryLevel.Error && + log.message.Contains("Sentry Java SDK is missing."))); + + Assert.Null(_options.ScopeObserver); + } + + [Test] + public void Configure_NativeAlreadyInitialized_LogsAndConfigures() + { + _testSentryJava.Enabled = true; + + SentryNativeAndroid.Configure(_options, _sentryUnityInfo); + + Assert.IsTrue(_logger.Logs.Any(log => + log.logLevel == SentryLevel.Debug && + log.message.Contains("The Android SDK is already initialized"))); + + Assert.NotNull(_options.ScopeObserver); + } + + [Test] + public void Configure_NativeInitFails_LogsErrorAndReturns() + { + _testSentryJava.Enabled = false; + _testSentryJava.InitSuccessful = false; + + SentryNativeAndroid.Configure(_options, _sentryUnityInfo); + + Assert.IsTrue(_logger.Logs.Any(log => + log.logLevel == SentryLevel.Error && + log.message.Contains("Failed to initialize Android Native Support"))); - SentryNativeAndroid.Configure(options, _sentryUnityInfo); - Assert.False(string.IsNullOrEmpty(options.DefaultUserId)); + Assert.Null(_options.ScopeObserver); } } diff --git a/test/Sentry.Unity.Android.Tests/TestJniExecutor.cs b/test/Sentry.Unity.Android.Tests/TestJniExecutor.cs deleted file mode 100644 index 2e3fbf049..000000000 --- a/test/Sentry.Unity.Android.Tests/TestJniExecutor.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Sentry.Unity.Android.Tests; - -public class TestJniExecutor : IJniExecutor -{ - public TResult? Run(Func jniOperation) - { - return default; - } - - public void Run(Action jniOperation) - { - } - - public void Dispose() - { - // TODO release managed resources here - } -} diff --git a/test/Sentry.Unity.Android.Tests/TestSentryJava.cs b/test/Sentry.Unity.Android.Tests/TestSentryJava.cs index 26d1d8d0a..6df74c584 100644 --- a/test/Sentry.Unity.Android.Tests/TestSentryJava.cs +++ b/test/Sentry.Unity.Android.Tests/TestSentryJava.cs @@ -1,10 +1,19 @@ +using System; + namespace Sentry.Unity.Android.Tests; internal class TestSentryJava : ISentryJava { + public bool Enabled { get; set; } = true; + public bool InitSuccessful { get; set; } = true; + public bool SentryPresent { get; set; } = true; public string? InstallationId { get; set; } public bool? IsCrashedLastRun { get; set; } + public bool IsEnabled(IJniExecutor jniExecutor) => Enabled; + + public bool Init(IJniExecutor jniExecutor, SentryUnityOptions options, TimeSpan timeout) => InitSuccessful; + public string? GetInstallationId(IJniExecutor jniExecutor) => InstallationId; public bool? CrashedLastRun(IJniExecutor jniExecutor) => IsCrashedLastRun; @@ -30,5 +39,5 @@ public void WriteScope( string? GpuGraphicsShaderLevel) { } - public bool IsSentryJavaPresent() => true; + public bool IsSentryJavaPresent() => SentryPresent; } diff --git a/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs b/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs index 5d99159e2..954c6b194 100644 --- a/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs +++ b/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs @@ -28,6 +28,8 @@ public Fixture() Enabled = true, Dsn = "https://k@h/p", AndroidNativeSupportEnabled = true, + // Setting to `BuildTime` and not (default)`Runtime` as most testcases deal with the manipulation of the AndroidManifest + AndroidNativeInitializationType = NativeInitializationType.BuildTime, Debug = true }; SentryUnityOptions.DiagnosticLogger = new UnityLogger(SentryUnityOptions, UnityTestLogger); @@ -123,6 +125,30 @@ public void ModifyManifest_UnityOptions_AndroidNativeSupportEnabledFalse_LogDebu Assert.False(manifest.Contains("io.sentry.dsn")); } + [Test] + public void ModifyManifest_UnityOptions_AndroidNativeSupportEnabled_InitTypeRuntime_AddsSentryAndAutoInitIsFalse() + { + _fixture.SentryUnityOptions!.AndroidNativeInitializationType = NativeInitializationType.Runtime; + var sut = _fixture.GetSut(); + var manifest = WithAndroidManifest(basePath => sut.ModifyManifest(basePath)); + + _fixture.UnityTestLogger.AssertLogContains(SentryLevel.Debug, "Setting 'auto-init' to 'false'. The Android SDK will be initialized at runtime."); // Sanity Check + + StringAssert.Contains("", manifest); + } + + [Test] + public void ModifyManifest_UnityOptions_AndroidNativeSupportEnabled_InitTypeBuildTime_AddsSentryAndAutoInits() + { + _fixture.SentryUnityOptions!.AndroidNativeInitializationType = NativeInitializationType.BuildTime; + var sut = _fixture.GetSut(); + var manifest = WithAndroidManifest(basePath => sut.ModifyManifest(basePath)); + + _fixture.UnityTestLogger.AssertLogContains(SentryLevel.Info, "Adding Sentry options to the AndroidManifest."); // Sanity Check + + StringAssert.Contains($"", manifest); + } + [Test] public void ModifyManifest_ManifestHasDsn() {