Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/agent-guides/platform-android.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/agent-guides/platform-apple.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 21 additions & 2 deletions src/Sentry.Unity.Android/SentryNativeAndroid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 " +
Expand All @@ -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");
Expand All @@ -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(
Expand Down Expand Up @@ -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");

Expand Down
4 changes: 2 additions & 2 deletions src/Sentry.Unity.Editor.iOS/BuildPostProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
20 changes: 18 additions & 2 deletions src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ public void OnPostGenerateGradleAndroidProject(string basePath)
/// </summary>
private bool AndroidSdkEnabled => _androidSdkEnabled ??= EvaluateAndroidSdkEnabled();

/// <summary>
/// Whether the Android SDK gets copied into the Gradle project. With <see cref="NativeInitializationType.External"/>
/// the app that embeds the game provides it.
/// </summary>
private bool BundlesAndroidSdk =>
AndroidSdkEnabled && _options?.AndroidNativeInitializationType is not NativeInitializationType.External;

private bool EvaluateAndroidSdkEnabled()
{
if (_options is null)
Expand Down Expand Up @@ -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.");
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -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
{
Expand Down
24 changes: 21 additions & 3 deletions src/Sentry.Unity.iOS/SentryNativeCocoa.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -55,15 +64,19 @@ 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
options.DisableAnrIntegration();
#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;
Expand All @@ -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();
Expand Down
10 changes: 10 additions & 0 deletions src/Sentry.Unity/SentryUnityOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ internal bool NativeAppHangTrackingEnabled
/// When set to <see cref="NativeInitializationType.BuildTime"/>, 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 <see cref="NativeInitializationType.External"/>, the app that embeds the game initializes the native
/// SDK, e.g. when using Unity as a Library.
/// </remarks>
public NativeInitializationType IosNativeInitializationType { get; set; } = NativeInitializationType.Runtime;

Expand All @@ -284,6 +286,8 @@ internal bool NativeAppHangTrackingEnabled
/// When set to <see cref="NativeInitializationType.BuildTime"/>, 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 <see cref="NativeInitializationType.External"/>, the app that embeds the game initializes the native
/// SDK, e.g. when using Unity as a Library.
/// </remarks>
public NativeInitializationType AndroidNativeInitializationType { get; set; } = NativeInitializationType.Runtime;

Expand Down Expand Up @@ -748,4 +752,10 @@ public enum NativeInitializationType
/// game. Options that you modify programmatically will not apply to the native SDK.
/// </summary>
BuildTime,
/// <summary>
/// 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.
/// </summary>
External,
}
40 changes: 40 additions & 0 deletions test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AndroidJavaScopeObserver>(options.ScopeObserver);
Assert.True(options.EnableScopeSync);
Assert.True(_reinstallCalled);
Assert.Null(options.NativeSupportCloseCallback);
}
}
6 changes: 4 additions & 2 deletions test/Sentry.Unity.Android.Tests/TestSentryJava.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,19 @@ public void ModifyManifest_UnityOptions_AndroidNativeSupportEnabled_InitTypeRunt
StringAssert.Contains("<meta-data android:name=\"io.sentry.auto-init\" android:value=\"false\" />", 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()
{
Expand Down Expand Up @@ -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()
{
Expand Down
20 changes: 20 additions & 0 deletions test/Sentry.Unity.Editor.iOS.Tests/BuildPostProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading