diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fa74a63d..8b032d356 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Features
+
+- Added `AndroidNativeAnrEnabled` (default `true`) to enable ANR detection through `sentry-java` SDK. The native ANR integration monitors the Android UI thread. On API ≥ 30 this uses [ANR v2](https://docs.sentry.io/platforms/android/configuration/app-not-respond/) via `ApplicationExitInfo` to report OS-detected ANRs from prior runs; on API < 30 it falls back to an in-process watchdog. This is complementary to the Unity SDK's C# watchdog, which monitors the Unity player loop. ([#2671](https://github.com/getsentry/sentry-unity/pull/2671))
+
### Dependencies
- Bump Cocoa SDK from v9.12.0 to v9.12.1 ([#2670](https://github.com/getsentry/sentry-unity/pull/2670))
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 620b8f644..8f6613858 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -25,95 +25,23 @@
$(SentryArtifactsDestination)Windows/Sentry/
-
- $(AppData)\UnityHub\secondaryInstallPath.json
- $(AppData)\UnityHub\defaultEditor.json
-
- C:\Program Files\Unity\Hub\Editor
- \Applications\Unity\Hub\Editor
- $(Home)\Unity\Hub\Editor
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @(item1->Replace('"', ''))
-
-
- @(item2->Replace('"', ''))
-
-
-
-
-
-
+
-
-
- <_AllUnityInstallDirs Include="$([System.IO.Directory]::GetDirectories('$(HubInstallDir)'))" />
- <_UnityInstalls Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(Filename)', '^[\d]{4}.*$'))" Include="@(_AllUnityInstallDirs->'%(Filename)%(Extension)')" />
-
-
-
-
- %(_UnityInstalls.Identity)
-
-
-
- <_UnityInnerPath Condition="!$([MSBuild]::IsOSPlatform('OSX'))">Editor\Data
- <_UnityInnerPath Condition="$([MSBuild]::IsOSPlatform('OSX'))">Unity.App\Contents
-
-
-
- <_PotentialUnityPaths Condition="$([MSBuild]::IsOSPlatform('Linux')) AND '$(UNITY_PATH)' != ''" Include="$(UNITY_PATH)\$(_UnityInnerPath)\Managed\UnityEngine.dll" />
- <_PotentialUnityPaths Condition="'$(HubInstallDir)' != ''" Include="$(HubInstallDir)\$(UnityVersion)\$(_UnityInnerPath)\Managed\UnityEngine.dll" />
- <_PotentialUnityPaths Condition="'$(HubInstallDir)' != '' AND '$(HubDefaultEditor)' != '' AND '$(UnityVersion)' != '$(HubDefaultEditor)'" Include="$(HubInstallDir)\$(HubDefaultEditor)\$(_UnityInnerPath)\Managed\UnityEngine.dll" />
- <_PotentialUnityPaths Condition="$([MSBuild]::IsOSPlatform('Windows'))" Include="C:\Program Files\Unity\$(_UnityInnerPath)\Managed\UnityEngine.dll" />
- <_PotentialUnityPaths Condition="$([MSBuild]::IsOSPlatform('OSX'))" Include="\Applications\Unity\$(_UnityInnerPath)\Managed\UnityEngine.dll" />
- <_UnityPathsFound Include="@(_PotentialUnityPaths->Exists())" />
- <_UnityPathsFoundReversed Include="@(_UnityPathsFound->Reverse())" />
-
+
+
+
-
- <_UnityPathProp>%(_UnityPathsFoundReversed.Identity)
+ $([System.Text.RegularExpressions.Regex]::Match($(_FindUnityOutput), 'UnityRoot=([^;\r\n]+)').Groups[1].Value)
+ $([System.Text.RegularExpressions.Regex]::Match($(_FindUnityOutput), 'UnityDataPath=([^;\r\n]+)').Groups[1].Value)
+ $([System.Text.RegularExpressions.Regex]::Match($(_FindUnityOutput), 'UnityManagedPath=([^;\r\n]+)').Groups[1].Value)
+ $(UnityDataPath)Resources/PackageManager/ProjectTemplates/libcache/
-
-
-
-
- <_UnityPath Include="$(_UnityPathProp)" />
-
-
-
- @(_UnityPath->DirectoryName())\
- @(_UnityPath->DirectoryName()->DirectoryName())\
- @(_UnityPath->DirectoryName()->DirectoryName()->DirectoryName())\
- $(UnityDataPath)Resources\PackageManager\ProjectTemplates\libcache\
-
-
-
-
+
diff --git a/scripts/find-unity.ps1 b/scripts/find-unity.ps1
new file mode 100644
index 000000000..51409619f
--- /dev/null
+++ b/scripts/find-unity.ps1
@@ -0,0 +1,119 @@
+#!/usr/bin/env pwsh
+# Locate a Unity installation and emit the paths the build needs.
+#
+# Output (stdout, one per line):
+# UnityRoot=
+# UnityDataPath=
+# UnityManagedPath=
+#
+# Warnings + the "candidates tried" error go to stderr. Exits non-zero on failure.
+
+[CmdletBinding()]
+param (
+ [string] $UnityVersion = '',
+ [string] $HubInstallDir = '',
+ [string] $HubDefaultEditor = ''
+)
+
+$ErrorActionPreference = 'Stop'
+
+function Read-HubFile([string] $path) {
+ if (-not (Test-Path $path)) { return '' }
+ # Unity Hub writes a single quoted string per file (not strict JSON).
+ return (Get-Content -Raw $path).Trim().Trim('"')
+}
+
+# --- Resolve Hub install dir + default editor -------------------------------
+
+# AppData is Windows-only; on macOS/Linux we just rely on the platform default below.
+$hubConfigDir = if ($env:APPDATA) { Join-Path $env:APPDATA 'UnityHub' } else { '' }
+
+if (-not $HubInstallDir -and $hubConfigDir) {
+ $HubInstallDir = Read-HubFile (Join-Path $hubConfigDir 'secondaryInstallPath.json')
+}
+if (-not $HubInstallDir) {
+ $HubInstallDir = if ($IsWindows) { 'C:/Program Files/Unity/Hub/Editor' }
+ elseif ($IsMacOS) { '/Applications/Unity/Hub/Editor' }
+ elseif ($IsLinux) { Join-Path $env:HOME 'Unity/Hub/Editor' }
+}
+if (-not (Test-Path $HubInstallDir)) { $HubInstallDir = '' }
+
+if (-not $HubDefaultEditor -and $hubConfigDir) {
+ $HubDefaultEditor = Read-HubFile (Join-Path $hubConfigDir 'defaultEditor.json')
+ if ($HubDefaultEditor -and -not (Test-Path (Join-Path $HubInstallDir $HubDefaultEditor))) {
+ $HubDefaultEditor = ''
+ }
+}
+# Fallback: pick the highest version-named dir under the hub install root.
+if (-not $HubDefaultEditor -and $HubInstallDir) {
+ $HubDefaultEditor = Get-ChildItem -Path $HubInstallDir -Directory -ErrorAction SilentlyContinue |
+ Where-Object Name -Match '^\d{4}' |
+ Sort-Object Name -Descending |
+ Select-Object -First 1 -ExpandProperty Name
+}
+
+# --- Build the probe list ---------------------------------------------------
+
+# Layout pieces. macOS Unity ships as a .app bundle so the inner path differs.
+$inner = if ($IsMacOS) { 'Unity.app/Contents' } else { 'Editor/Data' }
+$rootName = if ($IsMacOS) { 'Unity.app' } else { 'Editor' }
+
+# Each "install" candidate is a directory like // that contains
+# either Editor/ (win/linux) or Unity.app/ (mac).
+$installs = [System.Collections.Generic.List[string]]::new()
+if ($IsLinux -and $env:UNITY_PATH) { $installs.Add($env:UNITY_PATH) }
+if ($HubInstallDir -and $UnityVersion) { $installs.Add((Join-Path $HubInstallDir $UnityVersion)) }
+if ($HubInstallDir -and $HubDefaultEditor -and $UnityVersion -ne $HubDefaultEditor) {
+ $installs.Add((Join-Path $HubInstallDir $HubDefaultEditor))
+}
+if ($IsWindows) { $installs.Add('C:/Program Files/Unity') }
+if ($IsMacOS) { $installs.Add('/Applications/Unity') }
+
+# Unity 6000.3+ moved managed assemblies under Resources/Scripting/Managed.
+# Probe the new layout first within each install.
+$dllRelatives = @('Resources/Scripting/Managed/UnityEngine.dll', 'Managed/UnityEngine.dll')
+
+function Find-UnityDll([string] $install) {
+ foreach ($rel in $dllRelatives) {
+ $dll = Join-Path $install (Join-Path $inner $rel)
+ if (Test-Path $dll) { return $dll }
+ }
+ return $null
+}
+
+# Did the requested-version install yield a DLL? Drives the fallback warning below.
+$expected = if ($UnityVersion -and $HubInstallDir) { Join-Path $HubInstallDir $UnityVersion } else { '' }
+$shouldWarnFallback = $expected -and -not (Find-UnityDll $expected)
+
+$tried = [System.Collections.Generic.List[string]]::new()
+
+foreach ($install in $installs) {
+ $dll = Find-UnityDll $install
+ if (-not $dll) {
+ $dllRelatives | ForEach-Object { $tried.Add((Join-Path $install (Join-Path $inner $_))) }
+ continue
+ }
+
+ if ($shouldWarnFallback) {
+ Write-Warning "Unity version $UnityVersion is not installed. Falling back to default Unity installation."
+ }
+
+ $unityRoot = (Join-Path $install $rootName) + '/'
+ $unityDataPath = (Join-Path $install $inner) + '/'
+ $unityManagedPath = (Split-Path $dll -Parent) + '/'
+
+ # Normalize to forward slashes — MSBuild accepts both, and it keeps the output diff-clean across platforms.
+ Write-Output ("UnityRoot=" + ($unityRoot -replace '\\', '/'))
+ Write-Output ("UnityDataPath=" + ($unityDataPath -replace '\\', '/'))
+ Write-Output ("UnityManagedPath=" + ($unityManagedPath -replace '\\', '/'))
+ exit 0
+}
+
+$msg = @"
+Unity installation not found. See CONTRIBUTING.md.
+UnityVersion: '$UnityVersion'
+Expected one of:
+ * $($tried -join "`n * ")
+"@
+Write-Error $msg
+exit 1
diff --git a/src/Sentry.Unity.Android/SentryJava.cs b/src/Sentry.Unity.Android/SentryJava.cs
index b2435928b..965d41ca9 100644
--- a/src/Sentry.Unity.Android/SentryJava.cs
+++ b/src/Sentry.Unity.Android/SentryJava.cs
@@ -140,6 +140,11 @@ public void Init(SentryUnityOptions options)
androidOptions.Call("setEnableScopeSync", options.NdkScopeSyncEnabled);
androidOptions.Call("setNativeSdkName", "sentry.native.android.unity");
+ androidOptions.Call("setAnrEnabled", options.AndroidNativeAnrEnabled);
+ androidOptions.Call("setEnableScopePersistence", options.AndroidNativeAnrEnabled);
+ androidOptions.Call("setReportHistoricalAnrs", options.AndroidReportHistoricalAnrs);
+ androidOptions.Call("setAttachAnrThreadDump", options.AndroidAttachAnrThreadDump);
+
using (var logsOptions = androidOptions.Call("getLogs"))
{
logsOptions.Call("setEnabled", options.EnableLogs);
@@ -158,8 +163,6 @@ public void Init(SentryUnityOptions options)
androidOptions.Call("setAttachScreenshot", false);
androidOptions.Call("setEnableAutoSessionTracking", false);
androidOptions.Call("setEnableActivityLifecycleBreadcrumbs", false);
- androidOptions.Call("setAnrEnabled", false);
- androidOptions.Call("setEnableScopePersistence", false);
// Disable user interaction tracking to prevent conflicts with VR platforms (e.g., Oculus InputHooks)
androidOptions.Call("setEnableUserInteractionBreadcrumbs", false);
androidOptions.Call("setEnableUserInteractionTracing", false);
diff --git a/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs b/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs
index 1c90cacb6..8ab05ac8c 100644
--- a/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs
+++ b/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs
@@ -218,8 +218,9 @@ internal void ModifyManifest(string basePath)
androidManifest.SetAutoTraceIdGeneration(false);
androidManifest.SetAutoSessionTracking(false);
androidManifest.SetAutoAppLifecycleBreadcrumbs(false);
- androidManifest.SetAnr(false);
- androidManifest.SetPersistentScopeObserver(false);
+ androidManifest.SetAnr(_options.AndroidNativeAnrEnabled);
+ androidManifest.SetPersistentScopeObserver(_options.AndroidNativeAnrEnabled);
+ androidManifest.SetAttachAnrThreadDump(_options.AndroidAttachAnrThreadDump);
// Disable user interaction tracking to prevent conflicts with VR platforms (e.g., Oculus InputHooks)
androidManifest.SetEnableUserInteractionBreadcrumbs(false);
androidManifest.SetEnableUserInteractionTracing(false);
@@ -495,6 +496,9 @@ internal void SetAnr(bool enableAnr)
internal void SetPersistentScopeObserver(bool enableScopePersistence)
=> SetMetaData($"{SentryPrefix}.enable-scope-persistence", enableScopePersistence.ToString());
+ internal void SetAttachAnrThreadDump(bool attachAnrThreadDump)
+ => SetMetaData($"{SentryPrefix}.anr.attach-thread-dumps", attachAnrThreadDump.ToString());
+
internal void SetNdkEnabled(bool enableNdk)
=> SetMetaData($"{SentryPrefix}.ndk.enable", enableNdk.ToString());
diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs
index 76235352a..20da43cb0 100644
--- a/src/Sentry.Unity/SentryUnityOptions.cs
+++ b/src/Sentry.Unity/SentryUnityOptions.cs
@@ -214,6 +214,36 @@ public sealed class SentryUnityOptions : SentryOptions
///
public NativeInitializationType AndroidNativeInitializationType { get; set; } = NativeInitializationType.Runtime;
+ ///
+ /// Enables ANR detection on Android through the native (sentry-java) SDK.
+ /// On API ≥ 30 this uses Android's ApplicationExitInfo to report ANRs detected by the OS
+ /// in prior runs (ANR v2). On API < 30, sentry-java falls back to its in-process watchdog.
+ /// The Unity SDK's C# ANR watchdog continues to run on all API levels.
+ ///
+ ///
+ /// The Java and C# watchdogs observe different threads and are complementary: the Java watchdog
+ /// monitors the Android UI (Looper) main thread, while the C# watchdog monitors the Unity engine
+ /// main thread (the player loop). A hang that blocks both threads can produce one event from each.
+ ///
+ public bool AndroidNativeAnrEnabled { get; set; } = true;
+
+ ///
+ /// When is enabled, controls whether sentry-java reports historical ANRs
+ /// recorded by the OS (ApplicationExitInfo) from prior runs. Has no effect when
+ /// is false.
+ ///
+ ///
+ /// Runtime-only. There is no AndroidManifest meta-data tag for this option, so it is only
+ /// applied when is .
+ ///
+ public bool AndroidReportHistoricalAnrs { get; set; } = false;
+
+ ///
+ /// When is enabled, controls whether sentry-java attaches a thread dump
+ /// to ANR events. Has no effect when is false.
+ ///
+ public bool AndroidAttachAnrThreadDump { get; set; } = false;
+
///
/// Whether the SDK should add the NDK integration for Android
///
diff --git a/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs b/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs
index e8cb9a54c..7985af700 100644
--- a/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs
+++ b/test/Sentry.Unity.Editor.Tests/Android/AndroidManifestConfigurationTests.cs
@@ -150,6 +150,41 @@ public void ModifyManifest_UnityOptions_AndroidNativeSupportEnabled_InitTypeBuil
StringAssert.Contains($"", manifest);
}
+ [Test]
+ public void ModifyManifest_AndroidNativeAnrEnabled_True_WritesAnrMetadataEnabled()
+ {
+ _fixture.SentryUnityOptions!.AndroidNativeAnrEnabled = true;
+ var sut = _fixture.GetSut();
+
+ var manifest = WithAndroidManifest(basePath => sut.ModifyManifest(basePath));
+
+ StringAssert.Contains("", manifest);
+ StringAssert.Contains("", manifest);
+ }
+
+ [Test]
+ public void ModifyManifest_AndroidNativeAnrEnabled_False_WritesAnrMetadataDisabled()
+ {
+ _fixture.SentryUnityOptions!.AndroidNativeAnrEnabled = false;
+ var sut = _fixture.GetSut();
+
+ var manifest = WithAndroidManifest(basePath => sut.ModifyManifest(basePath));
+
+ StringAssert.Contains("", manifest);
+ StringAssert.Contains("", manifest);
+ }
+
+ [Test]
+ public void ModifyManifest_AndroidAttachAnrThreadDump_FlowsThroughToManifest()
+ {
+ _fixture.SentryUnityOptions!.AndroidAttachAnrThreadDump = true;
+ var sut = _fixture.GetSut();
+
+ var manifest = WithAndroidManifest(basePath => sut.ModifyManifest(basePath));
+
+ StringAssert.Contains("", manifest);
+ }
+
[Test]
public void ModifyManifest_ManifestHasDsn()
{