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() {