From 5e5c48ea86028b0d66c3b48863e696852c5ac353 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 16 Oct 2026 16:56:29 +0200 Subject: [PATCH] wip --- docs/agent-guides/platform-android.md | 1 + docs/agent-guides/platform-apple.md | 1 + .../SentryNativeAndroid.cs | 23 ++++++++- .../BuildPostProcess.cs | 4 +- .../Android/AndroidManifestConfiguration.cs | 20 +++++++- src/Sentry.Unity.iOS/SentryNativeCocoa.cs | 24 +++++++-- src/Sentry.Unity/SentryUnityOptions.cs | 10 ++++ .../SentryNativeAndroidTests.cs | 40 +++++++++++++++ .../TestSentryJava.cs | 6 ++- .../AndroidManifestConfigurationTests.cs | 51 +++++++++++++++++++ .../BuildPostProcessorTests.cs | 20 ++++++++ 11 files changed, 189 insertions(+), 11 deletions(-) diff --git a/docs/agent-guides/platform-android.md b/docs/agent-guides/platform-android.md index 0f3b4f4f7..040b95860 100644 --- a/docs/agent-guides/platform-android.md +++ b/docs/agent-guides/platform-android.md @@ -12,6 +12,7 @@ - Runtime initialization writes `io.sentry.auto-init=false`; C# initializes the Java SDK later. - Build-time initialization writes options to the Android manifest and lets Java auto-initialize. - Runtime option changes cannot alter build-time initialization behavior. +- External initialization leaves `auto-init` unset (it would merge into the host app's manifest) and does not copy the Android SDK or add its Gradle dependencies; the host app depends on `io.sentry:sentry-android`. ProGuard keep rules still ship. C# never initializes or closes the Java SDK, attaches only if the host already initialized it, and keeps the C# ANR watchdog. ## Export Processing diff --git a/docs/agent-guides/platform-apple.md b/docs/agent-guides/platform-apple.md index cf68019e4..0df7848ba 100644 --- a/docs/agent-guides/platform-apple.md +++ b/docs/agent-guides/platform-apple.md @@ -19,6 +19,7 @@ - App-hang tracking disables the C# ANR watchdog on iOS and macOS when the Cocoa backend is active. - macOS Cocoa screenshots are disabled because UIKit is unavailable. - Cocoa attachment synchronization is not implemented. +- iOS external initialization: C# never initializes or closes Cocoa, attaches only if the host app already initialized it, keeps the host's SDK name, and keeps the C# ANR watchdog. ## iOS Export diff --git a/src/Sentry.Unity.Android/SentryNativeAndroid.cs b/src/Sentry.Unity.Android/SentryNativeAndroid.cs index 005aed753..0b8de6dee 100644 --- a/src/Sentry.Unity.Android/SentryNativeAndroid.cs +++ b/src/Sentry.Unity.Android/SentryNativeAndroid.cs @@ -39,6 +39,13 @@ public static void Configure(SentryUnityOptions options) SentryJava ??= new SentryJava(Logger); if (!SentryJava.IsSentryJavaPresent()) { + if (options.AndroidNativeInitializationType is NativeInitializationType.External) + { + Logger?.LogWarning("The Android SDK is expected to be provided by the app but it is missing. " + + "Add 'io.sentry:sentry-android' to the app. Skipping native support."); + return; + } + Logger?.LogError("Android Native Support has been enabled but the " + "Android SDK is missing. This could have been caused by a mismatching" + "build time / runtime configuration. Please make sure you have " + @@ -48,10 +55,17 @@ public static void Configure(SentryUnityOptions options) Logger?.LogDebug("Checking whether the Android SDK has already been initialized"); + var isExternal = options.AndroidNativeInitializationType is NativeInitializationType.External; if (SentryJava.IsEnabled() is true) { Logger?.LogDebug("The Android SDK is already initialized"); } + else if (isExternal) + { + Logger?.LogWarning("The Android SDK is expected to be initialized by the app but it is not. " + + "Make sure the app initializes Sentry before the game starts. Skipping native support."); + return; + } else { Logger?.LogInfo("Initializing the Android SDK"); @@ -74,7 +88,8 @@ public static void Configure(SentryUnityOptions options) options.EnableScopeSync = true; options.NativeDebugImageProvider = new Native.NativeDebugImageProvider(); - if (options.NativeAppHangTrackingEnabled && options.NdkIntegrationEnabled) + // The app's options decide whether sentry-native tracks app hangs, so the C# ANR watchdog stays in charge. + if (!isExternal && options.NativeAppHangTrackingEnabled && options.NdkIntegrationEnabled) { Logger?.LogDebug("Starting the app-hang heartbeat coroutine."); SentryMonoBehaviour.Instance.StartAppHangHeartbeat( @@ -119,7 +134,11 @@ public static void Configure(SentryUnityOptions options) e, "Failed to reinstall backend. Captured native crashes will miss scope data and tag."); } - options.NativeSupportCloseCallback = () => Close(options); + // The app owns the SDK's lifetime. + if (!isExternal) + { + options.NativeSupportCloseCallback = () => Close(options); + } Logger?.LogDebug("Fetching installation ID"); diff --git a/src/Sentry.Unity.Editor.iOS/BuildPostProcess.cs b/src/Sentry.Unity.Editor.iOS/BuildPostProcess.cs index 1285ed51d..0691b9e22 100644 --- a/src/Sentry.Unity.Editor.iOS/BuildPostProcess.cs +++ b/src/Sentry.Unity.Editor.iOS/BuildPostProcess.cs @@ -76,8 +76,8 @@ internal static void AddSentryToXcodeProject(SentryUnityOptions? options, // We want to avoid users getting stuck on a cached built output. // This can happen if the user appends builds // - and toggles the `IosNativeSupportEnabled` from `true` to `false` - // - and toggles the `IosNativeInitializationType` from `Standalone` to `Runtime` - if (options.IosNativeInitializationType is NativeInitializationType.Runtime) + // - and toggles the `IosNativeInitializationType` from `BuildTime` to `Runtime` or `External` + if (options.IosNativeInitializationType is not NativeInitializationType.BuildTime) { var mainPath = Path.Combine(pathToProject, SentryXcodeProject.MainPath); if (File.Exists(mainPath)) diff --git a/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs b/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs index f2519ba9c..9b0601c8b 100644 --- a/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs +++ b/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs @@ -120,6 +120,13 @@ public void OnPostGenerateGradleAndroidProject(string basePath) /// private bool AndroidSdkEnabled => _androidSdkEnabled ??= EvaluateAndroidSdkEnabled(); + /// + /// Whether the Android SDK gets copied into the Gradle project. With + /// the app that embeds the game provides it. + /// + private bool BundlesAndroidSdk => + AndroidSdkEnabled && _options?.AndroidNativeInitializationType is not NativeInitializationType.External; + private bool EvaluateAndroidSdkEnabled() { if (_options is null) @@ -167,6 +174,15 @@ internal void ModifyManifest(string basePath) return; } + if (_options?.AndroidNativeInitializationType is NativeInitializationType.External) + { + // 'auto-init' would merge into the app's manifest and turn off the app's SDK. + _logger.LogDebug("Leaving 'auto-init' unset. The app initializes the Android SDK."); + _ = androidManifest.Save(); + + return; + } + if (_options?.AndroidNativeInitializationType is NativeInitializationType.Runtime) { _logger.LogDebug("Setting 'auto-init' to 'false'. The Android SDK will be initialized at runtime."); @@ -258,7 +274,7 @@ internal void CopyAndroidSdkToGradleProject(string unityProjectPath, string grad var androidSdkPath = Path.Combine(unityProjectPath, "Packages", SentryPackageInfo.GetName(), "Plugins", "Android", "Sentry~"); var targetPath = Path.Combine(gradlePath, "unityLibrary", "libs"); - if (AndroidSdkEnabled) + if (BundlesAndroidSdk) { if (!Directory.Exists(androidSdkPath)) { @@ -301,7 +317,7 @@ internal void CopyAndroidSdkToGradleProject(string unityProjectPath, string grad internal void AddAndroidSdkDependencies(string gradleProjectPath) { var tool = new GradleSetup(_logger, gradleProjectPath); - var nativeSupportEnabled = AndroidSdkEnabled; + var nativeSupportEnabled = BundlesAndroidSdk; try { diff --git a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs index 3d6293238..e2d67d04d 100644 --- a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs +++ b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs @@ -30,12 +30,21 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf return; } + var isExternal = platform == RuntimePlatform.IPhonePlayer && + options.IosNativeInitializationType is NativeInitializationType.External; + if (platform == RuntimePlatform.IPhonePlayer) { if (SentryCocoaBridgeProxy.IsEnabled()) { Logger?.LogDebug("The native SDK is already initialized"); } + else if (isExternal) + { + Logger?.LogWarning("The native SDK is expected to be initialized by the app but it is not. " + + "Make sure the app initializes Sentry before the game starts. Skipping native support."); + return; + } else if (!SentryCocoaBridgeProxy.Init(options)) { Logger?.LogWarning("Failed to initialize the native SDK"); @@ -55,7 +64,8 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf options.ScopeObserver = new NativeScopeObserver("macOS", options); } - if (options.EnableAppHangTracking) + // The app's options decide whether sentry-cocoa tracks app hangs, so the C# ANR watchdog stays in charge. + if (!isExternal && options.EnableAppHangTracking) { Logger?.LogDebug("Disabling the C# ANR watchdog - sentry-cocoa handles app hang detection."); #pragma warning disable CS0618 // Type or member is obsolete @@ -63,7 +73,10 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf #pragma warning restore CS0618 // Type or member is obsolete } - SentryCocoaBridgeProxy.SetSdkName(); // Since we're not building the SDK we have to overwrite the name here + if (!isExternal) + { + SentryCocoaBridgeProxy.SetSdkName(); // Since we're not building the SDK we have to overwrite the name here + } options.NativeContextWriter = new NativeContextWriter(); options.EnableScopeSync = true; @@ -77,7 +90,12 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf return crashedLastRun; }; - options.NativeSupportCloseCallback += () => Close(options); + // The app owns the SDK's lifetime. + if (!isExternal) + { + options.NativeSupportCloseCallback += () => Close(options); + } + if (options.UnityInfo.IL2CPP) { var installationId = SentryCocoaBridgeProxy.GetInstallationId(); diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 95249276a..5d4f7af7e 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -267,6 +267,8 @@ internal bool NativeAppHangTrackingEnabled /// 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. + /// When set to , the app that embeds the game initializes the native + /// SDK, e.g. when using Unity as a Library. /// public NativeInitializationType IosNativeInitializationType { get; set; } = NativeInitializationType.Runtime; @@ -284,6 +286,8 @@ internal bool NativeAppHangTrackingEnabled /// 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. + /// When set to , the app that embeds the game initializes the native + /// SDK, e.g. when using Unity as a Library. /// public NativeInitializationType AndroidNativeInitializationType { get; set; } = NativeInitializationType.Runtime; @@ -748,4 +752,10 @@ public enum NativeInitializationType /// game. Options that you modify programmatically will not apply to the native SDK. /// BuildTime, + /// + /// The app that embeds the game owns the native SDK, e.g. when using Unity as a Library. The SDK does not initialize + /// or close the native SDK. It attaches to it if the app has initialized it before the game starts. Options that + /// you set in Unity do not apply to the native SDK. + /// + External, } diff --git a/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs b/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs index db5b37d7c..d0e560093 100644 --- a/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs +++ b/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs @@ -231,4 +231,44 @@ public void Configure_NativeInitFails_LogsErrorAndReturns() Assert.Null(options.ScopeObserver); } + + [Test] + public void Configure_ExternalNotInitialized_LogsWarningAndDoesNotInitialize() + { + var options = new SentryUnityOptions + { + Debug = true, + DiagnosticLevel = SentryLevel.Debug, + DiagnosticLogger = _logger, + AndroidNativeInitializationType = NativeInitializationType.External + }; + _testSentryJava.Enabled = false; + + SentryNativeAndroid.Configure(options); + + Assert.IsTrue(_logger.Logs.Any(log => + log.logLevel == SentryLevel.Warning && + log.message.Contains("expected to be initialized by the app"))); + Assert.False(_testSentryJava.InitCalled); + Assert.Null(options.ScopeObserver); + Assert.False(options.EnableScopeSync); + } + + [Test] + public void Configure_ExternalInitialized_AttachesWithoutTakingOwnership() + { + var options = new SentryUnityOptions + { + AndroidNativeInitializationType = NativeInitializationType.External + }; + _testSentryJava.Enabled = true; + + SentryNativeAndroid.Configure(options); + + Assert.False(_testSentryJava.InitCalled); + Assert.IsAssignableFrom(options.ScopeObserver); + Assert.True(options.EnableScopeSync); + Assert.True(_reinstallCalled); + Assert.Null(options.NativeSupportCloseCallback); + } } diff --git a/test/Sentry.Unity.Android.Tests/TestSentryJava.cs b/test/Sentry.Unity.Android.Tests/TestSentryJava.cs index 84c7f3735..08deedc51 100644 --- a/test/Sentry.Unity.Android.Tests/TestSentryJava.cs +++ b/test/Sentry.Unity.Android.Tests/TestSentryJava.cs @@ -10,16 +10,18 @@ internal class TestSentryJava : ISentryJava public bool SentryPresent { get; set; } = true; public string? InstallationId { get; set; } public bool? IsCrashedLastRun { get; set; } + public bool InitCalled { get; private set; } + public bool CloseCalled { get; private set; } public bool? IsEnabled() => Enabled; - public void Init(SentryUnityOptions options) { } + public void Init(SentryUnityOptions options) => InitCalled = true; public string? GetInstallationId() => InstallationId; public bool? CrashedLastRun() => IsCrashedLastRun; - public void Close() { } + public void Close() => CloseCalled = true; public void WriteScope( string? AppStartTime, diff --git a/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs b/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs index 6fb65b643..ab36d9676 100644 --- a/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs +++ b/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs @@ -170,6 +170,19 @@ public void ModifyManifest_UnityOptions_AndroidNativeSupportEnabled_InitTypeRunt StringAssert.Contains("", manifest); } + [Test] + public void ModifyManifest_UnityOptions_AndroidNativeSupportEnabled_InitTypeExternal_DoesNotSetAutoInitOrOptions() + { + _fixture.SentryUnityOptions!.AndroidNativeInitializationType = NativeInitializationType.External; + var sut = _fixture.GetSut(); + var manifest = WithAndroidManifest(basePath => sut.ModifyManifest(basePath)); + + _fixture.UnityTestLogger.AssertLogContains(SentryLevel.Debug, "Leaving 'auto-init' unset. The app initializes the Android SDK."); // Sanity Check + + StringAssert.DoesNotContain("io.sentry.auto-init", manifest); + StringAssert.DoesNotContain("io.sentry.dsn", manifest); + } + [Test] public void ModifyManifest_UnityOptions_AndroidNativeSupportEnabled_InitTypeBuildTime_AddsSentryAndAutoInits() { @@ -538,6 +551,44 @@ public void CopyAndroidSdkToGradleProject_AndroidNativeSupportEnabled_CopiesAndr Directory.Delete(fakeProjectPath, true); } + [Test] + public void CopyAndroidSdkToGradleProject_InitTypeExternalAndSdkAlreadyCopied_RemovesAndroidSdkFromGradleProject() + { + var fakeProjectPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var unityProjectPath = Path.Combine(fakeProjectPath, "UnityProject"); + var gradleProjectPath = Path.Combine(fakeProjectPath, "GradleProject"); + DebugSymbolUploadTests.SetupFakeProject(fakeProjectPath); + var androidSdk = Path.Combine(gradleProjectPath, "unityLibrary", "libs", "androidSdk.jar"); + File.Create(androidSdk).Close(); + + _fixture.SentryUnityOptions!.AndroidNativeInitializationType = NativeInitializationType.External; + var sut = _fixture.GetSut(); + + sut.CopyAndroidSdkToGradleProject(unityProjectPath, gradleProjectPath); + + Assert.IsFalse(File.Exists(androidSdk)); + + Directory.Delete(fakeProjectPath, true); + } + + [Test] + public void AddAndroidSdkDependencies_InitTypeExternal_DoesNotAddDependencies() + { + var fakeProjectPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var gradleProjectPath = Path.Combine(fakeProjectPath, "GradleProject"); + DebugSymbolUploadTests.SetupFakeProject(fakeProjectPath); + + _fixture.SentryUnityOptions!.AndroidNativeInitializationType = NativeInitializationType.External; + var sut = _fixture.GetSut(); + + sut.AddAndroidSdkDependencies(gradleProjectPath); + + var unityLibraryGradle = File.ReadAllText(Path.Combine(gradleProjectPath, "unityLibrary", "build.gradle")); + StringAssert.DoesNotContain(GradleSetup.SdkDependencies, unityLibraryGradle); + + Directory.Delete(fakeProjectPath, true); + } + [Test] public void CopyAndroidSdkToGradleProject_AndroidNativeSupportDisabledButSdkAlreadyCopied_RemovesAndroidSdkFromGradleProject() { diff --git a/test/Sentry.Unity.Editor.iOS.Tests/BuildPostProcessorTests.cs b/test/Sentry.Unity.Editor.iOS.Tests/BuildPostProcessorTests.cs index 445f682bd..9be06888e 100644 --- a/test/Sentry.Unity.Editor.iOS.Tests/BuildPostProcessorTests.cs +++ b/test/Sentry.Unity.Editor.iOS.Tests/BuildPostProcessorTests.cs @@ -100,6 +100,26 @@ public void AddSentryToXcodeProject_NativeSupportEnabledAndInitializationTypeRun Assert.IsFalse(NativeMain.ContainsSentry(mainFile, null)); // The main.mm must not contain Sentry } + [Test] + public void AddSentryToXcodeProject_NativeSupportEnabledAndInitializationTypeExternal_SetsUpSdkButDoesNotInitialize() + { + var options = new SentryUnityOptions + { + Dsn = "https://k@h/p", + IosNativeSupportEnabled = true, + IosNativeInitializationType = NativeInitializationType.External + }; + var testLogger = new TestLogger(); + + BuildPostProcess.AddSentryToXcodeProject(options, null, testLogger, _outputProjectPath); + + var mainFile = File.ReadAllText(Path.Combine(_outputProjectPath, SentryXcodeProject.MainPath)); + var bridgePath = Path.Combine(_outputProjectPath, "Libraries", SentryPackageInfo.GetName(), SentryXcodeProject.BridgeName); + + Assert.IsTrue(File.Exists(bridgePath)); + Assert.IsFalse(NativeMain.ContainsSentry(mainFile, null)); // The main.mm must not contain Sentry + } + [Test] public void AddSentryToXcodeProject_NativeSupportEnabledAndInitializationTypeBuildTime_SetsUpSdkAndAddsAutoInitialize() {